diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index d57bc1c69da..3c0f8325631 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -154,21 +154,27 @@ If these are provided, the identifier must start with one of the provided values ### Selector Options -- `selector` (see "Allowed Selectors, Modifiers and Types" below). +- `selector` allows you to specify what types of identifiers to target. - Accepts one or array of selectors to define an option block that applies to one or multiple selectors. - For example, if you provide `{ selector: ['variable', 'function'] }`, then it will apply the same option to variable and function nodes. + - See [Allowed Selectors, Modifiers and Types](#allowed-selectors-modifiers-and-types) below for the complete list of allowed selectors. - `modifiers` allows you to specify which modifiers to granularly apply to, such as the accessibility (`private`/`public`/`protected`), or if the thing is `static`, etc. - The name must match _all_ of the modifiers. - For example, if you provide `{ modifiers: ['private', 'static', 'readonly'] }`, then it will only match something that is `private static readonly`, and something that is just `private` will not match. + - The following `modifiers` are allowed: + - `const` - matches a variable declared as being `const` (`const x = 1`). + - `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`). - The name must match _one_ of the types. - **_NOTE - Using this option will require that you lint with type information._** - For example, this lets you do things like enforce that `boolean` variables are prefixed with a verb. - - `boolean` matches any type assignable to `boolean | null | undefined` - - `string` matches any type assignable to `string | null | undefined` - - `number` matches any type assignable to `number | null | undefined` - - `array` matches any type assignable to `Array | null | undefined` - - `function` matches any type assignable to `Function | null | undefined` + - The following `types` are allowed: + - `boolean` matches any type assignable to `boolean | null | undefined` + - `string` matches any type assignable to `string | null | undefined` + - `number` matches any type assignable to `number | null | undefined` + - `array` matches any type assignable to `Array | null | undefined` + - `function` matches any type assignable to `Function | null | undefined` The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. @@ -202,13 +208,25 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - `parameter` - matches any function parameter. Does not match parameter properties. - Allowed `modifiers`: none. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. -- `property` - matches any object, class, or object type property. Does not match properties that have direct function expression or arrow function expression values. +- `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 `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 `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 `types`: `boolean`, `string`, `number`, `function`, `array`. - `parameterProperty` - matches any parameter property. - Allowed `modifiers`: `private`, `protected`, `public`, `readonly`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. -- `method` - matches any object, class, or object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. +- `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 `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 `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 `types`: none. - `accessor` - matches any accessor. @@ -249,6 +267,12 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. - Allowed `modifiers`: `abstract`. - Allowed `types`: none. +- `property` - matches the same as `classProperty`, `objectLiteralProperty`, `typeProperty`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `method` - matches the same as `classMethod`, `objectLiteralMethod`, `typeMethod`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. ## Examples diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 295e8209e89..9cc12967c9d 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -41,21 +41,24 @@ enum Selectors { parameter = 1 << 2, // memberLike - property = 1 << 3, - parameterProperty = 1 << 4, - method = 1 << 5, - accessor = 1 << 6, - enumMember = 1 << 7, + parameterProperty = 1 << 3, + accessor = 1 << 4, + enumMember = 1 << 5, + classMethod = 1 << 6, + objectLiteralMethod = 1 << 7, + typeMethod = 1 << 8, + classProperty = 1 << 9, + objectLiteralProperty = 1 << 10, + typeProperty = 1 << 11, // typeLike - class = 1 << 8, - interface = 1 << 9, - typeAlias = 1 << 10, - enum = 1 << 11, - typeParameter = 1 << 12, + class = 1 << 12, + interface = 1 << 13, + typeAlias = 1 << 14, + enum = 1 << 15, + typeParameter = 1 << 17, } type SelectorsString = keyof typeof Selectors; -const SELECTOR_COUNT = util.getEnumNames(Selectors).length; enum MetaSelectors { default = -1, @@ -64,10 +67,14 @@ enum MetaSelectors { Selectors.function | Selectors.parameter, memberLike = 0 | - Selectors.property | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeProperty | Selectors.parameterProperty | Selectors.enumMember | - Selectors.method | + Selectors.classMethod | + Selectors.objectLiteralMethod | + Selectors.typeMethod | Selectors.accessor, typeLike = 0 | Selectors.class | @@ -75,6 +82,14 @@ enum MetaSelectors { Selectors.typeAlias | Selectors.enum | Selectors.typeParameter, + method = 0 | + Selectors.classMethod | + Selectors.objectLiteralMethod | + Selectors.typeProperty, + property = 0 | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeMethod, } type MetaSelectorsString = keyof typeof MetaSelectors; type IndividualAndMetaSelectorsString = SelectorsString | MetaSelectorsString; @@ -321,7 +336,23 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', 'abstract', ]), - ...selectorSchema('property', true, [ + ...selectorSchema('classProperty', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + ...selectorSchema('objectLiteralProperty', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + ...selectorSchema('typeProperty', true, [ 'private', 'protected', 'public', @@ -335,6 +366,36 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'readonly', ]), + ...selectorSchema('property', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + + ...selectorSchema('classMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), + ...selectorSchema('objectLiteralMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), + ...selectorSchema('typeMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), ...selectorSchema('method', false, [ 'private', 'protected', @@ -584,7 +645,7 @@ export default util.createRule({ node: TSESTree.PropertyNonComputedName, ): void { const modifiers = new Set([Modifiers.public]); - handleMember(validators.property, node, modifiers); + handleMember(validators.objectLiteralProperty, node, modifiers); }, ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( @@ -593,7 +654,7 @@ export default util.createRule({ | TSESTree.TSAbstractClassPropertyNonComputedName, ): void { const modifiers = getMemberModifiers(node); - handleMember(validators.property, node, modifiers); + handleMember(validators.classProperty, node, modifiers); }, 'TSPropertySignature[computed = false]'( @@ -604,7 +665,7 @@ export default util.createRule({ modifiers.add(Modifiers.readonly); } - handleMember(validators.property, node, modifiers); + handleMember(validators.typeProperty, node, modifiers); }, // #endregion property @@ -615,14 +676,13 @@ export default util.createRule({ 'Property[computed = false][kind = "init"][value.type = "ArrowFunctionExpression"]', 'Property[computed = false][kind = "init"][value.type = "FunctionExpression"]', 'Property[computed = false][kind = "init"][value.type = "TSEmptyBodyFunctionExpression"]', - 'TSMethodSignature[computed = false]', ].join(', ')]( node: | TSESTree.PropertyNonComputedName | TSESTree.TSMethodSignatureNonComputedName, ): void { const modifiers = new Set([Modifiers.public]); - handleMember(validators.method, node, modifiers); + handleMember(validators.objectLiteralMethod, node, modifiers); }, [[ @@ -638,7 +698,14 @@ export default util.createRule({ | TSESTree.TSAbstractMethodDefinitionNonComputedName, ): void { const modifiers = getMemberModifiers(node); - handleMember(validators.method, node, modifiers); + handleMember(validators.classMethod, node, modifiers); + }, + + 'TSMethodSignature[computed = false]'( + node: TSESTree.TSMethodSignatureNonComputedName, + ): void { + const modifiers = new Set([Modifiers.public]); + handleMember(validators.typeMethod, node, modifiers); }, // #endregion method @@ -851,21 +918,20 @@ function createValidator( return b.modifierWeight - a.modifierWeight; } - /* - meta selectors will always be larger numbers than the normal selectors they contain, as they are the sum of all - of the selectors that they contain. - to give normal selectors a higher priority, shift them all SELECTOR_COUNT bits to the left before comparison, so - they are instead always guaranteed to be larger than the meta selectors. - */ - const aSelector = isMetaSelector(a.selector) - ? a.selector - : a.selector << SELECTOR_COUNT; - const bSelector = isMetaSelector(b.selector) - ? b.selector - : b.selector << SELECTOR_COUNT; + const aIsMeta = isMetaSelector(a.selector); + const bIsMeta = isMetaSelector(b.selector); + // non-meta selectors should go ahead of meta selectors + if (aIsMeta && !bIsMeta) { + return 1; + } + if (!aIsMeta && bIsMeta) { + return -1; + } + + // both aren't meta selectors // sort descending - the meta selectors are "least important" - return bSelector - aSelector; + return b.selector - a.selector; }); return ( @@ -1314,13 +1380,14 @@ function normalizeOption(option: Selector): NormalizedSelector[] { ? option.selector : [option.selector]; - const selectorsAllowedToHaveTypes: (Selectors | MetaSelectors)[] = [ - Selectors.variable, - Selectors.parameter, - Selectors.property, - Selectors.parameterProperty, - Selectors.accessor, - ]; + const selectorsAllowedToHaveTypes = + Selectors.variable | + Selectors.parameter | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeProperty | + Selectors.parameterProperty | + Selectors.accessor; const config: NormalizedSelector[] = []; selectors @@ -1328,7 +1395,7 @@ function normalizeOption(option: Selector): NormalizedSelector[] { isMetaSelector(selector) ? MetaSelectors[selector] : Selectors[selector], ) .forEach(selector => - selectorsAllowedToHaveTypes.includes(selector) + (selectorsAllowedToHaveTypes & selector) !== 0 ? config.push({ selector: selector, ...normalizedOption }) : config.push({ selector: selector, diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index c57dfc984d4..adf93fa1490 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -214,7 +214,9 @@ function createInvalidTestCases( ...(selector !== 'default' && selector !== 'variableLike' && selector !== 'memberLike' && - selector !== 'typeLike' + selector !== 'typeLike' && + selector !== 'property' && + selector !== 'method' ? { data: { type: selectorTypeToMessageString(selector), @@ -444,12 +446,6 @@ const cases: Cases = [ // #region property { code: [ - 'const ignored = { % };', - 'const ignored = { "%": 1 };', - 'interface Ignored { % }', - 'interface Ignored { "%": string }', - 'type Ignored = { % }', - 'type Ignored = { "%": string }', 'class Ignored { private % }', 'class Ignored { private "%" = 1 }', 'class Ignored { private readonly % = 1 }', @@ -459,16 +455,24 @@ const cases: Cases = [ 'class Ignored { declare % }', ], options: { - selector: 'property', + selector: 'classProperty', + }, + }, + { + code: ['const ignored = { % };', 'const ignored = { "%": 1 };'], + options: { + selector: 'objectLiteralProperty', }, }, { code: [ - 'class Ignored { abstract private static readonly % = 1; ignoredDueToModifiers = 1; }', + 'interface Ignored { % }', + 'interface Ignored { "%": string }', + 'type Ignored = { % }', + 'type Ignored = { "%": string }', ], options: { - selector: 'property', - modifiers: ['static', 'readonly'], + selector: 'typeProperty', }, }, // #endregion property @@ -496,13 +500,6 @@ const cases: Cases = [ // #region method { code: [ - 'const ignored = { %() {} };', - 'const ignored = { "%"() {} };', - 'const ignored = { %: () => {} };', - 'interface Ignored { %(): string }', - 'interface Ignored { "%"(): string }', - 'type Ignored = { %(): string }', - 'type Ignored = { "%"(): string }', 'class Ignored { private %() {} }', 'class Ignored { private "%"() {} }', 'class Ignored { private readonly %() {} }', @@ -513,16 +510,28 @@ const cases: Cases = [ 'class Ignored { declare %() }', ], options: { - selector: 'method', + selector: 'classMethod', + }, + }, + { + code: [ + 'const ignored = { %() {} };', + 'const ignored = { "%"() {} };', + 'const ignored = { %: () => {} };', + ], + options: { + selector: 'objectLiteralMethod', }, }, { code: [ - 'class Ignored { abstract private static %() {}; ignoredDueToModifiers() {}; }', + 'interface Ignored { %(): string }', + 'interface Ignored { "%"(): string }', + 'type Ignored = { %(): string }', + 'type Ignored = { "%"(): string }', ], options: { - selector: 'method', - modifiers: ['abstract', 'static'], + selector: 'typeMethod', }, }, // #endregion method @@ -540,15 +549,6 @@ const cases: Cases = [ selector: 'accessor', }, }, - { - code: [ - 'class Ignored { private static get %() {}; get ignoredDueToModifiers() {}; }', - ], - options: { - selector: 'accessor', - modifiers: ['private', 'static'], - }, - }, // #endregion accessor // #region enumMember @@ -567,13 +567,6 @@ const cases: Cases = [ selector: 'class', }, }, - { - code: ['abstract class % {}; class ignoredDueToModifier {}'], - options: { - selector: 'class', - modifiers: ['abstract'], - }, - }, // #endregion class // #region interface @@ -914,12 +907,103 @@ ruleTester.run('naming-convention', rule, { export const { OtherConstant: otherConstant } = SomeClass; `, - parserOptions, options: [ { selector: 'property', format: ['PascalCase'] }, { selector: 'variable', format: ['camelCase'] }, ], }, + { + code: ` + class Ignored { + private static abstract readonly some_name = 1; + IgnoredDueToModifiers = 1; + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'classProperty', + format: ['snake_case'], + modifiers: ['static', 'readonly'], + }, + ], + }, + { + code: ` + class Ignored { + constructor(private readonly some_name, IgnoredDueToModifiers) {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'parameterProperty', + format: ['snake_case'], + modifiers: ['readonly'], + }, + ], + }, + { + code: ` + class Ignored { + private static abstract some_name() {} + IgnoredDueToModifiers() {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'classMethod', + format: ['snake_case'], + modifiers: ['abstract', 'static'], + }, + ], + }, + { + code: ` + class Ignored { + private static get some_name() {} + get IgnoredDueToModifiers() {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'accessor', + format: ['snake_case'], + modifiers: ['private', 'static'], + }, + ], + }, + { + code: ` + abstract class some_name {} + class IgnoredDueToModifier {} + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'class', + format: ['snake_case'], + modifiers: ['abstract'], + }, + ], + }, ], invalid: [ { @@ -1239,8 +1323,7 @@ ruleTester.run('naming-convention', rule, { line: 3, messageId: 'doesNotMatchFormat', data: { - // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum - type: 'Property', + type: 'Object Literal Property', name: 'Property Name', formats: 'strictCamelCase', }, @@ -1331,5 +1414,102 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + class Ignored { + private static abstract readonly some_name = 1; + IgnoredDueToModifiers = 1; + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'classProperty', + format: ['UPPER_CASE'], + modifiers: ['static', 'readonly'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, + { + code: ` + class Ignored { + constructor(private readonly some_name, IgnoredDueToModifiers) {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'parameterProperty', + format: ['UPPER_CASE'], + modifiers: ['readonly'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, + { + code: ` + class Ignored { + private static abstract some_name() {} + IgnoredDueToModifiers() {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'classMethod', + format: ['UPPER_CASE'], + modifiers: ['abstract', 'static'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, + { + code: ` + class Ignored { + private static get some_name() {} + get IgnoredDueToModifiers() {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'accessor', + format: ['UPPER_CASE'], + modifiers: ['private', 'static'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, + { + code: ` + abstract class some_name {} + class IgnoredDueToModifier {} + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'class', + format: ['UPPER_CASE'], + modifiers: ['abstract'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, ], });