Skip to content

Commit 4ae384f

Browse files
dylhunnAndrewKushnir
authored andcommitted
feat(language-service): Allow auto-imports of a pipe via quick fix when its selector is used, both directly and via reexports. (#48354)
A previous PR introduced a new compiler abstraction that tracks *all* known exports and re-exports of Angular traits. This PR harnesses that abstraction in the language service, in order to allow automatic imports of pipes. PR Close #48354
1 parent 1413334 commit 4ae384f

File tree

2 files changed

+133
-21
lines changed

2 files changed

+133
-21
lines changed

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

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ASTWithName} from '@angular/compiler';
910
import {ErrorCode as NgCompilerErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics/index';
10-
import {PotentialDirective, PotentialImport, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
11+
import {PotentialDirective, PotentialPipe} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
1112
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
1213
import ts from 'typescript';
1314

@@ -19,6 +20,7 @@ import {CodeActionContext, CodeActionMeta, FixIdForCodeFixesAll} from './utils';
1920

2021
const errorCodes: number[] = [
2122
ngErrorCode(NgCompilerErrorCode.SCHEMA_INVALID_ELEMENT),
23+
ngErrorCode(NgCompilerErrorCode.MISSING_PIPE),
2224
];
2325

2426
/**
@@ -40,43 +42,44 @@ function getCodeActions(
4042
{templateInfo, start, compiler, formatOptions, preferences, errorCode, tsLs}:
4143
CodeActionContext) {
4244
let codeActions: ts.CodeFixAction[] = [];
43-
4445
const checker = compiler.getTemplateTypeChecker();
4546
const tsChecker = compiler.programDriver.getProgram().getTypeChecker();
4647

47-
// The error must be an invalid element in tag, which is interpreted as an intended selector.
4848
const target = getTargetAtPosition(templateInfo.template, start);
49-
if (target === null || target.context.kind !== TargetNodeKind.ElementInTagContext ||
50-
target.context.node instanceof t.Template) {
49+
if (target === null) {
5150
return [];
5251
}
53-
const missingElement = target.context.node;
5452

55-
const importOn = standaloneTraitOrNgModule(checker, templateInfo.component);
56-
if (importOn === null) {
53+
let matches: Set<PotentialDirective>|Set<PotentialPipe>;
54+
if (target.context.kind === TargetNodeKind.ElementInTagContext &&
55+
target.context.node instanceof t.Element) {
56+
const allPossibleDirectives = checker.getPotentialTemplateDirectives(templateInfo.component);
57+
matches = getDirectiveMatchesForElementTag(target.context.node, allPossibleDirectives);
58+
} else if (
59+
target.context.kind === TargetNodeKind.RawExpression &&
60+
target.context.node instanceof ASTWithName) {
61+
const name = (target.context.node as any).name;
62+
const allPossiblePipes = checker.getPotentialPipes(templateInfo.component);
63+
matches = new Set(allPossiblePipes.filter(p => p.name === name));
64+
} else {
5765
return [];
5866
}
5967

6068
// Find all possible importable directives with a matching selector.
61-
const allPossibleDirectives = checker.getPotentialTemplateDirectives(templateInfo.component);
62-
const matchingDirectives =
63-
getDirectiveMatchesForElementTag(missingElement, allPossibleDirectives);
64-
const matches = matchingDirectives.values();
65-
66-
for (let currMatch of matches) {
67-
const currMatchSymbol = currMatch.tsSymbol.valueDeclaration;
69+
const importOn = standaloneTraitOrNgModule(checker, templateInfo.component);
70+
if (importOn === null) {
71+
return [];
72+
}
73+
for (const currMatch of matches.values()) {
74+
const currMatchSymbol = currMatch.tsSymbol.valueDeclaration!;
6875
const potentialImports = checker.getPotentialImportsFor(currMatch, importOn);
6976
for (let potentialImport of potentialImports) {
7077
let [fileImportChanges, importName] = updateImportsForTypescriptFile(
7178
tsChecker, importOn.getSourceFile(), potentialImport, currMatchSymbol.getSourceFile());
79+
// Always update the trait import, although the TS import might already be present.
7280
let traitImportChanges = updateImportsForAngularTrait(checker, importOn, importName);
73-
// All quick fixes should always update the trait import; however, the TypeScript import might
74-
// already be present.
75-
if (traitImportChanges.length === 0) {
76-
continue;
77-
}
81+
if (traitImportChanges.length === 0) continue;
7882

79-
// Create a code action for this import.
8083
let description = `Import ${importName}`;
8184
if (potentialImport.moduleSpecifier !== undefined) {
8285
description += ` from '${potentialImport.moduleSpecifier}' on ${importOn.name!.text}`;

packages/language-service/test/code_fixes_spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,115 @@ describe('code fixes', () => {
365365
]
366366
]);
367367
});
368+
369+
it('for a new standalone pipe import', () => {
370+
const standaloneFiles = {
371+
'foo.ts': `
372+
import {Component} from '@angular/core';
373+
@Component({
374+
selector: 'foo',
375+
template: '{{"hello"|bar}}',
376+
standalone: true
377+
})
378+
export class FooComponent {}
379+
`,
380+
'bar.ts': `
381+
import {Pipe} from '@angular/core';
382+
@Pipe({
383+
name: 'bar',
384+
standalone: true
385+
})
386+
export class BarPipe implements PipeTransform {
387+
transform(value: unknown, ...args: unknown[]): unknown {
388+
return null;
389+
}
390+
}
391+
`,
392+
};
393+
394+
const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
395+
const diags = project.getDiagnosticsForFile('foo.ts');
396+
const fixFile = project.openFile('foo.ts');
397+
fixFile.moveCursorToText('"hello"|b¦ar');
398+
399+
const codeActions =
400+
project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [diags[0].code]);
401+
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
402+
403+
actionChangesMatch(actionChanges, `Import BarPipe from './bar' on FooComponent`, [
404+
[
405+
``,
406+
`import { BarPipe } from "./bar";`,
407+
],
408+
[
409+
'{',
410+
`{ selector: 'foo', template: '{{"hello"|bar}}', standalone: true, imports: [BarPipe] }`,
411+
]
412+
]);
413+
});
414+
415+
it('for a transitive NgModule-based reexport', () => {
416+
const standaloneFiles = {
417+
'foo.ts': `
418+
import {Component} from '@angular/core';
419+
@Component({
420+
selector: 'foo',
421+
template: '<bar></bar>',
422+
standalone: true
423+
})
424+
export class FooComponent {}
425+
`,
426+
'bar.ts': `
427+
import {Component, NgModule} from '@angular/core';
428+
@Component({
429+
selector: 'bar',
430+
template: '<div>bar</div>',
431+
})
432+
export class BarComponent {}
433+
@NgModule({
434+
declarations: [BarComponent],
435+
exports: [BarComponent],
436+
imports: []
437+
})
438+
export class BarModule {}
439+
@NgModule({
440+
declarations: [],
441+
exports: [BarModule],
442+
imports: []
443+
})
444+
export class Bar2Module {}
445+
`,
446+
};
447+
448+
const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
449+
const diags = project.getDiagnosticsForFile('foo.ts');
450+
const fixFile = project.openFile('foo.ts');
451+
fixFile.moveCursorToText('<¦bar>');
452+
453+
const codeActions =
454+
project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [diags[0].code]);
455+
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
456+
actionChangesMatch(actionChanges, `Import BarModule from './bar' on FooComponent`, [
457+
[
458+
``,
459+
`import { BarModule } from "./bar";`,
460+
],
461+
[
462+
`{`,
463+
`{ selector: 'foo', template: '<bar></bar>', standalone: true, imports: [BarModule] }`,
464+
]
465+
]);
466+
actionChangesMatch(actionChanges, `Import Bar2Module from './bar' on FooComponent`, [
467+
[
468+
``,
469+
`import { Bar2Module } from "./bar";`,
470+
],
471+
[
472+
`{`,
473+
`{ selector: 'foo', template: '<bar></bar>', standalone: true, imports: [Bar2Module] }`,
474+
]
475+
]);
476+
});
368477
});
369478
});
370479

0 commit comments

Comments
 (0)