Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(eslint-plugin): add a default-off option to autofix remove unused imports #11243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e5394b2
test: add testcase
nayounsang May 23, 2025
e339973
feat: test successful logic
nayounsang May 23, 2025
7fc5d69
chore: generate schema
nayounsang May 24, 2025
7b06e7c
chore: Revert "test: add testcase"
nayounsang Jun 3, 2025
a2dce5f
chore: remove uninteded code changes
nayounsang Jun 3, 2025
855acdf
refactor: simplify conditional statements to increase converage
nayounsang Jun 4, 2025
233fb1e
test: add testacase autofixer should do nothing in import-autofix fea…
nayounsang Jun 4, 2025
622aaf0
test: add more testcases
nayounsang Jun 4, 2025
122509f
refactor: remove duplicate condition validate
nayounsang Jun 4, 2025
556ae92
test: add more test case
nayounsang Jun 4, 2025
6d35a71
chore: logic that is currently difficult to implement is left as TODO
nayounsang Jun 4, 2025
440d112
Merge branch 'main' into auto-unused
nayounsang Jun 5, 2025
d9f1609
chore: add line for test category
nayounsang Jun 5, 2025
11ac4fa
fix: remove type casting and support TSImportEqulasDeclaration node
nayounsang Jun 6, 2025
633d2c8
test: simplify test case with noFormat
nayounsang Jun 6, 2025
9f76441
fix: remove format-related logic
nayounsang Jun 6, 2025
99699fc
Merge branch 'main' into auto-unused
nayounsang Jun 8, 2025
2c8b740
feat: add suggestion for enableAUtofixRemoval
nayounsang Jun 8, 2025
9a15d49
test: add suggestion effected by import statement
nayounsang Jun 8, 2025
6de9050
test: add suggestion for no-unused-vars-eslint test
nayounsang Jun 8, 2025
a177e5c
feat: remove all unused specifers at once if no used specifers in decl
nayounsang Jun 11, 2025
1a05465
test: add test
nayounsang Jun 11, 2025
5662583
Merge branch 'main' into auto-unused
nayounsang Jun 11, 2025
90c2d9a
chore: resolve real conflict
nayounsang Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 164 additions & 1 deletion packages/eslint-plugin/src/rules/no-unused-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import {
collectVariables,
createRule,
getFixOrSuggest,
getNameLocationInGlobalDirectiveComment,
isDefinitionFile,
isFunction,
Expand All @@ -23,7 +24,11 @@
} 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'
Expand All @@ -38,6 +43,9 @@
reportUsedIgnorePattern?: boolean;
vars?: 'all' | 'local';
varsIgnorePattern?: string;
enableAutofixRemoval?: {
imports: boolean;
};
},
];

Expand All @@ -52,6 +60,9 @@
reportUsedIgnorePattern: boolean;
vars: 'all' | 'local';
varsIgnorePattern?: RegExp;
enableAutofixRemoval?: {
imports: boolean;
};
}

type VariableType =
Expand All @@ -74,8 +85,13 @@
extendsBaseRule: true,
recommended: 'recommended',
},
fixable: 'code',
// If generate suggest dynamically, disable the eslint rule.
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
hasSuggestions: true,
messages: {
unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.",
unusedVarSuggestion: 'Remove unused variable.',
usedIgnoredVar:
"'{{varName}}' is marked as ignored but is used{{additional}}.",
usedOnlyAsType:
Expand Down Expand Up @@ -117,6 +133,16 @@
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:
Expand Down Expand Up @@ -208,6 +234,10 @@
'u',
);
}

if (firstOption.enableAutofixRemoval) {
options.enableAutofixRemoval = firstOption.enableAutofixRemoval;
}
}

return options;
Expand Down Expand Up @@ -641,6 +671,63 @@
// 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<string, UnusedDecl>();
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.
Expand Down Expand Up @@ -681,12 +768,88 @@
},
};

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;

Check warning on line 792 in packages/eslint-plugin/src/rules/no-unused-vars.ts

View check run for this annotation

Codecov / codecov/patch

packages/eslint-plugin/src/rules/no-unused-vars.ts#L792

Added line #L792 was not covered by tests

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.type === AST_NODE_TYPES.ImportSpecifier,
);
if (
declInfo?.namedRange &&
restNamed.length === declInfo.unusedNodeRanges.length &&
prevBeforeNodeToken?.value === ','
) {
return fixer.removeRange([
prevBeforeNodeToken.range[0],
declInfo.namedRange[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;

Check warning on line 835 in packages/eslint-plugin/src/rules/no-unused-vars.ts

View check run for this annotation

Codecov / codecov/patch

packages/eslint-plugin/src/rules/no-unused-vars.ts#L835

Added line #L835 was not covered by tests
};

context.report({
loc,
messageId,
data: unusedVar.references.some(ref => ref.isWrite())
? getAssignedMessageData(unusedVar)
: getDefinedMessageData(unusedVar),
...getFixOrSuggest({
fixOrSuggest: options.enableAutofixRemoval?.imports
? 'fix'
: 'suggest',
suggestion: {
messageId: 'unusedVarSuggestion',
fix: fixer,
},
}),
});

// If there are no regular declaration, report the first `/*globals*/` comment directive.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
},
Expand Down
Loading