diff --git a/packages/eslint-plugin-internal/src/rules/index.ts b/packages/eslint-plugin-internal/src/rules/index.ts index 800c448e041..f781e01e6df 100644 --- a/packages/eslint-plugin-internal/src/rules/index.ts +++ b/packages/eslint-plugin-internal/src/rules/index.ts @@ -1,7 +1,9 @@ import noTypescriptDefaultImport from './no-typescript-default-import'; import noTypescriptEstreeImport from './no-typescript-estree-import'; +import preferASTTypesEnum from './prefer-ast-types-enum'; export default { 'no-typescript-default-import': noTypescriptDefaultImport, 'no-typescript-estree-import': noTypescriptEstreeImport, + 'prefer-ast-types-enum': preferASTTypesEnum, }; diff --git a/packages/eslint-plugin-internal/src/rules/prefer-ast-types-enum.ts b/packages/eslint-plugin-internal/src/rules/prefer-ast-types-enum.ts new file mode 100755 index 00000000000..09398ea11df --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/prefer-ast-types-enum.ts @@ -0,0 +1,70 @@ +import { + AST_NODE_TYPES, + AST_TOKEN_TYPES, + ESLintUtils, + TSESTree, +} from '@typescript-eslint/experimental-utils'; + +const isStringLiteral = ( + node: TSESTree.Literal, +): node is TSESTree.StringLiteral => typeof node.value === 'string'; + +export = ESLintUtils.RuleCreator(name => name)({ + name: __filename, + meta: { + type: 'problem', + docs: { + category: 'Best Practices', + recommended: 'error', + description: + 'Ensures consistent usage of AST_NODE_TYPES & AST_TOKEN_TYPES enums.', + }, + messages: { + preferEnum: 'Prefer {{ enumName }}.{{ literal }} over raw literal', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const report = ( + enumName: 'AST_NODE_TYPES' | 'AST_TOKEN_TYPES', + literal: TSESTree.StringLiteral, + ): void => + context.report({ + data: { enumName, literal: literal.value }, + messageId: 'preferEnum', + node: literal, + fix: fixer => + fixer.replaceText(literal, `${enumName}.${literal.value}`), + }); + + return { + Literal(node: TSESTree.Literal): void { + if ( + node.parent?.type === AST_NODE_TYPES.TSEnumMember && + node.parent.parent?.type === AST_NODE_TYPES.TSEnumDeclaration && + ['AST_NODE_TYPES', 'AST_TOKEN_TYPES'].includes( + node.parent.parent.id.name, + ) + ) { + return; + } + + if (!isStringLiteral(node)) { + return; + } + + const value = node.value; + + if (Object.prototype.hasOwnProperty.call(AST_NODE_TYPES, value)) { + report('AST_NODE_TYPES', node); + } + + if (Object.prototype.hasOwnProperty.call(AST_TOKEN_TYPES, value)) { + report('AST_TOKEN_TYPES', node); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin-internal/tests/rules/prefer-ast-types-enum.test.ts b/packages/eslint-plugin-internal/tests/rules/prefer-ast-types-enum.test.ts new file mode 100644 index 00000000000..52cab8b69c0 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/prefer-ast-types-enum.test.ts @@ -0,0 +1,50 @@ +import rule from '../../src/rules/prefer-ast-types-enum'; +import { RuleTester, batchedSingleLineTests } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + }, +}); + +ruleTester.run('prefer-ast-types-enum', rule, { + valid: [ + 'node.type === "constructor"', + 'node.type === AST_NODE_TYPES.Literal', + 'node.type === AST_TOKEN_TYPES.Keyword', + 'node.type === 1', + ` + enum MY_ENUM { + Literal = 1 + } + `, + ` + enum AST_NODE_TYPES { + Literal = 'Literal' + } + `, + ], + invalid: batchedSingleLineTests({ + code: ` +node.type === 'Literal' +node.type === 'Keyword' + `, + output: ` +node.type === AST_NODE_TYPES.Literal +node.type === AST_TOKEN_TYPES.Keyword + `, + errors: [ + { + data: { enumName: 'AST_NODE_TYPES', literal: 'Literal' }, + messageId: 'preferEnum', + line: 2, + }, + { + data: { enumName: 'AST_TOKEN_TYPES', literal: 'Keyword' }, + messageId: 'preferEnum', + line: 3, + }, + ], + }), +});