Skip to content

Commit

Permalink
refactor(compiler): rework and expose APIs to be used in schematics (#…
Browse files Browse the repository at this point in the history
…48730)

Reworks some of the existing compiler APIs to make them easier to use in a schematic and exposes a few new ones to surface information we already had. High-level list of changes:
* `getPotentialImportsFor` now requires a class reference, instead of a `PotentialDirective | PotentialPipe`.
* New `getNgModuleMetadata` method has been added to the type checker.
* New `getPipeMetadata` method has been added to the type checker.
* New `getUsedDirectives` method has been added to the type checker.
* New `getUsedPipes` method has been added to the type checker.
* The `decorator` property was exposed on the `TypeCheckableDirectiveMeta`. The property was already present at runtime, but it wasn't specified on the interface.

PR Close #48730
  • Loading branch information
crisbeto authored and AndrewKushnir committed Jan 13, 2023
1 parent 5f21c6d commit 6beff5e
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 38 deletions.
1 change: 1 addition & 0 deletions packages/compiler-cli/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/sourcemaps",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/transformers/downlevel_decorators_transform",
"@npm//typescript",
],
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/private/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export {forwardRefResolver} from '../src/ngtsc/annotations';
export {Reference} from '../src/ngtsc/imports';
export {DynamicValue, PartialEvaluator, ResolvedValue, ResolvedValueMap, StaticInterpreter} from '../src/ngtsc/partial_evaluator';
export {reflectObjectLiteral, TypeScriptReflectionHost} from '../src/ngtsc/reflection';
export {PotentialImport, PotentialImportKind, PotentialImportMode, TemplateTypeChecker} from '../src/ngtsc/typecheck/api';
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType
outputs: ClassPropertyMapping;
isStandalone: boolean;
hostDirectives: HostDirectiveMeta[]|null;
decorator: ts.Decorator|null;
}

export type TemplateId = string&{__brand: 'TemplateId'};
Expand Down
29 changes: 26 additions & 3 deletions packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import ts from 'typescript';

import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
import {ErrorCode} from '../../diagnostics';
import {Reference} from '../../imports';
import {NgModuleMeta, PipeMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';

import {FullTemplateMapping, NgTemplateDiagnostic, TypeCheckableDirectiveMeta} from './api';
import {GlobalCompletion} from './completion';
import {PotentialDirective, PotentialImport, PotentialPipe} from './scope';
import {PotentialDirective, PotentialImport, PotentialImportMode, PotentialPipe} from './scope';
import {ElementSymbol, Symbol, TcbLocation, TemplateSymbol} from './symbols';

/**
Expand Down Expand Up @@ -150,8 +153,8 @@ export interface TemplateTypeChecker {
* In the context of an Angular trait, generate potential imports for a directive.
*/
getPotentialImportsFor(
toImport: PotentialDirective|PotentialPipe,
inComponent: ts.ClassDeclaration): ReadonlyArray<PotentialImport>;
toImport: Reference<ClassDeclaration>, inComponent: ts.ClassDeclaration,
importMode: PotentialImportMode): ReadonlyArray<PotentialImport>;

/**
* Get the primary decorator for an Angular class (such as @Component). This does not work for
Expand Down Expand Up @@ -184,6 +187,26 @@ export interface TemplateTypeChecker {
*/
getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null;

/**
* Retrieve the type checking engine's metadata for the given NgModule class, if available.
*/
getNgModuleMetadata(module: ts.ClassDeclaration): NgModuleMeta|null;

/**
* Retrieve the type checking engine's metadata for the given pipe class, if available.
*/
getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta|null;

/**
* Gets the directives that have been used in a component's template.
*/
getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[]|null;

/**
* Gets the pipes that have been used in a component's template.
*/
getUsedPipes(component: ts.ClassDeclaration): string[]|null;

/**
* Reset the `TemplateTypeChecker`'s state for the given class, so that it will be recomputed on
* the next request.
Expand Down
15 changes: 15 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,18 @@ export interface PotentialPipe {
*/
isInScope: boolean;
}

/**
* Possible modes in which to look up a potential import.
*/
export enum PotentialImportMode {
/** Whether an import is standalone is inferred based on its metadata. */
Normal,

/**
* An import is assumed to be standalone and is imported directly. This is useful for migrations
* where a declaration wasn't standalone when the program was created, but will become standalone
* as a part of the migration.
*/
ForceDirect,
}
44 changes: 33 additions & 11 deletions packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/

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

import {ErrorCode, ngErrorCode} from '../../diagnostics';
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
import {Reference, ReferenceEmitKind, ReferenceEmitter} from '../../imports';
import {IncrementalBuild} from '../../incremental/api';
import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleIndex, PipeMeta} from '../../metadata';
import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleIndex, NgModuleMeta, PipeMeta} 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, PotentialImport, PotentialImportKind, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api';
import {ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PotentialDirective, PotentialImport, PotentialImportKind, PotentialImportMode, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';

import {CompletionEngine} from './completion';
Expand Down Expand Up @@ -99,6 +99,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
return data.template;
}

getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[]|null {
return this.getLatestComponentState(component).data?.boundTarget.getUsedDirectives() || null;
}

getUsedPipes(component: ts.ClassDeclaration): string[]|null {
return this.getLatestComponentState(component).data?.boundTarget.getUsedPipes() || null;
}

private getLatestComponentState(component: ts.ClassDeclaration):
{data: TemplateData|null, tcb: ts.Node|null, tcbPath: AbsoluteFsPath, tcbIsShim: boolean} {
this.ensureShimForComponent(component);
Expand Down Expand Up @@ -598,6 +606,20 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
return this.typeCheckScopeRegistry.getTypeCheckDirectiveMetadata(new Reference(dir));
}

getNgModuleMetadata(module: ts.ClassDeclaration): NgModuleMeta|null {
if (!isNamedClassDeclaration(module)) {
return null;
}
return this.metaReader.getNgModuleMetadata(new Reference(module));
}

getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta|null {
if (!isNamedClassDeclaration(pipe)) {
return null;
}
return this.metaReader.getPipeMetadata(new Reference(pipe));
}

getPotentialElementTags(component: ts.ClassDeclaration): Map<string, PotentialDirective|null> {
if (this.elementTagCache.has(component)) {
return this.elementTagCache.get(component)!;
Expand Down Expand Up @@ -712,18 +734,18 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
}

getPotentialImportsFor(
toImport: PotentialDirective|PotentialPipe,
inContext: ts.ClassDeclaration): ReadonlyArray<PotentialImport> {
toImport: Reference<ClassDeclaration>, inContext: ts.ClassDeclaration,
importMode: PotentialImportMode): ReadonlyArray<PotentialImport> {
const imports: PotentialImport[] = [];

const meta = this.metaReader.getDirectiveMetadata(toImport.ref) ??
this.metaReader.getPipeMetadata(toImport.ref);
const meta =
this.metaReader.getDirectiveMetadata(toImport) ?? this.metaReader.getPipeMetadata(toImport);
if (meta === null) {
return imports;
}

if (meta.isStandalone) {
const emitted = this.emit(PotentialImportKind.Standalone, toImport.ref, inContext);
if (meta.isStandalone || importMode === PotentialImportMode.ForceDirect) {
const emitted = this.emit(PotentialImportKind.Standalone, toImport, inContext);
if (emitted !== null) {
imports.push(emitted);
}
Expand All @@ -732,7 +754,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
const exportingNgModules = this.ngModuleIndex.getNgModulesExporting(meta.ref.node);
if (exportingNgModules !== null) {
for (const exporter of exportingNgModules) {
const emittedRef = this.emit(PotentialImportKind.Standalone, exporter, inContext);
const emittedRef = this.emit(PotentialImportKind.NgModule, exporter, inContext);
if (emittedRef !== null) {
imports.push(emittedRef);
}
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ function getDirectiveMetaFromDeclaration(
isStandalone: !!decl.isStandalone,
baseClass: null,
animationTriggerNames: null,
decorator: null,
hostDirectives: decl.hostDirectives === undefined ? null : decl.hostDirectives.map(hostDecl => {
return {
directive: new Reference(resolveDeclaration(hostDecl.directive)),
Expand Down
43 changes: 23 additions & 20 deletions packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PotentialImportMode} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import ts from 'typescript';

import {DiagnosticCategoryLabel} from '../../src/ngtsc/core/api';
Expand Down Expand Up @@ -34,7 +35,7 @@ runInEachFileSystem(() => {
it('for components', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'test-cmp',
Expand All @@ -52,7 +53,7 @@ runInEachFileSystem(() => {
it('for pipes', () => {
env.write('test.ts', `
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({name: 'expPipe'})
export class ExpPipe implements PipeTransform {
transform(value: number, exponent = 1): number {
Expand All @@ -70,7 +71,7 @@ runInEachFileSystem(() => {
it('for NgModules', () => {
env.write('test.ts', `
import {NgModule} from '@angular/core';
@NgModule({
declarations: [],
imports: [],
Expand All @@ -91,15 +92,15 @@ runInEachFileSystem(() => {
it('for components', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
@NgModule({
declarations: [AppCmp],
imports: [],
providers: [],
bootstrap: [AppCmp]
})
export class AppModule {}
@Component({
selector: 'app-cmp',
template: '<div></div>',
Expand All @@ -118,15 +119,15 @@ runInEachFileSystem(() => {
it('for standalone components (which should be null)', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
@NgModule({
declarations: [AppCmp],
imports: [],
providers: [],
bootstrap: [AppCmp]
})
export class AppModule {}
@Component({
selector: 'app-cmp',
template: '<div></div>',
Expand All @@ -146,14 +147,14 @@ runInEachFileSystem(() => {
it('for pipes', () => {
env.write('test.ts', `
import {Component, NgModule, Pipe, PipeTransform} from '@angular/core';
@NgModule({
declarations: [ExpPipe],
imports: [],
providers: [],
})
export class PipeModule {}
@Pipe({name: 'expPipe'})
export class ExpPipe implements PipeTransform {
transform(value: number, exponent = 1): number {
Expand All @@ -175,7 +176,7 @@ runInEachFileSystem(() => {
it('which are out of scope', () => {
env.write('one.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'one-cmp',
Expand All @@ -186,7 +187,7 @@ runInEachFileSystem(() => {

env.write('two.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'two-cmp',
Expand All @@ -206,7 +207,7 @@ runInEachFileSystem(() => {
it('which are out of scope', () => {
env.write('one.ts', `
import {Pipe} from '@angular/core';
@Pipe({
name: 'foo-pipe',
standalone: true,
Expand All @@ -217,7 +218,7 @@ runInEachFileSystem(() => {

env.write('two.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'two-cmp',
Expand All @@ -237,7 +238,7 @@ runInEachFileSystem(() => {
it('for out of scope standalone components', () => {
env.write('one.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'one-cmp',
Expand All @@ -248,7 +249,7 @@ runInEachFileSystem(() => {

env.write('two.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'two-cmp',
Expand All @@ -263,7 +264,8 @@ runInEachFileSystem(() => {

const TwoCmpDir = checker.getPotentialTemplateDirectives(OneCmpClass)
.filter(d => d.selector === 'two-cmp')[0];
const imports = checker.getPotentialImportsFor(TwoCmpDir, OneCmpClass);
const imports =
checker.getPotentialImportsFor(TwoCmpDir.ref, OneCmpClass, PotentialImportMode.Normal);

expect(imports.length).toBe(1);
expect(imports[0].moduleSpecifier).toBe('./two');
Expand All @@ -273,7 +275,7 @@ runInEachFileSystem(() => {
it('for out of scope ngModules', () => {
env.write('one.ts', `
import {Component} from '@angular/core';
@Component({
standalone: true,
selector: 'one-cmp',
Expand All @@ -284,7 +286,7 @@ runInEachFileSystem(() => {

env.write('two.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'two-cmp',
template: '<div></div>',
Expand All @@ -296,7 +298,7 @@ runInEachFileSystem(() => {
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TwoCmp } from './two';
@NgModule({
declarations: [
TwoCmp
Expand All @@ -318,7 +320,8 @@ runInEachFileSystem(() => {

const TwoNgMod = checker.getPotentialTemplateDirectives(OneCmpClass)
.filter(d => d.selector === 'two-cmp')[0];
const imports = checker.getPotentialImportsFor(TwoNgMod, OneCmpClass);
const imports =
checker.getPotentialImportsFor(TwoNgMod.ref, OneCmpClass, PotentialImportMode.Normal);

expect(imports.length).toBe(1);
expect(imports[0].moduleSpecifier).toBe('./twomod');
Expand Down
2 changes: 0 additions & 2 deletions packages/compiler/src/render3/view/t2_binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,6 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
class TemplateBinder extends RecursiveAstVisitor implements Visitor {
private visitNode: (node: Node) => void;

private pipesUsed: string[] = [];

private constructor(
private bindings: Map<AST, Reference|Variable>,
private symbols: Map<Reference|Variable, Template>, private usedPipes: Set<string>,
Expand Down
Loading

0 comments on commit 6beff5e

Please sign in to comment.