From 7c5f8503db768b9458583ba13877707acc3ab898 Mon Sep 17 00:00:00 2001 From: Eli <13420359+eliasm307@users.noreply.github.com> Date: Wed, 7 Sep 2022 01:00:52 +0100 Subject: [PATCH] feat(eslint-plugin): [naming-convention] add support for "override" and "async" modifiers (#5310) --- .../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 | 60 +++ .../naming-convention.test.ts | 433 +++++++++++++++++- 5 files changed, 527 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index ef6d82e83029..1a0bb9cea011 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -173,7 +173,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._** @@ -196,16 +197,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`, `override`, `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`. @@ -217,16 +218,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`. @@ -255,19 +256,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 c9460305847a..bc2547ffa3f6 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 4136f7186bf4..c55ad10c4852 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts @@ -167,15 +167,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, [ @@ -186,6 +192,8 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', 'requiresQuotes', 'static', + 'override', + 'async', ]), ...selectorSchema('classProperty', true, [ 'abstract', @@ -195,6 +203,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', 'requiresQuotes', 'static', + 'override', ]), ...selectorSchema('objectLiteralProperty', true, [ 'public', @@ -219,6 +228,8 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', 'requiresQuotes', 'static', + 'override', + 'async', ]), ...selectorSchema('classMethod', false, [ @@ -228,10 +239,13 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'requiresQuotes', 'static', + 'override', + 'async', ]), ...selectorSchema('objectLiteralMethod', false, [ 'public', 'requiresQuotes', + 'async', ]), ...selectorSchema('typeMethod', false, ['public', 'requiresQuotes']), ...selectorSchema('method', false, [ @@ -241,6 +255,8 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'requiresQuotes', 'static', + 'override', + 'async', ]), ...selectorSchema('accessor', true, [ 'abstract', @@ -249,6 +265,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 a27d9b09df0f..a12c10da1766 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); }, @@ -291,6 +330,10 @@ export default util.createRule({ modifiers.add(Modifiers.unused); } + if (node.async) { + modifiers.add(Modifiers.async); + } + validator(i, modifiers); }); }); @@ -360,6 +403,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 +424,11 @@ export default util.createRule({ | TSESTree.TSAbstractMethodDefinitionNonComputedName, ): void { const modifiers = getMemberModifiers(node); + + if (isAsyncMemberOrProperty(node)) { + modifiers.add(Modifiers.async); + } + handleMember(validators.classMethod, node, modifiers); }, @@ -404,6 +457,13 @@ export default util.createRule({ handleMember(validators.accessor, node, modifiers); }, + 'TSAbstractMethodDefinition[computed = false]:matches([kind = "get"], [kind = "set"])'( + node: TSESTree.MethodDefinitionNonComputedName, + ): void { + const modifiers = getMemberModifiers(node); + handleMember(validators.accessor, node, modifiers); + }, + // #endregion accessor // #region enumMember 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 4c496b6c127b..fe661eceb3fd 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 @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/internal/prefer-ast-types-enum */ -import rule from '../../../src/rules/naming-convention'; +import rule, { MessageIds } from '../../../src/rules/naming-convention'; import { getFixturesRootDir, noFormat, RuleTester } from '../../RuleTester'; +import { TestCaseError } from '@typescript-eslint/utils/src/ts-eslint'; +import { SelectorsString } from '../../../src/rules/naming-convention-utils/enums'; +import { PredefinedFormatsString } from '../../../src/rules/naming-convention-utils'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', @@ -13,6 +16,38 @@ const parserOptions = { project: './tsconfig.json', }; +/** Union of all case error configs with `messageId` as discriminant property for type narrowing */ +type CaseErrorWithDataConfig = + | { + messageId: 'doesNotMatchFormat'; + type: Capitalize; + name: string; + expectedFormats: PredefinedFormatsString[]; + } + | { messageId: 'unexpectedUnderscore' }; // todo add more detail and union members as required + +function createExpectedTestCaseErrorsWithData( + caseErrorConfigs: CaseErrorWithDataConfig[], +): TestCaseError[] { + return caseErrorConfigs.map(errorConfig => { + if (errorConfig.messageId === 'doesNotMatchFormat') { + const typeNotCamelCase = errorConfig.type + .replace(/([A-Z])/g, ' $1') + .trim(); + return { + messageId: 'doesNotMatchFormat', + data: { + type: typeNotCamelCase, + name: errorConfig.name, + formats: errorConfig.expectedFormats.join(','), + }, + }; + } + + throw Error(`${errorConfig.messageId} not yet supported`); + }); +} + ruleTester.run('naming-convention', rule, { valid: [ { @@ -768,6 +803,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 +1660,302 @@ 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: createExpectedTestCaseErrorsWithData([ + { + messageId: 'doesNotMatchFormat', + name: 'asyncBar', + type: 'ClassMethod', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'AsyncBar2', + type: 'ClassMethod', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'AsyncBar3', + type: 'ClassMethod', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'ASYNC_BAR', + type: 'ClassMethod', + expectedFormats: ['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: createExpectedTestCaseErrorsWithData([ + { + messageId: 'doesNotMatchFormat', + name: 'AsyncBar', + type: 'ObjectLiteralMethod', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'AsyncBar2', + type: 'ObjectLiteralMethod', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'AsyncBar3', + type: 'ObjectLiteralMethod', + expectedFormats: ['snake_case'], + }, + ]), + }, + { + code: ` + const syncbar1 = () => {}; + function syncBar2() {} + const syncBar3 = function syncBar4() {}; + + // ❌ error + const AsyncBar1 = async () => {}; + 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() {}; + // ❌ error + const asyncBar5 = async function async_bar6() {}; + `, + parserOptions, + options: [ + { + selector: 'variableLike', + format: ['camelCase'], + }, + { + selector: ['variableLike'], + modifiers: ['async'], + format: ['snake_case'], + }, + ], + errors: createExpectedTestCaseErrorsWithData([ + { + messageId: 'doesNotMatchFormat', + name: 'AsyncBar1', + type: 'Variable', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'asyncBar2', + type: 'Function', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'ASYNC_BAR4', + type: 'Function', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'asyncBar5', + type: 'Variable', + expectedFormats: ['snake_case'], + }, + ]), + }, + { + code: ` + class foo extends bar { + public someAttribute = 1; + public override some_attribute_override = 1; + // ❌ error + public override someAttributeOverride = 1; + public someMethod() { + return 42; + } + public override some_method_override() { + return 42; + } + // ❌ error + public override someMethodOverride() { + return 42; + } + 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); + } + abstract class foo2 extends bar { + public abstract someAttribute2: string; + public abstract override some_attribute_override2: string; + // ❌ error + public abstract override someAttributeOverride2: string; + public abstract someMethod2(): string; + public abstract override some_method_override2(): string; + // ❌ error + public abstract override someMethodOverride2(): string; + public abstract get someGetter2(): string; + public abstract override get some_getter_override2(): string; + // ❌ error + public abstract override get someGetterOverride2(): string; + public abstract set someSetter2(val: string); + public abstract override set some_setter_override2(val: string); + // ❌ error + public abstract override set someSetterOverride2(val: string); + } + `, + parserOptions, + options: [ + { + selector: 'memberLike', + format: ['camelCase'], + }, + { + selector: ['memberLike'], + modifiers: ['override'], + format: ['snake_case'], + }, + ], + errors: createExpectedTestCaseErrorsWithData([ + { + messageId: 'doesNotMatchFormat', + name: 'someAttributeOverride', + type: 'ClassProperty', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'someMethodOverride', + type: 'ClassMethod', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'someGetterOverride', + type: 'Accessor', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'someSetterOverride', + type: 'Accessor', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'someAttributeOverride2', + type: 'ClassProperty', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'someMethodOverride2', + type: 'ClassMethod', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'someGetterOverride2', + type: 'Accessor', + expectedFormats: ['snake_case'], + }, + { + messageId: 'doesNotMatchFormat', + name: 'someSetterOverride2', + type: 'Accessor', + expectedFormats: ['snake_case'], + }, + ]), + }, ], });