From c759da169390ba490eee9ef773cc9edc88a32817 Mon Sep 17 00:00:00 2001 From: eliasm307 <13420359+eliasm307@users.noreply.github.com> Date: Mon, 7 Nov 2022 22:15:57 +0100 Subject: [PATCH] feat(eslint-plugin): [naming-convention] add support for "override" and "async" modifiers (#5310) (#5610) * feat(eslint-plugin): [naming-convention] add support for "override" and "async" modifiers (#5310) * apply pr feedback ie remove test case util, split tests by type, remove abstract getter change, remove override for variable selector in docs * remove async parameter logic which is impossible Co-authored-by: Josh Goldberg --- .../docs/rules/naming-convention.md | 23 +- .../rules/naming-convention-utils/enums.ts | 4 + .../rules/naming-convention-utils/schema.ts | 21 +- .../src/rules/naming-convention.ts | 49 ++ .../naming-convention.test.ts | 446 ++++++++++++++++++ 5 files changed, 530 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 50bd1751518..ef273df8f16 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -171,7 +171,8 @@ If these are provided, the identifier must start with one of the provided values - `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. + - `readonly`, `static`, `abstract`, `protected`, `private`, `override` - matches any member explicitly declared with the given modifier. + - `async` - matches any method, function, or function variable which is async via the `async` keyword (e.g. does not match functions that return promises without using `async` keyword) - `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._** @@ -194,16 +195,16 @@ There are two types of selectors, individual selectors, and grouped selectors. Individual Selectors match specific, well-defined sets. There is no overlap between each of the individual selectors. - `variable` - matches any `var` / `let` / `const` variable name. - - Allowed `modifiers`: `const`, `destructured`, `global`, `exported`, `unused`. + - Allowed `modifiers`: `const`, `destructured`, `global`, `exported`, `unused`, `async`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `function` - matches any named function declaration or named function expression. - - Allowed `modifiers`: `global`, `exported`, `unused`. + - Allowed `modifiers`: `global`, `exported`, `unused`, `async`. - Allowed `types`: none. - `parameter` - matches any function parameter. Does not match parameter properties. - Allowed `modifiers`: `destructured`, `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`: `abstract`, `private`, `protected`, `public`, `readonly`, `requiresQuotes`, `static`. + - Allowed `modifiers`: `abstract`, `private`, `protected`, `public`, `readonly`, `requiresQuotes`, `static`, `override`. - 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`: `public`, `requiresQuotes`. @@ -215,16 +216,16 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - 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`: `abstract`, `private`, `protected`, `public`, `requiresQuotes`, `static`. + - Allowed `modifiers`: `abstract`, `private`, `protected`, `public`, `requiresQuotes`, `static`, `override`, `async`. - 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`: `public`, `requiresQuotes`. + - Allowed `modifiers`: `public`, `requiresQuotes`, `async`. - 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`: `public`, `requiresQuotes`. - Allowed `types`: none. - `accessor` - matches any accessor. - - Allowed `modifiers`: `abstract`, `private`, `protected`, `public`, `requiresQuotes`, `static`. + - Allowed `modifiers`: `abstract`, `private`, `protected`, `public`, `requiresQuotes`, `static`, `override`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `enumMember` - matches any enum member. - Allowed `modifiers`: `requiresQuotes`. @@ -253,19 +254,19 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - Allowed `modifiers`: all modifiers. - Allowed `types`: none. - `variableLike` - matches the same as `variable`, `function` and `parameter`. - - Allowed `modifiers`: `unused`. + - Allowed `modifiers`: `unused`, `async`. - Allowed `types`: none. - `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`, `override`, `async`. - 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`, `requiresQuotes`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`, `override`, `async`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `method` - matches the same as `classMethod`, `objectLiteralMethod`, `typeMethod`. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`, `override`, `async`. - Allowed `types`: none. ## FAQ 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 c9460305847..bc2547ffa3f 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts @@ -102,6 +102,10 @@ enum Modifiers { unused = 1 << 10, // properties that require quoting requiresQuotes = 1 << 11, + // class members that are overridden + override = 1 << 12, + // class methods, object function properties, or functions that are async via the `async` keyword + async = 1 << 13, // make sure TypeModifiers starts at Modifiers + 1 or else sorting won't work } 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 fab1d1bc2b6..179b6dd7f43 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts @@ -170,15 +170,21 @@ const SCHEMA: JSONSchema.JSONSchema4 = { selectorsSchema(), ...selectorSchema('default', false, util.getEnumNames(Modifiers)), - ...selectorSchema('variableLike', false, ['unused']), + ...selectorSchema('variableLike', false, ['unused', 'async']), ...selectorSchema('variable', true, [ 'const', 'destructured', 'exported', 'global', 'unused', + 'async', + ]), + ...selectorSchema('function', false, [ + 'exported', + 'global', + 'unused', + 'async', ]), - ...selectorSchema('function', false, ['exported', 'global', 'unused']), ...selectorSchema('parameter', true, ['destructured', 'unused']), ...selectorSchema('memberLike', false, [ @@ -189,6 +195,8 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', 'requiresQuotes', 'static', + 'override', + 'async', ]), ...selectorSchema('classProperty', true, [ 'abstract', @@ -198,6 +206,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', 'requiresQuotes', 'static', + 'override', ]), ...selectorSchema('objectLiteralProperty', true, [ 'public', @@ -222,6 +231,8 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', 'requiresQuotes', 'static', + 'override', + 'async', ]), ...selectorSchema('classMethod', false, [ @@ -231,10 +242,13 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'requiresQuotes', 'static', + 'override', + 'async', ]), ...selectorSchema('objectLiteralMethod', false, [ 'public', 'requiresQuotes', + 'async', ]), ...selectorSchema('typeMethod', false, ['public', 'requiresQuotes']), ...selectorSchema('method', false, [ @@ -244,6 +258,8 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'requiresQuotes', 'static', + 'override', + 'async', ]), ...selectorSchema('accessor', true, [ 'abstract', @@ -252,6 +268,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'requiresQuotes', 'static', + 'override', ]), ...selectorSchema('enumMember', false, ['requiresQuotes']), diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 2984c49f5a2..05f3dd0f2a6 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -138,6 +138,9 @@ export default util.createRule({ if ('readonly' in node && node.readonly) { modifiers.add(Modifiers.readonly); } + if ('override' in node && node.override) { + modifiers.add(Modifiers.override); + } if ( node.type === AST_NODE_TYPES.TSAbstractPropertyDefinition || node.type === AST_NODE_TYPES.TSAbstractMethodDefinition @@ -182,6 +185,34 @@ export default util.createRule({ ); } + function isAsyncMemberOrProperty( + propertyOrMemberNode: + | TSESTree.PropertyNonComputedName + | TSESTree.TSMethodSignatureNonComputedName + | TSESTree.PropertyDefinitionNonComputedName + | TSESTree.TSAbstractPropertyDefinitionNonComputedName + | TSESTree.MethodDefinitionNonComputedName + | TSESTree.TSAbstractMethodDefinitionNonComputedName, + ): boolean { + return Boolean( + 'value' in propertyOrMemberNode && + propertyOrMemberNode.value && + 'async' in propertyOrMemberNode.value && + propertyOrMemberNode.value.async, + ); + } + + function isAsyncVariableIdentifier(id: TSESTree.Identifier): boolean { + return Boolean( + id.parent && + (('async' in id.parent && id.parent.async) || + ('init' in id.parent && + id.parent.init && + 'async' in id.parent.init && + id.parent.init.async)), + ); + } + return { // #region variable @@ -219,6 +250,10 @@ export default util.createRule({ modifiers.add(Modifiers.unused); } + if (isAsyncVariableIdentifier(id)) { + modifiers.add(Modifiers.async); + } + validator(id, modifiers); }); }, @@ -254,6 +289,10 @@ export default util.createRule({ modifiers.add(Modifiers.unused); } + if (node.async) { + modifiers.add(Modifiers.async); + } + validator(node.id, modifiers); }, @@ -360,6 +399,11 @@ export default util.createRule({ | TSESTree.TSMethodSignatureNonComputedName, ): void { const modifiers = new Set([Modifiers.public]); + + if (isAsyncMemberOrProperty(node)) { + modifiers.add(Modifiers.async); + } + handleMember(validators.objectLiteralMethod, node, modifiers); }, @@ -376,6 +420,11 @@ export default util.createRule({ | TSESTree.TSAbstractMethodDefinitionNonComputedName, ): void { const modifiers = getMemberModifiers(node); + + if (isAsyncMemberOrProperty(node)) { + modifiers.add(Modifiers.async); + } + handleMember(validators.classMethod, node, modifiers); }, 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 4c496b6c127..9917aec3cb7 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 @@ -768,6 +768,105 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + const obj = { + Bar() { + return 42; + }, + async async_bar() { + return 42; + }, + }; + class foo { + public Bar() { + return 42; + } + public async async_bar() { + return 42; + } + } + abstract class foo { + public abstract Bar() { + return 42; + } + public abstract async async_bar() { + return 42; + } + } + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: ['method', 'objectLiteralMethod'], + format: ['snake_case'], + modifiers: ['async'], + }, + { + selector: 'method', + format: ['PascalCase'], + }, + ], + }, + { + code: ` + const async_bar1 = async () => {}; + async function async_bar2() {} + const async_bar3 = async function async_bar4() {}; + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: 'method', + format: ['PascalCase'], + }, + { + selector: ['variable'], + format: ['snake_case'], + modifiers: ['async'], + }, + ], + }, + { + code: ` + class foo extends bar { + public someAttribute = 1; + public override some_attribute_override = 1; + public someMethod() { + return 42; + } + public override some_method_override2() { + return 42; + } + } + abstract class foo extends bar { + public abstract someAttribute: string; + public abstract override some_attribute_override: string; + public abstract someMethod(): string; + public abstract override some_method_override2(): string; + } + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: ['memberLike'], + modifiers: ['override'], + format: ['snake_case'], + }, + ], + }, ], invalid: [ { @@ -1526,5 +1625,352 @@ ruleTester.run('naming-convention', rule, { // 6, not 7 because 'foo' is valid errors: Array(6).fill({ messageId: 'doesNotMatchFormat' }), }, + { + code: ` + class foo { + public Bar() { + return 42; + } + public async async_bar() { + return 42; + } + // ❌ error + public async asyncBar() { + return 42; + } + // ❌ error + public AsyncBar2 = async () => { + return 42; + }; + // ❌ error + public AsyncBar3 = async function () { + return 42; + }; + } + abstract class foo { + public abstract Bar(): number; + public abstract async async_bar(): number; + // ❌ error + public abstract async ASYNC_BAR(): number; + } + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: 'method', + format: ['PascalCase'], + }, + { + selector: ['method', 'objectLiteralMethod'], + format: ['snake_case'], + modifiers: ['async'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Class Method', + name: 'asyncBar', + formats: 'snake_case', + }, + }, + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Class Method', + name: 'AsyncBar2', + formats: 'snake_case', + }, + }, + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Class Method', + name: 'AsyncBar3', + formats: 'snake_case', + }, + }, + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Class Method', + name: 'ASYNC_BAR', + formats: 'snake_case', + }, + }, + ], + }, + { + code: ` + const obj = { + Bar() { + return 42; + }, + async async_bar() { + return 42; + }, + // ❌ error + async AsyncBar() { + return 42; + }, + // ❌ error + AsyncBar2: async () => { + return 42; + }, + // ❌ error + AsyncBar3: async function () { + return 42; + }, + }; + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: 'method', + format: ['PascalCase'], + }, + { + selector: ['method', 'objectLiteralMethod'], + format: ['snake_case'], + modifiers: ['async'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Object Literal Method', + name: 'AsyncBar', + formats: 'snake_case', + }, + }, + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Object Literal Method', + name: 'AsyncBar2', + formats: 'snake_case', + }, + }, + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Object Literal Method', + name: 'AsyncBar3', + formats: 'snake_case', + }, + }, + ], + }, + { + code: ` + const syncbar1 = () => {}; + function syncBar2() {} + const syncBar3 = function syncBar4() {}; + + // ❌ error + const AsyncBar1 = async () => {}; + const async_bar1 = async () => {}; + const async_bar3 = async function async_bar4() {}; + async function async_bar2() {} + // ❌ error + const asyncBar5 = async function async_bar6() {}; + `, + parserOptions, + options: [ + { + selector: 'variableLike', + format: ['camelCase'], + }, + { + selector: ['variableLike'], + modifiers: ['async'], + format: ['snake_case'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Variable', + name: 'AsyncBar1', + formats: 'snake_case', + }, + }, + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Variable', + name: 'asyncBar5', + formats: 'snake_case', + }, + }, + ], + }, + { + code: ` + const syncbar1 = () => {}; + function syncBar2() {} + const syncBar3 = function syncBar4() {}; + + const async_bar1 = async () => {}; + // ❌ error + async function asyncBar2() {} + const async_bar3 = async function async_bar4() {}; + async function async_bar2() {} + // ❌ error + const async_bar3 = async function ASYNC_BAR4() {}; + `, + parserOptions, + options: [ + { + selector: 'variableLike', + format: ['camelCase'], + }, + { + selector: ['variableLike'], + modifiers: ['async'], + format: ['snake_case'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Function', + name: 'asyncBar2', + formats: 'snake_case', + }, + }, + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Function', + name: 'ASYNC_BAR4', + formats: 'snake_case', + }, + }, + ], + }, + { + code: ` + class foo extends bar { + public someAttribute = 1; + public override some_attribute_override = 1; + // ❌ error + public override someAttributeOverride = 1; + } + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: ['memberLike'], + modifiers: ['override'], + format: ['snake_case'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Class Property', + name: 'someAttributeOverride', + formats: 'snake_case', + }, + }, + ], + }, + { + code: ` + class foo extends bar { + public override some_method_override() { + return 42; + } + // ❌ error + public override someMethodOverride() { + return 42; + } + } + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: ['memberLike'], + modifiers: ['override'], + format: ['snake_case'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Class Method', + name: 'someMethodOverride', + formats: 'snake_case', + }, + }, + ], + }, + { + code: ` + class foo extends bar { + public get someGetter(): string; + public override get some_getter_override(): string; + // ❌ error + public override get someGetterOverride(): string; + public set someSetter(val: string); + public override set some_setter_override(val: string); + // ❌ error + public override set someSetterOverride(val: string); + } + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: ['memberLike'], + modifiers: ['override'], + format: ['snake_case'], + }, + ], + errors: [ + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Accessor', + name: 'someGetterOverride', + formats: 'snake_case', + }, + }, + { + messageId: 'doesNotMatchFormat', + data: { + type: 'Accessor', + name: 'someSetterOverride', + formats: 'snake_case', + }, + }, + ], + }, ], });