diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 0d180bbf7ae7..6a116ebece66 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -183,6 +183,7 @@ If these are provided, the identifier must start with one of the provided values - `global` - matches a variable/function declared in the top-level scope. - `exported` - matches anything that is exported from the module. - `unused` - matches anything that is not used. + - `requiresQuotes` - matches any name that requires quotes as it is not a valid identifier (i.e. has a space, a dash, etc in it). - `public` - matches any member that is either explicitly declared as `public`, or has no visibility modifier (i.e. implicitly public). - `readonly`, `static`, `abstract`, `protected`, `private` - matches any member explicitly declared with the given modifier. - `types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `array`, `function`). @@ -229,31 +230,31 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - Allowed `modifiers`: `unused`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `classProperty` - matches any class property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `objectLiteralProperty` - matches any object literal property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `typeProperty` - matches any object type property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `parameterProperty` - matches any parameter property. - Allowed `modifiers`: `private`, `protected`, `public`, `readonly`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `classMethod` - matches any class method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. - `objectLiteralMethod` - matches any object literal method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. - `typeMethod` - matches any object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. - `accessor` - matches any accessor. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `enumMember` - matches any enum member. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `requiresQuotes`. - Allowed `types`: none. - `class` - matches any class declaration. - Allowed `modifiers`: `abstract`, `exported`, `unused`. @@ -276,22 +277,22 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw Group Selectors are provided for convenience, and essentially bundle up sets of individual selectors. - `default` - matches everything. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: all modifiers. - Allowed `types`: none. - `variableLike` - matches the same as `variable`, `function` and `parameter`. - Allowed `modifiers`: `unused`. - Allowed `types`: none. - `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. - `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. - Allowed `modifiers`: `abstract`, `unused`. - Allowed `types`: none. - `property` - matches the same as `classProperty`, `objectLiteralProperty`, `typeProperty`. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `method` - matches the same as `classMethod`, `objectLiteralMethod`, `typeMethod`. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. ## Examples @@ -424,12 +425,36 @@ This allows you to lint multiple type with same pattern. } ``` -### Ignore properties that require quotes +### Ignore properties that **_require_** quotes Sometimes you have to use a quoted name that breaks the convention (for example, HTTP headers). -If this is a common thing in your codebase, then you can use the `filter` option in one of two ways: +If this is a common thing in your codebase, then you have a few options. -You can use the `filter` option to ignore specific names only: +If you simply want to allow all property names that require quotes, you can use the `requiresQuotes` modifier to match any property name that _requires_ quoting, and use `format: null` to ignore the name. + +```jsonc +{ + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": [ + "classProperty", + "objectLiteralProperty", + "typeProperty", + "classMethod", + "objectLiteralMethod", + "typeMethod", + "accessor", + "enumMember" + ], + "format": null, + "modifiers": ["requiresQuotes"] + } + ] +} +``` + +If you have a small and known list of exceptions, you can use the `filter` option to ignore these specific names only: ```jsonc { @@ -448,7 +473,7 @@ You can use the `filter` option to ignore specific names only: } ``` -You can use the `filter` option to ignore names that require quoting: +You can use the `filter` option to ignore names with specific characters: ```jsonc { @@ -467,6 +492,10 @@ You can use the `filter` option to ignore names that require quoting: } ``` +Note that there is no way to ignore any name that is quoted - only names that are required to be quoted. +This is intentional - adding quotes around a name is not an escape hatch for proper naming. +If you want an escape hatch for a specific name - you should can use an [`eslint-disable` comment](https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments). + ### Ignore destructured names Sometimes you might want to allow destructured properties to retain their original name, even if it breaks your naming convention. diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index fe025389af9c..c0211bb64ef2 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -120,6 +120,8 @@ enum Modifiers { exported = 1 << 9, // things that are unused unused = 1 << 10, + // properties that require quoting + requiresQuotes = 1 << 11, } type ModifiersString = keyof typeof Modifiers; @@ -359,6 +361,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('classProperty', true, [ 'private', @@ -367,6 +370,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('objectLiteralProperty', true, [ 'private', @@ -375,6 +379,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('typeProperty', true, [ 'private', @@ -383,6 +388,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('parameterProperty', true, [ 'private', @@ -397,6 +403,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('classMethod', false, [ @@ -405,6 +412,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('objectLiteralMethod', false, [ 'private', @@ -412,6 +420,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('typeMethod', false, [ 'private', @@ -419,6 +428,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('method', false, [ 'private', @@ -426,6 +436,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('accessor', true, [ 'private', @@ -433,8 +444,9 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), - ...selectorSchema('enumMember', false), + ...selectorSchema('enumMember', false, ['requiresQuotes']), ...selectorSchema('typeLike', false, ['abstract', 'exported', 'unused']), ...selectorSchema('class', false, ['abstract', 'exported', 'unused']), @@ -516,6 +528,9 @@ export default util.createRule({ const validators = parseOptions(context); + const compilerOptions = util + .getParserServices(context, true) + .program.getCompilerOptions(); function handleMember( validator: ValidatorFunction | null, node: @@ -533,6 +548,10 @@ export default util.createRule({ } const key = node.key; + if (requiresQuoting(key, compilerOptions.target)) { + modifiers.add(Modifiers.requiresQuotes); + } + validator(key, modifiers); } @@ -829,7 +848,13 @@ export default util.createRule({ } const id = node.id; - validator(id); + const modifiers = new Set(); + + if (requiresQuoting(id, compilerOptions.target)) { + modifiers.add(Modifiers.requiresQuotes); + } + + validator(id, modifiers); }, // #endregion enumMember @@ -1020,8 +1045,17 @@ function isGlobal(scope: TSESLint.Scope.Scope | null): boolean { ); } -type ValidatorFunction = ( +function requiresQuoting( node: TSESTree.Identifier | TSESTree.Literal, + target: ts.ScriptTarget | undefined, +): boolean { + const name = + node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; + return util.requiresQuoting(name, target); +} + +type ValidatorFunction = ( + node: TSESTree.Identifier | TSESTree.StringLiteral | TSESTree.NumberLiteral, modifiers?: Set, ) => void; type ParsedOptions = Record; diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index d8a7750efbaa..8881473da051 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -6,6 +6,7 @@ import { getParserServices, isClosingBraceToken, isOpeningBraceToken, + requiresQuoting, } from '../util'; import { isTypeFlagSet, unionTypeParts } from 'tsutils'; @@ -34,24 +35,6 @@ export default createRule({ const checker = service.program.getTypeChecker(); const compilerOptions = service.program.getCompilerOptions(); - function requiresQuoting(name: string): boolean { - if (name.length === 0) { - return true; - } - - if (!ts.isIdentifierStart(name.charCodeAt(0), compilerOptions.target)) { - return true; - } - - for (let i = 1; i < name.length; i += 1) { - if (!ts.isIdentifierPart(name.charCodeAt(i), compilerOptions.target)) { - return true; - } - } - - return false; - } - function getNodeType(node: TSESTree.Node): ts.Type { const tsNode = service.esTreeNodeToTSNodeMap.get(node); return getConstrainedTypeAtLocation(checker, tsNode); @@ -93,7 +76,7 @@ export default createRule({ if ( symbolName && (missingBranchName || missingBranchName === '') && - requiresQuoting(missingBranchName.toString()) + requiresQuoting(missingBranchName.toString(), compilerOptions.target) ) { caseTest = `${symbolName}['${missingBranchName}']`; } diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index af0a64eddbfc..86e0ec233fd4 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -8,6 +8,7 @@ export * from './misc'; export * from './nullThrows'; export * from './objectIterators'; export * from './propertyTypes'; +export * from './requiresQuoting'; export * from './types'; // this is done for convenience - saves migrating all of the old rules diff --git a/packages/eslint-plugin/src/util/requiresQuoting.ts b/packages/eslint-plugin/src/util/requiresQuoting.ts new file mode 100644 index 000000000000..27c9a2ff77cf --- /dev/null +++ b/packages/eslint-plugin/src/util/requiresQuoting.ts @@ -0,0 +1,24 @@ +import * as ts from 'typescript'; + +function requiresQuoting( + name: string, + target: ts.ScriptTarget = ts.ScriptTarget.ESNext, +): boolean { + if (name.length === 0) { + return true; + } + + if (!ts.isIdentifierStart(name.charCodeAt(0), target)) { + return true; + } + + for (let i = 1; i < name.length; i += 1) { + if (!ts.isIdentifierPart(name.charCodeAt(i), target)) { + return true; + } + } + + return false; +} + +export { requiresQuoting }; diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index 87123331153c..b53d7b9304cc 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -1325,6 +1325,113 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + const ignored1 = { + 'a a': 1, + 'b b'() {}, + get 'c c'() { + return 1; + }, + set 'd d'(value: string) {}, + }; + class ignored2 { + 'a a' = 1; + 'b b'() {} + get 'c c'() { + return 1; + } + set 'd d'(value: string) {} + } + interface ignored3 { + 'a a': 1; + 'b b'(): void; + } + type ignored4 = { + 'a a': 1; + 'b b'(): void; + }; + enum ignored5 { + 'a a', + } + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: 'default', + format: null, + modifiers: ['requiresQuotes'], + }, + ], + }, + { + code: ` + const ignored1 = { + 'a a': 1, + 'b b'() {}, + get 'c c'() { + return 1; + }, + set 'd d'(value: string) {}, + }; + class ignored2 { + 'a a' = 1; + 'b b'() {} + get 'c c'() { + return 1; + } + set 'd d'(value: string) {} + } + interface ignored3 { + 'a a': 1; + 'b b'(): void; + } + type ignored4 = { + 'a a': 1; + 'b b'(): void; + }; + enum ignored5 { + 'a a', + } + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: [ + 'classProperty', + 'objectLiteralProperty', + 'typeProperty', + 'classMethod', + 'objectLiteralMethod', + 'typeMethod', + 'accessor', + 'enumMember', + ], + format: null, + modifiers: ['requiresQuotes'], + }, + // making sure the `requoresQuotes` modifier appropriately overrides this + { + selector: [ + 'classProperty', + 'objectLiteralProperty', + 'typeProperty', + 'classMethod', + 'objectLiteralMethod', + 'typeMethod', + 'accessor', + 'enumMember', + ], + format: ['PascalCase'], + }, + ], + }, ], invalid: [ { @@ -2000,5 +2107,48 @@ ruleTester.run('naming-convention', rule, { ], errors: Array(7).fill({ messageId: 'doesNotMatchFormat' }), }, + { + code: ` + const ignored1 = { + 'a a': 1, + 'b b'() {}, + get 'c c'() { + return 1; + }, + set 'd d'(value: string) {}, + }; + class ignored2 { + 'a a' = 1; + 'b b'() {} + get 'c c'() { + return 1; + } + set 'd d'(value: string) {} + } + interface ignored3 { + 'a a': 1; + 'b b'(): void; + } + type ignored4 = { + 'a a': 1; + 'b b'(): void; + }; + enum ignored5 { + 'a a', + } + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: 'default', + format: ['PascalCase'], + modifiers: ['requiresQuotes'], + }, + ], + errors: Array(13).fill({ messageId: 'doesNotMatchFormat' }), + }, ], }); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy