From 6a06944e60677a402e7ab432e6ac1209737a7027 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 13:27:08 -0800 Subject: [PATCH] feat(eslint-plugin): [naming-convention] add modifier `unused` (#2810) --- .../docs/rules/naming-convention.md | 21 ++-- .../src/rules/naming-convention.ts | 105 +++++++++++++++--- .../tests/rules/naming-convention.test.ts | 67 +++++++++++ 3 files changed, 167 insertions(+), 26 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 089bf6d3f81..70722707b87 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -167,6 +167,7 @@ If these are provided, the identifier must start with one of the provided values - Note that this does not match renamed destructured properties (`const {x: y, a: b = 2}`). - `global` - matches a variable/function declared in the top-level scope. - `exported` - matches anything that is exported from the module. + - `unused` - matches anything that is not used. - `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`). @@ -204,13 +205,13 @@ 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`. + - Allowed `modifiers`: `const`, `destructured`, `global`, `exported`, `unused`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `function` - matches any named function declaration or named function expression. - - Allowed `modifiers`: `global`, `exported`. + - Allowed `modifiers`: `global`, `exported`, `unused`. - Allowed `types`: none. - `parameter` - matches any function parameter. Does not match parameter properties. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `unused`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `classProperty` - matches any class property. Does not match properties that have direct function expression or arrow function expression values. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. @@ -240,19 +241,19 @@ 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`, `exported`. + - Allowed `modifiers`: `abstract`, `exported`, `unused`. - Allowed `types`: none. - `interface` - matches any interface declaration. - - Allowed `modifiers`: `exported`. + - Allowed `modifiers`: `exported`, `unused`. - Allowed `types`: none. - `typeAlias` - matches any type alias declaration. - - Allowed `modifiers`: `exported`. + - Allowed `modifiers`: `exported`, `unused`. - Allowed `types`: none. - `enum` - matches any enum declaration. - - Allowed `modifiers`: `exported`. + - Allowed `modifiers`: `exported`, `unused`. - Allowed `types`: none. - `typeParameter` - matches any generic type parameter declaration. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `unused`. - Allowed `types`: none. ##### Group Selectors @@ -263,13 +264,13 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. - Allowed `types`: none. - `variableLike` - matches the same as `variable`, `function` and `parameter`. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `unused`. - Allowed `types`: none. - `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. - Allowed `types`: none. - `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. - - Allowed `modifiers`: `abstract`. + - Allowed `modifiers`: `abstract`, `unused`. - Allowed `types`: none. - `property` - matches the same as `classProperty`, `objectLiteralProperty`, `typeProperty`. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 597a7492428..2b5dac564e1 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -113,6 +113,8 @@ enum Modifiers { global = 1 << 8, // things that are exported exported = 1 << 9, + // things that are unused + unused = 1 << 10, } type ModifiersString = keyof typeof Modifiers; @@ -334,15 +336,16 @@ const SCHEMA: JSONSchema.JSONSchema4 = { selectorsSchema(), ...selectorSchema('default', false, util.getEnumNames(Modifiers)), - ...selectorSchema('variableLike', false), + ...selectorSchema('variableLike', false, ['unused']), ...selectorSchema('variable', true, [ 'const', 'destructured', 'global', 'exported', + 'unused', ]), - ...selectorSchema('function', false, ['global', 'exported']), - ...selectorSchema('parameter', true), + ...selectorSchema('function', false, ['global', 'exported', 'unused']), + ...selectorSchema('parameter', true, ['unused']), ...selectorSchema('memberLike', false, [ 'private', @@ -428,12 +431,12 @@ const SCHEMA: JSONSchema.JSONSchema4 = { ]), ...selectorSchema('enumMember', 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), + ...selectorSchema('typeLike', false, ['abstract', 'exported', 'unused']), + ...selectorSchema('class', false, ['abstract', 'exported', 'unused']), + ...selectorSchema('interface', false, ['exported', 'unused']), + ...selectorSchema('typeAlias', false, ['exported', 'unused']), + ...selectorSchema('enum', false, ['exported', 'unused']), + ...selectorSchema('typeParameter', false, ['unused']), ], }, additionalItems: false, @@ -558,6 +561,27 @@ export default util.createRule({ return modifiers; } + const unusedVariables = util.collectUnusedVariables(context); + function isUnused( + name: string, + initialScope: TSESLint.Scope.Scope | null = context.getScope(), + ): boolean { + let variable: TSESLint.Scope.Variable | null = null; + let scope: TSESLint.Scope.Scope | null = initialScope; + while (scope) { + variable = scope.set.get(name) ?? null; + if (variable) { + break; + } + scope = scope.upper; + } + if (!variable) { + return false; + } + + return unusedVariables.has(variable); + } + return { // #region variable @@ -574,6 +598,7 @@ export default util.createRule({ if (parent.kind === 'const') { baseModifiers.add(Modifiers.const); } + if (isGlobal(context.getScope())) { baseModifiers.add(Modifiers.global); } @@ -581,6 +606,7 @@ export default util.createRule({ identifiers.forEach(id => { const modifiers = new Set(baseModifiers); + if ( // `const { x }` // does not match `const { x: y }` @@ -599,6 +625,10 @@ export default util.createRule({ modifiers.add(Modifiers.exported); } + if (isUnused(id.name)) { + modifiers.add(Modifiers.unused); + } + validator(id, modifiers); }); }, @@ -621,13 +651,19 @@ export default util.createRule({ const modifiers = new Set(); // functions create their own nested scope const scope = context.getScope().upper; + if (isGlobal(scope)) { modifiers.add(Modifiers.global); } + if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(node.id, modifiers); }, @@ -655,7 +691,13 @@ export default util.createRule({ const identifiers = getIdentifiersFromPattern(param); identifiers.forEach(i => { - validator(i); + const modifiers = new Set(); + + if (isUnused(i.name)) { + modifiers.add(Modifiers.unused); + } + + validator(i, modifiers); }); }); }, @@ -803,15 +845,21 @@ export default util.createRule({ } const modifiers = new Set(); + // classes create their own nested scope + const scope = context.getScope().upper; + if (node.abstract) { modifiers.add(Modifiers.abstract); } - // classes create their own nested scope - if (isExported(node, id.name, context.getScope().upper)) { + if (isExported(node, id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(id, modifiers); }, @@ -826,10 +874,16 @@ export default util.createRule({ } const modifiers = new Set(); - if (isExported(node, node.id.name, context.getScope())) { + const scope = context.getScope(); + + if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(node.id, modifiers); }, @@ -844,10 +898,16 @@ export default util.createRule({ } const modifiers = new Set(); - if (isExported(node, node.id.name, context.getScope())) { + const scope = context.getScope(); + + if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(node.id, modifiers); }, @@ -863,10 +923,16 @@ export default util.createRule({ const modifiers = new Set(); // enums create their own nested scope - if (isExported(node, node.id.name, context.getScope().upper)) { + const scope = context.getScope().upper; + + if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(node.id, modifiers); }, @@ -882,7 +948,14 @@ export default util.createRule({ return; } - validator(node.name); + const modifiers = new Set(); + const scope = context.getScope(); + + if (isUnused(node.name.name, scope)) { + modifiers.add(Modifiers.unused); + } + + validator(node.name, modifiers); }, // #endregion typeParameter diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index aed9bf9c47d..28fdc4c060b 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -1175,6 +1175,46 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + const UnusedVar = 1; + function UnusedFunc( + // this line is intentionally broken out + UnusedParam: string, + ) {} + class UnusedClass {} + interface UnusedInterface {} + type UnusedType< + // this line is intentionally broken out + UnusedTypeParam + > = {}; + + export const used_var = 1; + export function used_func( + // this line is intentionally broken out + used_param: string, + ) { + return used_param; + } + export class used_class {} + export interface used_interface {} + export type used_type< + // this line is intentionally broken out + used_typeparam + > = used_typeparam; + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: 'default', + modifiers: ['unused'], + format: ['PascalCase'], + }, + ], + }, ], invalid: [ { @@ -1823,5 +1863,32 @@ ruleTester.run('naming-convention', rule, { ], errors: [{ messageId: 'doesNotMatchFormat' }], }, + { + code: ` + const UnusedVar = 1; + function UnusedFunc( + // this line is intentionally broken out + UnusedParam: string, + ) {} + class UnusedClass {} + interface UnusedInterface {} + type UnusedType< + // this line is intentionally broken out + UnusedTypeParam + > = {}; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'default', + modifiers: ['unused'], + format: ['snake_case'], + }, + ], + errors: Array(7).fill({ messageId: 'doesNotMatchFormat' }), + }, ], });