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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 102 additions & 0 deletions packages/eslint-plugin/src/rules/no-unused-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
reportUsedIgnorePattern?: boolean;
vars?: 'all' | 'local';
varsIgnorePattern?: string;
enableAutofixRemoval?: {
imports: boolean;
};
},
];

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

type VariableType =
Expand All @@ -74,6 +80,7 @@
extendsBaseRule: true,
recommended: 'recommended',
},
fixable: 'code',
messages: {
unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.",
usedIgnoredVar:
Expand Down Expand Up @@ -117,6 +124,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 +225,10 @@
'u',
);
}

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

return options;
Expand Down Expand Up @@ -687,6 +708,87 @@
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;
}

Check warning on line 722 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#L721-L722

Added lines #L721 - L722 were not covered by tests

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]],
};

Check warning on line 732 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#L731-L732

Added lines #L731 - L732 were not covered by tests
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],

Check warning on line 759 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#L759

Added line #L759 was not covered by tests
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],

Check warning on line 776 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#L776

Added line #L776 was not covered by tests
]);
}

// 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);

Check warning on line 789 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#L789

Added line #L789 was not covered by tests
}
: undefined,
});

// If there are no regular declaration, report the first `/*globals*/` comment directive.
Expand Down
149 changes: 116 additions & 33 deletions packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -2946,39 +3062,6 @@ declare module 'foo' {
{
code: `
export import Bar = Something.Bar;
const foo: 1234;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is github doing the comparison wrong? I checked and there is no part that has been deleted. There is no reason to delete it.

`,
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 {
Expand Down
14 changes: 14 additions & 0 deletions packages/eslint-plugin/tests/schema-snapshots/no-unused-vars.shot

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.