feat(ivy): first steps towards ngtsc mode (#23455)
This commit adds a new compiler pipeline that isn't dependent on global analysis, referred to as 'ngtsc'. This new compiler is accessed by running ngc with "enableIvy" set to "ngtsc". It reuses the same initialization logic but creates a new implementation of Program which does not perform the global-level analysis that AngularCompilerProgram does. It will be the foundation for the production Ivy compiler. PR Close #23455
This commit is contained in:

committed by
Igor Minar

parent
f567e1898f
commit
ab5bc42da0
16
packages/compiler-cli/src/ngtsc/transform/BUILD.bazel
Normal file
16
packages/compiler-cli/src/ngtsc/transform/BUILD.bazel
Normal file
@ -0,0 +1,16 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "transform",
|
||||
srcs = glob([
|
||||
"index.ts",
|
||||
"src/**/*.ts",
|
||||
]),
|
||||
module_name = "@angular/compiler-cli/src/ngtsc/transform",
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||
],
|
||||
)
|
11
packages/compiler-cli/src/ngtsc/transform/index.ts
Normal file
11
packages/compiler-cli/src/ngtsc/transform/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
export {IvyCompilation} from './src/compilation';
|
||||
export {InjectableCompilerAdapter} from './src/injectable';
|
||||
export {ivyTransformFactory} from './src/transform';
|
61
packages/compiler-cli/src/ngtsc/transform/src/api.ts
Normal file
61
packages/compiler-cli/src/ngtsc/transform/src/api.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {Expression, Type} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Decorator} from '../../metadata';
|
||||
|
||||
/**
|
||||
* Provides the interface between a decorator compiler from @angular/compiler and the Typescript
|
||||
* compiler/transform.
|
||||
*
|
||||
* The decorator compilers in @angular/compiler do not depend on Typescript. The adapter is
|
||||
* responsible for extracting the information required to perform compilation from the decorators
|
||||
* and Typescript source, invoking the decorator compiler, and returning the result.
|
||||
*/
|
||||
export interface CompilerAdapter<A> {
|
||||
/**
|
||||
* Scan a set of reflected decorators and determine if this adapter is responsible for compilation
|
||||
* of one of them.
|
||||
*/
|
||||
detect(decorator: Decorator[]): Decorator|undefined;
|
||||
|
||||
/**
|
||||
* Perform analysis on the decorator/class combination, producing instructions for compilation
|
||||
* if successful, or an array of diagnostic messages if the analysis fails or the decorator
|
||||
* isn't valid.
|
||||
*/
|
||||
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<A>;
|
||||
|
||||
/**
|
||||
* Generate a description of the field which should be added to the class, including any
|
||||
* initialization code to be generated.
|
||||
*/
|
||||
compile(node: ts.ClassDeclaration, analysis: A): AddStaticFieldInstruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* The output of an analysis operation, consisting of possibly an arbitrary analysis object (used as
|
||||
* the input to code generation) and potentially diagnostics if there were errors uncovered during
|
||||
* analysis.
|
||||
*/
|
||||
export interface AnalysisOutput<A> {
|
||||
analysis?: A;
|
||||
diagnostics?: ts.Diagnostic[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A description of the static field to add to a class, including an initialization expression
|
||||
* and a type for the .d.ts file.
|
||||
*/
|
||||
export interface AddStaticFieldInstruction {
|
||||
field: string;
|
||||
initializer: Expression;
|
||||
type: Type;
|
||||
}
|
157
packages/compiler-cli/src/ngtsc/transform/src/compilation.ts
Normal file
157
packages/compiler-cli/src/ngtsc/transform/src/compilation.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {Expression, Type} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Decorator, reflectDecorator} from '../../metadata';
|
||||
|
||||
import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api';
|
||||
import {DtsFileTransformer} from './declaration';
|
||||
import {ImportManager, translateType} from './translator';
|
||||
|
||||
|
||||
/**
|
||||
* Record of an adapter which decided to emit a static field, and the analysis it performed to
|
||||
* prepare for that operation.
|
||||
*/
|
||||
interface EmitFieldOperation<T> {
|
||||
adapter: CompilerAdapter<T>;
|
||||
analysis: AnalysisOutput<T>;
|
||||
decorator: ts.Decorator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages a compilation of Ivy decorators into static fields across an entire ts.Program.
|
||||
*
|
||||
* The compilation is stateful - source files are analyzed and records of the operations that need
|
||||
* to be performed during the transform/emit process are maintained internally.
|
||||
*/
|
||||
export class IvyCompilation {
|
||||
/**
|
||||
* Tracks classes which have been analyzed and found to have an Ivy decorator, and the
|
||||
* information recorded about them for later compilation.
|
||||
*/
|
||||
private analysis = new Map<ts.ClassDeclaration, EmitFieldOperation<any>>();
|
||||
|
||||
/**
|
||||
* Tracks the `DtsFileTransformer`s for each TS file that needs .d.ts transformations.
|
||||
*/
|
||||
private dtsMap = new Map<string, DtsFileTransformer>();
|
||||
|
||||
constructor(private adapters: CompilerAdapter<any>[], private checker: ts.TypeChecker) {}
|
||||
|
||||
/**
|
||||
* Analyze a source file and produce diagnostics for it (if any).
|
||||
*/
|
||||
analyze(sf: ts.SourceFile): ts.Diagnostic[] {
|
||||
const diagnostics: ts.Diagnostic[] = [];
|
||||
const visit = (node: ts.Node) => {
|
||||
// Process nodes recursively, and look for class declarations with decorators.
|
||||
if (ts.isClassDeclaration(node) && node.decorators !== undefined) {
|
||||
// The first step is to reflect the decorators, which will identify decorators
|
||||
// that are imported from another module.
|
||||
const decorators =
|
||||
node.decorators.map(decorator => reflectDecorator(decorator, this.checker))
|
||||
.filter(decorator => decorator !== null) as Decorator[];
|
||||
|
||||
// Look through the CompilerAdapters to see if any are relevant.
|
||||
this.adapters.forEach(adapter => {
|
||||
// An adapter is relevant if it matches one of the decorators on the class.
|
||||
const decorator = adapter.detect(decorators);
|
||||
if (decorator === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for multiple decorators on the same node. Technically speaking this
|
||||
// could be supported, but right now it's an error.
|
||||
if (this.analysis.has(node)) {
|
||||
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
|
||||
}
|
||||
|
||||
// Run analysis on the decorator. This will produce either diagnostics, an
|
||||
// analysis result, or both.
|
||||
const analysis = adapter.analyze(node, decorator);
|
||||
if (analysis.diagnostics !== undefined) {
|
||||
diagnostics.push(...analysis.diagnostics);
|
||||
}
|
||||
if (analysis.analysis !== undefined) {
|
||||
this.analysis.set(node, {
|
||||
adapter,
|
||||
analysis: analysis.analysis,
|
||||
decorator: decorator.node,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sf);
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a compilation operation on the given class declaration and return instructions to an
|
||||
* AST transformer if any are available.
|
||||
*/
|
||||
compileIvyFieldFor(node: ts.ClassDeclaration): AddStaticFieldInstruction|undefined {
|
||||
// Look to see whether the original node was analyzed. If not, there's nothing to do.
|
||||
const original = ts.getOriginalNode(node) as ts.ClassDeclaration;
|
||||
if (!this.analysis.has(original)) {
|
||||
return undefined;
|
||||
}
|
||||
const op = this.analysis.get(original) !;
|
||||
|
||||
// Run the actual compilation, which generates an Expression for the Ivy field.
|
||||
const res = op.adapter.compile(node, op.analysis);
|
||||
|
||||
// Look up the .d.ts transformer for the input file and record that a field was generated,
|
||||
// which will allow the .d.ts to be transformed later.
|
||||
const fileName = node.getSourceFile().fileName;
|
||||
const dtsTransformer = this.getDtsTransformer(fileName);
|
||||
dtsTransformer.recordStaticField(node.name !.text, res);
|
||||
|
||||
// Return the instruction to the transformer so the field will be added.
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the `ts.Decorator` which triggered transformation of a particular class declaration.
|
||||
*/
|
||||
ivyDecoratorFor(node: ts.ClassDeclaration): ts.Decorator|undefined {
|
||||
const original = ts.getOriginalNode(node) as ts.ClassDeclaration;
|
||||
if (!this.analysis.has(original)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.analysis.get(original) !.decorator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a .d.ts source string and return a transformed version that incorporates the changes
|
||||
* made to the source file.
|
||||
*/
|
||||
transformedDtsFor(tsFileName: string, dtsOriginalSource: string): string {
|
||||
// No need to transform if no changes have been requested to the input file.
|
||||
if (!this.dtsMap.has(tsFileName)) {
|
||||
return dtsOriginalSource;
|
||||
}
|
||||
|
||||
// Return the transformed .d.ts source.
|
||||
return this.dtsMap.get(tsFileName) !.transform(dtsOriginalSource);
|
||||
}
|
||||
|
||||
private getDtsTransformer(tsFileName: string): DtsFileTransformer {
|
||||
if (!this.dtsMap.has(tsFileName)) {
|
||||
this.dtsMap.set(tsFileName, new DtsFileTransformer());
|
||||
}
|
||||
return this.dtsMap.get(tsFileName) !;
|
||||
}
|
||||
}
|
56
packages/compiler-cli/src/ngtsc/transform/src/declaration.ts
Normal file
56
packages/compiler-cli/src/ngtsc/transform/src/declaration.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 * as ts from 'typescript';
|
||||
|
||||
import {AddStaticFieldInstruction} from './api';
|
||||
import {ImportManager, translateType} from './translator';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Processes .d.ts file text and adds static field declarations, with types.
|
||||
*/
|
||||
export class DtsFileTransformer {
|
||||
private ivyFields = new Map<string, AddStaticFieldInstruction>();
|
||||
private imports = new ImportManager();
|
||||
|
||||
/**
|
||||
* Track that a static field was added to the code for a class.
|
||||
*/
|
||||
recordStaticField(name: string, decl: AddStaticFieldInstruction): void {
|
||||
this.ivyFields.set(name, decl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the .d.ts text for a file and add any declarations which were recorded.
|
||||
*/
|
||||
transform(dts: string): string {
|
||||
const dtsFile =
|
||||
ts.createSourceFile('out.d.ts', dts, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
||||
|
||||
for (let i = dtsFile.statements.length - 1; i >= 0; i--) {
|
||||
const stmt = dtsFile.statements[i];
|
||||
if (ts.isClassDeclaration(stmt) && stmt.name !== undefined &&
|
||||
this.ivyFields.has(stmt.name.text)) {
|
||||
const desc = this.ivyFields.get(stmt.name.text) !;
|
||||
const before = dts.substring(0, stmt.end - 1);
|
||||
const after = dts.substring(stmt.end - 1);
|
||||
const type = translateType(desc.type, this.imports);
|
||||
dts = before + ` static ${desc.field}: ${type};\n` + after;
|
||||
}
|
||||
}
|
||||
|
||||
const imports = this.imports.getAllImports();
|
||||
if (imports.length !== 0) {
|
||||
dts = imports.map(i => `import * as ${i.as} from '${i.name}';\n`).join() + dts;
|
||||
}
|
||||
|
||||
return dts;
|
||||
}
|
||||
}
|
181
packages/compiler-cli/src/ngtsc/transform/src/injectable.ts
Normal file
181
packages/compiler-cli/src/ngtsc/transform/src/injectable.ts
Normal file
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {Expression, IvyInjectableDep, IvyInjectableMetadata, LiteralExpr, WrappedNodeExpr, compileIvyInjectable} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Decorator} from '../../metadata';
|
||||
import {reflectConstructorParameters, reflectImportedIdentifier, reflectObjectLiteral} from '../../metadata/src/reflector';
|
||||
|
||||
import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api';
|
||||
|
||||
|
||||
/**
|
||||
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
|
||||
*/
|
||||
export class InjectableCompilerAdapter implements CompilerAdapter<IvyInjectableMetadata> {
|
||||
constructor(private checker: ts.TypeChecker) {}
|
||||
|
||||
detect(decorator: Decorator[]): Decorator|undefined {
|
||||
return decorator.find(dec => dec.name === 'Injectable' && dec.from === '@angular/core');
|
||||
}
|
||||
|
||||
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<IvyInjectableMetadata> {
|
||||
return {
|
||||
analysis: extractInjectableMetadata(node, decorator, this.checker),
|
||||
};
|
||||
}
|
||||
|
||||
compile(node: ts.ClassDeclaration, analysis: IvyInjectableMetadata): AddStaticFieldInstruction {
|
||||
const res = compileIvyInjectable(analysis);
|
||||
return {
|
||||
field: 'ngInjectableDef',
|
||||
initializer: res.expression,
|
||||
type: res.type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the input
|
||||
* metadata needed to run `compileIvyInjectable`.
|
||||
*/
|
||||
function extractInjectableMetadata(
|
||||
clazz: ts.ClassDeclaration, decorator: Decorator,
|
||||
checker: ts.TypeChecker): IvyInjectableMetadata {
|
||||
if (clazz.name === undefined) {
|
||||
throw new Error(`@Injectables must have names`);
|
||||
}
|
||||
const name = clazz.name.text;
|
||||
const type = new WrappedNodeExpr(clazz.name);
|
||||
if (decorator.args.length === 0) {
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
providedIn: new LiteralExpr(null),
|
||||
useType: getUseType(clazz, checker),
|
||||
};
|
||||
} else if (decorator.args.length === 1) {
|
||||
const metaNode = decorator.args[0];
|
||||
// Firstly make sure the decorator argument is an inline literal - if not, it's illegal to
|
||||
// transport references from one location to another. This is the problem that lowering
|
||||
// used to solve - if this restriction proves too undesirable we can re-implement lowering.
|
||||
if (!ts.isObjectLiteralExpression(metaNode)) {
|
||||
throw new Error(`In Ivy, decorator metadata must be inline.`);
|
||||
}
|
||||
|
||||
// Resolve the fields of the literal into a map of field name to expression.
|
||||
const meta = reflectObjectLiteral(metaNode);
|
||||
let providedIn: Expression = new LiteralExpr(null);
|
||||
if (meta.has('providedIn')) {
|
||||
providedIn = new WrappedNodeExpr(meta.get('providedIn') !);
|
||||
}
|
||||
if (meta.has('useValue')) {
|
||||
return {name, type, providedIn, useValue: new WrappedNodeExpr(meta.get('useValue') !)};
|
||||
} else if (meta.has('useExisting')) {
|
||||
return {name, type, providedIn, useExisting: new WrappedNodeExpr(meta.get('useExisting') !)};
|
||||
} else if (meta.has('useClass')) {
|
||||
return {name, type, providedIn, useClass: new WrappedNodeExpr(meta.get('useClass') !)};
|
||||
} else if (meta.has('useFactory')) {
|
||||
// useFactory is special - the 'deps' property must be analyzed.
|
||||
const factory = new WrappedNodeExpr(meta.get('useFactory') !);
|
||||
const deps: IvyInjectableDep[] = [];
|
||||
if (meta.has('deps')) {
|
||||
const depsExpr = meta.get('deps') !;
|
||||
if (!ts.isArrayLiteralExpression(depsExpr)) {
|
||||
throw new Error(`In Ivy, deps metadata must be inline.`);
|
||||
}
|
||||
if (depsExpr.elements.length > 0) {
|
||||
throw new Error(`deps not yet supported`);
|
||||
}
|
||||
deps.push(...depsExpr.elements.map(dep => getDep(dep, checker)));
|
||||
}
|
||||
return {name, type, providedIn, useFactory: {factory, deps}};
|
||||
} else {
|
||||
const useType = getUseType(clazz, checker);
|
||||
return {name, type, providedIn, useType};
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Too many arguments to @Injectable`);
|
||||
}
|
||||
}
|
||||
|
||||
function getUseType(clazz: ts.ClassDeclaration, checker: ts.TypeChecker): IvyInjectableDep[] {
|
||||
const useType: IvyInjectableDep[] = [];
|
||||
const ctorParams = (reflectConstructorParameters(clazz, checker) || []);
|
||||
ctorParams.forEach(param => {
|
||||
let tokenExpr = param.typeValueExpr;
|
||||
let optional = false, self = false, skipSelf = false;
|
||||
param.decorators.filter(dec => dec.from === '@angular/core').forEach(dec => {
|
||||
if (dec.name === 'Inject') {
|
||||
if (dec.args.length !== 1) {
|
||||
throw new Error(`Unexpected number of arguments to @Inject().`);
|
||||
}
|
||||
tokenExpr = dec.args[0];
|
||||
} else if (dec.name === 'Optional') {
|
||||
optional = true;
|
||||
} else if (dec.name === 'SkipSelf') {
|
||||
skipSelf = true;
|
||||
} else if (dec.name === 'Self') {
|
||||
self = true;
|
||||
} else {
|
||||
throw new Error(`Unexpected decorator ${dec.name} on parameter.`);
|
||||
}
|
||||
if (tokenExpr === null) {
|
||||
throw new Error(`No suitable token for parameter!`);
|
||||
}
|
||||
});
|
||||
const token = new WrappedNodeExpr(tokenExpr);
|
||||
useType.push({token, optional, self, skipSelf});
|
||||
});
|
||||
return useType;
|
||||
}
|
||||
|
||||
function getDep(dep: ts.Expression, checker: ts.TypeChecker): IvyInjectableDep {
|
||||
const depObj = {
|
||||
token: new WrappedNodeExpr(dep),
|
||||
optional: false,
|
||||
self: false,
|
||||
skipSelf: false,
|
||||
};
|
||||
|
||||
function maybeUpdateDecorator(dec: ts.Identifier, token?: ts.Expression): void {
|
||||
const source = reflectImportedIdentifier(dec, checker);
|
||||
if (source === null || source.from !== '@angular/core') {
|
||||
return;
|
||||
}
|
||||
switch (source.name) {
|
||||
case 'Inject':
|
||||
if (token !== undefined) {
|
||||
depObj.token = new WrappedNodeExpr(token);
|
||||
}
|
||||
break;
|
||||
case 'Optional':
|
||||
depObj.optional = true;
|
||||
break;
|
||||
case 'SkipSelf':
|
||||
depObj.skipSelf = true;
|
||||
break;
|
||||
case 'Self':
|
||||
depObj.self = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isArrayLiteralExpression(dep)) {
|
||||
dep.elements.forEach(el => {
|
||||
if (ts.isIdentifier(el)) {
|
||||
maybeUpdateDecorator(el);
|
||||
} else if (ts.isNewExpression(el) && ts.isIdentifier(el.expression)) {
|
||||
const token = el.arguments && el.arguments.length > 0 && el.arguments[0] || undefined;
|
||||
maybeUpdateDecorator(el.expression, token);
|
||||
}
|
||||
});
|
||||
}
|
||||
return depObj;
|
||||
}
|
96
packages/compiler-cli/src/ngtsc/transform/src/transform.ts
Normal file
96
packages/compiler-cli/src/ngtsc/transform/src/transform.ts
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {WrappedNodeExpr, compileIvyInjectable} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {IvyCompilation} from './compilation';
|
||||
import {ImportManager, translateExpression} from './translator';
|
||||
|
||||
export function ivyTransformFactory(compilation: IvyCompilation):
|
||||
ts.TransformerFactory<ts.SourceFile> {
|
||||
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
|
||||
return (file: ts.SourceFile): ts.SourceFile => {
|
||||
return transformIvySourceFile(compilation, context, file);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`.
|
||||
*/
|
||||
function transformIvySourceFile(
|
||||
compilation: IvyCompilation, context: ts.TransformationContext,
|
||||
file: ts.SourceFile): ts.SourceFile {
|
||||
const importManager = new ImportManager();
|
||||
|
||||
// Recursively scan through the AST and perform any updates requested by the IvyCompilation.
|
||||
const sf = visitNode(file);
|
||||
|
||||
// Generate the import statements to prepend.
|
||||
const imports = importManager.getAllImports().map(
|
||||
i => ts.createImportDeclaration(
|
||||
undefined, undefined,
|
||||
ts.createImportClause(undefined, ts.createNamespaceImport(ts.createIdentifier(i.as))),
|
||||
ts.createLiteral(i.name)));
|
||||
|
||||
// Prepend imports if needed.
|
||||
if (imports.length > 0) {
|
||||
sf.statements = ts.createNodeArray([...imports, ...sf.statements]);
|
||||
}
|
||||
return sf;
|
||||
|
||||
// Helper function to process a class declaration.
|
||||
function visitClassDeclaration(node: ts.ClassDeclaration): ts.ClassDeclaration {
|
||||
// Determine if this class has an Ivy field that needs to be added, and compile the field
|
||||
// to an expression if so.
|
||||
const res = compilation.compileIvyFieldFor(node);
|
||||
if (res !== undefined) {
|
||||
// There is a field to add. Translate the initializer for the field into TS nodes.
|
||||
const exprNode = translateExpression(res.initializer, importManager);
|
||||
|
||||
// Create a static property declaration for the new field.
|
||||
const property = ts.createProperty(
|
||||
undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], res.field, undefined, undefined,
|
||||
exprNode);
|
||||
|
||||
// Replace the class declaration with an updated version.
|
||||
node = ts.updateClassDeclaration(
|
||||
node,
|
||||
// Remove the decorator which triggered this compilation, leaving the others alone.
|
||||
maybeFilterDecorator(node.decorators, compilation.ivyDecoratorFor(node) !),
|
||||
node.modifiers, node.name, node.typeParameters, node.heritageClauses || [],
|
||||
[...node.members, property]);
|
||||
}
|
||||
|
||||
// Recurse into the class declaration in case there are nested class declarations.
|
||||
return ts.visitEachChild(node, child => visitNode(child), context);
|
||||
}
|
||||
|
||||
// Helper function that recurses through the nodes and processes each one.
|
||||
function visitNode<T extends ts.Node>(node: T): T;
|
||||
function visitNode(node: ts.Node): ts.Node {
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
return visitClassDeclaration(node);
|
||||
} else {
|
||||
return ts.visitEachChild(node, child => visitNode(child), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
function maybeFilterDecorator(
|
||||
decorators: ts.NodeArray<ts.Decorator>| undefined,
|
||||
toRemove: ts.Decorator): ts.NodeArray<ts.Decorator>|undefined {
|
||||
if (decorators === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const filtered = decorators.filter(dec => ts.getOriginalNode(dec) !== toRemove);
|
||||
if (filtered.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return ts.createNodeArray(filtered);
|
||||
}
|
319
packages/compiler-cli/src/ngtsc/transform/src/translator.ts
Normal file
319
packages/compiler-cli/src/ngtsc/transform/src/translator.ts
Normal file
@ -0,0 +1,319 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* 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 {ArrayType, AssertNotNull, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export class ImportManager {
|
||||
private moduleToIndex = new Map<string, string>();
|
||||
private nextIndex = 0;
|
||||
|
||||
generateNamedImport(moduleName: string): string {
|
||||
if (!this.moduleToIndex.has(moduleName)) {
|
||||
this.moduleToIndex.set(moduleName, `i${this.nextIndex++}`);
|
||||
}
|
||||
return this.moduleToIndex.get(moduleName) !;
|
||||
}
|
||||
|
||||
getAllImports(): {name: string, as: string}[] {
|
||||
return Array.from(this.moduleToIndex.keys()).map(name => {
|
||||
const as = this.moduleToIndex.get(name) !;
|
||||
return {name, as};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function translateExpression(expression: Expression, imports: ImportManager): ts.Expression {
|
||||
return expression.visitExpression(new ExpressionTranslatorVisitor(imports), null);
|
||||
}
|
||||
|
||||
export function translateType(type: Type, imports: ImportManager): string {
|
||||
return type.visitType(new TypeTranslatorVisitor(imports), null);
|
||||
}
|
||||
|
||||
class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor {
|
||||
constructor(private imports: ImportManager) {}
|
||||
|
||||
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitExpressionStmt(stmt: ExpressionStatement, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitReturnStmt(stmt: ReturnStatement, context: any): ts.ReturnStatement {
|
||||
return ts.createReturn(stmt.value.visitExpression(this, context));
|
||||
}
|
||||
|
||||
visitDeclareClassStmt(stmt: ClassStmt, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitIfStmt(stmt: IfStmt, context: any) { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitTryCatchStmt(stmt: TryCatchStmt, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitThrowStmt(stmt: ThrowStmt, context: any) { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitCommentStmt(stmt: CommentStmt, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitJSDocCommentStmt(stmt: JSDocCommentStmt, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitReadVarExpr(ast: ReadVarExpr, context: any): ts.Identifier {
|
||||
return ts.createIdentifier(ast.name !);
|
||||
}
|
||||
|
||||
visitWriteVarExpr(expr: WriteVarExpr, context: any): ts.BinaryExpression {
|
||||
return ts.createBinary(
|
||||
ts.createIdentifier(expr.name), ts.SyntaxKind.EqualsToken,
|
||||
expr.value.visitExpression(this, context));
|
||||
}
|
||||
|
||||
visitWriteKeyExpr(expr: WriteKeyExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitWritePropExpr(expr: WritePropExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): ts.CallExpression {
|
||||
return ts.createCall(
|
||||
ast.fn.visitExpression(this, context), undefined,
|
||||
ast.args.map(arg => arg.visitExpression(this, context)));
|
||||
}
|
||||
|
||||
visitInstantiateExpr(ast: InstantiateExpr, context: any): ts.NewExpression {
|
||||
return ts.createNew(
|
||||
ast.classExpr.visitExpression(this, context), undefined,
|
||||
ast.args.map(arg => arg.visitExpression(this, context)));
|
||||
}
|
||||
|
||||
visitLiteralExpr(ast: LiteralExpr, context: any): ts.Expression {
|
||||
if (ast.value === undefined) {
|
||||
return ts.createIdentifier('undefined');
|
||||
} else if (ast.value === null) {
|
||||
return ts.createNull();
|
||||
} else {
|
||||
return ts.createLiteral(ast.value);
|
||||
}
|
||||
}
|
||||
|
||||
visitExternalExpr(ast: ExternalExpr, context: any): ts.PropertyAccessExpression {
|
||||
if (ast.value.moduleName === null || ast.value.name === null) {
|
||||
throw new Error(`Import unknown module or symbol ${ast.value}`);
|
||||
}
|
||||
return ts.createPropertyAccess(
|
||||
ts.createIdentifier(this.imports.generateNamedImport(ast.value.moduleName)),
|
||||
ts.createIdentifier(ast.value.name));
|
||||
}
|
||||
|
||||
visitConditionalExpr(ast: ConditionalExpr, context: any): ts.ParenthesizedExpression {
|
||||
return ts.createParen(ts.createConditional(
|
||||
ast.condition.visitExpression(this, context), ast.trueCase.visitExpression(this, context),
|
||||
ast.falseCase !.visitExpression(this, context)));
|
||||
}
|
||||
|
||||
visitNotExpr(ast: NotExpr, context: any): ts.PrefixUnaryExpression {
|
||||
return ts.createPrefix(
|
||||
ts.SyntaxKind.ExclamationToken, ast.condition.visitExpression(this, context));
|
||||
}
|
||||
|
||||
visitAssertNotNullExpr(ast: AssertNotNull, context: any): ts.NonNullExpression {
|
||||
return ts.createNonNullExpression(ast.condition.visitExpression(this, context));
|
||||
}
|
||||
|
||||
visitCastExpr(ast: CastExpr, context: any): ts.Expression {
|
||||
return ast.value.visitExpression(this, context);
|
||||
}
|
||||
|
||||
visitFunctionExpr(ast: FunctionExpr, context: any): ts.FunctionExpression {
|
||||
return ts.createFunctionExpression(
|
||||
undefined, undefined, ast.name || undefined, undefined,
|
||||
ast.params.map(
|
||||
param => ts.createParameter(
|
||||
undefined, undefined, undefined, param.name, undefined, undefined, undefined)),
|
||||
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitReadPropExpr(ast: ReadPropExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitReadKeyExpr(ast: ReadKeyExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): ts.ObjectLiteralExpression {
|
||||
const entries = ast.entries.map(
|
||||
entry => ts.createPropertyAssignment(
|
||||
entry.quoted ? ts.createLiteral(entry.key) : ts.createIdentifier(entry.key),
|
||||
entry.value.visitExpression(this, context)));
|
||||
return ts.createObjectLiteral(entries);
|
||||
}
|
||||
|
||||
visitCommaExpr(ast: CommaExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any): any { return ast.node; }
|
||||
}
|
||||
|
||||
export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||
constructor(private imports: ImportManager) {}
|
||||
|
||||
visitBuiltinType(type: BuiltinType, context: any): string {
|
||||
switch (type.name) {
|
||||
case BuiltinTypeName.Bool:
|
||||
return 'boolean';
|
||||
case BuiltinTypeName.Dynamic:
|
||||
return 'any';
|
||||
case BuiltinTypeName.Int:
|
||||
case BuiltinTypeName.Number:
|
||||
return 'number';
|
||||
case BuiltinTypeName.String:
|
||||
return 'string';
|
||||
default:
|
||||
throw new Error(`Unsupported builtin type: ${BuiltinTypeName[type.name]}`);
|
||||
}
|
||||
}
|
||||
|
||||
visitExpressionType(type: ExpressionType, context: any): any {
|
||||
return type.value.visitExpression(this, context);
|
||||
}
|
||||
|
||||
visitArrayType(type: ArrayType, context: any): string {
|
||||
return `Array<${type.visitType(this, context)}>`;
|
||||
}
|
||||
|
||||
visitMapType(type: MapType, context: any): string {
|
||||
if (type.valueType !== null) {
|
||||
return `{[key: string]: ${type.valueType.visitType(this, context)}}`;
|
||||
} else {
|
||||
return '{[key: string]: any}';
|
||||
}
|
||||
}
|
||||
|
||||
visitReadVarExpr(ast: ReadVarExpr, context: any): string {
|
||||
if (ast.name === null) {
|
||||
throw new Error(`ReadVarExpr with no variable name in type`);
|
||||
}
|
||||
return ast.name;
|
||||
}
|
||||
|
||||
visitWriteVarExpr(expr: WriteVarExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitWriteKeyExpr(expr: WriteKeyExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitWritePropExpr(expr: WritePropExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitInstantiateExpr(ast: InstantiateExpr, context: any): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitLiteralExpr(ast: LiteralExpr, context: any): string {
|
||||
if (typeof ast.value === 'string') {
|
||||
const escaped = ast.value.replace(/\'/g, '\\\'');
|
||||
return `'${escaped}'`;
|
||||
} else {
|
||||
return `${ast.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
visitExternalExpr(ast: ExternalExpr, context: any): string {
|
||||
if (ast.value.moduleName === null || ast.value.name === null) {
|
||||
throw new Error(`Import unknown module or symbol`);
|
||||
}
|
||||
const base = `${this.imports.generateNamedImport(ast.value.moduleName)}.${ast.value.name}`;
|
||||
if (ast.typeParams !== null) {
|
||||
const generics = ast.typeParams.map(type => type.visitType(this, context)).join(', ');
|
||||
return `${base}<${generics}>`;
|
||||
} else {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
visitConditionalExpr(ast: ConditionalExpr, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitNotExpr(ast: NotExpr, context: any) { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitAssertNotNullExpr(ast: AssertNotNull, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitCastExpr(ast: CastExpr, context: any) { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitFunctionExpr(ast: FunctionExpr, context: any) { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitReadPropExpr(ast: ReadPropExpr, context: any) { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitReadKeyExpr(ast: ReadKeyExpr, context: any) { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitLiteralMapExpr(ast: LiteralMapExpr, context: any) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitCommaExpr(ast: CommaExpr, context: any) { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any) {
|
||||
const node: ts.Node = ast.node;
|
||||
if (ts.isIdentifier(node)) {
|
||||
return node.text;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user