Skip to content

Commit

Permalink
feat(language-service): add code fix for unused standalone imports (a…
Browse files Browse the repository at this point in the history
…ngular#57605)

Adds an automatic code fix to the language service that will remove unused standalone imports.

PR Close angular#57605
  • Loading branch information
crisbeto authored and AndrewKushnir committed Sep 3, 2024
1 parent a2e4ee0 commit 8da9fb4
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import {fixInvalidBananaInBoxMeta} from './fix_invalid_banana_in_box';
import {missingImportMeta} from './fix_missing_import';
import {missingMemberMeta} from './fix_missing_member';
import {fixUnusedStandaloneImportsMeta} from './fix_unused_standalone_imports';
import {CodeActionMeta} from './utils';

export const ALL_CODE_FIXES_METAS: CodeActionMeta[] = [
missingMemberMeta,
fixInvalidBananaInBoxMeta,
missingImportMeta,
fixUnusedStandaloneImportsMeta,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
import tss from 'typescript';

import {CodeActionMeta, FixIdForCodeFixesAll} from './utils';
import {findFirstMatchingNode} from '../utils/ts_utils';

/**
* Fix for [unused standalone imports](https://angular.io/extended-diagnostics/NG8113)
*/
export const fixUnusedStandaloneImportsMeta: CodeActionMeta = {
errorCodes: [ngErrorCode(ErrorCode.UNUSED_STANDALONE_IMPORTS)],
getCodeActions: () => [],
fixIds: [FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS],
getAllCodeActions: ({diagnostics}) => {
const changes: tss.FileTextChanges[] = [];

for (const diag of diagnostics) {
const {start, length, file, relatedInformation} = diag;
if (file === undefined || start === undefined || length == undefined) {
continue;
}

const node = findFirstMatchingNode(file, {
filter: (current): current is tss.ArrayLiteralExpression =>
current.getStart() === start &&
current.getWidth() === length &&
tss.isArrayLiteralExpression(current),
});

if (node === null) {
continue;
}

let newText: string;

// If `relatedInformation` is empty, it means that all the imports are unused.
// Replace the array with an empty array.
if (relatedInformation === undefined || relatedInformation.length === 0) {
newText = '[]';
} else {
// Otherwise each `relatedInformation` entry points to an unused import that should be
// filtered out. We make a set of ranges corresponding to nodes which will be deleted and
// remove all nodes that belong to the set.
const excludeRanges = new Set(
relatedInformation.map((info) => `${info.start}-${info.length}`),
);
const newArray = tss.factory.updateArrayLiteralExpression(
node,
node.elements.filter((el) => !excludeRanges.has(`${el.getStart()}-${el.getWidth()}`)),
);

newText = tss.createPrinter().printNode(tss.EmitHint.Unspecified, newArray, file);
}

changes.push({
fileName: file.fileName,
textChanges: [
{
span: {start, length},
newText,
},
],
});
}

return {changes};
},
};
1 change: 1 addition & 0 deletions packages/language-service/src/codefixes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,5 @@ export enum FixIdForCodeFixesAll {
FIX_MISSING_MEMBER = 'fixMissingMember',
FIX_INVALID_BANANA_IN_BOX = 'fixInvalidBananaInBox',
FIX_MISSING_IMPORT = 'fixMissingImport',
FIX_UNUSED_STANDALONE_IMPORTS = 'fixUnusedStandaloneImports',
}
92 changes: 92 additions & 0 deletions packages/language-service/test/code_fixes_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,98 @@ describe('code fixes', () => {
]);
});
});

describe('unused standalone imports', () => {
it('should fix imports array where some imports are not used', () => {
const files = {
'app.ts': `
import {Component, Directive, Pipe} from '@angular/core';
@Directive({selector: '[used]', standalone: true})
export class UsedDirective {}
@Directive({selector: '[unused]', standalone: true})
export class UnusedDirective {}
@Pipe({name: 'unused', standalone: true})
export class UnusedPipe {}
@Component({
selector: 'used-cmp',
standalone: true,
template: '',
})
export class UsedComponent {}
@Component({
template: \`
<section>
<div></div>
<span used></span>
<div>
<used-cmp/>
</div>
</section>
\`,
standalone: true,
imports: [UnusedDirective, UsedDirective, UnusedPipe, UsedComponent],
})
export class AppComponent {}
`,
};

const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const appFile = project.openFile('app.ts');

const fixesAllActions = project.getCombinedCodeFix(
'app.ts',
FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
);
expectIncludeReplacementTextForFileTextChange({
fileTextChanges: fixesAllActions.changes,
content: appFile.contents,
text: '[UnusedDirective, UsedDirective, UnusedPipe, UsedComponent]',
newText: '[UsedDirective, UsedComponent]',
fileName: 'app.ts',
});
});

it('should fix imports array where all imports are not used', () => {
const files = {
'app.ts': `
import {Component, Directive, Pipe} from '@angular/core';
@Directive({selector: '[unused]', standalone: true})
export class UnusedDirective {}
@Pipe({name: 'unused', standalone: true})
export class UnusedPipe {}
@Component({
template: '',
standalone: true,
imports: [UnusedDirective, UnusedPipe],
})
export class AppComponent {}
`,
};

const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const appFile = project.openFile('app.ts');

const fixesAllActions = project.getCombinedCodeFix(
'app.ts',
FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
);
expectIncludeReplacementTextForFileTextChange({
fileTextChanges: fixesAllActions.changes,
content: appFile.contents,
text: '[UnusedDirective, UnusedPipe]',
newText: '[]',
fileName: 'app.ts',
});
});
});
});

type ActionChanges = {
Expand Down

0 comments on commit 8da9fb4

Please sign in to comment.