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

Skip to content

Commit 8da9fb4

Browse files
crisbetoAndrewKushnir
authored andcommitted
feat(language-service): add code fix for unused standalone imports (#57605)
Adds an automatic code fix to the language service that will remove unused standalone imports. PR Close #57605
1 parent a2e4ee0 commit 8da9fb4

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
lines changed

‎packages/language-service/src/codefixes/all_codefixes_metas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
import {fixInvalidBananaInBoxMeta} from './fix_invalid_banana_in_box';
1010
import {missingImportMeta} from './fix_missing_import';
1111
import {missingMemberMeta} from './fix_missing_member';
12+
import {fixUnusedStandaloneImportsMeta} from './fix_unused_standalone_imports';
1213
import {CodeActionMeta} from './utils';
1314

1415
export const ALL_CODE_FIXES_METAS: CodeActionMeta[] = [
1516
missingMemberMeta,
1617
fixInvalidBananaInBoxMeta,
1718
missingImportMeta,
19+
fixUnusedStandaloneImportsMeta,
1820
];
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
10+
import tss from 'typescript';
11+
12+
import {CodeActionMeta, FixIdForCodeFixesAll} from './utils';
13+
import {findFirstMatchingNode} from '../utils/ts_utils';
14+
15+
/**
16+
* Fix for [unused standalone imports](https://angular.io/extended-diagnostics/NG8113)
17+
*/
18+
export const fixUnusedStandaloneImportsMeta: CodeActionMeta = {
19+
errorCodes: [ngErrorCode(ErrorCode.UNUSED_STANDALONE_IMPORTS)],
20+
getCodeActions: () => [],
21+
fixIds: [FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS],
22+
getAllCodeActions: ({diagnostics}) => {
23+
const changes: tss.FileTextChanges[] = [];
24+
25+
for (const diag of diagnostics) {
26+
const {start, length, file, relatedInformation} = diag;
27+
if (file === undefined || start === undefined || length == undefined) {
28+
continue;
29+
}
30+
31+
const node = findFirstMatchingNode(file, {
32+
filter: (current): current is tss.ArrayLiteralExpression =>
33+
current.getStart() === start &&
34+
current.getWidth() === length &&
35+
tss.isArrayLiteralExpression(current),
36+
});
37+
38+
if (node === null) {
39+
continue;
40+
}
41+
42+
let newText: string;
43+
44+
// If `relatedInformation` is empty, it means that all the imports are unused.
45+
// Replace the array with an empty array.
46+
if (relatedInformation === undefined || relatedInformation.length === 0) {
47+
newText = '[]';
48+
} else {
49+
// Otherwise each `relatedInformation` entry points to an unused import that should be
50+
// filtered out. We make a set of ranges corresponding to nodes which will be deleted and
51+
// remove all nodes that belong to the set.
52+
const excludeRanges = new Set(
53+
relatedInformation.map((info) => `${info.start}-${info.length}`),
54+
);
55+
const newArray = tss.factory.updateArrayLiteralExpression(
56+
node,
57+
node.elements.filter((el) => !excludeRanges.has(`${el.getStart()}-${el.getWidth()}`)),
58+
);
59+
60+
newText = tss.createPrinter().printNode(tss.EmitHint.Unspecified, newArray, file);
61+
}
62+
63+
changes.push({
64+
fileName: file.fileName,
65+
textChanges: [
66+
{
67+
span: {start, length},
68+
newText,
69+
},
70+
],
71+
});
72+
}
73+
74+
return {changes};
75+
},
76+
};

‎packages/language-service/src/codefixes/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,5 @@ export enum FixIdForCodeFixesAll {
137137
FIX_MISSING_MEMBER = 'fixMissingMember',
138138
FIX_INVALID_BANANA_IN_BOX = 'fixInvalidBananaInBox',
139139
FIX_MISSING_IMPORT = 'fixMissingImport',
140+
FIX_UNUSED_STANDALONE_IMPORTS = 'fixUnusedStandaloneImports',
140141
}

‎packages/language-service/test/code_fixes_spec.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,98 @@ describe('code fixes', () => {
570570
]);
571571
});
572572
});
573+
574+
describe('unused standalone imports', () => {
575+
it('should fix imports array where some imports are not used', () => {
576+
const files = {
577+
'app.ts': `
578+
import {Component, Directive, Pipe} 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+
@Pipe({name: 'unused', standalone: true})
587+
export class UnusedPipe {}
588+
589+
@Component({
590+
selector: 'used-cmp',
591+
standalone: true,
592+
template: '',
593+
})
594+
export class UsedComponent {}
595+
596+
@Component({
597+
template: \`
598+
<section>
599+
<div></div>
600+
<span used></span>
601+
<div>
602+
<used-cmp/>
603+
</div>
604+
</section>
605+
\`,
606+
standalone: true,
607+
imports: [UnusedDirective, UsedDirective, UnusedPipe, UsedComponent],
608+
})
609+
export class AppComponent {}
610+
`,
611+
};
612+
613+
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
614+
const appFile = project.openFile('app.ts');
615+
616+
const fixesAllActions = project.getCombinedCodeFix(
617+
'app.ts',
618+
FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
619+
);
620+
expectIncludeReplacementTextForFileTextChange({
621+
fileTextChanges: fixesAllActions.changes,
622+
content: appFile.contents,
623+
text: '[UnusedDirective, UsedDirective, UnusedPipe, UsedComponent]',
624+
newText: '[UsedDirective, UsedComponent]',
625+
fileName: 'app.ts',
626+
});
627+
});
628+
629+
it('should fix imports array where all imports are not used', () => {
630+
const files = {
631+
'app.ts': `
632+
import {Component, Directive, Pipe} from '@angular/core';
633+
634+
@Directive({selector: '[unused]', standalone: true})
635+
export class UnusedDirective {}
636+
637+
@Pipe({name: 'unused', standalone: true})
638+
export class UnusedPipe {}
639+
640+
@Component({
641+
template: '',
642+
standalone: true,
643+
imports: [UnusedDirective, UnusedPipe],
644+
})
645+
export class AppComponent {}
646+
`,
647+
};
648+
649+
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
650+
const appFile = project.openFile('app.ts');
651+
652+
const fixesAllActions = project.getCombinedCodeFix(
653+
'app.ts',
654+
FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
655+
);
656+
expectIncludeReplacementTextForFileTextChange({
657+
fileTextChanges: fixesAllActions.changes,
658+
content: appFile.contents,
659+
text: '[UnusedDirective, UnusedPipe]',
660+
newText: '[]',
661+
fileName: 'app.ts',
662+
});
663+
});
664+
});
573665
});
574666

575667
type ActionChanges = {

0 commit comments

Comments
 (0)