refactor(ivy): translate template nodes to typescript using a visitor (#30177)
PR Close #30177
This commit is contained in:
parent
f03475cac8
commit
6f073885b0
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, ASTWithSource, Binary, Conditional, Interpolation, KeyedRead, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
|
||||
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {TypeCheckingConfig} from './api';
|
||||
|
@ -42,30 +42,82 @@ const BINARY_OPS = new Map<string, ts.SyntaxKind>([
|
|||
export function astToTypescript(
|
||||
ast: AST, maybeResolve: (ast: AST) => ts.Expression | null,
|
||||
config: TypeCheckingConfig): ts.Expression {
|
||||
const resolved = maybeResolve(ast);
|
||||
if (resolved !== null) {
|
||||
return resolved;
|
||||
const translator = new AstTranslator(maybeResolve, config);
|
||||
return translator.translate(ast);
|
||||
}
|
||||
|
||||
class AstTranslator implements AstVisitor {
|
||||
constructor(
|
||||
private maybeResolve: (ast: AST) => ts.Expression | null,
|
||||
private config: TypeCheckingConfig) {}
|
||||
|
||||
translate(ast: AST): ts.Expression {
|
||||
// Skip over an `ASTWithSource` as its `visit` method calls directly into its ast's `visit`,
|
||||
// which would prevent any custom resolution through `maybeResolve` for that node.
|
||||
if (ast instanceof ASTWithSource) {
|
||||
ast = ast.ast;
|
||||
}
|
||||
|
||||
// First attempt to let any custom resolution logic provide a translation for the given node.
|
||||
const resolved = this.maybeResolve(ast);
|
||||
if (resolved !== null) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return ast.visit(this);
|
||||
}
|
||||
// Branch based on the type of expression being processed.
|
||||
if (ast instanceof ASTWithSource) {
|
||||
// Fall through to the underlying AST.
|
||||
return astToTypescript(ast.ast, maybeResolve, config);
|
||||
} else if (ast instanceof PropertyRead) {
|
||||
// This is a normal property read - convert the receiver to an expression and emit the correct
|
||||
// TypeScript expression to read the property.
|
||||
const receiver = astToTypescript(ast.receiver, maybeResolve, config);
|
||||
return ts.createPropertyAccess(receiver, ast.name);
|
||||
} else if (ast instanceof Interpolation) {
|
||||
return astArrayToExpression(ast.expressions, maybeResolve, config);
|
||||
} else if (ast instanceof Binary) {
|
||||
const lhs = astToTypescript(ast.left, maybeResolve, config);
|
||||
const rhs = astToTypescript(ast.right, maybeResolve, config);
|
||||
|
||||
visitBinary(ast: Binary): ts.Expression {
|
||||
const lhs = this.translate(ast.left);
|
||||
const rhs = this.translate(ast.right);
|
||||
const op = BINARY_OPS.get(ast.operation);
|
||||
if (op === undefined) {
|
||||
throw new Error(`Unsupported Binary.operation: ${ast.operation}`);
|
||||
}
|
||||
return ts.createBinary(lhs, op as any, rhs);
|
||||
} else if (ast instanceof LiteralPrimitive) {
|
||||
}
|
||||
|
||||
visitChain(ast: Chain): never { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitConditional(ast: Conditional): ts.Expression {
|
||||
const condExpr = this.translate(ast.condition);
|
||||
const trueExpr = this.translate(ast.trueExp);
|
||||
const falseExpr = this.translate(ast.falseExp);
|
||||
return ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr));
|
||||
}
|
||||
|
||||
visitFunctionCall(ast: FunctionCall): never { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitImplicitReceiver(ast: ImplicitReceiver): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitInterpolation(ast: Interpolation): ts.Expression {
|
||||
return this.astArrayToExpression(ast.expressions);
|
||||
}
|
||||
|
||||
visitKeyedRead(ast: KeyedRead): ts.Expression {
|
||||
const receiver = this.translate(ast.obj);
|
||||
const key = this.translate(ast.key);
|
||||
return ts.createElementAccess(receiver, key);
|
||||
}
|
||||
|
||||
visitKeyedWrite(ast: KeyedWrite): never { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitLiteralArray(ast: LiteralArray): ts.Expression {
|
||||
const elements = ast.expressions.map(expr => this.translate(expr));
|
||||
return ts.createArrayLiteral(elements);
|
||||
}
|
||||
|
||||
visitLiteralMap(ast: LiteralMap): ts.Expression {
|
||||
const properties = ast.keys.map(({key}, idx) => {
|
||||
const value = this.translate(ast.values[idx]);
|
||||
return ts.createPropertyAssignment(ts.createStringLiteral(key), value);
|
||||
});
|
||||
return ts.createObjectLiteral(properties, true);
|
||||
}
|
||||
|
||||
visitLiteralPrimitive(ast: LiteralPrimitive): ts.Expression {
|
||||
if (ast.value === undefined) {
|
||||
return ts.createIdentifier('undefined');
|
||||
} else if (ast.value === null) {
|
||||
|
@ -73,72 +125,72 @@ export function astToTypescript(
|
|||
} else {
|
||||
return ts.createLiteral(ast.value);
|
||||
}
|
||||
} else if (ast instanceof MethodCall) {
|
||||
const receiver = astToTypescript(ast.receiver, maybeResolve, config);
|
||||
}
|
||||
|
||||
visitMethodCall(ast: MethodCall): ts.Expression {
|
||||
const receiver = this.translate(ast.receiver);
|
||||
const method = ts.createPropertyAccess(receiver, ast.name);
|
||||
const args = ast.args.map(expr => astToTypescript(expr, maybeResolve, config));
|
||||
const args = ast.args.map(expr => this.translate(expr));
|
||||
return ts.createCall(method, undefined, args);
|
||||
} else if (ast instanceof Conditional) {
|
||||
const condExpr = astToTypescript(ast.condition, maybeResolve, config);
|
||||
const trueExpr = astToTypescript(ast.trueExp, maybeResolve, config);
|
||||
const falseExpr = astToTypescript(ast.falseExp, maybeResolve, config);
|
||||
return ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr));
|
||||
} else if (ast instanceof LiteralArray) {
|
||||
const elements = ast.expressions.map(expr => astToTypescript(expr, maybeResolve, config));
|
||||
return ts.createArrayLiteral(elements);
|
||||
} else if (ast instanceof LiteralMap) {
|
||||
const properties = ast.keys.map(({key}, idx) => {
|
||||
const value = astToTypescript(ast.values[idx], maybeResolve, config);
|
||||
return ts.createPropertyAssignment(ts.createStringLiteral(key), value);
|
||||
});
|
||||
return ts.createObjectLiteral(properties, true);
|
||||
} else if (ast instanceof KeyedRead) {
|
||||
const receiver = astToTypescript(ast.obj, maybeResolve, config);
|
||||
const key = astToTypescript(ast.key, maybeResolve, config);
|
||||
return ts.createElementAccess(receiver, key);
|
||||
} else if (ast instanceof NonNullAssert) {
|
||||
const expr = astToTypescript(ast.expression, maybeResolve, config);
|
||||
}
|
||||
|
||||
visitNonNullAssert(ast: NonNullAssert): ts.Expression {
|
||||
const expr = this.translate(ast.expression);
|
||||
return ts.createNonNullExpression(expr);
|
||||
} else if (ast instanceof PrefixNot) {
|
||||
return ts.createLogicalNot(astToTypescript(ast.expression, maybeResolve, config));
|
||||
} else if (ast instanceof SafePropertyRead) {
|
||||
}
|
||||
|
||||
visitPipe(ast: BindingPipe): never { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitPrefixNot(ast: PrefixNot): ts.Expression {
|
||||
return ts.createLogicalNot(this.translate(ast.expression));
|
||||
}
|
||||
|
||||
visitPropertyRead(ast: PropertyRead): ts.Expression {
|
||||
// This is a normal property read - convert the receiver to an expression and emit the correct
|
||||
// TypeScript expression to read the property.
|
||||
const receiver = this.translate(ast.receiver);
|
||||
return ts.createPropertyAccess(receiver, ast.name);
|
||||
}
|
||||
|
||||
visitPropertyWrite(ast: PropertyWrite): never { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitQuote(ast: Quote): never { throw new Error('Method not implemented.'); }
|
||||
|
||||
visitSafeMethodCall(ast: SafeMethodCall): ts.Expression {
|
||||
// See the comment in SafePropertyRead above for an explanation of the need for the non-null
|
||||
// assertion here.
|
||||
const receiver = this.translate(ast.receiver);
|
||||
const method = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name);
|
||||
const args = ast.args.map(expr => this.translate(expr));
|
||||
const expr = ts.createCall(method, undefined, args);
|
||||
const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
|
||||
return safeTernary(receiver, expr, whenNull);
|
||||
}
|
||||
|
||||
visitSafePropertyRead(ast: SafePropertyRead): ts.Expression {
|
||||
// A safe property expression a?.b takes the form `(a != null ? a!.b : whenNull)`, where
|
||||
// whenNull is either of type 'any' or or 'undefined' depending on strictness. The non-null
|
||||
// assertion is necessary because in practice 'a' may be a method call expression, which won't
|
||||
// have a narrowed type when repeated in the ternary true branch.
|
||||
const receiver = astToTypescript(ast.receiver, maybeResolve, config);
|
||||
const receiver = this.translate(ast.receiver);
|
||||
const expr = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name);
|
||||
const whenNull = config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
|
||||
const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
|
||||
return safeTernary(receiver, expr, whenNull);
|
||||
} else if (ast instanceof SafeMethodCall) {
|
||||
const receiver = astToTypescript(ast.receiver, maybeResolve, config);
|
||||
// See the comment in SafePropertyRead above for an explanation of the need for the non-null
|
||||
// assertion here.
|
||||
const method = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name);
|
||||
const args = ast.args.map(expr => astToTypescript(expr, maybeResolve, config));
|
||||
const expr = ts.createCall(method, undefined, args);
|
||||
const whenNull = config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
|
||||
return safeTernary(receiver, expr, whenNull);
|
||||
} else {
|
||||
throw new Error(`Unknown node type: ${Object.getPrototypeOf(ast).constructor}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of `AST` expressions into a single `ts.Expression`, by converting them all
|
||||
* and separating them with commas.
|
||||
*/
|
||||
function astArrayToExpression(
|
||||
astArray: AST[], maybeResolve: (ast: AST) => ts.Expression | null,
|
||||
config: TypeCheckingConfig): ts.Expression {
|
||||
// Reduce the `asts` array into a `ts.Expression`. Multiple expressions are combined into a
|
||||
// `ts.BinaryExpression` with a comma separator. First make a copy of the input array, as
|
||||
// it will be modified during the reduction.
|
||||
const asts = astArray.slice();
|
||||
return asts.reduce(
|
||||
(lhs, ast) => ts.createBinary(
|
||||
lhs, ts.SyntaxKind.CommaToken, astToTypescript(ast, maybeResolve, config)),
|
||||
astToTypescript(asts.pop() !, maybeResolve, config));
|
||||
/**
|
||||
* Convert an array of `AST` expressions into a single `ts.Expression`, by converting them all
|
||||
* and separating them with commas.
|
||||
*/
|
||||
private astArrayToExpression(astArray: AST[]): ts.Expression {
|
||||
// Reduce the `asts` array into a `ts.Expression`. Multiple expressions are combined into a
|
||||
// `ts.BinaryExpression` with a comma separator. First make a copy of the input array, as
|
||||
// it will be modified during the reduction.
|
||||
const asts = astArray.slice();
|
||||
return asts.reduce(
|
||||
(lhs, ast) => ts.createBinary(lhs, ts.SyntaxKind.CommaToken, this.translate(ast)),
|
||||
this.translate(asts.pop() !));
|
||||
}
|
||||
}
|
||||
|
||||
function safeTernary(
|
||||
|
|
Loading…
Reference in New Issue