From bb15aae877ae260f59aa5e6cfc338b1eefc6d85c Mon Sep 17 00:00:00 2001 From: Michael Kim Date: Tue, 17 Oct 2023 16:01:43 -0400 Subject: [PATCH] feat(eslint-plugin): [naming-convention] add support for default and namespace imports (#7269) * [naming-convention] add support for default and namespace imports * split tests --- .../docs/rules/naming-convention.md | 3 + .../rules/naming-convention-utils/enums.ts | 17 ++- .../rules/naming-convention-utils/schema.ts | 1 + .../src/rules/naming-convention.ts | 35 +++++ .../naming-convention.test.ts | 135 ++++++++++++++++++ .../schema-snapshots/naming-convention.shot | 75 ++++++++++ 6 files changed, 261 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 53c381c9e74..25c3a5c12bf 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -216,6 +216,9 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - `function` - matches any named function declaration or named function expression. - Allowed `modifiers`: `async`, `exported`, `global`, `unused`. - Allowed `types`: none. +- `import` - matches namespace imports and default imports (i.e. does not match named imports). + - Allowed `modifiers`: `default`, `namespace`. + - Allowed `types`: none. - `interface` - matches any interface declaration. - Allowed `modifiers`: `exported`, `unused`. - Allowed `types`: none. diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts index 1b26c18ecb2..b92928b8647 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts @@ -43,6 +43,9 @@ enum Selectors { typeAlias = 1 << 14, enum = 1 << 15, typeParameter = 1 << 17, + + // other + import = 1 << 18, } type SelectorsString = keyof typeof Selectors; @@ -107,17 +110,21 @@ enum Modifiers { override = 1 << 13, // class methods, object function properties, or functions that are async via the `async` keyword async = 1 << 14, + // default imports + default = 1 << 15, + // namespace imports + namespace = 1 << 16, // make sure TypeModifiers starts at Modifiers + 1 or else sorting won't work } type ModifiersString = keyof typeof Modifiers; enum TypeModifiers { - boolean = 1 << 15, - string = 1 << 16, - number = 1 << 17, - function = 1 << 18, - array = 1 << 19, + boolean = 1 << 17, + string = 1 << 18, + number = 1 << 19, + function = 1 << 20, + array = 1 << 21, } type TypeModifiersString = keyof typeof TypeModifiers; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts index 3e44a0c3495..d963f3101c5 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts @@ -307,6 +307,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { ...selectorSchema('typeAlias', false, ['exported', 'unused']), ...selectorSchema('enum', false, ['exported', 'unused']), ...selectorSchema('typeParameter', false, ['unused']), + ...selectorSchema('import', false, ['default', 'namespace']), ], }, additionalItems: false, diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index dd287ccadd1..15bb22b955c 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -226,6 +226,41 @@ export default createRule({ ) => void; }>; } = { + // #region import + + 'ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier': { + validator: validators.import, + handler: ( + node: + | TSESTree.ImportDefaultSpecifier + | TSESTree.ImportNamespaceSpecifier + | TSESTree.ImportSpecifier, + validator, + ): void => { + const modifiers = new Set(); + + switch (node.type) { + case AST_NODE_TYPES.ImportDefaultSpecifier: + modifiers.add(Modifiers.default); + break; + case AST_NODE_TYPES.ImportNamespaceSpecifier: + modifiers.add(Modifiers.namespace); + break; + case AST_NODE_TYPES.ImportSpecifier: + // Handle `import { default as Foo }` + if (node.imported.name !== 'default') { + return; + } + modifiers.add(Modifiers.default); + break; + } + + validator(node.local, modifiers); + }, + }, + + // #endregion + // #region variable VariableDeclarator: { diff --git a/packages/eslint-plugin/tests/rules/naming-convention/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention/naming-convention.test.ts index 75156ebb2ce..731fc22d9c4 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention/naming-convention.test.ts @@ -932,6 +932,66 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: "import * as FooBar from 'foo_bar';", + parserOptions, + options: [ + { + selector: ['import'], + format: ['PascalCase'], + }, + { + selector: ['import'], + modifiers: ['default'], + format: ['camelCase'], + }, + ], + }, + { + code: "import fooBar from 'foo_bar';", + parserOptions, + options: [ + { + selector: ['import'], + format: ['PascalCase'], + }, + { + selector: ['import'], + modifiers: ['default'], + format: ['camelCase'], + }, + ], + }, + { + code: "import { default as fooBar } from 'foo_bar';", + parserOptions, + options: [ + { + selector: ['import'], + format: ['PascalCase'], + }, + { + selector: ['import'], + modifiers: ['default'], + format: ['camelCase'], + }, + ], + }, + { + code: "import { foo_bar } from 'foo_bar';", + parserOptions, + options: [ + { + selector: ['import'], + format: ['PascalCase'], + }, + { + selector: ['import'], + modifiers: ['default'], + format: ['camelCase'], + }, + ], + }, ], invalid: [ { @@ -2121,5 +2181,80 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: "import * as fooBar from 'foo_bar';", + parserOptions, + options: [ + { + selector: ['import'], + format: ['camelCase'], + }, + { + selector: ['import'], + modifiers: ['namespace'], + format: ['PascalCase'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Import', + name: 'fooBar', + formats: 'PascalCase', + }, + }, + ], + }, + { + code: "import FooBar from 'foo_bar';", + parserOptions, + options: [ + { + selector: ['import'], + format: ['camelCase'], + }, + { + selector: ['import'], + modifiers: ['namespace'], + format: ['PascalCase'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Import', + name: 'FooBar', + formats: 'camelCase', + }, + }, + ], + }, + { + code: "import { default as foo_bar } from 'foo_bar';", + parserOptions, + options: [ + { + selector: ['import'], + format: ['camelCase'], + }, + { + selector: ['import'], + modifiers: ['namespace'], + format: ['PascalCase'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Import', + name: 'foo_bar', + formats: 'camelCase', + }, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/schema-snapshots/naming-convention.shot b/packages/eslint-plugin/tests/schema-snapshots/naming-convention.shot index 1f360bc55c2..8ae5a4a339d 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/naming-convention.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/naming-convention.shot @@ -106,9 +106,11 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "abstract", "async", "const", + "default", "destructured", "exported", "global", + "namespace", "override", "private", "protected", @@ -137,6 +139,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "enum", "enumMember", "function", + "import", "interface", "memberLike", "method", @@ -209,9 +212,11 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "abstract", "async", "const", + "default", "destructured", "exported", "global", + "namespace", "override", "private", "protected", @@ -1508,6 +1513,58 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos }, "required": ["selector", "format"], "type": "object" + }, + { + "additionalProperties": false, + "description": "Selector 'import'", + "properties": { + "custom": { + "$ref": "#/$defs/matchRegexConfig" + }, + "failureMessage": { + "type": "string" + }, + "filter": { + "oneOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "$ref": "#/$defs/matchRegexConfig" + } + ] + }, + "format": { + "$ref": "#/$defs/formatOptionsConfig" + }, + "leadingUnderscore": { + "$ref": "#/$defs/underscoreOptions" + }, + "modifiers": { + "additionalItems": false, + "items": { + "enum": ["default", "namespace"], + "type": "string" + }, + "type": "array" + }, + "prefix": { + "$ref": "#/$defs/prefixSuffixConfig" + }, + "selector": { + "enum": ["import"], + "type": "string" + }, + "suffix": { + "$ref": "#/$defs/prefixSuffixConfig" + }, + "trailingUnderscore": { + "$ref": "#/$defs/underscoreOptions" + } + }, + "required": ["selector", "format"], + "type": "object" } ] }, @@ -1556,9 +1613,11 @@ type Options = /** Multiple selectors in one config */ | 'abstract' | 'async' | 'const' + | 'default' | 'destructured' | 'exported' | 'global' + | 'namespace' | 'override' | 'private' | 'protected' @@ -1578,6 +1637,7 @@ type Options = /** Multiple selectors in one config */ | 'enum' | 'enumMember' | 'function' + | 'import' | 'interface' | 'memberLike' | 'method' @@ -1692,9 +1752,11 @@ type Options = /** Multiple selectors in one config */ | 'abstract' | 'async' | 'const' + | 'default' | 'destructured' | 'exported' | 'global' + | 'namespace' | 'override' | 'private' | 'protected' @@ -1748,6 +1810,19 @@ type Options = /** Multiple selectors in one config */ suffix?: PrefixSuffixConfig; trailingUnderscore?: UnderscoreOptions; } + /** Selector 'import' */ + | { + custom?: MatchRegexConfig; + failureMessage?: string; + filter?: MatchRegexConfig | string; + format: FormatOptionsConfig; + leadingUnderscore?: UnderscoreOptions; + modifiers?: ('default' | 'namespace')[]; + prefix?: PrefixSuffixConfig; + selector: 'import'; + suffix?: PrefixSuffixConfig; + trailingUnderscore?: UnderscoreOptions; + } /** Selector 'interface' */ | { custom?: MatchRegexConfig;