Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/language-service/src/codefixes/code_fixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class CodeFixes {
*/
getCodeFixesAtPosition(
fileName: string,
templateInfo: TemplateInfo,
templateInfo: TemplateInfo | null,
compiler: NgCompiler,
start: number,
end: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {CodeActionMeta, FixIdForCodeFixesAll} from './utils';
export const fixInvalidBananaInBoxMeta: CodeActionMeta = {
errorCodes: [ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX)],
getCodeActions({start, fileName, templateInfo}) {
const boundEvent = getTheBoundEventAtPosition(templateInfo, start);
const boundEvent =
templateInfo === null ? null : getTheBoundEventAtPosition(templateInfo, start);
if (boundEvent === null) {
return [];
}
Expand Down
16 changes: 5 additions & 11 deletions packages/language-service/src/codefixes/fix_missing_import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,13 @@ export const missingImportMeta: CodeActionMeta = {
},
};

function getCodeActions({
templateInfo,
start,
compiler,
formatOptions,
preferences,
errorCode,
tsLs,
}: CodeActionContext) {
function getCodeActions({templateInfo, start, compiler}: CodeActionContext) {
if (templateInfo === null) {
return [];
}

let codeActions: ts.CodeFixAction[] = [];
const checker = compiler.getTemplateTypeChecker();
const tsChecker = compiler.programDriver.getProgram().getTypeChecker();

const target = getTargetAtPosition(templateInfo.template, start);
if (target === null) {
return [];
Expand Down
9 changes: 3 additions & 6 deletions packages/language-service/src/codefixes/fix_missing_member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@

import tss from 'typescript';

import {
getTargetAtPosition,
getTcbNodesOfTemplateAtPosition,
TargetNodeKind,
} from '../template_target';
import {getTcbNodesOfTemplateAtPosition} from '../template_target';
import {getTemplateInfoAtPosition} from '../utils';

import {CodeActionMeta, convertFileTextChangeInTcb, FixIdForCodeFixesAll} from './utils';
Expand All @@ -37,7 +33,8 @@ export const missingMemberMeta: CodeActionMeta = {
errorCode,
tsLs,
}) {
const tcbNodesInfo = getTcbNodesOfTemplateAtPosition(templateInfo, start, compiler);
const tcbNodesInfo =
templateInfo === null ? null : getTcbNodesOfTemplateAtPosition(templateInfo, start, compiler);
if (tcbNodesInfo === null) {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,78 @@ import {findFirstMatchingNode} from '../utils/ts_utils';
*/
export const fixUnusedStandaloneImportsMeta: CodeActionMeta = {
errorCodes: [ngErrorCode(ErrorCode.UNUSED_STANDALONE_IMPORTS)],
getCodeActions: () => [],
getCodeActions: ({start, fileName, compiler}) => {
const file = compiler.programDriver.getProgram().getSourceFile(fileName) || null;

if (file === null) {
return [];
}

const node = findFirstMatchingNode(file, {
filter: (n): n is tss.Identifier =>
tss.isIdentifier(n) && start >= n.getStart() && start <= n.getEnd(),
});
const parent = node?.parent || null;

if (node === null || parent === null) {
return [];
}

if (isFullyUnusedArray(node, parent)) {
return [
{
fixName: FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
fixId: FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
fixAllDescription: `Remove all unused imports`,
description: `Remove all unused imports`,
changes: [
{
fileName,
textChanges: [
{
span: {
start: parent.initializer.getStart(),
length: parent.initializer.getWidth(),
},
newText: '[]',
},
],
},
],
},
];
} else if (tss.isArrayLiteralExpression(parent)) {
const newArray = tss.factory.updateArrayLiteralExpression(
parent,
parent.elements.filter((el) => el !== node),
);

return [
{
fixName: FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
fixId: FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
fixAllDescription: `Remove all unused imports`,
description: `Remove unused import ${node.text}`,
changes: [
{
fileName,
textChanges: [
{
span: {
start: parent.getStart(),
length: parent.getWidth(),
},
newText: tss.createPrinter().printNode(tss.EmitHint.Unspecified, newArray, file),
},
],
},
],
},
];
}

return [];
},
fixIds: [FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS],
getAllCodeActions: ({diagnostics}) => {
const arrayUpdates = new Map<tss.ArrayLiteralExpression, Set<tss.Expression>>();
Expand All @@ -42,11 +113,7 @@ export const fixUnusedStandaloneImportsMeta: CodeActionMeta = {
// If the diagnostic is reported on the name of the `imports` array initializer, it means
// that all imports are unused so we can clear the entire array. Otherwise if it's reported
// on a single element, we only have to remove that element.
if (
tss.isPropertyAssignment(parent) &&
parent.name === node &&
tss.isArrayLiteralExpression(parent.initializer)
) {
if (isFullyUnusedArray(node, parent)) {
arraysToClear.add(parent.initializer);
} else if (tss.isArrayLiteralExpression(parent)) {
if (!arrayUpdates.has(parent)) {
Expand Down Expand Up @@ -93,3 +160,15 @@ export const fixUnusedStandaloneImportsMeta: CodeActionMeta = {
return {changes};
},
};

/** Checks whether a diagnostic was reported on a node where all imports are unused. */
function isFullyUnusedArray(
node: tss.Node,
parent: tss.Node,
): parent is tss.PropertyAssignment & {initializer: tss.ArrayLiteralExpression} {
return (
tss.isPropertyAssignment(parent) &&
parent.name === node &&
tss.isArrayLiteralExpression(parent.initializer)
);
}
2 changes: 1 addition & 1 deletion packages/language-service/src/codefixes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {TemplateInfo} from '../utils';
* context will be provided to the `CodeActionMeta` which could handle the `errorCode`.
*/
export interface CodeActionContext {
templateInfo: TemplateInfo;
templateInfo: TemplateInfo | null;
fileName: string;
compiler: NgCompiler;
start: number;
Expand Down
6 changes: 1 addition & 5 deletions packages/language-service/src/language_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,17 +404,13 @@ export class LanguageService {
return [];
}

const templateInfo = getTemplateInfoAtPosition(fileName, start, compiler);
if (templateInfo === undefined) {
return [];
}
const diags = this.getSemanticDiagnostics(fileName);
if (diags.length === 0) {
return [];
}
return this.codeFixes.getCodeFixesAtPosition(
fileName,
templateInfo,
getTemplateInfoAtPosition(fileName, start, compiler) ?? null,
compiler,
start,
end,
Expand Down
81 changes: 75 additions & 6 deletions packages/language-service/test/code_fixes_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

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

import {FixIdForCodeFixesAll} from '../src/codefixes/utils';
Expand Down Expand Up @@ -357,7 +356,7 @@ describe('code fixes', () => {
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooModule`, [
[``, `import { BarComponent } from "./bar";`],
[`imp`, `imports: [BarComponent]`],
[`imports: []`, `imports: [BarComponent]`],
]);
});

Expand Down Expand Up @@ -527,7 +526,7 @@ describe('code fixes', () => {
]);
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooComponent`, [
[`{te`, `BarComponent, { test }`],
[`{test}`, `BarComponent, { test }`],
[``, `, imports: [BarComponent]`],
]);
});
Expand Down Expand Up @@ -573,7 +572,77 @@ describe('code fixes', () => {
});

describe('unused standalone imports', () => {
it('should fix imports array where some imports are not used', () => {
it('should fix single diagnostic about individual imports that are not used', () => {
const files = {
'app.ts': `
import {Component, Directive} from '@angular/core';

@Directive({selector: '[used]', standalone: true})
export class UsedDirective {}

@Directive({selector: '[unused]', standalone: true})
export class UnusedDirective {}

@Component({
template: '<span used></span>',
standalone: true,
imports: [UnusedDirective, UsedDirective],
})
export class AppComponent {}
`,
};

const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const fixFile = project.openFile('app.ts');
fixFile.moveCursorToText('Unused¦Directive,');

const diags = project.getDiagnosticsForFile('app.ts');
const codeActions = project.getCodeFixesAtPosition('app.ts', fixFile.cursor, fixFile.cursor, [
diags[0].code,
]);
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);

actionChangesMatch(actionChanges, 'Remove unused import UnusedDirective', [
['[UnusedDirective, UsedDirective]', '[UsedDirective]'],
]);
});

it('should fix single diagnostic about all imports that 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 fixFile = project.openFile('app.ts');
fixFile.moveCursorToText('impo¦rts:');

const diags = project.getDiagnosticsForFile('app.ts');
const codeActions = project.getCodeFixesAtPosition('app.ts', fixFile.cursor, fixFile.cursor, [
diags[0].code,
]);
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);

actionChangesMatch(actionChanges, 'Remove all unused imports', [
['[UnusedDirective, UnusedPipe]', '[]'],
]);
});

it('should fix all imports arrays where some imports are not used', () => {
const files = {
'app.ts': `
import {Component, Directive, Pipe} from '@angular/core';
Expand Down Expand Up @@ -627,7 +696,7 @@ describe('code fixes', () => {
});
});

it('should fix imports array where all imports are not used', () => {
it('should fix all imports arrays where all imports are not used', () => {
const files = {
'app.ts': `
import {Component, Directive, Pipe} from '@angular/core';
Expand Down Expand Up @@ -697,7 +766,7 @@ function allChangesForCodeActions(
for (const action of codeActions) {
const actionChanges = action.changes.flatMap((change) => {
return change.textChanges.map((tc) => {
const oldText = collapse(fileContents.slice(tc.span.start, tc.span.start + spawn.length));
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note the typo here was spawn.length instead of tc.span.length which meant that the assertion was always incorrect. I've fixed it plus a handful of tests that weren't asserting properly.

const oldText = collapse(fileContents.slice(tc.span.start, tc.span.start + tc.span.length));
const newText = collapse(tc.newText);
return [oldText, newText] as const;
});
Expand Down