From e5394b29b407fdc3039a5a271411d599924a607d Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 24 May 2025 00:47:33 +0900 Subject: [PATCH 01/22] test: add testcase --- .../no-unused-vars/no-unused-vars.test.ts | 149 ++++++++++++++---- 1 file changed, 116 insertions(+), 33 deletions(-) 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 5ebc8d9a406b..e9da670e5045 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 @@ -1734,6 +1734,122 @@ export {}; ], filename: 'foo.d.ts', }, + { + code: ` +import * as Unused from 'foo'; +import * as Used from 'bar'; +export { Used }; + `, + errors: [ + { + column: 13, + data: { + action: 'defined', + additional: '', + varName: 'Unused', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import * as Used from 'bar'; +export { Used }; + `, + }, + { + code: ` +import Unused1 from 'foo'; +import Unused2, { Used } from 'bar'; +export { Used }; + `, + errors: [ + { + column: 8, + data: { + action: 'defined', + additional: '', + varName: 'Unused1', + }, + line: 2, + messageId: 'unusedVar', + }, + { + column: 8, + data: { + action: 'defined', + additional: '', + varName: 'Unused2', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used } from 'bar'; +export { Used }; + `, + }, + { + code: ` +import { Unused1 } from 'foo'; +import Used1, { Unused2 } from 'bar'; +import { Used2, Unused3 } from 'baz'; +import Used3, { Unused4, Used4 } from 'foobar'; +export { Used1, Used2, Used3, Used4 }; + `, + errors: [ + { + column: 10, + data: { + action: 'defined', + additional: '', + varName: 'Unused1', + }, + line: 2, + messageId: 'unusedVar', + }, + { + column: 17, + data: { + action: 'defined', + additional: '', + varName: 'Unused2', + }, + line: 3, + messageId: 'unusedVar', + }, + { + column: 17, + data: { + action: 'defined', + additional: '', + varName: 'Unused3', + }, + line: 4, + messageId: 'unusedVar', + }, + { + column: 17, + data: { + action: 'defined', + additional: '', + varName: 'Unused4', + }, + line: 5, + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import Used1 from 'bar'; +import { Used2 } from 'baz'; +import Used3, { Used4 } from 'foobar'; +export { Used1, Used2, Used3, Used4 }; + `, + }, ], valid: [ @@ -2946,39 +3062,6 @@ declare module 'foo' { { code: ` export import Bar = Something.Bar; -const foo: 1234; - `, - filename: 'foo.d.ts', - }, - { - code: ` -declare module 'foo' { - export import Bar = Something.Bar; - const foo: 1234; - export const bar: string; - export namespace NS { - const baz: 1234; - } -} - `, - filename: 'foo.d.ts', - }, - { - code: ` -export namespace Foo { - export import Bar = Something.Bar; - const foo: 1234; - export const bar: string; - export namespace NS { - const baz: 1234; - } -} - `, - filename: 'foo.d.ts', - }, - { - code: ` -export import Bar = Something.Bar; const foo: 1234; export const bar: string; export namespace NS { From e3399731cb2f1be8d128d636886e9dfb5406a885 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 24 May 2025 00:47:52 +0900 Subject: [PATCH 02/22] feat: test successful logic --- .../eslint-plugin/src/rules/no-unused-vars.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index f2bfa26bb1a8..8d70db0926d7 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -38,6 +38,9 @@ export type Options = [ reportUsedIgnorePattern?: boolean; vars?: 'all' | 'local'; varsIgnorePattern?: string; + enableAutofixRemoval?: { + imports: boolean; + }; }, ]; @@ -52,6 +55,9 @@ interface TranslatedOptions { reportUsedIgnorePattern: boolean; vars: 'all' | 'local'; varsIgnorePattern?: RegExp; + enableAutofixRemoval?: { + imports: boolean; + }; } type VariableType = @@ -74,6 +80,7 @@ export default createRule({ extendsBaseRule: true, recommended: 'recommended', }, + fixable: 'code', messages: { unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.", usedIgnoredVar: @@ -117,6 +124,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 +225,10 @@ export default createRule({ 'u', ); } + + if (firstOption.enableAutofixRemoval) { + options.enableAutofixRemoval = firstOption.enableAutofixRemoval; + } } return options; @@ -687,6 +708,87 @@ export default createRule({ data: unusedVar.references.some(ref => ref.isWrite()) ? getAssignedMessageData(unusedVar) : getDefinedMessageData(unusedVar), + fix: + options.enableAutofixRemoval?.imports && + unusedVar.defs.some( + d => d.type === DefinitionType.ImportBinding, + ) + ? fixer => { + 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 as TSESTree.ImportDeclaration; + + // Remove import declaration line if no specifiers are left + if (decl.specifiers.length === 1) { + const next = source.getTokenAfter(decl) ?? { + range: [decl.range[1], decl.range[1]], + }; + return fixer.removeRange([ + decl.range[0], + next.range[0], + ]); + } + + // case: remove { unused } + const restNamed = decl.specifiers.filter( + s => + s === node && + s.type === AST_NODE_TYPES.ImportSpecifier, + ); + if (restNamed.length === 1) { + const nextBraceToken = source.getTokenAfter(node); + const prevBraceToken = source.getTokenBefore(node); + if ( + nextBraceToken?.value === '}' && + prevBraceToken?.value === '{' + ) { + // remove comma + const prevComma = + source.getTokenBefore(prevBraceToken); + + return fixer.removeRange([ + prevComma?.value === ',' + ? prevComma.range[0] + : prevBraceToken.range[0], + nextBraceToken.range[1], + ]); + } + } + + // case: Remove comma after node + const nextCommaToken = source.getTokenAfter(node); + if (nextCommaToken?.value === ',') { + const nextToken = source.getTokenAfter(nextCommaToken, { + includeComments: true, + }); + + return fixer.removeRange([ + node.range[0], + nextToken + ? nextToken.range[0] + : nextCommaToken.range[1], + ]); + } + + // case: Remove comma before node + const prevCommaToken = source.getTokenBefore(node); + if (prevCommaToken?.value === ',') { + return fixer.removeRange([ + prevCommaToken.range[0], + node.range[1], + ]); + } + // Remove the current specifier and all tokens until the next specifier + return fixer.remove(node); + } + : undefined, }); // If there are no regular declaration, report the first `/*globals*/` comment directive. From 7fc5d69e837f30b98ddc26746150a12d821a5821 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 24 May 2025 22:00:01 +0900 Subject: [PATCH 03/22] chore: generate schema --- .../tests/schema-snapshots/no-unused-vars.shot | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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. */ From 7b06e7cc51c98fa691226a4e6345412fd347a37b Mon Sep 17 00:00:00 2001 From: nayounsang Date: Tue, 3 Jun 2025 17:42:37 +0900 Subject: [PATCH 04/22] chore: Revert "test: add testcase" This reverts commit e5394b29b407fdc3039a5a271411d599924a607d. --- .../no-unused-vars/no-unused-vars.test.ts | 149 ++++-------------- 1 file changed, 33 insertions(+), 116 deletions(-) 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 e9da670e5045..5ebc8d9a406b 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 @@ -1734,122 +1734,6 @@ export {}; ], filename: 'foo.d.ts', }, - { - code: ` -import * as Unused from 'foo'; -import * as Used from 'bar'; -export { Used }; - `, - errors: [ - { - column: 13, - data: { - action: 'defined', - additional: '', - varName: 'Unused', - }, - line: 2, - messageId: 'unusedVar', - }, - ], - options: [{ enableAutofixRemoval: { imports: true } }], - output: ` -import * as Used from 'bar'; -export { Used }; - `, - }, - { - code: ` -import Unused1 from 'foo'; -import Unused2, { Used } from 'bar'; -export { Used }; - `, - errors: [ - { - column: 8, - data: { - action: 'defined', - additional: '', - varName: 'Unused1', - }, - line: 2, - messageId: 'unusedVar', - }, - { - column: 8, - data: { - action: 'defined', - additional: '', - varName: 'Unused2', - }, - line: 3, - messageId: 'unusedVar', - }, - ], - options: [{ enableAutofixRemoval: { imports: true } }], - output: ` -import { Used } from 'bar'; -export { Used }; - `, - }, - { - code: ` -import { Unused1 } from 'foo'; -import Used1, { Unused2 } from 'bar'; -import { Used2, Unused3 } from 'baz'; -import Used3, { Unused4, Used4 } from 'foobar'; -export { Used1, Used2, Used3, Used4 }; - `, - errors: [ - { - column: 10, - data: { - action: 'defined', - additional: '', - varName: 'Unused1', - }, - line: 2, - messageId: 'unusedVar', - }, - { - column: 17, - data: { - action: 'defined', - additional: '', - varName: 'Unused2', - }, - line: 3, - messageId: 'unusedVar', - }, - { - column: 17, - data: { - action: 'defined', - additional: '', - varName: 'Unused3', - }, - line: 4, - messageId: 'unusedVar', - }, - { - column: 17, - data: { - action: 'defined', - additional: '', - varName: 'Unused4', - }, - line: 5, - messageId: 'unusedVar', - }, - ], - options: [{ enableAutofixRemoval: { imports: true } }], - output: ` -import Used1 from 'bar'; -import { Used2 } from 'baz'; -import Used3, { Used4 } from 'foobar'; -export { Used1, Used2, Used3, Used4 }; - `, - }, ], valid: [ @@ -3062,6 +2946,39 @@ declare module 'foo' { { code: ` export import Bar = Something.Bar; +const foo: 1234; + `, + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + export import Bar = Something.Bar; + const foo: 1234; + export const bar: string; + export namespace NS { + const baz: 1234; + } +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + export import Bar = Something.Bar; + const foo: 1234; + export const bar: string; + export namespace NS { + const baz: 1234; + } +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +export import Bar = Something.Bar; const foo: 1234; export const bar: string; export namespace NS { From a2dce5f7e7e12754466b36113d457c301081f603 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Tue, 3 Jun 2025 20:15:48 +0900 Subject: [PATCH 05/22] chore: remove uninteded code changes --- .../no-unused-vars/no-unused-vars.test.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) 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 5ebc8d9a406b..ac60033d2a5e 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 @@ -1734,6 +1734,122 @@ export {}; ], filename: 'foo.d.ts', }, + { + code: ` +import * as Unused from 'foo'; +import * as Used from 'bar'; +export { Used }; + `, + errors: [ + { + column: 13, + data: { + action: 'defined', + additional: '', + varName: 'Unused', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import * as Used from 'bar'; +export { Used }; + `, + }, + { + code: ` +import Unused1 from 'foo'; +import Unused2, { Used } from 'bar'; +export { Used }; + `, + errors: [ + { + column: 8, + data: { + action: 'defined', + additional: '', + varName: 'Unused1', + }, + line: 2, + messageId: 'unusedVar', + }, + { + column: 8, + data: { + action: 'defined', + additional: '', + varName: 'Unused2', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used } from 'bar'; +export { Used }; + `, + }, + { + code: ` +import { Unused1 } from 'foo'; +import Used1, { Unused2 } from 'bar'; +import { Used2, Unused3 } from 'baz'; +import Used3, { Unused4, Used4 } from 'foobar'; +export { Used1, Used2, Used3, Used4 }; + `, + errors: [ + { + column: 10, + data: { + action: 'defined', + additional: '', + varName: 'Unused1', + }, + line: 2, + messageId: 'unusedVar', + }, + { + column: 17, + data: { + action: 'defined', + additional: '', + varName: 'Unused2', + }, + line: 3, + messageId: 'unusedVar', + }, + { + column: 17, + data: { + action: 'defined', + additional: '', + varName: 'Unused3', + }, + line: 4, + messageId: 'unusedVar', + }, + { + column: 17, + data: { + action: 'defined', + additional: '', + varName: 'Unused4', + }, + line: 5, + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import Used1 from 'bar'; +import { Used2 } from 'baz'; +import Used3, { Used4 } from 'foobar'; +export { Used1, Used2, Used3, Used4 }; + `, + }, ], valid: [ From 855acdf59b8f4d4d68109f858ed0399e43d3786d Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 4 Jun 2025 16:47:22 +0900 Subject: [PATCH 06/22] refactor: simplify conditional statements to increase converage --- .../eslint-plugin/src/rules/no-unused-vars.ts | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 8d70db0926d7..2df7ad2e5cb8 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -714,6 +714,7 @@ export default createRule({ d => d.type === DefinitionType.ImportBinding, ) ? fixer => { + // Find the import statement const def = unusedVar.defs.find( d => d.type === DefinitionType.ImportBinding, ); @@ -725,47 +726,44 @@ export default createRule({ const node = def.node; const decl = node.parent as TSESTree.ImportDeclaration; - // Remove import declaration line if no specifiers are left - if (decl.specifiers.length === 1) { - const next = source.getTokenAfter(decl) ?? { - range: [decl.range[1], decl.range[1]], - }; + const afterDeclToken = source.getTokenAfter(decl); + const afterNodeToken = source.getTokenAfter(node); + const beforeNodeToken = source.getTokenBefore(node); + + // Remove import declaration line if no specifiers are left, import unused from 'a'; + if (decl.specifiers.length === 1 && afterDeclToken) { return fixer.removeRange([ decl.range[0], - next.range[0], + afterDeclToken.range[0], ]); } - // case: remove { unused } + // case: remove braces, import used, { unused } from 'a'; const restNamed = decl.specifiers.filter( s => s === node && s.type === AST_NODE_TYPES.ImportSpecifier, ); - if (restNamed.length === 1) { - const nextBraceToken = source.getTokenAfter(node); - const prevBraceToken = source.getTokenBefore(node); - if ( - nextBraceToken?.value === '}' && - prevBraceToken?.value === '{' - ) { - // remove comma - const prevComma = - source.getTokenBefore(prevBraceToken); - - return fixer.removeRange([ - prevComma?.value === ',' - ? prevComma.range[0] - : prevBraceToken.range[0], - nextBraceToken.range[1], - ]); - } + if ( + restNamed.length === 1 && + afterNodeToken?.value === '}' && + beforeNodeToken?.value === '{' + ) { + // remove comma before braces + const prevComma = + source.getTokenBefore(beforeNodeToken); + + return fixer.removeRange([ + prevComma?.value === ',' + ? prevComma.range[0] + : beforeNodeToken.range[0], + afterNodeToken.range[1], + ]); } - // case: Remove comma after node - const nextCommaToken = source.getTokenAfter(node); - if (nextCommaToken?.value === ',') { - const nextToken = source.getTokenAfter(nextCommaToken, { + // case: Remove comma after node, import { unused, used } from 'a'; + if (afterNodeToken?.value === ',') { + const nextToken = source.getTokenAfter(afterNodeToken, { includeComments: true, }); @@ -773,20 +771,19 @@ export default createRule({ node.range[0], nextToken ? nextToken.range[0] - : nextCommaToken.range[1], + : afterNodeToken.range[1], ]); } - // case: Remove comma before node - const prevCommaToken = source.getTokenBefore(node); - if (prevCommaToken?.value === ',') { + // case: Remove comma before node, import { used, unused } from 'a'; + if (beforeNodeToken?.value === ',') { return fixer.removeRange([ - prevCommaToken.range[0], + beforeNodeToken.range[0], node.range[1], ]); } - // Remove the current specifier and all tokens until the next specifier - return fixer.remove(node); + + return null; } : undefined, }); From 233fb1e9d35059ef603eccea4a62088fe66c8b8b Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 4 Jun 2025 16:48:33 +0900 Subject: [PATCH 07/22] test: add testacase autofixer should do nothing in import-autofix feature --- .../no-unused-vars/no-unused-vars.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 ac60033d2a5e..a972e927cd4e 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 @@ -1850,6 +1850,25 @@ import Used3, { Used4 } from 'foobar'; export { Used1, Used2, Used3, Used4 }; `, }, + { + code: ` +let unused; + `, + errors: [ + { + column: 5, + data: { + action: 'defined', + additional: '', + varName: 'unused', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: null, + }, ], valid: [ From 622aaf0fb5ae2dae0ab502ce8455d0aa32d96935 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 4 Jun 2025 17:54:54 +0900 Subject: [PATCH 08/22] test: add more testcases --- .../no-unused-vars/no-unused-vars.test.ts | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) 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 a972e927cd4e..47a1870704b5 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 @@ -1869,8 +1869,73 @@ let unused; options: [{ enableAutofixRemoval: { imports: true } }], output: null, }, + { + code: ` +import { /* cmt */ Unused1, Used1 } from 'foo'; +export { Used1 }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { /* cmt */ Used1 } from 'foo'; +export { Used1 }; + `, + }, + { + code: ` +import type { UnusedType } from 'foo'; +import { Used1, Unused1 } from 'foo'; +export { Used1 }; + `, + errors: [{ messageId: 'unusedVar' }, { messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used1 } from 'foo'; +export { Used1 }; + `, + }, + { + code: ` +import { Unused1 as u1, Used1 as u2 } from 'foo'; +export { u2 }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used1 as u2 } from 'foo'; +export { u2 }; + `, + }, + { + code: ` +import { + Unused1, + Unused2, + Unused3, + Unused4, + Used1, + /* cmt */ + Unused5, + Unused6, + Used2, +} from 'foo'; +export { Used1, Used2 }; + `, + errors: [ + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used1, /* cmt */ Used2 } from 'foo'; +export { Used1, Used2 }; + `, + }, ], - valid: [ ` import { ClassDecoratorFactory } from 'decorators'; From 122509f5c802b31c245f2e4af16c35837d399ccf Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 4 Jun 2025 17:55:20 +0900 Subject: [PATCH 09/22] refactor: remove duplicate condition validate --- .../eslint-plugin/src/rules/no-unused-vars.ts | 146 +++++++++--------- 1 file changed, 70 insertions(+), 76 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 2df7ad2e5cb8..a0f6d9381636 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -708,84 +708,78 @@ export default createRule({ data: unusedVar.references.some(ref => ref.isWrite()) ? getAssignedMessageData(unusedVar) : getDefinedMessageData(unusedVar), - fix: - options.enableAutofixRemoval?.imports && - unusedVar.defs.some( - d => d.type === DefinitionType.ImportBinding, - ) - ? 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 as TSESTree.ImportDeclaration; - - const afterDeclToken = source.getTokenAfter(decl); - const afterNodeToken = source.getTokenAfter(node); - const beforeNodeToken = source.getTokenBefore(node); - - // Remove import declaration line if no specifiers are left, import unused from 'a'; - if (decl.specifiers.length === 1 && afterDeclToken) { - return fixer.removeRange([ - decl.range[0], - afterDeclToken.range[0], - ]); - } - - // case: remove braces, import used, { unused } from 'a'; - const restNamed = decl.specifiers.filter( - s => - s === node && - s.type === AST_NODE_TYPES.ImportSpecifier, - ); - if ( - restNamed.length === 1 && - afterNodeToken?.value === '}' && - beforeNodeToken?.value === '{' - ) { - // remove comma before braces - const prevComma = - source.getTokenBefore(beforeNodeToken); - - return fixer.removeRange([ - prevComma?.value === ',' - ? prevComma.range[0] - : beforeNodeToken.range[0], - afterNodeToken.range[1], - ]); - } - - // case: Remove comma after node, import { unused, used } from 'a'; - if (afterNodeToken?.value === ',') { - const nextToken = source.getTokenAfter(afterNodeToken, { - includeComments: true, - }); - - return fixer.removeRange([ - node.range[0], - nextToken - ? nextToken.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], - ]); - } - + fix: options.enableAutofixRemoval?.imports + ? fixer => { + // Find the import statement + const def = unusedVar.defs.find( + d => d.type === DefinitionType.ImportBinding, + ); + if (!def) { return null; } - : undefined, + + const source = context.sourceCode; + const node = def.node; + const decl = node.parent as TSESTree.ImportDeclaration; + + const afterDeclToken = source.getTokenAfter(decl); + const afterNodeToken = source.getTokenAfter(node); + const beforeNodeToken = source.getTokenBefore(node); + + // Remove import declaration line if no specifiers are left, import unused from 'a'; + if (decl.specifiers.length === 1 && afterDeclToken) { + return fixer.removeRange([ + decl.range[0], + afterDeclToken.range[0], + ]); + } + + // case: remove braces, import used, { unused } from 'a'; + const restNamed = decl.specifiers.filter( + s => + s === node && s.type === AST_NODE_TYPES.ImportSpecifier, + ); + if ( + restNamed.length === 1 && + afterNodeToken?.value === '}' && + beforeNodeToken?.value === '{' + ) { + // remove comma before braces + const prevComma = source.getTokenBefore(beforeNodeToken); + + return fixer.removeRange([ + prevComma?.value === ',' + ? prevComma.range[0] + : beforeNodeToken.range[0], + afterNodeToken.range[1], + ]); + } + + // case: Remove comma after node, import { unused, used } from 'a'; + if (afterNodeToken?.value === ',') { + const nextToken = source.getTokenAfter(afterNodeToken, { + includeComments: true, + }); + + return fixer.removeRange([ + node.range[0], + nextToken + ? nextToken.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; + } + : undefined, }); // If there are no regular declaration, report the first `/*globals*/` comment directive. From 556ae92f878f612c97418257d84ba4c926424647 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 4 Jun 2025 22:57:05 +0900 Subject: [PATCH 10/22] test: add more test case --- .../no-unused-vars/no-unused-vars.test.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 47a1870704b5..5551576926e4 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 @@ -1908,6 +1908,24 @@ export { u2 }; }, { code: ` +import { Unused1, Unused2, Used1 } from 'foo'; +import { Unused3, Unused4 } from 'bar'; +export { Used1 }; + `, + errors: [ + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used1 } from 'foo'; +export { Used1 }; + `, + }, + { + code: ` import { Unused1, Unused2, @@ -1931,7 +1949,11 @@ export { Used1, Used2 }; ], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import { Used1, /* cmt */ Used2 } from 'foo'; +import { + Used1, + /* cmt */ + Used2, +} from 'foo'; export { Used1, Used2 }; `, }, From 6d35a7181dd93f311855faf35456c625226f17c5 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 01:55:29 +0900 Subject: [PATCH 11/22] chore: logic that is currently difficult to implement is left as TODO --- .../eslint-plugin/src/rules/no-unused-vars.ts | 36 +++--- .../no-unused-vars/no-unused-vars.test.ts | 105 +++++++++--------- 2 files changed, 73 insertions(+), 68 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index a0f6d9381636..ca9ddd616c5f 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -721,16 +721,17 @@ export default createRule({ const source = context.sourceCode; const node = def.node; const decl = node.parent as TSESTree.ImportDeclaration; - - const afterDeclToken = source.getTokenAfter(decl); const afterNodeToken = source.getTokenAfter(node); const beforeNodeToken = source.getTokenBefore(node); + const prevBeforeNodeToken = beforeNodeToken + ? source.getTokenBefore(beforeNodeToken) + : null; // Remove import declaration line if no specifiers are left, import unused from 'a'; - if (decl.specifiers.length === 1 && afterDeclToken) { + if (decl.specifiers.length === 1) { return fixer.removeRange([ decl.range[0], - afterDeclToken.range[0], + decl.range[1] + 1, // +1 to include "\n" ]); } @@ -742,15 +743,11 @@ export default createRule({ if ( restNamed.length === 1 && afterNodeToken?.value === '}' && - beforeNodeToken?.value === '{' + beforeNodeToken?.value === '{' && + prevBeforeNodeToken?.value === ',' ) { - // remove comma before braces - const prevComma = source.getTokenBefore(beforeNodeToken); - return fixer.removeRange([ - prevComma?.value === ',' - ? prevComma.range[0] - : beforeNodeToken.range[0], + prevBeforeNodeToken.range[0], afterNodeToken.range[1], ]); } @@ -761,12 +758,17 @@ export default createRule({ includeComments: true, }); - return fixer.removeRange([ - node.range[0], - nextToken - ? nextToken.range[0] - : afterNodeToken.range[1], - ]); + if ( + nextToken?.loc.end.line === afterNodeToken.loc.end.line + ) { + return fixer.removeRange([ + node.range[0], + nextToken.range[0], + ]); + } + + // TODO: remove multi-line import + return null; } // case: Remove comma before node, import { used, unused } from 'a'; 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 5551576926e4..3fa478fd405d 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 @@ -1906,57 +1906,60 @@ import { Used1 as u2 } from 'foo'; export { u2 }; `, }, - { - code: ` -import { Unused1, Unused2, Used1 } from 'foo'; -import { Unused3, Unused4 } from 'bar'; -export { Used1 }; - `, - errors: [ - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - ], - options: [{ enableAutofixRemoval: { imports: true } }], - output: ` -import { Used1 } from 'foo'; -export { Used1 }; - `, - }, - { - code: ` -import { - Unused1, - Unused2, - Unused3, - Unused4, - Used1, - /* cmt */ - Unused5, - Unused6, - Used2, -} from 'foo'; -export { Used1, Used2 }; - `, - errors: [ - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - ], - options: [{ enableAutofixRemoval: { imports: true } }], - output: ` -import { - Used1, - /* cmt */ - Used2, -} from 'foo'; -export { Used1, Used2 }; - `, - }, + // TODO: Logic to remove multiple unused vars in one-line + // { + // code: ` + // import { Unused1, Unused2, Used1 } from 'foo'; + // import { Unused3, Unused4 } from 'bar'; + // export { Used1 }; + // `, + // errors: [ + // { messageId: 'unusedVar' }, + // { messageId: 'unusedVar' }, + // { messageId: 'unusedVar' }, + // { messageId: 'unusedVar' }, + // ], + // options: [{ enableAutofixRemoval: { imports: true } }], + // output: ` + // import { Used1 } from 'foo'; + // export { Used1 }; + // `, + // }, + + // TODO: multi-line import + // { + // code: ` + // import { + // Unused1, + // Unused2, + // Unused3, + // Unused4, + // Used1, + // /* cmt */ + // Unused5, + // Unused6, + // Used2, + // } from 'foo'; + // export { Used1, Used2 }; + // `, + // errors: [ + // { messageId: 'unusedVar' }, + // { messageId: 'unusedVar' }, + // { messageId: 'unusedVar' }, + // { messageId: 'unusedVar' }, + // { messageId: 'unusedVar' }, + // { messageId: 'unusedVar' }, + // ], + // options: [{ enableAutofixRemoval: { imports: true } }], + // output: ` + // import { + // Used1, + // /* cmt */ + // Used2, + // } from 'foo'; + // export { Used1, Used2 }; + // `, + // }, ], valid: [ ` From d9f16097c99c873cde131a1809c7a6678c96fc3b Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 5 Jun 2025 15:15:02 +0900 Subject: [PATCH 12/22] chore: add line for test category --- .../tests/rules/no-unused-vars/no-unused-vars.test.ts | 1 + 1 file changed, 1 insertion(+) 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 3fa478fd405d..ef359795176d 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 @@ -1961,6 +1961,7 @@ export { u2 }; // `, // }, ], + valid: [ ` import { ClassDecoratorFactory } from 'decorators'; From 11ac4fa7833a17b0883875952b82be34ad39126d Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 6 Jun 2025 18:20:38 +0900 Subject: [PATCH 13/22] fix: remove type casting and support TSImportEqulasDeclaration node --- packages/eslint-plugin/src/rules/no-unused-vars.ts | 7 ++++++- .../rules/no-unused-vars/no-unused-vars.test.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index ca9ddd616c5f..ad3547197823 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -720,7 +720,12 @@ export default createRule({ const source = context.sourceCode; const node = def.node; - const decl = node.parent as TSESTree.ImportDeclaration; + 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 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 ef359795176d..95faf938ca9f 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 @@ -1906,6 +1906,20 @@ import { Used1 as u2 } from 'foo'; export { u2 }; `, }, + { + code: ` +import x = require('foo'); +import y = require('bar'); +export { y }; + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + +import y = require('bar'); +export { y }; + `, + }, // TODO: Logic to remove multiple unused vars in one-line // { // code: ` From 633d2c8c00d7882f7e98331bec09d24fd0502288 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 6 Jun 2025 23:55:21 +0900 Subject: [PATCH 14/22] test: simplify test case with noFormat --- .../no-unused-vars/no-unused-vars.test.ts | 150 +++++++----------- 1 file changed, 55 insertions(+), 95 deletions(-) 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 95faf938ca9f..b1a99165757b 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 @@ -1735,20 +1735,12 @@ export {}; filename: 'foo.d.ts', }, { - code: ` -import * as Unused from 'foo'; -import * as Used from 'bar'; + code: noFormat` +import * as Unused from 'foo';import * as Used from 'bar'; export { Used }; `, errors: [ { - column: 13, - data: { - action: 'defined', - additional: '', - varName: 'Unused', - }, - line: 2, messageId: 'unusedVar', }, ], @@ -1759,91 +1751,51 @@ export { Used }; `, }, { - code: ` + code: noFormat` import Unused1 from 'foo'; -import Unused2, { Used } from 'bar'; +import Unused2,{ Used } from 'bar'; export { Used }; `, errors: [ { - column: 8, - data: { - action: 'defined', - additional: '', - varName: 'Unused1', - }, - line: 2, messageId: 'unusedVar', }, { - column: 8, - data: { - action: 'defined', - additional: '', - varName: 'Unused2', - }, - line: 3, messageId: 'unusedVar', }, ], options: [{ enableAutofixRemoval: { imports: true } }], output: ` + import { Used } from 'bar'; export { Used }; `, }, { - code: ` + code: noFormat` import { Unused1 } from 'foo'; import Used1, { Unused2 } from 'bar'; import { Used2, Unused3 } from 'baz'; -import Used3, { Unused4, Used4 } from 'foobar'; +import Used3, { Unused4,Used4 } from 'foobar'; export { Used1, Used2, Used3, Used4 }; `, errors: [ { - column: 10, - data: { - action: 'defined', - additional: '', - varName: 'Unused1', - }, - line: 2, messageId: 'unusedVar', }, { - column: 17, - data: { - action: 'defined', - additional: '', - varName: 'Unused2', - }, - line: 3, messageId: 'unusedVar', }, { - column: 17, - data: { - action: 'defined', - additional: '', - varName: 'Unused3', - }, - line: 4, messageId: 'unusedVar', }, { - column: 17, - data: { - action: 'defined', - additional: '', - varName: 'Unused4', - }, - line: 5, messageId: 'unusedVar', }, ], options: [{ enableAutofixRemoval: { imports: true } }], output: ` + import Used1 from 'bar'; import { Used2 } from 'baz'; import Used3, { Used4 } from 'foobar'; @@ -1877,14 +1829,13 @@ export { Used1 }; errors: [{ messageId: 'unusedVar' }], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import { /* cmt */ Used1 } from 'foo'; +import { /* cmt */ Used1 } from 'foo'; export { Used1 }; `, }, { - code: ` -import type { UnusedType } from 'foo'; -import { Used1, Unused1 } from 'foo'; + code: noFormat` +import type { UnusedType } from 'foo';import { Used1, Unused1 } from 'foo'; export { Used1 }; `, errors: [{ messageId: 'unusedVar' }, { messageId: 'unusedVar' }], @@ -1902,7 +1853,7 @@ export { u2 }; errors: [{ messageId: 'unusedVar' }], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import { Used1 as u2 } from 'foo'; +import { Used1 as u2 } from 'foo'; export { u2 }; `, }, @@ -1925,40 +1876,13 @@ export { y }; // code: ` // import { Unused1, Unused2, Used1 } from 'foo'; // import { Unused3, Unused4 } from 'bar'; - // export { Used1 }; + // export { Used1, Used2 }; // `, // errors: [ // { messageId: 'unusedVar' }, // { messageId: 'unusedVar' }, // { messageId: 'unusedVar' }, // { messageId: 'unusedVar' }, - // ], - // options: [{ enableAutofixRemoval: { imports: true } }], - // output: ` - // import { Used1 } from 'foo'; - // export { Used1 }; - // `, - // }, - - // TODO: multi-line import - // { - // code: ` - // import { - // Unused1, - // Unused2, - // Unused3, - // Unused4, - // Used1, - // /* cmt */ - // Unused5, - // Unused6, - // Used2, - // } from 'foo'; - // export { Used1, Used2 }; - // `, - // errors: [ - // { messageId: 'unusedVar' }, - // { messageId: 'unusedVar' }, // { messageId: 'unusedVar' }, // { messageId: 'unusedVar' }, // { messageId: 'unusedVar' }, @@ -1966,14 +1890,50 @@ export { y }; // ], // options: [{ enableAutofixRemoval: { imports: true } }], // output: ` - // import { - // Used1, - // /* cmt */ - // Used2, - // } from 'foo'; + // import { Used1,Used2 } from 'foo'; + // export { Used1, Used2 }; - // `, + // `, // }, + { + code: noFormat` +import { +Unused1, +Unused2, +Unused3, +Unused4, +Used1, +/* cmt */ +Unused5, +Unused6, +Used2, +} from 'foo'; +export { Used1, Used2 }; + `, + errors: [ + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: noFormat` +import { + + + + +Used1, +/* cmt */ + + +Used2, +} from 'foo'; +export { Used1, Used2 }; + `, + }, ], valid: [ From 9f76441a8a109bbc4d63317f95eeb321769ef300 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 6 Jun 2025 23:56:00 +0900 Subject: [PATCH 15/22] fix: remove format-related logic --- .../eslint-plugin/src/rules/no-unused-vars.ts | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index ad3547197823..4d0456d4a46c 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -734,10 +734,7 @@ export default createRule({ // Remove import declaration line if no specifiers are left, import unused from 'a'; if (decl.specifiers.length === 1) { - return fixer.removeRange([ - decl.range[0], - decl.range[1] + 1, // +1 to include "\n" - ]); + return fixer.removeRange([decl.range[0], decl.range[1]]); } // case: remove braces, import used, { unused } from 'a'; @@ -759,21 +756,10 @@ export default createRule({ // case: Remove comma after node, import { unused, used } from 'a'; if (afterNodeToken?.value === ',') { - const nextToken = source.getTokenAfter(afterNodeToken, { - includeComments: true, - }); - - if ( - nextToken?.loc.end.line === afterNodeToken.loc.end.line - ) { - return fixer.removeRange([ - node.range[0], - nextToken.range[0], - ]); - } - - // TODO: remove multi-line import - return null; + return fixer.removeRange([ + node.range[0], + afterNodeToken.range[1], + ]); } // case: Remove comma before node, import { used, unused } from 'a'; From 2c8b7406a98205f95f5413cb0522f23c824a7abf Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sun, 8 Jun 2025 22:10:23 +0900 Subject: [PATCH 16/22] feat: add suggestion for enableAUtofixRemoval --- .../eslint-plugin/src/rules/no-unused-vars.ts | 148 ++++++++++-------- 1 file changed, 82 insertions(+), 66 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 4d0456d4a46c..8a8a538b851d 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' @@ -81,8 +86,12 @@ export default createRule({ 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: @@ -702,77 +711,84 @@ 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 line if no specifiers are left, import unused from 'a'; + if (decl.specifiers.length === 1) { + return fixer.removeRange([decl.range[0], decl.range[1]]); + } + + // case: remove braces, import used, { unused } from 'a'; + const restNamed = decl.specifiers.filter( + s => s === node && s.type === AST_NODE_TYPES.ImportSpecifier, + ); + if ( + restNamed.length === 1 && + afterNodeToken?.value === '}' && + beforeNodeToken?.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), - fix: options.enableAutofixRemoval?.imports - ? 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 line if no specifiers are left, import unused from 'a'; - if (decl.specifiers.length === 1) { - return fixer.removeRange([decl.range[0], decl.range[1]]); - } - - // case: remove braces, import used, { unused } from 'a'; - const restNamed = decl.specifiers.filter( - s => - s === node && s.type === AST_NODE_TYPES.ImportSpecifier, - ); - if ( - restNamed.length === 1 && - afterNodeToken?.value === '}' && - beforeNodeToken?.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; - } - : undefined, + ...getFixOrSuggest({ + fixOrSuggest: options.enableAutofixRemoval?.imports + ? 'fix' + : 'suggest', + suggestion: { + messageId: 'unusedVarSuggestion', + fix: fixer, + }, + }), }); // If there are no regular declaration, report the first `/*globals*/` comment directive. From 9a15d4955b77dcd35a50e1e8f93b6517336cb58a Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sun, 8 Jun 2025 23:20:13 +0900 Subject: [PATCH 17/22] test: add suggestion effected by import statement --- .../no-unused-vars/no-unused-vars.test.ts | 279 +++++++++++++++++- 1 file changed, 276 insertions(+), 3 deletions(-) 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 b1a99165757b..a9e9b7874950 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 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 {} + `, + }, + ], }, ], }, @@ -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 {} + `, + }, + ], }, ], }, @@ -410,6 +617,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 {} + `, + }, + ], }, ], }, @@ -579,6 +797,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: { @@ -608,6 +840,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: { @@ -638,6 +884,19 @@ export const ComponentFoo = () => { }, line: 2, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'React' }, + messageId: 'unusedVarSuggestion', + output: ` + + +export const ComponentFoo = () => { + return
Foo Foo
; +}; + `, + }, + ], }, ], languageOptions: { @@ -851,17 +1110,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: '', @@ -869,6 +1128,20 @@ export namespace Bar { }, line: 6, messageId: 'unusedVar', + suggestions: [ + { + data: { varName: 'TheFoo' }, + messageId: 'unusedVarSuggestion', + output: ` +namespace Foo { + export const foo = 1; +} +export namespace Bar { + +} + `, + }, + ], }, ], }, From 6de905049e8ad86b03abe918b0cd923ac5aa09c0 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sun, 8 Jun 2025 23:43:17 +0900 Subject: [PATCH 18/22] test: add suggestion for no-unused-vars-eslint test --- .../no-unused-vars/no-unused-vars-eslint.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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' }, }, From a177e5c2669a0eb206b07ddbcc27f12fe79385b9 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 11 Jun 2025 18:02:54 +0900 Subject: [PATCH 19/22] feat: remove all unused specifers at once if no used specifers in decl --- .../eslint-plugin/src/rules/no-unused-vars.ts | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 8a8a538b851d..9eb7cc7ad4d6 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -671,6 +671,63 @@ export default createRule({ // collect 'Program:exit'(programNode): void { const unusedVars = collectUnusedVariables(); + /** + * metadata of import declaration include unused specifiers + */ + interface UnusedDecl { + /** + * Ranges of all unused default & named specifiers + */ + unusedNodeRanges: TSESLint.AST.Range[]; + /** + * Range of named imports include braces + */ + namedRange?: TSESLint.AST.Range; + /** + * Range of import declaration + */ + declRange: TSESLint.AST.Range; + } + + // Structuring unused specifiers for each decl + const unusedDecl = new Map(); + for (const unusedVar of unusedVars) { + const def = unusedVar.defs.find( + d => d.type === DefinitionType.ImportBinding, + ); + if (!def) { + continue; + } + const node = def.node; + const decl = node.parent; + if (decl.type !== AST_NODE_TYPES.ImportDeclaration) { + continue; + } + const mapKey = decl.range.toString(); + const afterNodeToken = context.sourceCode.getTokenAfter(node); + const beforeNodeToken = context.sourceCode.getTokenBefore(node); + + const prevValue = unusedDecl.get(mapKey); + // get range of { A, B, C, ... , D } + const namedRange = prevValue?.namedRange ?? [0, 0]; + if (beforeNodeToken?.value === '{') { + namedRange[0] = beforeNodeToken.range[0]; + } + if (afterNodeToken?.value === '}') { + namedRange[1] = afterNodeToken.range[1]; + } + + unusedDecl.set(mapKey, { + declRange: decl.range, + namedRange: namedRange.every(v => v === 0) + ? prevValue?.namedRange + : namedRange, + unusedNodeRanges: [ + ...(prevValue?.unusedNodeRanges ?? []), + node.range, + ], + }); + } for (const unusedVar of unusedVars) { // Report the first declaration. @@ -734,24 +791,28 @@ export default createRule({ ? source.getTokenBefore(beforeNodeToken) : null; - // Remove import declaration line if no specifiers are left, import unused from 'a'; - if (decl.specifiers.length === 1) { - return fixer.removeRange([decl.range[0], decl.range[1]]); + const declInfo = unusedDecl.get(decl.range.toString()); + + // Remove import declaration if no used specifiers are left, import unused from 'a'; + if ( + declInfo && + decl.specifiers.length === declInfo.unusedNodeRanges.length + ) { + return fixer.removeRange(declInfo.declRange); } // case: remove braces, import used, { unused } from 'a'; const restNamed = decl.specifiers.filter( - s => s === node && s.type === AST_NODE_TYPES.ImportSpecifier, + s => s.type === AST_NODE_TYPES.ImportSpecifier, ); if ( - restNamed.length === 1 && - afterNodeToken?.value === '}' && - beforeNodeToken?.value === '{' && + declInfo?.namedRange && + restNamed.length === declInfo.unusedNodeRanges.length && prevBeforeNodeToken?.value === ',' ) { return fixer.removeRange([ prevBeforeNodeToken.range[0], - afterNodeToken.range[1], + declInfo.namedRange[1], ]); } From 1a054652aaffe47bd479a92e6b1794f42f679606 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 11 Jun 2025 18:03:14 +0900 Subject: [PATCH 20/22] test: add test --- .../no-unused-vars/no-unused-vars.test.ts | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) 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 a9e9b7874950..4ba98c274cd0 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 @@ -2144,30 +2144,27 @@ import y = require('bar'); export { y }; `, }, - // TODO: Logic to remove multiple unused vars in one-line - // { - // code: ` - // import { Unused1, Unused2, Used1 } from 'foo'; - // import { Unused3, Unused4 } from 'bar'; - // export { Used1, Used2 }; - // `, - // errors: [ - // { messageId: 'unusedVar' }, - // { messageId: 'unusedVar' }, - // { messageId: 'unusedVar' }, - // { messageId: 'unusedVar' }, - // { messageId: 'unusedVar' }, - // { messageId: 'unusedVar' }, - // { messageId: 'unusedVar' }, - // { messageId: 'unusedVar' }, - // ], - // options: [{ enableAutofixRemoval: { imports: true } }], - // output: ` - // import { Used1,Used2 } from 'foo'; - - // export { Used1, Used2 }; - // `, - // }, + { + code: ` +import { Unused1, Unused2, Unused3, Used1 } from 'foo'; +import Used2, { Unused4, Unused5, Unused6 } from 'bar'; +export { Used1, Used2 }; + `, + errors: [ + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + { messageId: 'unusedVar' }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` +import { Used1 } from 'foo'; +import Used2 from 'bar'; +export { Used1, Used2 }; + `, + }, { code: noFormat` import { From 90c2d9a7f74e141d78665f171285d9414b1ebf5a Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 11 Jun 2025 21:28:23 +0900 Subject: [PATCH 21/22] chore: resolve real conflict --- .../no-unused-vars/no-unused-vars.test.ts | 30 ------------------- 1 file changed, 30 deletions(-) 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 4ba98c274cd0..f260ef9c0761 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 @@ -545,36 +545,6 @@ export interface Bar extends baz.test {} code: ` import test from 'test'; import baz from 'baz'; -export interface Bar extends baz().test {} - `, - errors: [ - { - column: 8, - data: { - action: 'defined', - additional: '', - varName: 'test', - }, - line: 2, - messageId: 'unusedVar', - suggestions: [ - { - data: { varName: 'test' }, - messageId: 'unusedVarSuggestion', - output: ` - -import baz from 'baz'; -export interface Bar extends baz().test {} - `, - }, - ], - }, - ], - }, - { - code: ` -import test from 'test'; -import baz from 'baz'; export class Bar implements baz.test {} `, errors: [ From eb3fd2b2443f45b4dbb3d766e01ea1b0d453f67e Mon Sep 17 00:00:00 2001 From: nayounsang Date: Mon, 14 Jul 2025 15:41:52 +0900 Subject: [PATCH 22/22] fix: simplyfy logic and modify tc to fit unit tests --- .../eslint-plugin/src/rules/no-unused-vars.ts | 76 +------ .../no-unused-vars/no-unused-vars.test.ts | 213 +++++++++--------- 2 files changed, 115 insertions(+), 174 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 9eb7cc7ad4d6..6b7873f1303e 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -671,63 +671,6 @@ export default createRule({ // collect 'Program:exit'(programNode): void { const unusedVars = collectUnusedVariables(); - /** - * metadata of import declaration include unused specifiers - */ - interface UnusedDecl { - /** - * Ranges of all unused default & named specifiers - */ - unusedNodeRanges: TSESLint.AST.Range[]; - /** - * Range of named imports include braces - */ - namedRange?: TSESLint.AST.Range; - /** - * Range of import declaration - */ - declRange: TSESLint.AST.Range; - } - - // Structuring unused specifiers for each decl - const unusedDecl = new Map(); - for (const unusedVar of unusedVars) { - const def = unusedVar.defs.find( - d => d.type === DefinitionType.ImportBinding, - ); - if (!def) { - continue; - } - const node = def.node; - const decl = node.parent; - if (decl.type !== AST_NODE_TYPES.ImportDeclaration) { - continue; - } - const mapKey = decl.range.toString(); - const afterNodeToken = context.sourceCode.getTokenAfter(node); - const beforeNodeToken = context.sourceCode.getTokenBefore(node); - - const prevValue = unusedDecl.get(mapKey); - // get range of { A, B, C, ... , D } - const namedRange = prevValue?.namedRange ?? [0, 0]; - if (beforeNodeToken?.value === '{') { - namedRange[0] = beforeNodeToken.range[0]; - } - if (afterNodeToken?.value === '}') { - namedRange[1] = afterNodeToken.range[1]; - } - - unusedDecl.set(mapKey, { - declRange: decl.range, - namedRange: namedRange.every(v => v === 0) - ? prevValue?.namedRange - : namedRange, - unusedNodeRanges: [ - ...(prevValue?.unusedNodeRanges ?? []), - node.range, - ], - }); - } for (const unusedVar of unusedVars) { // Report the first declaration. @@ -791,28 +734,23 @@ export default createRule({ ? source.getTokenBefore(beforeNodeToken) : null; - const declInfo = unusedDecl.get(decl.range.toString()); - - // Remove import declaration if no used specifiers are left, import unused from 'a'; - if ( - declInfo && - decl.specifiers.length === declInfo.unusedNodeRanges.length - ) { - return fixer.removeRange(declInfo.declRange); + // Remove import declaration if no used specifiers are left + if (decl.specifiers.length === 1) { + return fixer.removeRange(decl.range); } - // case: remove braces, import used, { unused } from 'a'; + // case: remove braces if no used named specifiers are left const restNamed = decl.specifiers.filter( s => s.type === AST_NODE_TYPES.ImportSpecifier, ); if ( - declInfo?.namedRange && - restNamed.length === declInfo.unusedNodeRanges.length && + restNamed.length === 1 && + afterNodeToken?.value === '}' && prevBeforeNodeToken?.value === ',' ) { return fixer.removeRange([ prevBeforeNodeToken.range[0], - declInfo.namedRange[1], + afterNodeToken.range[1], ]); } 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 f260ef9c0761..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 @@ -1978,9 +1978,8 @@ export {}; filename: 'foo.d.ts', }, { - code: noFormat` -import * as Unused from 'foo';import * as Used from 'bar'; -export { Used }; + code: ` +import Unused from 'foo'; `, errors: [ { @@ -1989,189 +1988,193 @@ export { Used }; ], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import * as Used from 'bar'; -export { Used }; + `, }, { - code: noFormat` -import Unused1 from 'foo'; -import Unused2,{ Used } from 'bar'; -export { Used }; + code: ` +import { Unused } from 'foo'; `, errors: [ { messageId: 'unusedVar', }, - { - messageId: 'unusedVar', - }, ], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import { Used } from 'bar'; -export { Used }; `, }, { - code: noFormat` -import { Unused1 } from 'foo'; -import Used1, { Unused2 } from 'bar'; -import { Used2, Unused3 } from 'baz'; -import Used3, { Unused4,Used4 } from 'foobar'; -export { Used1, Used2, Used3, Used4 }; + code: ` +import * as Unused from 'foo'; `, errors: [ { messageId: 'unusedVar', }, + ], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` + + `, + }, + { + code: ` +import { Unused as u1 } from 'foo'; + `, + errors: [ { messageId: 'unusedVar', }, - { - 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 Used1 from 'bar'; -import { Used2 } from 'baz'; -import Used3, { Used4 } from 'foobar'; -export { Used1, Used2, Used3, Used4 }; +import { Used } from 'bar'; +export { Used }; `, }, { - code: ` -let unused; + code: noFormat` +import Used,{ Unused } from 'bar'; +export { Used }; `, errors: [ { - column: 5, - data: { - action: 'defined', - additional: '', - varName: 'unused', - }, - line: 2, messageId: 'unusedVar', }, ], options: [{ enableAutofixRemoval: { imports: true } }], - output: null, + output: ` +import Used from 'bar'; +export { Used }; + `, }, { code: ` -import { /* cmt */ Unused1, Used1 } from 'foo'; -export { Used1 }; +import { /* cmt */ Unused, Used } from 'foo'; +export { Used }; `, errors: [{ messageId: 'unusedVar' }], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import { /* cmt */ Used1 } from 'foo'; -export { Used1 }; +import { /* cmt */ Used } from 'foo'; +export { Used }; `, }, { - code: noFormat` -import type { UnusedType } from 'foo';import { Used1, Unused1 } from 'foo'; -export { Used1 }; + code: ` +import { Used, Unused /* cmt */ } from 'foo'; +export { Used }; `, - errors: [{ messageId: 'unusedVar' }, { messageId: 'unusedVar' }], + errors: [{ messageId: 'unusedVar' }], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import { Used1 } from 'foo'; -export { Used1 }; +import { Used /* cmt */ } from 'foo'; +export { Used }; `, }, { code: ` -import { Unused1 as u1, Used1 as u2 } from 'foo'; -export { u2 }; +import { Used1 /* cmt1 */, Unused, /* cmt2 */ Used2 } from 'foo'; +export { Used1, Used2 }; `, errors: [{ messageId: 'unusedVar' }], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import { Used1 as u2 } from 'foo'; -export { u2 }; +import { Used1 /* cmt1 */, /* cmt2 */ Used2 } from 'foo'; +export { Used1, Used2 }; `, }, { code: ` -import x = require('foo'); -import y = require('bar'); -export { y }; +import type { UnusedType, UsedType } from 'foo'; +export { UsedType }; `, errors: [{ messageId: 'unusedVar' }], options: [{ enableAutofixRemoval: { imports: true } }], output: ` - -import y = require('bar'); -export { y }; +import type { UsedType } from 'foo'; +export { UsedType }; `, }, { code: ` -import { Unused1, Unused2, Unused3, Used1 } from 'foo'; -import Used2, { Unused4, Unused5, Unused6 } from 'bar'; -export { Used1, Used2 }; +import { type UnusedType, type UsedType } from 'foo'; +export { UsedType }; `, - errors: [ - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - ], + errors: [{ messageId: 'unusedVar' }], options: [{ enableAutofixRemoval: { imports: true } }], output: ` -import { Used1 } from 'foo'; -import Used2 from 'bar'; -export { Used1, Used2 }; +import { type UsedType } from 'foo'; +export { UsedType }; `, }, { - code: noFormat` -import { -Unused1, -Unused2, -Unused3, -Unused4, -Used1, -/* cmt */ -Unused5, -Unused6, -Used2, -} from 'foo'; -export { Used1, Used2 }; + code: ` +import { Unused as u1, Used as u2 } from 'foo'; +export { u2 }; `, - errors: [ - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - { messageId: 'unusedVar' }, - ], + errors: [{ messageId: 'unusedVar' }], options: [{ enableAutofixRemoval: { imports: true } }], - output: noFormat` -import { - - - - -Used1, -/* cmt */ - + output: ` +import { Used as u2 } from 'foo'; +export { u2 }; + `, + }, + { + code: ` +import unused = require('foo'); + `, + errors: [{ messageId: 'unusedVar' }], + options: [{ enableAutofixRemoval: { imports: true } }], + output: ` -Used2, -} from 'foo'; -export { Used1, Used2 }; `, }, ], 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