Skip to content

Commit

Permalink
refactor(compiler): introduce internal transplanted type (#50104)
Browse files Browse the repository at this point in the history
Adds a new AST for a `TransplantedType` in the compiler which will be used for some upcoming work. A transplanted type is a type node that is defined in one place in the app, but needs to be copied to a different one (e.g. the generated .d.ts). These changes also include updates to the type translator that will rewrite any type references within the type to point to the new context file.

PR Close #50104
  • Loading branch information
crisbeto authored and alxhub committed May 9, 2023
1 parent bc65112 commit 25756e8
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 20 deletions.
5 changes: 3 additions & 2 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,8 +625,9 @@ export class NgCompiler {

const afterDeclarations: ts.TransformerFactory<ts.SourceFile>[] = [];
if (compilation.dtsTransforms !== null) {
afterDeclarations.push(
declarationTransformFactory(compilation.dtsTransforms, importRewriter));
afterDeclarations.push(declarationTransformFactory(
compilation.dtsTransforms, compilation.reflector, compilation.refEmitter,
importRewriter));
}

// Only add aliasing re-exports to the .d.ts output if the `AliasingHost` requests it.
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler-cli/src/ngtsc/transform/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import {ConstantPool, Expression, Statement, Type} from '@angular/compiler';
import ts from 'typescript';

import {Reexport} from '../../imports';
import {Reexport, ReferenceEmitter} from '../../imports';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {IndexingContext} from '../../indexer';
import {ClassDeclaration, Decorator} from '../../reflection';
import {ClassDeclaration, Decorator, ReflectionHost} from '../../reflection';
import {ImportManager} from '../../translator';
import {TypeCheckContext} from '../../typecheck/api';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
Expand Down Expand Up @@ -278,5 +278,6 @@ export interface DtsTransform {
(element: ts.FunctionDeclaration, imports: ImportManager): ts.FunctionDeclaration;
transformClass?
(clazz: ts.ClassDeclaration, elements: ReadonlyArray<ts.ClassElement>,
reflector: ReflectionHost, refEmitter: ReferenceEmitter,
imports: ImportManager): ts.ClassDeclaration;
}
20 changes: 13 additions & 7 deletions packages/compiler-cli/src/ngtsc/transform/src/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import {Type} from '@angular/compiler';
import ts from 'typescript';

import {ImportRewriter} from '../../imports';
import {ClassDeclaration} from '../../reflection';
import {ImportRewriter, ReferenceEmitter} from '../../imports';
import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {ImportManager, translateType} from '../../translator';

import {DtsTransform} from './api';
Expand Down Expand Up @@ -54,10 +54,12 @@ export class DtsTransformRegistry {
}

export function declarationTransformFactory(
transformRegistry: DtsTransformRegistry, importRewriter: ImportRewriter,
transformRegistry: DtsTransformRegistry, reflector: ReflectionHost,
refEmitter: ReferenceEmitter, importRewriter: ImportRewriter,
importPrefix?: string): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
const transformer = new DtsTransformer(context, importRewriter, importPrefix);
const transformer =
new DtsTransformer(context, reflector, refEmitter, importRewriter, importPrefix);
return (fileOrBundle) => {
if (ts.isBundle(fileOrBundle)) {
// Only attempt to transform source files.
Expand All @@ -77,7 +79,8 @@ export function declarationTransformFactory(
*/
class DtsTransformer {
constructor(
private ctx: ts.TransformationContext, private importRewriter: ImportRewriter,
private ctx: ts.TransformationContext, private reflector: ReflectionHost,
private refEmitter: ReferenceEmitter, private importRewriter: ImportRewriter,
private importPrefix?: string) {}

/**
Expand Down Expand Up @@ -133,7 +136,8 @@ class DtsTransformer {
// not yet been incorporated. Otherwise, `newClazz.members` holds the latest class members.
const inputMembers = (clazz === newClazz ? elements : newClazz.members);

newClazz = transform.transformClass(newClazz, inputMembers, imports);
newClazz = transform.transformClass(
newClazz, inputMembers, this.reflector, this.refEmitter, imports);
}
}

Expand Down Expand Up @@ -181,6 +185,7 @@ export class IvyDeclarationDtsTransform implements DtsTransform {

transformClass(
clazz: ts.ClassDeclaration, members: ReadonlyArray<ts.ClassElement>,
reflector: ReflectionHost, refEmitter: ReferenceEmitter,
imports: ImportManager): ts.ClassDeclaration {
const original = ts.getOriginalNode(clazz) as ClassDeclaration;

Expand All @@ -191,7 +196,8 @@ export class IvyDeclarationDtsTransform implements DtsTransform {

const newMembers = fields.map(decl => {
const modifiers = [ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)];
const typeRef = translateType(decl.type, imports);
const typeRef =
translateType(decl.type, original.getSourceFile(), reflector, refEmitter, imports);
markForEmitAsSingleLine(typeRef);
return ts.factory.createPropertyDeclaration(
/* modifiers */ modifiers,
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/translator/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ts_library(
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",
],
Expand Down
91 changes: 87 additions & 4 deletions packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@
import * as o from '@angular/compiler';
import ts from 'typescript';

import {assertSuccessfulReferenceEmit, ImportFlags, Reference, ReferenceEmitter} from '../../imports';
import {ReflectionHost} from '../../reflection';

import {Context} from './context';
import {ImportManager} from './import_manager';


export function translateType(type: o.Type, imports: ImportManager): ts.TypeNode {
return type.visitType(new TypeTranslatorVisitor(imports), new Context(false));
export function translateType(
type: o.Type, contextFile: ts.SourceFile, reflector: ReflectionHost,
refEmitter: ReferenceEmitter, imports: ImportManager): ts.TypeNode {
return type.visitType(
new TypeTranslatorVisitor(imports, contextFile, reflector, refEmitter), new Context(false));
}

export class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
constructor(private imports: ImportManager) {}
class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
constructor(
private imports: ImportManager, private contextFile: ts.SourceFile,
private reflector: ReflectionHost, private refEmitter: ReferenceEmitter) {}

visitBuiltinType(type: o.BuiltinType, context: Context): ts.KeywordTypeNode {
switch (type.name) {
Expand Down Expand Up @@ -71,6 +79,14 @@ export class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor
return ts.factory.createTypeLiteralNode([indexSignature]);
}

visitTransplantedType(ast: o.TransplantedType<ts.Node>, context: any) {
if (!ts.isTypeNode(ast.type)) {
throw new Error(`A TransplantedType must wrap a TypeNode`);
}

return this.translateTransplantedTypeNode(ast.type, context);
}

visitReadVarExpr(ast: o.ReadVarExpr, context: Context): ts.TypeQueryNode {
if (ast.name === null) {
throw new Error(`ReadVarExpr with no variable name in type`);
Expand Down Expand Up @@ -228,4 +244,71 @@ export class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor
}
return typeNode;
}

/**
* Translates a type reference node so that all of its references
* are imported into the context file.
*/
private translateTransplantedTypeReferenceNode(
node: ts.TypeReferenceNode&{typeName: ts.Identifier}, context: any): ts.TypeReferenceNode {
const declaration = this.reflector.getDeclarationOfIdentifier(node.typeName);

if (declaration === null) {
throw new Error(
`Unable to statically determine the declaration file of type node ${node.typeName.text}`);
}

const emittedType = this.refEmitter.emit(
new Reference(declaration.node), this.contextFile,
ImportFlags.NoAliasing | ImportFlags.AllowTypeImports |
ImportFlags.AllowRelativeDtsImports);

assertSuccessfulReferenceEmit(emittedType, node, 'type');

const result = emittedType.expression.visitExpression(this, context);

if (!ts.isTypeReferenceNode(result)) {
throw new Error(`Expected TypeReferenceNode when referencing the type for ${
node.typeName.text}, but received ${ts.SyntaxKind[result.kind]}`);
}

// If the original node doesn't have any generic parameters we return the results.
if (node.typeArguments === undefined || node.typeArguments.length === 0) {
return result;
}

// If there are any generics, we have to reflect them as well.
const translatedArgs =
node.typeArguments.map(arg => this.translateTransplantedTypeNode(arg, context));

return ts.factory.updateTypeReferenceNode(
result, result.typeName, ts.factory.createNodeArray(translatedArgs));
}

/**
* Translates a type node so that all of the type references it
* contains are imported and can be referenced in the context file.
*/
private translateTransplantedTypeNode(rootNode: ts.TypeNode, context: any): ts.TypeNode {
const factory: ts.TransformerFactory<ts.Node> = transformContext => root => {
const walk = (node: ts.Node): ts.Node => {
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
const translated =
this.translateTransplantedTypeReferenceNode(node as ts.TypeReferenceNode & {
typeName: ts.Identifier;
}, context);

if (translated !== node) {
return translated;
}
}

return ts.visitEachChild(node, walk, transformContext);
};

return ts.visitNode(root, walk);
};

return ts.transform(rootNode, [factory]).transformed[0] as ts.TypeNode;
}
}
20 changes: 16 additions & 4 deletions packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ExpressionType, ExternalExpr, Type, TypeModifier} from '@angular/compiler';
import {ExpressionType, ExternalExpr, TransplantedType, Type, TypeModifier} from '@angular/compiler';
import ts from 'typescript';

import {assertSuccessfulReferenceEmit, ImportFlags, Reference, ReferenceEmitKind, ReferenceEmitter} from '../../imports';
Expand Down Expand Up @@ -149,7 +149,9 @@ export class Environment implements ReferenceEmitEnvironment {

// Create an `ExpressionType` from the `Expression` and translate it via `translateType`.
// TODO(alxhub): support references to types with generic arguments in a clean way.
return translateType(new ExpressionType(ngExpr.expression), this.importManager);
return translateType(
new ExpressionType(ngExpr.expression), this.contextFile, this.reflector, this.refEmitter,
this.importManager);
}

private emitTypeParameters(declaration: ClassDeclaration<ts.ClassDeclaration>):
Expand All @@ -167,8 +169,18 @@ export class Environment implements ReferenceEmitEnvironment {
referenceExternalType(moduleName: string, name: string, typeParams?: Type[]): ts.TypeNode {
const external = new ExternalExpr({moduleName, name});
return translateType(
new ExpressionType(external, /* modifiers */ TypeModifier.None, typeParams),
this.importManager);
new ExpressionType(external, TypeModifier.None, typeParams), this.contextFile,
this.reflector, this.refEmitter, this.importManager);
}

/**
* Generates a `ts.TypeNode` representing a type that is being referenced from a different place
* in the program. Any type references inside the transplanted type will be rewritten so that
* they can be imported in the context fiel.
*/
referenceTransplantedType(type: TransplantedType<ts.TypeNode>): ts.TypeNode {
return translateType(
type, this.contextFile, this.reflector, this.refEmitter, this.importManager);
}

getPreludeStatements(): ts.Statement[] {
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export * from './ml_parser/tags';
export {ParseTreeResult, TreeError} from './ml_parser/parser';
export {LexerRange} from './ml_parser/lexer';
export * from './ml_parser/xml_parser';
export {ArrayType, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, NONE_TYPE, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, TaggedTemplateExpr, TemplateLiteral, TemplateLiteralElement, Type, TypeModifier, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, STRING_TYPE, TypeofExpr, jsDocComment, leadingComment, LeadingComment, JSDocComment, UnaryOperator, UnaryOperatorExpr, LocalizedString} from './output/output_ast';
export {ArrayType, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, NONE_TYPE, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, TaggedTemplateExpr, TemplateLiteral, TemplateLiteralElement, Type, TypeModifier, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, STRING_TYPE, TypeofExpr, jsDocComment, leadingComment, LeadingComment, JSDocComment, UnaryOperator, UnaryOperatorExpr, LocalizedString, TransplantedType} from './output/output_ast';
export {EmitterVisitorContext} from './output/abstract_emitter';
export {JitEvaluator} from './output/output_jit';
export * from './parse_util';
Expand Down
19 changes: 19 additions & 0 deletions packages/compiler/src/output/output_ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ export class MapType extends Type {
}
}


export class TransplantedType<T> extends Type {
constructor(readonly type: T, modifiers?: TypeModifier) {
super(modifiers);
}
override visitType(visitor: TypeVisitor, context: any): any {
return visitor.visitTransplantedType(this, context);
}
}


export const DYNAMIC_TYPE = new BuiltinType(BuiltinTypeName.Dynamic);
export const INFERRED_TYPE = new BuiltinType(BuiltinTypeName.Inferred);
export const BOOL_TYPE = new BuiltinType(BuiltinTypeName.Bool);
Expand All @@ -92,6 +103,7 @@ export interface TypeVisitor {
visitExpressionType(type: ExpressionType, context: any): any;
visitArrayType(type: ArrayType, context: any): any;
visitMapType(type: MapType, context: any): any;
visitTransplantedType(type: TransplantedType<unknown>, context: any): any;
}

///// Expressions
Expand Down Expand Up @@ -1111,6 +1123,9 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor
visitMapType(type: MapType, context: any): any {
return this.visitType(type, context);
}
visitTransplantedType(type: TransplantedType<unknown>, context: any): any {
return type;
}
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any): any {
return ast;
}
Expand Down Expand Up @@ -1276,6 +1291,10 @@ export function expressionType(
return new ExpressionType(expr, typeModifiers, typeParams);
}

export function transplantedType<T>(type: T, typeModifiers?: TypeModifier): TransplantedType<T> {
return new TransplantedType(type, typeModifiers);
}

export function typeofExpr(expr: Expression) {
return new TypeofExpr(expr);
}
Expand Down

0 comments on commit 25756e8

Please sign in to comment.