Skip to content

Commit a983865

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(language-service): add fix for individual unused imports (#58719)
Fixes that `getCodeActions` wasn't implemented for the unused imports fixer which meant that it wouldn't show up in the most common cases. PR Close #58719
1 parent e6e7a4e commit a983865

File tree

2 files changed

+160
-12
lines changed

2 files changed

+160
-12
lines changed

packages/language-service/src/codefixes/fix_unused_standalone_imports.ts

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,78 @@ import {findFirstMatchingNode} from '../utils/ts_utils';
1717
*/
1818
export const fixUnusedStandaloneImportsMeta: CodeActionMeta = {
1919
errorCodes: [ngErrorCode(ErrorCode.UNUSED_STANDALONE_IMPORTS)],
20-
getCodeActions: () => [],
20+
getCodeActions: ({start, fileName, compiler}) => {
21+
const file = compiler.programDriver.getProgram().getSourceFile(fileName) || null;
22+
23+
if (file === null) {
24+
return [];
25+
}
26+
27+
const node = findFirstMatchingNode(file, {
28+
filter: (n): n is tss.Identifier =>
29+
tss.isIdentifier(n) && start >= n.getStart() && start <= n.getEnd(),
30+
});
31+
const parent = node?.parent || null;
32+
33+
if (node === null || parent === null) {
34+
return [];
35+
}
36+
37+
if (isFullyUnusedArray(node, parent)) {
38+
return [
39+
{
40+
fixName: FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
41+
fixId: FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
42+
fixAllDescription: `Remove all unused imports`,
43+
description: `Remove all unused imports`,
44+
changes: [
45+
{
46+
fileName,
47+
textChanges: [
48+
{
49+
span: {
50+
start: parent.initializer.getStart(),
51+
length: parent.initializer.getWidth(),
52+
},
53+
newText: '[]',
54+
},
55+
],
56+
},
57+
],
58+
},
59+
];
60+
} else if (tss.isArrayLiteralExpression(parent)) {
61+
const newArray = tss.factory.updateArrayLiteralExpression(
62+
parent,
63+
parent.elements.filter((el) => el !== node),
64+
);
65+
66+
return [
67+
{
68+
fixName: FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
69+
fixId: FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
70+
fixAllDescription: `Remove all unused imports`,
71+
description: `Remove unused import ${node.text}`,
72+
changes: [
73+
{
74+
fileName,
75+
textChanges: [
76+
{
77+
span: {
78+
start: parent.getStart(),
79+
length: parent.getWidth(),
80+
},
81+
newText: tss.createPrinter().printNode(tss.EmitHint.Unspecified, newArray, file),
82+
},
83+
],
84+
},
85+
],
86+
},
87+
];
88+
}
89+
90+
return [];
91+
},
2192
fixIds: [FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS],
2293
getAllCodeActions: ({diagnostics}) => {
2394
const arrayUpdates = new Map<tss.ArrayLiteralExpression, Set<tss.Expression>>();
@@ -42,11 +113,7 @@ export const fixUnusedStandaloneImportsMeta: CodeActionMeta = {
42113
// If the diagnostic is reported on the name of the `imports` array initializer, it means
43114
// that all imports are unused so we can clear the entire array. Otherwise if it's reported
44115
// on a single element, we only have to remove that element.
45-
if (
46-
tss.isPropertyAssignment(parent) &&
47-
parent.name === node &&
48-
tss.isArrayLiteralExpression(parent.initializer)
49-
) {
116+
if (isFullyUnusedArray(node, parent)) {
50117
arraysToClear.add(parent.initializer);
51118
} else if (tss.isArrayLiteralExpression(parent)) {
52119
if (!arrayUpdates.has(parent)) {
@@ -93,3 +160,15 @@ export const fixUnusedStandaloneImportsMeta: CodeActionMeta = {
93160
return {changes};
94161
},
95162
};
163+
164+
/** Checks whether a diagnostic was reported on a node where all imports are unused. */
165+
function isFullyUnusedArray(
166+
node: tss.Node,
167+
parent: tss.Node,
168+
): parent is tss.PropertyAssignment & {initializer: tss.ArrayLiteralExpression} {
169+
return (
170+
tss.isPropertyAssignment(parent) &&
171+
parent.name === node &&
172+
tss.isArrayLiteralExpression(parent.initializer)
173+
);
174+
}

packages/language-service/test/code_fixes_spec.ts

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
10-
import {spawn} from 'child_process';
1110
import ts from 'typescript';
1211

1312
import {FixIdForCodeFixesAll} from '../src/codefixes/utils';
@@ -357,7 +356,7 @@ describe('code fixes', () => {
357356
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
358357
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooModule`, [
359358
[``, `import { BarComponent } from "./bar";`],
360-
[`imp`, `imports: [BarComponent]`],
359+
[`imports: []`, `imports: [BarComponent]`],
361360
]);
362361
});
363362

@@ -527,7 +526,7 @@ describe('code fixes', () => {
527526
]);
528527
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
529528
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooComponent`, [
530-
[`{te`, `BarComponent, { test }`],
529+
[`{test}`, `BarComponent, { test }`],
531530
[``, `, imports: [BarComponent]`],
532531
]);
533532
});
@@ -573,7 +572,77 @@ describe('code fixes', () => {
573572
});
574573

575574
describe('unused standalone imports', () => {
576-
it('should fix imports array where some imports are not used', () => {
575+
it('should fix single diagnostic about individual imports that are not used', () => {
576+
const files = {
577+
'app.ts': `
578+
import {Component, Directive} from '@angular/core';
579+
580+
@Directive({selector: '[used]', standalone: true})
581+
export class UsedDirective {}
582+
583+
@Directive({selector: '[unused]', standalone: true})
584+
export class UnusedDirective {}
585+
586+
@Component({
587+
template: '<span used></span>',
588+
standalone: true,
589+
imports: [UnusedDirective, UsedDirective],
590+
})
591+
export class AppComponent {}
592+
`,
593+
};
594+
595+
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
596+
const fixFile = project.openFile('app.ts');
597+
fixFile.moveCursorToText('Unused¦Directive,');
598+
599+
const diags = project.getDiagnosticsForFile('app.ts');
600+
const codeActions = project.getCodeFixesAtPosition('app.ts', fixFile.cursor, fixFile.cursor, [
601+
diags[0].code,
602+
]);
603+
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
604+
605+
actionChangesMatch(actionChanges, 'Remove unused import UnusedDirective', [
606+
['[UnusedDirective, UsedDirective]', '[UsedDirective]'],
607+
]);
608+
});
609+
610+
it('should fix single diagnostic about all imports that are not used', () => {
611+
const files = {
612+
'app.ts': `
613+
import {Component, Directive, Pipe} from '@angular/core';
614+
615+
@Directive({selector: '[unused]', standalone: true})
616+
export class UnusedDirective {}
617+
618+
@Pipe({name: 'unused', standalone: true})
619+
export class UnusedPipe {}
620+
621+
@Component({
622+
template: '',
623+
standalone: true,
624+
imports: [UnusedDirective, UnusedPipe],
625+
})
626+
export class AppComponent {}
627+
`,
628+
};
629+
630+
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
631+
const fixFile = project.openFile('app.ts');
632+
fixFile.moveCursorToText('impo¦rts:');
633+
634+
const diags = project.getDiagnosticsForFile('app.ts');
635+
const codeActions = project.getCodeFixesAtPosition('app.ts', fixFile.cursor, fixFile.cursor, [
636+
diags[0].code,
637+
]);
638+
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
639+
640+
actionChangesMatch(actionChanges, 'Remove all unused imports', [
641+
['[UnusedDirective, UnusedPipe]', '[]'],
642+
]);
643+
});
644+
645+
it('should fix all imports arrays where some imports are not used', () => {
577646
const files = {
578647
'app.ts': `
579648
import {Component, Directive, Pipe} from '@angular/core';
@@ -627,7 +696,7 @@ describe('code fixes', () => {
627696
});
628697
});
629698

630-
it('should fix imports array where all imports are not used', () => {
699+
it('should fix all imports arrays where all imports are not used', () => {
631700
const files = {
632701
'app.ts': `
633702
import {Component, Directive, Pipe} from '@angular/core';
@@ -697,7 +766,7 @@ function allChangesForCodeActions(
697766
for (const action of codeActions) {
698767
const actionChanges = action.changes.flatMap((change) => {
699768
return change.textChanges.map((tc) => {
700-
const oldText = collapse(fileContents.slice(tc.span.start, tc.span.start + spawn.length));
769+
const oldText = collapse(fileContents.slice(tc.span.start, tc.span.start + tc.span.length));
701770
const newText = collapse(tc.newText);
702771
return [oldText, newText] as const;
703772
});

0 commit comments

Comments
 (0)