mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(compiler): rework and expose APIs to be used in schematics (#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
This commit is contained in:
committed by
Andrew Kushnir
parent
5f21c6d627
commit
6beff5e8d7
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'};
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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)!;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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';
|
||||
@@ -34,7 +35,7 @@ runInEachFileSystem(() => {
|
||||
it('for components', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'test-cmp',
|
||||
@@ -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 {
|
||||
@@ -70,7 +71,7 @@ runInEachFileSystem(() => {
|
||||
it('for NgModules', () => {
|
||||
env.write('test.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
@@ -91,7 +92,7 @@ runInEachFileSystem(() => {
|
||||
it('for components', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppCmp],
|
||||
imports: [],
|
||||
@@ -99,7 +100,7 @@ runInEachFileSystem(() => {
|
||||
bootstrap: [AppCmp]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: '<div></div>',
|
||||
@@ -118,7 +119,7 @@ runInEachFileSystem(() => {
|
||||
it('for standalone components (which should be null)', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppCmp],
|
||||
imports: [],
|
||||
@@ -126,7 +127,7 @@ runInEachFileSystem(() => {
|
||||
bootstrap: [AppCmp]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: '<div></div>',
|
||||
@@ -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 {
|
||||
@@ -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',
|
||||
@@ -186,7 +187,7 @@ runInEachFileSystem(() => {
|
||||
|
||||
env.write('two.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'two-cmp',
|
||||
@@ -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,
|
||||
@@ -217,7 +218,7 @@ runInEachFileSystem(() => {
|
||||
|
||||
env.write('two.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'two-cmp',
|
||||
@@ -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',
|
||||
@@ -248,7 +249,7 @@ runInEachFileSystem(() => {
|
||||
|
||||
env.write('two.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'two-cmp',
|
||||
@@ -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');
|
||||
@@ -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',
|
||||
@@ -284,7 +286,7 @@ runInEachFileSystem(() => {
|
||||
|
||||
env.write('two.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'two-cmp',
|
||||
template: '<div></div>',
|
||||
@@ -296,7 +298,7 @@ runInEachFileSystem(() => {
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TwoCmp } from './two';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TwoCmp
|
||||
@@ -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');
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {ASTWithName} from '@angular/compiler';
|
||||
import {ErrorCode as NgCompilerErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics/index';
|
||||
import {PotentialDirective, PotentialPipe} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||
import {PotentialDirective, PotentialImportMode, PotentialPipe} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
|
||||
import ts from 'typescript';
|
||||
|
||||
@@ -72,7 +72,8 @@ function getCodeActions(
|
||||
}
|
||||
for (const currMatch of matches.values()) {
|
||||
const currMatchSymbol = currMatch.tsSymbol.valueDeclaration!;
|
||||
const potentialImports = checker.getPotentialImportsFor(currMatch, importOn);
|
||||
const potentialImports =
|
||||
checker.getPotentialImportsFor(currMatch.ref, importOn, PotentialImportMode.Normal);
|
||||
for (let potentialImport of potentialImports) {
|
||||
let [fileImportChanges, importName] = updateImportsForTypescriptFile(
|
||||
tsChecker, importOn.getSourceFile(), potentialImport, currMatchSymbol.getSourceFile());
|
||||
|
||||
Reference in New Issue
Block a user