Skip to content

Commit 8df8c77

Browse files
dylhunnthePunderWoman
authored andcommitted
refactor(compiler): Add getPotentialImportsFor method on template type checker (#47631)
`getPotentialImportsFor` returns an array of possible imports, including TypeScript module specifier and identifier name, for a requested trait in the context of a given component. PR Close #47631
1 parent 0035ccf commit 8df8c77

File tree

4 files changed

+155
-5
lines changed

4 files changed

+155
-5
lines changed

packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {ErrorCode} from '../../diagnostics';
1414

1515
import {FullTemplateMapping, NgTemplateDiagnostic, TypeCheckableDirectiveMeta} from './api';
1616
import {GlobalCompletion} from './completion';
17-
import {PotentialDirective, PotentialPipe} from './scope';
17+
import {PotentialDirective, PotentialImport, PotentialPipe} from './scope';
1818
import {ElementSymbol, Symbol, TcbLocation, TemplateSymbol} from './symbols';
1919

2020
/**
@@ -146,6 +146,12 @@ export interface TemplateTypeChecker {
146146
*/
147147
getPotentialElementTags(component: ts.ClassDeclaration): Map<string, PotentialDirective|null>;
148148

149+
/**
150+
* In the context of an Angular trait, generate potential imports for a directive.
151+
*/
152+
getPotentialImportsFor(directive: PotentialDirective, inComponent: ts.ClassDeclaration):
153+
ReadonlyArray<PotentialImport>;
154+
149155
/**
150156
* Get the primary decorator for an Angular class (such as @Component). This does not work for
151157
* `@Injectable`.

packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@ import {EmittedReference, Reference} from '../../imports';
1212
import {ClassDeclaration} from '../../reflection';
1313
import {SymbolWithValueDeclaration} from '../../util/src/typescript';
1414

15+
/**
16+
* A PotentialImport for some Angular trait has a TypeScript module specifier, which can be
17+
* relative, as well as an identifier name.
18+
*/
19+
export interface PotentialImport {
20+
kind: PotentialImportKind;
21+
moduleSpecifier: string;
22+
symbolName: string;
23+
}
24+
25+
/**
26+
* Which kind of Angular Trait the import targets.
27+
*/
28+
export enum PotentialImportKind {
29+
NgModule,
30+
Standalone,
31+
}
32+
1533
/**
1634
* Metadata on a directive which is available in a template.
1735
*/

packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts

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

9-
import {AST, CssSelector, DomElementSchemaRegistry, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler';
9+
import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler';
1010
import ts from 'typescript';
1111

1212
import {ErrorCode, ngErrorCode} from '../../diagnostics';
1313
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
14-
import {Reference, ReferenceEmitter} from '../../imports';
14+
import {Reference, ReferenceEmitKind, ReferenceEmitter} from '../../imports';
1515
import {IncrementalBuild} from '../../incremental/api';
16-
import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind} from '../../metadata';
16+
import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleMeta} from '../../metadata';
1717
import {PerfCheckpoint, PerfEvent, PerfPhase, PerfRecorder} from '../../perf';
1818
import {ProgramDriver, UpdateMode} from '../../program_driver';
1919
import {ClassDeclaration, DeclarationNode, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
2020
import {ComponentScopeKind, ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope';
2121
import {isShim} from '../../shims';
2222
import {getSourceFileOrNull, isSymbolWithValueDeclaration} from '../../util/src/typescript';
23-
import {ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PotentialDirective, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api';
23+
import {ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PotentialDirective, PotentialImport, PotentialImportKind, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api';
2424
import {makeTemplateDiagnostic} from '../diagnostics';
2525

2626
import {CompletionEngine} from './completion';
@@ -673,6 +673,39 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
673673
return scope.ngModule;
674674
}
675675

676+
getPotentialImportsFor(toImport: PotentialDirective, inContext: ts.ClassDeclaration):
677+
ReadonlyArray<PotentialImport> {
678+
// Look up the original reference associated with the trait's ngModule, so we don't lose the
679+
// Reference context (such as identifiers). If the trait is standalone, this will be
680+
// `undefined`.
681+
let ngModuleRef: Reference<ClassDeclaration<DeclarationNode>>|undefined;
682+
if (toImport.ngModule !== null) {
683+
ngModuleRef = this.metaReader.getNgModuleMetadata(new Reference(toImport.ngModule))?.ref;
684+
}
685+
686+
// Import the ngModule if one exists. Otherwise, import the standalone trait directly.
687+
const importTarget = ngModuleRef ?? toImport.ref;
688+
689+
// Using the compiler's ReferenceEmitter, try to emit a reference to the trait.
690+
// TODO(dylhunn): In the future, we can use a more sophisticated strategy for generating and
691+
// ranking references, such as keeping a record of import specifiers used in existing code.
692+
const emittedRef = this.refEmitter.emit(importTarget, inContext.getSourceFile());
693+
if (emittedRef.kind === ReferenceEmitKind.Failed) return [];
694+
695+
// The resulting import expression should have a module name and an identifier name.
696+
const emittedExpression: ExternalExpr = emittedRef.expression as ExternalExpr;
697+
if (emittedExpression.value.moduleName === null || emittedExpression.value.name === null)
698+
return [];
699+
700+
// Extract and return the TS module and identifier names.
701+
const preferredImport: PotentialImport = {
702+
kind: ngModuleRef ? PotentialImportKind.NgModule : PotentialImportKind.Standalone,
703+
moduleSpecifier: emittedExpression.value.moduleName,
704+
symbolName: emittedExpression.value.name,
705+
};
706+
return [preferredImport];
707+
}
708+
676709
private getScopeData(component: ts.ClassDeclaration): ScopeData|null {
677710
if (this.scopeCache.has(component)) {
678711
return this.scopeCache.get(component)!;

packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,5 +201,98 @@ runInEachFileSystem(() => {
201201
expect(directives.map(d => d.selector)).toContain('two-cmp');
202202
});
203203
});
204+
205+
describe('can generate imports` ', () => {
206+
it('for out of scope standalone components', () => {
207+
env.write('one.ts', `
208+
import {Component} from '@angular/core';
209+
210+
@Component({
211+
standalone: true,
212+
selector: 'one-cmp',
213+
template: '<div></div>',
214+
})
215+
export class OneCmp {}
216+
`);
217+
218+
env.write('two.ts', `
219+
import {Component} from '@angular/core';
220+
221+
@Component({
222+
standalone: true,
223+
selector: 'two-cmp',
224+
template: '<div></div>',
225+
})
226+
export class TwoCmp {}
227+
`);
228+
const {program, checker} = env.driveTemplateTypeChecker();
229+
const sfOne = program.getSourceFile(_('/one.ts'));
230+
expect(sfOne).not.toBeNull();
231+
const OneCmpClass = getClass(sfOne!, 'OneCmp');
232+
233+
const TwoCmpDir = checker.getPotentialTemplateDirectives(OneCmpClass)
234+
.filter(d => d.selector === 'two-cmp')[0];
235+
const imports = checker.getPotentialImportsFor(TwoCmpDir, OneCmpClass);
236+
237+
expect(imports.length).toBe(1);
238+
expect(imports[0].moduleSpecifier).toBe('./two');
239+
expect(imports[0].symbolName).toBe('TwoCmp');
240+
});
241+
242+
it('for out of scope ngModules', () => {
243+
env.write('one.ts', `
244+
import {Component} from '@angular/core';
245+
246+
@Component({
247+
standalone: true,
248+
selector: 'one-cmp',
249+
template: '<div></div>',
250+
})
251+
export class OneCmp {}
252+
`);
253+
254+
env.write('two.ts', `
255+
import {Component} from '@angular/core';
256+
257+
@Component({
258+
selector: 'two-cmp',
259+
template: '<div></div>',
260+
})
261+
export class TwoCmp {}
262+
`);
263+
264+
env.write('twomod.ts', `
265+
import { NgModule } from '@angular/core';
266+
import { CommonModule } from '@angular/common';
267+
import { TwoCmp } from './two';
268+
269+
@NgModule({
270+
declarations: [
271+
TwoCmp
272+
],
273+
exports: [
274+
TwoCmp
275+
],
276+
imports: [
277+
CommonModule
278+
]
279+
})
280+
export class TwoModule { }
281+
`);
282+
283+
const {program, checker} = env.driveTemplateTypeChecker();
284+
const sfOne = program.getSourceFile(_('/one.ts'));
285+
expect(sfOne).not.toBeNull();
286+
const OneCmpClass = getClass(sfOne!, 'OneCmp');
287+
288+
const TwoNgMod = checker.getPotentialTemplateDirectives(OneCmpClass)
289+
.filter(d => d.selector === 'two-cmp')[0];
290+
const imports = checker.getPotentialImportsFor(TwoNgMod, OneCmpClass);
291+
292+
expect(imports.length).toBe(1);
293+
expect(imports[0].moduleSpecifier).toBe('./twomod');
294+
expect(imports[0].symbolName).toBe('TwoModule');
295+
});
296+
});
204297
});
205298
});

0 commit comments

Comments
 (0)