diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index f2bfa26bb1a8..6b7873f1303e 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -15,6 +15,7 @@ import type { MakeRequired } from '../util'; import { collectVariables, createRule, + getFixOrSuggest, getNameLocationInGlobalDirectiveComment, isDefinitionFile, isFunction, @@ -23,7 +24,11 @@ import { } from '../util'; import { referenceContainsTypeQuery } from '../util/referenceContainsTypeQuery'; -export type MessageIds = 'unusedVar' | 'usedIgnoredVar' | 'usedOnlyAsType'; +export type MessageIds = + | 'unusedVar' + | 'unusedVarSuggestion' + | 'usedIgnoredVar' + | 'usedOnlyAsType'; export type Options = [ | 'all' | 'local' @@ -38,6 +43,9 @@ export type Options = [ reportUsedIgnorePattern?: boolean; vars?: 'all' | 'local'; varsIgnorePattern?: string; + enableAutofixRemoval?: { + imports: boolean; + }; }, ]; @@ -52,6 +60,9 @@ interface TranslatedOptions { reportUsedIgnorePattern: boolean; vars: 'all' | 'local'; varsIgnorePattern?: RegExp; + enableAutofixRemoval?: { + imports: boolean; + }; } type VariableType = @@ -74,8 +85,13 @@ export default createRule({ extendsBaseRule: true, recommended: 'recommended', }, + fixable: 'code', + // If generate suggest dynamically, disable the eslint rule. + // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions + hasSuggestions: true, messages: { unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.", + unusedVarSuggestion: 'Remove unused variable.', usedIgnoredVar: "'{{varName}}' is marked as ignored but is used{{additional}}.", usedOnlyAsType: @@ -117,6 +133,16 @@ export default createRule({ description: 'Regular expressions of destructured array variable names to not check for usage.', }, + enableAutofixRemoval: { + type: 'object', + properties: { + imports: { + type: 'boolean', + description: + 'Whether to enable autofix for removing unused imports.', + }, + }, + }, ignoreClassWithStaticInitBlock: { type: 'boolean', description: @@ -208,6 +234,10 @@ export default createRule({ 'u', ); } + + if (firstOption.enableAutofixRemoval) { + options.enableAutofixRemoval = firstOption.enableAutofixRemoval; + } } return options; @@ -681,12 +711,83 @@ export default createRule({ }, }; + const fixer: TSESLint.ReportFixFunction = fixer => { + // Find the import statement + const def = unusedVar.defs.find( + d => d.type === DefinitionType.ImportBinding, + ); + if (!def) { + return null; + } + + const source = context.sourceCode; + const node = def.node; + const decl = node.parent; + if (decl.type !== AST_NODE_TYPES.ImportDeclaration) { + // decl.type is Program, import foo = require('bar'); + return fixer.remove(node); + } + + const afterNodeToken = source.getTokenAfter(node); + const beforeNodeToken = source.getTokenBefore(node); + const prevBeforeNodeToken = beforeNodeToken + ? source.getTokenBefore(beforeNodeToken) + : null; + + // Remove import declaration if no used specifiers are left + if (decl.specifiers.length === 1) { + return fixer.removeRange(decl.range); + } + + // case: remove braces if no used named specifiers are left + const restNamed = decl.specifiers.filter( + s => s.type === AST_NODE_TYPES.ImportSpecifier, + ); + if ( + restNamed.length === 1 && + afterNodeToken?.value === '}' && + prevBeforeNodeToken?.value === ',' + ) { + return fixer.removeRange([ + prevBeforeNodeToken.range[0], + afterNodeToken.range[1], + ]); + } + + // case: Remove comma after node, import { unused, used } from 'a'; + if (afterNodeToken?.value === ',') { + return fixer.removeRange([ + node.range[0], + afterNodeToken.range[1], + ]); + } + + // case: Remove comma before node, import { used, unused } from 'a'; + if (beforeNodeToken?.value === ',') { + return fixer.removeRange([ + beforeNodeToken.range[0], + node.range[1], + ]); + } + + return null; + }; + context.report({ loc, messageId, data: unusedVar.references.some(ref => ref.isWrite()) ? getAssignedMessageData(unusedVar) : getDefinedMessageData(unusedVar), + ...getFixOrSuggest({ + fixOrSuggest: options.enableAutofixRemoval?.imports + ? 'fix' + : 'suggest', + suggestion: { + messageId: 'unusedVarSuggestion', + fix: fixer, + }, + }), }); // If there are no regular declaration, report the first `/*globals*/` comment directive. diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts index c9048412d8c0..a68c0f7abee4 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts @@ -385,7 +385,17 @@ function f() { }, { code: "import x from 'y';", - errors: [definedError('x')], + errors: [ + { + ...definedError('x'), + suggestions: [ + { + messageId: 'unusedVarSuggestion', + output: '', + }, + ], + }, + ], languageOptions: { parserOptions: { ecmaVersion: 6, sourceType: 'module' }, }, diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts index 5f40bbcdc3cc..eb0f2d712f2b 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -53,6 +53,16 @@ export class Foo {} endLine: 2, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'ClassDecoratorFactory' }, + messageId: 'unusedVarSuggestion', + output: ` + +export class Foo {} + `, + }, + ], }, ], }, @@ -72,6 +82,17 @@ baz(); }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Foo' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Bar } from 'foo'; +function baz(): Foo {} +baz(); + `, + }, + ], }, ], }, @@ -91,6 +112,17 @@ console.log(a); }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Nullable' }, + messageId: 'unusedVarSuggestion', + output: ` + +const a: string = 'hello'; +console.log(a); + `, + }, + ], }, ], }, @@ -111,6 +143,18 @@ console.log(a); }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'SomeOther' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Nullable } from 'nullable'; + +const a: Nullable = 'hello'; +console.log(a); + `, + }, + ], }, ], }, @@ -136,6 +180,22 @@ new A(); }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Another' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Nullable } from 'nullable'; + +class A { + do = (a: Nullable) => { + console.log(a); + }; +} +new A(); + `, + }, + ], }, ], }, @@ -160,6 +220,22 @@ new A(); }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Another' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Nullable } from 'nullable'; + +class A { + do(a: Nullable) { + console.log(a); + } +} +new A(); + `, + }, + ], }, ], }, @@ -184,6 +260,22 @@ new A(); }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Another' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Nullable } from 'nullable'; + +class A { + do(): Nullable { + return null; + } +} +new A(); + `, + }, + ], }, ], }, @@ -205,6 +297,19 @@ export interface A { }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Another' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Nullable } from 'nullable'; + +export interface A { + do(a: Nullable); +} + `, + }, + ], }, ], }, @@ -226,6 +331,19 @@ export interface A { }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Nullable' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Nullable } from 'nullable'; + +export interface A { + other: Nullable; +} + `, + }, + ], }, ], }, @@ -247,6 +365,19 @@ foo(); }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Nullable' }, + messageId: 'unusedVarSuggestion', + output: ` + +function foo(a: string) { + console.log(a); +} +foo(); + `, + }, + ], }, ], }, @@ -268,6 +399,19 @@ foo(); }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Nullable' }, + messageId: 'unusedVarSuggestion', + output: ` + +function foo(): string | null { + return null; +} +foo(); + `, + }, + ], }, ], }, @@ -291,6 +435,21 @@ new A(); }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'SomeOther' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Nullable } from 'nullable'; + +import { Another } from 'some'; +class A extends Nullable { + other: Nullable; +} +new A(); + `, + }, + ], }, ], }, @@ -314,6 +473,21 @@ new A(); }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'SomeOther' }, + messageId: 'unusedVarSuggestion', + output: ` +import { Nullable } from 'nullable'; + +import { Another } from 'some'; +abstract class A extends Nullable { + other: Nullable; +} +new A(); + `, + }, + ], }, ], }, @@ -353,6 +527,17 @@ export interface Bar extends baz.test {} }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'test' }, + messageId: 'unusedVarSuggestion', + output: ` + +import baz from 'baz'; +export interface Bar extends baz.test {} + `, + }, + ], }, ], }, @@ -372,6 +557,17 @@ export class Bar implements baz.test {} }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'test' }, + messageId: 'unusedVarSuggestion', + output: ` + +import baz from 'baz'; +export class Bar implements baz.test {} + `, + }, + ], }, ], }, @@ -391,6 +587,17 @@ export class Bar implements baz().test {} }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'test' }, + messageId: 'unusedVarSuggestion', + output: ` + +import baz from 'baz'; +export class Bar implements baz().test {} + `, + }, + ], }, ], }, @@ -560,6 +767,20 @@ export const ComponentFoo = () => { }, line: 3, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'Fragment' }, + messageId: 'unusedVarSuggestion', + output: ` +import React from 'react'; + + +export const ComponentFoo = () => { + return
Foo Foo
; +}; + `, + }, + ], }, ], languageOptions: { @@ -589,6 +810,20 @@ export const ComponentFoo = () => { }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'React' }, + messageId: 'unusedVarSuggestion', + output: ` + +import { h } from 'some-other-jsx-lib'; + +export const ComponentFoo = () => { + return
Foo Foo
; +}; + `, + }, + ], }, ], languageOptions: { @@ -619,6 +854,19 @@ export const ComponentFoo = () => { }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'React' }, + messageId: 'unusedVarSuggestion', + output: ` + + +export const ComponentFoo = () => { + return
Foo Foo
; +}; + `, + }, + ], }, ], languageOptions: { @@ -832,17 +1080,17 @@ export = Foo; ], }, { - code: ` + code: noFormat` namespace Foo { export const foo = 1; } export namespace Bar { - import TheFoo = Foo; +import TheFoo = Foo; } `, errors: [ { - column: 10, + column: 8, data: { action: 'defined', additional: '', @@ -850,6 +1098,20 @@ export namespace Bar { }, line: 6, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'TheFoo' }, + messageId: 'unusedVarSuggestion', + output: ` +namespace Foo { + export const foo = 1; +} +export namespace Bar { + +} + `, + }, + ], }, ], }, @@ -1715,6 +1977,206 @@ export {}; ], filename: 'foo.d.ts', }, + { + code: ` +import Unused from 'foo'; + `, + errors: [ + { + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, + { + code: ` +import { Unused } from 'foo'; + `, + errors: [ + { + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, + { + code: ` +import * as Unused from 'foo'; + `, + errors: [ + { + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, + { + code: ` +import { Unused as u1 } from 'foo'; + `, + errors: [ + { + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, + { + code: ` +import type { UnusedType } from 'foo'; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, + { + code: ` +import { type UnusedType } from 'foo'; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, + { + code: ` +import type * as UnusedType from 'foo'; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, + { + code: noFormat` +import Unused,{ Used } from 'bar'; +export { Used }; + `, + errors: [ + { + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used } from 'bar'; +export { Used }; + `, + }, + { + code: noFormat` +import Used,{ Unused } from 'bar'; +export { Used }; + `, + errors: [ + { + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import Used from 'bar'; +export { Used }; + `, + }, + { + code: ` +import { /* cmt */ Unused, Used } from 'foo'; +export { Used }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { /* cmt */ Used } from 'foo'; +export { Used }; + `, + }, + { + code: ` +import { Used, Unused /* cmt */ } from 'foo'; +export { Used }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used /* cmt */ } from 'foo'; +export { Used }; + `, + }, + { + code: ` +import { Used1 /* cmt1 */, Unused, /* cmt2 */ Used2 } from 'foo'; +export { Used1, Used2 }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used1 /* cmt1 */, /* cmt2 */ Used2 } from 'foo'; +export { Used1, Used2 }; + `, + }, + { + code: ` +import type { UnusedType, UsedType } from 'foo'; +export { UsedType }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import type { UsedType } from 'foo'; +export { UsedType }; + `, + }, + { + code: ` +import { type UnusedType, type UsedType } from 'foo'; +export { UsedType }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { type UsedType } from 'foo'; +export { UsedType }; + `, + }, + { + code: ` +import { Unused as u1, Used as u2 } from 'foo'; +export { u2 }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used as u2 } from 'foo'; +export { u2 }; + `, + }, + { + code: ` +import unused = require('foo'); + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, ], valid: [ diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unused-vars.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unused-vars.shot index 0d9872aa6a11..afe9fbe0945b 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-unused-vars.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unused-vars.shot @@ -33,6 +33,15 @@ "description": "Regular expressions of destructured array variable names to not check for usage.", "type": "string" }, + "enableAutofixRemoval": { + "properties": { + "imports": { + "description": "Whether to enable autofix for removing unused imports.", + "type": "boolean" + } + }, + "type": "object" + }, "ignoreClassWithStaticInitBlock": { "description": "Whether to ignore classes with at least one static initialization block.", "type": "boolean" @@ -85,6 +94,11 @@ type Options = [ caughtErrorsIgnorePattern?: string; /** Regular expressions of destructured array variable names to not check for usage. */ destructuredArrayIgnorePattern?: string; + enableAutofixRemoval?: { + /** Whether to enable autofix for removing unused imports. */ + imports?: boolean; + [k: string]: unknown; + }; /** Whether to ignore classes with at least one static initialization block. */ ignoreClassWithStaticInitBlock?: boolean; /** Whether to ignore sibling properties in `...` destructurings. */ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy