diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index d57bc1c69daf..06808dfff3f1 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -154,21 +154,30 @@ 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`). + - `destructured` - matches a variable declared via an object destructuring pattern (`const {x, ignored: y, z = 2}`). + - `global` - matches a variable/function declared in the top-level scope. + - `exported` - matches anything that is exported from the module. + - `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. @@ -194,21 +203,33 @@ 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`. + - Allowed `modifiers`: `const`, `destructured`, `global`, `exported`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `function` - matches any named function declaration or named function expression. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `global`, `exported`. - Allowed `types`: none. - `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. @@ -218,16 +239,16 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - Allowed `modifiers`: none. - Allowed `types`: none. - `class` - matches any class declaration. - - Allowed `modifiers`: `abstract`. + - Allowed `modifiers`: `abstract`, `exported`. - Allowed `types`: none. - `interface` - matches any interface declaration. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `exported`. - Allowed `types`: none. - `typeAlias` - matches any type alias declaration. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `exported`. - Allowed `types`: none. - `enum` - matches any enum declaration. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `exported`. - Allowed `types`: none. - `typeParameter` - matches any generic type parameter declaration. - Allowed `modifiers`: none. @@ -243,11 +264,17 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - `variableLike` - matches the same as `variable`, `function` and `parameter`. - Allowed `modifiers`: none. - Allowed `types`: none. -- `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`. +- `memberLike` - matches the same as `classProperty`, `objectLiteralProperty`, `typeProperty`, `parameterProperty`, `classMethod`, `objectLiteralMethod`, `typeMethod`, `accessor`, `enumMember`. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. - Allowed `types`: none. - `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. - - Allowed `modifiers`: `abstract`. + - Allowed `modifiers`: `abstract`, `exported`. + - 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 @@ -423,6 +450,25 @@ You can use the `filter` option to ignore names that require quoting: } ``` +### Ignore destructured names + +Sometimes you might want to allow destructured properties to retain their original name, even if it breaks your naming convention. + +You can use the `destructured` modifier to match these names, and explicitly set `format: null` to apply no formatting: + +```jsonc +{ + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "variable", + "modifiers": ["destructured"], + "format": null + } + ] +} +``` + ### Enforce the codebase follows ESLint's `camelcase` conventions ```json diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 295e8209e89b..229463ec6d3e 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -4,6 +4,7 @@ import { TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; +import { PatternVisitor } from '@typescript-eslint/scope-manager'; import * as ts from 'typescript'; import * as util from '../util'; @@ -41,21 +42,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 +68,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,18 +83,36 @@ 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; enum Modifiers { + // const variable const = 1 << 0, + // readonly members readonly = 1 << 1, + // static members static = 1 << 2, + // member accessibility public = 1 << 3, protected = 1 << 4, private = 1 << 5, abstract = 1 << 6, + // destructured variable + destructured = 1 << 7, + // variables declared in the top-level scope + global = 1 << 8, + // things that are exported + exported = 1 << 9, } type ModifiersString = keyof typeof Modifiers; @@ -309,8 +335,13 @@ const SCHEMA: JSONSchema.JSONSchema4 = { ...selectorSchema('default', false, util.getEnumNames(Modifiers)), ...selectorSchema('variableLike', false), - ...selectorSchema('variable', true, ['const']), - ...selectorSchema('function', false), + ...selectorSchema('variable', true, [ + 'const', + 'destructured', + 'global', + 'exported', + ]), + ...selectorSchema('function', false, ['global', 'exported']), ...selectorSchema('parameter', true), ...selectorSchema('memberLike', false, [ @@ -321,7 +352,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 +382,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', @@ -342,6 +419,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'abstract', ]), + ...selectorSchema('accessor', true, [ 'private', 'protected', @@ -351,11 +429,11 @@ const SCHEMA: JSONSchema.JSONSchema4 = { ]), ...selectorSchema('enumMember', false), - ...selectorSchema('typeLike', false, ['abstract']), - ...selectorSchema('class', false, ['abstract']), - ...selectorSchema('interface', false), - ...selectorSchema('typeAlias', false), - ...selectorSchema('enum', false), + ...selectorSchema('typeLike', false, ['abstract', 'exported']), + ...selectorSchema('class', false, ['abstract', 'exported']), + ...selectorSchema('interface', false, ['exported']), + ...selectorSchema('typeAlias', false, ['exported']), + ...selectorSchema('enum', false, ['exported']), ...selectorSchema('typeParameter', false), ], }, @@ -490,21 +568,40 @@ export default util.createRule({ return; } - const identifiers: TSESTree.Identifier[] = []; - getIdentifiersFromPattern(node.id, identifiers); + const identifiers = getIdentifiersFromPattern(node.id); - const modifiers = new Set(); + const baseModifiers = new Set(); const parent = node.parent; - if ( - parent && - parent.type === AST_NODE_TYPES.VariableDeclaration && - parent.kind === 'const' - ) { - modifiers.add(Modifiers.const); + if (parent?.type === AST_NODE_TYPES.VariableDeclaration) { + if (parent.kind === 'const') { + baseModifiers.add(Modifiers.const); + } + if (isGlobal(context.getScope())) { + baseModifiers.add(Modifiers.global); + } } - identifiers.forEach(i => { - validator(i, modifiers); + identifiers.forEach(id => { + const modifiers = new Set(baseModifiers); + if ( + // `const { x }` + // does not match `const { x: y }` + (id.parent?.type === AST_NODE_TYPES.Property && + id.parent.shorthand) || + // `const { x = 2 }` + // does not match const `{ x: y = 2 }` + (id.parent?.type === AST_NODE_TYPES.AssignmentPattern && + id.parent.parent?.type === AST_NODE_TYPES.Property && + id.parent.parent.shorthand) + ) { + modifiers.add(Modifiers.destructured); + } + + if (isExported(parent, id.name, context.getScope())) { + modifiers.add(Modifiers.exported); + } + + validator(id, modifiers); }); }, @@ -523,7 +620,17 @@ export default util.createRule({ return; } - validator(node.id); + const modifiers = new Set(); + const scope = context.getScope().upper; + // functions will + if (isGlobal(scope)) { + modifiers.add(Modifiers.global); + } + if (isExported(node, node.id.name, scope)) { + modifiers.add(Modifiers.exported); + } + + validator(node.id, modifiers); }, // #endregion function @@ -547,8 +654,7 @@ export default util.createRule({ return; } - const identifiers: TSESTree.Identifier[] = []; - getIdentifiersFromPattern(param, identifiers); + const identifiers = getIdentifiersFromPattern(param); identifiers.forEach(i => { validator(i); @@ -568,8 +674,7 @@ export default util.createRule({ const modifiers = getMemberModifiers(node); - const identifiers: TSESTree.Identifier[] = []; - getIdentifiersFromPattern(node.parameter, identifiers); + const identifiers = getIdentifiersFromPattern(node.parameter); identifiers.forEach(i => { validator(i, modifiers); @@ -584,7 +689,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 +698,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 +709,7 @@ export default util.createRule({ modifiers.add(Modifiers.readonly); } - handleMember(validators.property, node, modifiers); + handleMember(validators.typeProperty, node, modifiers); }, // #endregion property @@ -615,14 +720,20 @@ 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); + }, + + 'TSMethodSignature[computed = false]'( + node: TSESTree.TSMethodSignatureNonComputedName, + ): void { + const modifiers = new Set([Modifiers.public]); + handleMember(validators.typeMethod, node, modifiers); }, [[ @@ -638,7 +749,7 @@ export default util.createRule({ | TSESTree.TSAbstractMethodDefinitionNonComputedName, ): void { const modifiers = getMemberModifiers(node); - handleMember(validators.method, node, modifiers); + handleMember(validators.classMethod, node, modifiers); }, // #endregion method @@ -698,6 +809,10 @@ export default util.createRule({ modifiers.add(Modifiers.abstract); } + if (isExported(node, id.name, context.getScope().upper)) { + modifiers.add(Modifiers.exported); + } + validator(id, modifiers); }, @@ -711,7 +826,12 @@ export default util.createRule({ return; } - validator(node.id); + const modifiers = new Set(); + if (isExported(node, node.id.name, context.getScope())) { + modifiers.add(Modifiers.exported); + } + + validator(node.id, modifiers); }, // #endregion interface @@ -724,7 +844,12 @@ export default util.createRule({ return; } - validator(node.id); + const modifiers = new Set(); + if (isExported(node, node.id.name, context.getScope())) { + modifiers.add(Modifiers.exported); + } + + validator(node.id, modifiers); }, // #endregion typeAlias @@ -737,7 +862,12 @@ export default util.createRule({ return; } - validator(node.id); + const modifiers = new Set(); + if (isExported(node, node.id.name, context.getScope().upper)) { + modifiers.add(Modifiers.exported); + } + + validator(node.id, modifiers); }, // #endregion enum @@ -762,55 +892,54 @@ export default util.createRule({ function getIdentifiersFromPattern( pattern: TSESTree.DestructuringPattern, - identifiers: TSESTree.Identifier[], -): void { - switch (pattern.type) { - case AST_NODE_TYPES.Identifier: - identifiers.push(pattern); - break; - - case AST_NODE_TYPES.ArrayPattern: - pattern.elements.forEach(element => { - if (element !== null) { - getIdentifiersFromPattern(element, identifiers); - } - }); - break; - - case AST_NODE_TYPES.ObjectPattern: - pattern.properties.forEach(property => { - if (property.type === AST_NODE_TYPES.RestElement) { - getIdentifiersFromPattern(property, identifiers); - } else { - // this is a bit weird, but it's because ESTree doesn't have a new node type - // for object destructuring properties - it just reuses Property... - // https://github.com/estree/estree/blob/9ae284b71130d53226e7153b42f01bf819e6e657/es2015.md#L206-L211 - // However, the parser guarantees this is safe (and there is error handling) - getIdentifiersFromPattern( - property.value as TSESTree.DestructuringPattern, - identifiers, - ); - } - }); - break; +): TSESTree.Identifier[] { + const identifiers: TSESTree.Identifier[] = []; + const visitor = new PatternVisitor({}, pattern, id => identifiers.push(id)); + visitor.visit(pattern); + return identifiers; +} - case AST_NODE_TYPES.RestElement: - getIdentifiersFromPattern(pattern.argument, identifiers); - break; +function isExported( + node: TSESTree.Node | undefined, + name: string, + scope: TSESLint.Scope.Scope | null, +): boolean { + if ( + node?.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration || + node?.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration + ) { + return true; + } - case AST_NODE_TYPES.AssignmentPattern: - getIdentifiersFromPattern(pattern.left, identifiers); - break; + if (scope == null) { + return false; + } - case AST_NODE_TYPES.MemberExpression: - // ignore member expressions, as the everything must already be defined - break; + const variable = scope.set.get(name); + if (variable) { + for (const ref of variable.references) { + const refParent = ref.identifier.parent; + if ( + refParent?.type === AST_NODE_TYPES.ExportDefaultDeclaration || + refParent?.type === AST_NODE_TYPES.ExportSpecifier + ) { + return true; + } + } + } + + return false; +} - default: - // https://github.com/typescript-eslint/typescript-eslint/issues/1282 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - throw new Error(`Unexpected pattern type ${pattern!.type}`); +function isGlobal(scope: TSESLint.Scope.Scope | null): boolean { + if (scope == null) { + return false; } + + return ( + scope.type === TSESLint.Scope.ScopeType.global || + scope.type === TSESLint.Scope.ScopeType.module + ); } type ValidatorFunction = ( @@ -851,21 +980,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 +1442,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 +1457,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 c57dfc984d47..e462ea238e43 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 @@ -484,25 +488,11 @@ const cases: Cases = [ selector: 'parameterProperty', }, }, - { - code: ['class Ignored { constructor(private readonly %) {} }'], - options: { - selector: 'parameterProperty', - modifiers: ['readonly'], - }, - }, // #endregion parameterProperty // #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 +503,28 @@ const cases: Cases = [ 'class Ignored { declare %() }', ], options: { - selector: 'method', + selector: 'classMethod', }, }, { code: [ - 'class Ignored { abstract private static %() {}; ignoredDueToModifiers() {}; }', + 'const ignored = { %() {} };', + 'const ignored = { "%"() {} };', + 'const ignored = { %: () => {} };', ], options: { - selector: 'method', - modifiers: ['abstract', 'static'], + selector: 'objectLiteralMethod', + }, + }, + { + code: [ + 'interface Ignored { %(): string }', + 'interface Ignored { "%"(): string }', + 'type Ignored = { %(): string }', + 'type Ignored = { "%"(): string }', + ], + options: { + selector: 'typeMethod', }, }, // #endregion method @@ -540,15 +542,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 +560,6 @@ const cases: Cases = [ selector: 'class', }, }, - { - code: ['abstract class % {}; class ignoredDueToModifier {}'], - options: { - selector: 'class', - modifiers: ['abstract'], - }, - }, // #endregion class // #region interface @@ -620,6 +606,29 @@ const cases: Cases = [ ruleTester.run('naming-convention', rule, { valid: [ + { + code: ` + const {ignore: IgnoredDueToModifiers1} = {}; + + + const {some_name1} = {}; + const {ignore: IgnoredDueToModifiers1} = {}; + const {some_name2 = 2} = {}; + + const IgnoredDueToModifiers2 = 1; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'variable', + format: ['snake_case'], + modifiers: ['destructured'], + }, + ], + }, ...createValidTestCases(cases), { code: ` @@ -914,12 +923,280 @@ ruleTester.run('naming-convention', rule, { export const { OtherConstant: otherConstant } = SomeClass; `, - parserOptions, options: [ { selector: 'property', format: ['PascalCase'] }, { selector: 'variable', format: ['camelCase'] }, ], }, + { + code: ` + const camelCaseVar = 1; + enum camelCaseEnum {} + class camelCaseClass {} + function camelCaseFunction() {} + interface camelCaseInterface {} + type camelCaseType = {}; + + export const PascalCaseVar = 1; + export enum PascalCaseEnum {} + export class PascalCaseClass {} + export function PascalCaseFunction() {} + export interface PascalCaseInterface {} + export type PascalCaseType = {}; + `, + options: [ + { selector: 'default', format: ['camelCase'] }, + { + selector: 'variable', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'function', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'class', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'interface', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'typeAlias', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'enum', + format: ['PascalCase'], + modifiers: ['exported'], + }, + ], + }, + { + code: ` + const camelCaseVar = 1; + enum camelCaseEnum {} + class camelCaseClass {} + function camelCaseFunction() {} + interface camelCaseInterface {} + type camelCaseType = {}; + + const PascalCaseVar = 1; + enum PascalCaseEnum {} + class PascalCaseClass {} + function PascalCaseFunction() {} + interface PascalCaseInterface {} + type PascalCaseType = {}; + + export { + PascalCaseVar, + PascalCaseEnum, + PascalCaseClass, + PascalCaseFunction, + PascalCaseInterface, + PascalCaseType, + }; + `, + options: [ + { selector: 'default', format: ['camelCase'] }, + { + selector: 'variable', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'function', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'class', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'interface', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'typeAlias', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'enum', + format: ['PascalCase'], + modifiers: ['exported'], + }, + ], + }, + { + code: ` + { + const camelCaseVar = 1; + function camelCaseFunction() {} + declare function camelCaseDeclaredFunction() { + }; + } + + const PascalCaseVar = 1; + function PascalCaseFunction() {} + declare function PascalCaseDeclaredFunction() { + }; + `, + options: [ + { selector: 'default', format: ['camelCase'] }, + { + selector: 'variable', + format: ['PascalCase'], + modifiers: ['global'], + }, + { + selector: 'function', + format: ['PascalCase'], + modifiers: ['global'], + }, + ], + }, + { + code: ` + const {some_name1} = {}; + const {ignore: IgnoredDueToModifiers1} = {}; + const {some_name2 = 2} = {}; + + const IgnoredDueToModifiers2 = 1; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'variable', + format: ['snake_case'], + modifiers: ['destructured'], + }, + ], + }, + { + code: ` + class Ignored { + abstract private static 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 { + abstract private static 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'], + }, + ], + }, + { + code: ` + const {some_name1} = {}; + const {ignore: IgnoredDueToModifiers1} = {}; + const {some_name2 = 2} = {}; + + const IgnoredDueToModifiers2 = 1; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'variable', + format: null, + modifiers: ['destructured'], + }, + ], + }, ], invalid: [ { @@ -1239,8 +1516,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 +1607,245 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + export const PascalCaseVar = 1; + export enum PascalCaseEnum {} + export class PascalCaseClass {} + export function PascalCaseFunction() {} + export interface PascalCaseInterface {} + export type PascalCaseType = {}; + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: 'variable', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'function', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'class', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'interface', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'typeAlias', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'enum', + format: ['camelCase'], + modifiers: ['exported'], + }, + ], + errors: Array(6).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + const PascalCaseVar = 1; + enum PascalCaseEnum {} + class PascalCaseClass {} + function PascalCaseFunction() {} + interface PascalCaseInterface {} + type PascalCaseType = {}; + + export { + PascalCaseVar, + PascalCaseEnum, + PascalCaseClass, + PascalCaseFunction, + PascalCaseInterface, + PascalCaseType, + }; + `, + options: [ + { selector: 'default', format: ['snake_case'] }, + { + selector: 'variable', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'function', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'class', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'interface', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'typeAlias', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'enum', + format: ['camelCase'], + modifiers: ['exported'], + }, + ], + errors: Array(6).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + const PascalCaseVar = 1; + function PascalCaseFunction() {} + declare function PascalCaseDeclaredFunction() { + }; + `, + options: [ + { selector: 'default', format: ['snake_case'] }, + { + selector: 'variable', + format: ['camelCase'], + modifiers: ['global'], + }, + { + selector: 'function', + format: ['camelCase'], + modifiers: ['global'], + }, + ], + errors: Array(3).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + const {some_name1} = {}; + const {ignore: IgnoredDueToModifiers1} = {}; + const {some_name2 = 2} = {}; + + const IgnoredDueToModifiers2 = 1; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'variable', + format: ['UPPER_CASE'], + modifiers: ['destructured'], + }, + ], + errors: Array(2).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + class Ignored { + abstract private static 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 { + abstract private static 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' }], + }, ], }); diff --git a/packages/scope-manager/src/referencer/PatternVisitor.ts b/packages/scope-manager/src/referencer/PatternVisitor.ts index eaca842cbd59..a969851ea87d 100644 --- a/packages/scope-manager/src/referencer/PatternVisitor.ts +++ b/packages/scope-manager/src/referencer/PatternVisitor.ts @@ -52,6 +52,15 @@ class PatternVisitor extends VisitorBase { this.#callback = callback; } + private visitAssignment( + node: TSESTree.AssignmentExpression | TSESTree.AssignmentPattern, + ): void { + this.#assignments.push(node); + this.visit(node.left); + this.rightHandNodes.push(node.right); + this.#assignments.pop(); + } + protected ArrayExpression(node: TSESTree.ArrayExpression): void { node.elements.forEach(this.visit, this); } @@ -62,19 +71,9 @@ class PatternVisitor extends VisitorBase { } } - protected AssignmentExpression(node: TSESTree.AssignmentExpression): void { - this.#assignments.push(node); - this.visit(node.left); - this.rightHandNodes.push(node.right); - this.#assignments.pop(); - } + protected AssignmentExpression = this.visitAssignment; - protected AssignmentPattern(pattern: TSESTree.AssignmentPattern): void { - this.#assignments.push(pattern); - this.visit(pattern.left); - this.rightHandNodes.push(pattern.right); - this.#assignments.pop(); - } + protected AssignmentPattern = this.visitAssignment; protected CallExpression(node: TSESTree.CallExpression): void { // arguments are right hand nodes.