Content-Length: 96871 | pFad | http://github.com/typescript-eslint/typescript-eslint/pull/11243.patch

thub.com 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 }; `, }, ],








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/typescript-eslint/typescript-eslint/pull/11243.patch

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy