Skip to content

Commit

Permalink
refactor(compiler): Add getPotentialImportsFor method on template t…
Browse files Browse the repository at this point in the history
…ype 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
  • Loading branch information
dylhunn authored and thePunderWoman committed Oct 5, 2022
1 parent 0035ccf commit 8df8c77
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 5 deletions.
8 changes: 7 additions & 1 deletion packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts
Expand Up @@ -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';

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

/**
* In the context of an Angular trait, generate potential imports for a directive.
*/
getPotentialImportsFor(directive: PotentialDirective, inComponent: ts.ClassDeclaration):
ReadonlyArray<PotentialImport>;

/**
* Get the primary decorator for an Angular class (such as @Component). This does not work for
* `@Injectable`.
Expand Down
18 changes: 18 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts
Expand Up @@ -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.
*/
Expand Down
41 changes: 37 additions & 4 deletions packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -673,6 +673,39 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
return scope.ngModule;
}

getPotentialImportsFor(toImport: PotentialDirective, inContext: ts.ClassDeclaration):
ReadonlyArray<PotentialImport> {
// 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<ClassDeclaration<DeclarationNode>>|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)!;
Expand Down
93 changes: 93 additions & 0 deletions packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts
Expand Up @@ -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: '<div></div>',
})
export class OneCmp {}
`);

env.write('two.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'two-cmp',
template: '<div></div>',
})
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: '<div></div>',
})
export class OneCmp {}
`);

env.write('two.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'two-cmp',
template: '<div></div>',
})
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');
});
});
});
});

0 comments on commit 8df8c77

Please sign in to comment.