From 8df8c77915d3f9313010348094776de2065cc689 Mon Sep 17 00:00:00 2001 From: Dylan Hunn Date: Tue, 4 Oct 2022 17:00:54 -0700 Subject: [PATCH] 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 --- .../src/ngtsc/typecheck/api/checker.ts | 8 +- .../src/ngtsc/typecheck/api/scope.ts | 18 ++++ .../src/ngtsc/typecheck/src/checker.ts | 41 +++++++- .../test/ngtsc/ls_typecheck_helpers_spec.ts | 93 +++++++++++++++++++ 4 files changed, 155 insertions(+), 5 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index cb5d1b28c30c6..678abf8a9d135 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -14,7 +14,7 @@ import {ErrorCode} from '../../diagnostics'; import {FullTemplateMapping, NgTemplateDiagnostic, TypeCheckableDirectiveMeta} from './api'; import {GlobalCompletion} from './completion'; -import {PotentialDirective, PotentialPipe} from './scope'; +import {PotentialDirective, PotentialImport, PotentialPipe} from './scope'; import {ElementSymbol, Symbol, TcbLocation, TemplateSymbol} from './symbols'; /** @@ -146,6 +146,12 @@ export interface TemplateTypeChecker { */ getPotentialElementTags(component: ts.ClassDeclaration): Map; + /** + * In the context of an Angular trait, generate potential imports for a directive. + */ + getPotentialImportsFor(directive: PotentialDirective, inComponent: ts.ClassDeclaration): + ReadonlyArray; + /** * Get the primary decorator for an Angular class (such as @Component). This does not work for * `@Injectable`. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts index 2e5434e61eca8..cc3faf62f88c4 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts @@ -12,6 +12,24 @@ import {EmittedReference, Reference} from '../../imports'; import {ClassDeclaration} from '../../reflection'; import {SymbolWithValueDeclaration} from '../../util/src/typescript'; +/** + * A PotentialImport for some Angular trait has a TypeScript module specifier, which can be + * relative, as well as an identifier name. + */ +export interface PotentialImport { + kind: PotentialImportKind; + moduleSpecifier: string; + symbolName: string; +} + +/** + * Which kind of Angular Trait the import targets. + */ +export enum PotentialImportKind { + NgModule, + Standalone, +} + /** * Metadata on a directive which is available in a template. */ diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index d273fef1040c5..9788fb26faebe 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -6,21 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, CssSelector, DomElementSchemaRegistry, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler'; +import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, ngErrorCode} from '../../diagnostics'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; -import {Reference, ReferenceEmitter} from '../../imports'; +import {Reference, ReferenceEmitKind, ReferenceEmitter} from '../../imports'; import {IncrementalBuild} from '../../incremental/api'; -import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind} from '../../metadata'; +import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleMeta} from '../../metadata'; import {PerfCheckpoint, PerfEvent, PerfPhase, PerfRecorder} from '../../perf'; import {ProgramDriver, UpdateMode} from '../../program_driver'; import {ClassDeclaration, DeclarationNode, isNamedClassDeclaration, ReflectionHost} from '../../reflection'; import {ComponentScopeKind, ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope'; import {isShim} from '../../shims'; import {getSourceFileOrNull, isSymbolWithValueDeclaration} from '../../util/src/typescript'; -import {ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PotentialDirective, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api'; +import {ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PotentialDirective, PotentialImport, PotentialImportKind, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api'; import {makeTemplateDiagnostic} from '../diagnostics'; import {CompletionEngine} from './completion'; @@ -673,6 +673,39 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return scope.ngModule; } + getPotentialImportsFor(toImport: PotentialDirective, inContext: ts.ClassDeclaration): + ReadonlyArray { + // Look up the original reference associated with the trait's ngModule, so we don't lose the + // Reference context (such as identifiers). If the trait is standalone, this will be + // `undefined`. + let ngModuleRef: Reference>|undefined; + if (toImport.ngModule !== null) { + ngModuleRef = this.metaReader.getNgModuleMetadata(new Reference(toImport.ngModule))?.ref; + } + + // Import the ngModule if one exists. Otherwise, import the standalone trait directly. + const importTarget = ngModuleRef ?? toImport.ref; + + // Using the compiler's ReferenceEmitter, try to emit a reference to the trait. + // TODO(dylhunn): In the future, we can use a more sophisticated strategy for generating and + // ranking references, such as keeping a record of import specifiers used in existing code. + const emittedRef = this.refEmitter.emit(importTarget, inContext.getSourceFile()); + if (emittedRef.kind === ReferenceEmitKind.Failed) return []; + + // The resulting import expression should have a module name and an identifier name. + const emittedExpression: ExternalExpr = emittedRef.expression as ExternalExpr; + if (emittedExpression.value.moduleName === null || emittedExpression.value.name === null) + return []; + + // Extract and return the TS module and identifier names. + const preferredImport: PotentialImport = { + kind: ngModuleRef ? PotentialImportKind.NgModule : PotentialImportKind.Standalone, + moduleSpecifier: emittedExpression.value.moduleName, + symbolName: emittedExpression.value.name, + }; + return [preferredImport]; + } + private getScopeData(component: ts.ClassDeclaration): ScopeData|null { if (this.scopeCache.has(component)) { return this.scopeCache.get(component)!; diff --git a/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts b/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts index a0e9aa06b8ca9..13776c8beac9d 100644 --- a/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts @@ -201,5 +201,98 @@ runInEachFileSystem(() => { expect(directives.map(d => d.selector)).toContain('two-cmp'); }); }); + + describe('can generate imports` ', () => { + it('for out of scope standalone components', () => { + env.write('one.ts', ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + selector: 'one-cmp', + template: '
', + }) + export class OneCmp {} + `); + + env.write('two.ts', ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + selector: 'two-cmp', + template: '
', + }) + export class TwoCmp {} + `); + const {program, checker} = env.driveTemplateTypeChecker(); + const sfOne = program.getSourceFile(_('/one.ts')); + expect(sfOne).not.toBeNull(); + const OneCmpClass = getClass(sfOne!, 'OneCmp'); + + const TwoCmpDir = checker.getPotentialTemplateDirectives(OneCmpClass) + .filter(d => d.selector === 'two-cmp')[0]; + const imports = checker.getPotentialImportsFor(TwoCmpDir, OneCmpClass); + + expect(imports.length).toBe(1); + expect(imports[0].moduleSpecifier).toBe('./two'); + expect(imports[0].symbolName).toBe('TwoCmp'); + }); + + it('for out of scope ngModules', () => { + env.write('one.ts', ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + selector: 'one-cmp', + template: '
', + }) + export class OneCmp {} + `); + + env.write('two.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'two-cmp', + template: '
', + }) + export class TwoCmp {} + `); + + env.write('twomod.ts', ` + import { NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + import { TwoCmp } from './two'; + + @NgModule({ + declarations: [ + TwoCmp + ], + exports: [ + TwoCmp + ], + imports: [ + CommonModule + ] + }) + export class TwoModule { } + `); + + const {program, checker} = env.driveTemplateTypeChecker(); + const sfOne = program.getSourceFile(_('/one.ts')); + expect(sfOne).not.toBeNull(); + const OneCmpClass = getClass(sfOne!, 'OneCmp'); + + const TwoNgMod = checker.getPotentialTemplateDirectives(OneCmpClass) + .filter(d => d.selector === 'two-cmp')[0]; + const imports = checker.getPotentialImportsFor(TwoNgMod, OneCmpClass); + + expect(imports.length).toBe(1); + expect(imports[0].moduleSpecifier).toBe('./twomod'); + expect(imports[0].symbolName).toBe('TwoModule'); + }); + }); }); });