diff --git a/eslint-plugin-expensify/no-default-id-values.js b/eslint-plugin-expensify/no-default-id-values.js new file mode 100644 index 0000000..69272bd --- /dev/null +++ b/eslint-plugin-expensify/no-default-id-values.js @@ -0,0 +1,86 @@ +function createPatternRegex(pattern) { + return new RegExp(pattern.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'), 'g'); +} + +function searchForPatternsAndReport(context, sourceCode, soureCodeStr, pattern, messageId) { + const regex = createPatternRegex(pattern); + let match = regex.exec(soureCodeStr); + while (match !== null) { + const index = match.index; + + const defaultStr = match[0]; + const defaultStrPosition = sourceCode.getLocFromIndex(index); + + context.report({ + messageId, + loc: { + start: {line: defaultStrPosition.line, column: defaultStrPosition.column + defaultStr.indexOf(' ')}, + end: {line: defaultStrPosition.line, column: defaultStrPosition.column + defaultStr.length}, + }, + }); + + match = regex.exec(soureCodeStr); + } +} + +module.exports = { + name: 'no-default-id-values', + meta: { + type: 'problem', + docs: { + description: 'Restricts use of default number/string IDs in the project.', + recommended: 'error', + }, + schema: [], + messages: { + // eslint-disable-next-line max-len + disallowedNumberDefault: 'Default the number ID to `CONST.DEFAULT_NUMBER_ID` instead. See: https://github.com/Expensify/App/blob/main/contributingGuides/STYLE.md#default-value-for-inexistent-IDs', + // eslint-disable-next-line max-len + disallowedStringDefault: 'Do not default string IDs to any value. See: https://github.com/Expensify/App/blob/main/contributingGuides/STYLE.md#default-value-for-inexistent-IDs', + }, + }, + create(context) { + const sourceCode = context.getSourceCode(); + const soureCodeStr = sourceCode.text; // This gets all the text in the file + + const disallowedNumberDefaults = [ + 'ID ?? -1', + 'id ?? -1', + 'ID ?? 0', + 'id ?? 0', + 'ID || -1', + 'id || -1', + 'ID || 0', + 'id || 0', + 'ID : -1', + 'id : -1', + 'ID : 0', + 'id : 0', + ]; + + const disallowedStringDefaults = [ + " ?? '-1'", + "ID ?? ''", + "id ?? ''", + "ID ?? '0'", + "id ?? '0'", + " || '-1'", + "ID || ''", + "id || ''", + "ID || '0'", + "id || '0'", + " : '-1'", + " : '0'", + ]; + + disallowedNumberDefaults.forEach((pattern) => { + searchForPatternsAndReport(context, sourceCode, soureCodeStr, pattern, 'disallowedNumberDefault'); + }); + + disallowedStringDefaults.forEach((pattern) => { + searchForPatternsAndReport(context, sourceCode, soureCodeStr, pattern, 'disallowedStringDefault'); + }); + + return {}; + }, +}; diff --git a/eslint-plugin-expensify/tests/no-default-id-values.test.js b/eslint-plugin-expensify/tests/no-default-id-values.test.js new file mode 100644 index 0000000..e394199 --- /dev/null +++ b/eslint-plugin-expensify/tests/no-default-id-values.test.js @@ -0,0 +1,260 @@ +const RuleTester = require('eslint').RuleTester; +const rule = require('../no-default-id-values'); + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, +}); + +ruleTester.run('no-default-id-values', rule, { + valid: [ + // Number IDs + { + code: 'const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID;', + }, + { + code: 'const accountID = account?.id ?? CONST.DEFAULT_NUMBER_ID;', + }, + { + code: 'currentState = currentState?.routes[currentState.index ?? -1].state;', + }, + { + code: 'currentState = currentState?.routes[currentState.index ?? 0].state;', + }, + { + code: 'const accountID = report?.ownerAccountID || CONST.DEFAULT_NUMBER_ID;', + }, + { + code: 'const accountID = account?.id || CONST.DEFAULT_NUMBER_ID;', + }, + { + code: 'currentState = currentState?.routes[currentState.index || -1].state;', + }, + { + code: 'currentState = currentState?.routes[currentState.index || 0].state;', + }, + { + code: 'const managerID = report ? report.managerID : CONST.DEFAULT_NUMBER_ID;', + }, + { + code: 'const accountID = account ? account.id : CONST.DEFAULT_NUMBER_ID;', + }, + { + code: 'options.sort((method) => (method.value === exportMethod ? -1 : 0));', + }, + + // String IDs + { + code: 'const reportID = report?.reportID;', + }, + { + code: 'const iconName = icon.name ?? \'\'', + }, + { + code: 'const index = tempIndex ?? \'0\'', + }, + { + code: 'const iconName = icon.name || \'\'', + }, + { + code: 'const index = tempIndex || \'0\'', + }, + ], + invalid: [ + // Number IDs + { + code: 'const accountID = report?.ownerAccountID ?? -1;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const reportID = report?.id ?? -1;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const accountID = report?.ownerAccountID ?? 0;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const reportID = report?.id ?? 0;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const accountID = report?.ownerAccountID || -1;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const reportID = report?.id || -1;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const accountID = report?.ownerAccountID || 0;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const reportID = report?.id || 0;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const managerID = report ? report.managerID : -1;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const accountID = account ? account.id : -1;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const managerID = report ? report.managerID : 0;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + { + code: 'const accountID = account ? account.id : 0;', + errors: [{ + messageId: 'disallowedNumberDefault', + }], + }, + + // String IDs + { + code: 'const reportID = report?.reportID ?? \'-1\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const currentReportID = Navigation.getTopmostReportId() ?? \'-1\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const currentReportID = navigationRef?.isReady?.() ? Navigation.getTopmostReportId() ?? \'-1\' : \'-1\';', + errors: [ + { + messageId: 'disallowedStringDefault', + }, + { + messageId: 'disallowedStringDefault', + }, + ], + }, + { + code: 'const reportID = report?.reportID ?? \'-1\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const policyID = policy?.id ?? \'-1\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const reportID = report?.reportID ?? \'\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const policyID = policy?.id ?? \'\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const reportID = report?.reportID ?? \'0\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const policyID = policy?.id ?? \'0\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const reportID = report?.reportID || \'-1\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const currentReportID = Navigation.getTopmostReportId() || \'-1\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const currentReportID = navigationRef?.isReady?.() ? Navigation.getTopmostReportId() || \'-1\' : \'-1\';', + errors: [ + { + messageId: 'disallowedStringDefault', + }, + { + messageId: 'disallowedStringDefault', + }, + ], + }, + { + code: 'const reportID = report?.reportID || \'\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const policyID = policy?.id || \'\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const reportID = report?.reportID || \'0\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const policyID = policy?.id || \'0\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const reportID = report ? report.reportID : \'-1\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + { + code: 'const reportID = report ? report.reportID : \'0\';', + errors: [{ + messageId: 'disallowedStringDefault', + }], + }, + ], +});