refactor(compiler-cli): initial decoupling from TypeScript factory APIs

Initial pass to move usages of TS `factory` APIs to the new `TcbExpr`.
This commit is contained in:
Kristiyan Kostadinov 2026-03-02 10:54:13 +01:00 committed by Jessica Janiuk
parent 3828ef1917
commit befbae0dcf
34 changed files with 806 additions and 1234 deletions

View file

@ -461,7 +461,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
.map((op) => {
return {
pos: op.splitPoint,
text: op.execute(importManager, sf, this.refEmitter, printer),
text: op.execute(importManager, sf, this.refEmitter),
};
});
@ -525,7 +525,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
path: pendingShimData.file.fileName,
data: pendingShimData.data,
});
const sfText = pendingShimData.file.render(false /* removeComments */);
const sfText = pendingShimData.file.render();
updates.set(pendingShimData.file.fileName, {
newText: sfText,
@ -642,12 +642,7 @@ interface Op {
/**
* Execute the operation and return the generated code as text.
*/
execute(
im: ImportManager,
sf: ts.SourceFile,
refEmitter: ReferenceEmitter,
printer: ts.Printer,
): string;
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter): string;
}
/**
@ -670,12 +665,7 @@ class InlineTcbOp implements Op {
return this.ref.node.end + 1;
}
execute(
im: ImportManager,
sf: ts.SourceFile,
refEmitter: ReferenceEmitter,
printer: ts.Printer,
): string {
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter): string {
const env = new Environment(this.config, im, refEmitter, this.reflector, sf);
const fnName = ts.factory.createIdentifier(`_tcb_${this.ref.node.pos}`);
@ -691,7 +681,7 @@ class InlineTcbOp implements Op {
TcbGenericContextBehavior.CopyClassNodes,
);
return printer.printNode(ts.EmitHint.Unspecified, fn, sf);
return fn;
}
}
@ -712,36 +702,8 @@ class TypeCtorOp implements Op {
return this.ref.node.end - 1;
}
execute(
im: ImportManager,
sf: ts.SourceFile,
refEmitter: ReferenceEmitter,
printer: ts.Printer,
): string {
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter): string {
const emitEnv = new ReferenceEmitEnvironment(im, refEmitter, this.reflector, sf);
const tcb = generateInlineTypeCtor(emitEnv, this.ref.node, this.meta);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
return generateInlineTypeCtor(emitEnv, this.ref.node, this.meta);
}
}
/**
* Compare two operations and return their split point ordering.
*/
function orderOps(op1: Op, op2: Op): number {
return op1.splitPoint - op2.splitPoint;
}
/**
* Split a string into chunks at any number of split points.
*/
function splitStringAtPoints(str: string, points: number[]): string[] {
const splits: string[] = [];
let start = 0;
for (let i = 0; i < points.length; i++) {
const point = points[i];
splits.push(str.substring(start, point));
start = point;
}
splits.push(str.substring(start));
return splits;
}

View file

@ -19,9 +19,9 @@ import {ImportManager, translateExpression} from '../../translator';
import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from '../api';
import {ReferenceEmitEnvironment} from './reference_emit_environment';
import {tsDeclareVariable} from './ts_util';
import {generateTypeCtorDeclarationFn, requiresInlineTypeCtor} from './type_constructor';
import {TypeParameterEmitter} from './type_parameter_emitter';
import {declareVariable, TcbExpr, tempPrint} from './ops/codegen';
/**
* A context which hosts one or more Type Check Blocks (TCBs).
@ -40,11 +40,11 @@ export class Environment extends ReferenceEmitEnvironment {
typeCtor: 1,
};
private typeCtors = new Map<ClassDeclaration, ts.Expression>();
protected typeCtorStatements: ts.Statement[] = [];
private typeCtors = new Map<ClassDeclaration, string>();
protected typeCtorStatements: TcbExpr[] = [];
private pipeInsts = new Map<ClassDeclaration, ts.Expression>();
protected pipeInstStatements: ts.Statement[] = [];
private pipeInsts = new Map<ClassDeclaration, string>();
protected pipeInstStatements: TcbExpr[] = [];
constructor(
readonly config: TypeCheckingConfig,
@ -62,20 +62,20 @@ export class Environment extends ReferenceEmitEnvironment {
* Depending on the shape of the directive itself, this could be either a reference to a declared
* type constructor, or to an inline type constructor.
*/
typeCtorFor(dir: TypeCheckableDirectiveMeta): ts.Expression {
typeCtorFor(dir: TypeCheckableDirectiveMeta): TcbExpr {
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
const node = dirRef.node;
if (this.typeCtors.has(node)) {
return this.typeCtors.get(node)!;
return new TcbExpr(this.typeCtors.get(node)!);
}
if (requiresInlineTypeCtor(node, this.reflector, this)) {
// The constructor has already been created inline, we just need to construct a reference to
// it.
const ref = this.reference(dirRef);
const typeCtorExpr = ts.factory.createPropertyAccessExpression(ref, 'ngTypeCtor');
const typeCtorExpr = `${ref.print()}.ngTypeCtor`;
this.typeCtors.set(node, typeCtorExpr);
return typeCtorExpr;
return new TcbExpr(typeCtorExpr);
} else {
const fnName = `_ctor${this.nextIds.typeCtor++}`;
const nodeTypeRef = this.referenceType(dirRef);
@ -95,27 +95,27 @@ export class Environment extends ReferenceEmitEnvironment {
const typeParams = this.emitTypeParameters(node);
const typeCtor = generateTypeCtorDeclarationFn(this, meta, nodeTypeRef.typeName, typeParams);
this.typeCtorStatements.push(typeCtor);
const fnId = ts.factory.createIdentifier(fnName);
this.typeCtors.set(node, fnId);
return fnId;
this.typeCtors.set(node, fnName);
return new TcbExpr(fnName);
}
}
/*
* Get an expression referring to an instance of the given pipe.
*/
pipeInst(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
pipeInst(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): TcbExpr {
if (this.pipeInsts.has(ref.node)) {
return this.pipeInsts.get(ref.node)!;
return new TcbExpr(this.pipeInsts.get(ref.node)!);
}
const pipeType = this.referenceType(ref);
const pipeInstId = ts.factory.createIdentifier(`_pipe${this.nextIds.pipeInst++}`);
const pipeInstId = `_pipe${this.nextIds.pipeInst++}`;
this.pipeInstStatements.push(tsDeclareVariable(pipeInstId, pipeType));
this.pipeInsts.set(ref.node, pipeInstId);
return pipeInstId;
this.pipeInstStatements.push(
declareVariable(new TcbExpr(pipeInstId), new TcbExpr(tempPrint(pipeType, this.contextFile))),
);
return new TcbExpr(pipeInstId);
}
/**
@ -123,7 +123,7 @@ export class Environment extends ReferenceEmitEnvironment {
*
* This may involve importing the node into the file if it's not declared there already.
*/
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): TcbExpr {
// Disable aliasing for imports generated in a template type-checking context, as there is no
// guarantee that any alias re-exports exist in the .d.ts files. It's safe to use direct imports
// in these cases as there is no strict dependency checking during the template type-checking
@ -132,7 +132,13 @@ export class Environment extends ReferenceEmitEnvironment {
assertSuccessfulReferenceEmit(ngExpr, this.contextFile, 'class');
// Use `translateExpression` to convert the `Expression` into a `ts.Expression`.
return translateExpression(this.contextFile, ngExpr.expression, this.importManager);
const tsExpression = translateExpression(
this.contextFile,
ngExpr.expression,
this.importManager,
);
return new TcbExpr(tempPrint(tsExpression, this.contextFile));
}
private emitTypeParameters(
@ -142,7 +148,7 @@ export class Environment extends ReferenceEmitEnvironment {
return emitter.emit((ref) => this.referenceType(ref));
}
getPreludeStatements(): ts.Statement[] {
getPreludeStatements(): TcbExpr[] {
return [...this.pipeInstStatements, ...this.typeCtorStatements];
}
}

View file

@ -34,100 +34,34 @@ import {
SpreadElement,
TaggedTemplateLiteral,
TemplateLiteral,
TemplateLiteralElement,
ThisReceiver,
TypeofExpression,
Unary,
VoidExpression,
} from '@angular/compiler';
import ts from 'typescript';
import {TcbExpr} from './ops/codegen';
import {TypeCheckingConfig} from '../api';
import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics';
import {tsCastToAny, tsNumericExpression} from './ts_util';
import {markIgnoreDiagnostics} from './comments';
/**
* Gets an expression that is cast to any. Currently represented as `0 as any`.
*
* Historically this expression was using `null as any`, but a newly-added check in TypeScript 5.6
* (https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#disallowed-nullish-and-truthy-checks)
* started flagging it as always being nullish. Other options that were considered:
* - `NaN as any` or `Infinity as any` - not used, because they don't work if the `noLib` compiler
* option is enabled. Also they require more characters.
* - Some flavor of function call, like `isNan(0) as any` - requires even more characters than the
* NaN option and has the same issue with `noLib`.
*/
export function getAnyExpression(): ts.AsExpression {
return ts.factory.createAsExpression(
ts.factory.createNumericLiteral('0'),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
}
/**
* Convert an `AST` to TypeScript code directly, without going through an intermediate `Expression`
* Convert an `AST` to a `TcbExpr` directly, without going through an intermediate `Expression`
* AST.
*/
export function astToTypescript(
export function astToTcbExpr(
ast: AST,
maybeResolve: (ast: AST) => ts.Expression | null,
maybeResolve: (ast: AST) => TcbExpr | null,
config: TypeCheckingConfig,
): ts.Expression {
const translator = new AstTranslator(maybeResolve, config);
): TcbExpr {
const translator = new TcbExprTranslator(maybeResolve, config);
return translator.translate(ast);
}
class AstTranslator implements AstVisitor {
private readonly UNDEFINED = ts.factory.createIdentifier('undefined');
private readonly UNARY_OPS = new Map<string, ts.PrefixUnaryOperator>([
['+', ts.SyntaxKind.PlusToken],
['-', ts.SyntaxKind.MinusToken],
]);
private readonly BINARY_OPS = new Map<string, ts.BinaryOperator>([
['+', ts.SyntaxKind.PlusToken],
['-', ts.SyntaxKind.MinusToken],
['<', ts.SyntaxKind.LessThanToken],
['>', ts.SyntaxKind.GreaterThanToken],
['<=', ts.SyntaxKind.LessThanEqualsToken],
['>=', ts.SyntaxKind.GreaterThanEqualsToken],
['=', ts.SyntaxKind.EqualsToken],
['==', ts.SyntaxKind.EqualsEqualsToken],
['===', ts.SyntaxKind.EqualsEqualsEqualsToken],
['*', ts.SyntaxKind.AsteriskToken],
['**', ts.SyntaxKind.AsteriskAsteriskToken],
['/', ts.SyntaxKind.SlashToken],
['%', ts.SyntaxKind.PercentToken],
['!=', ts.SyntaxKind.ExclamationEqualsToken],
['!==', ts.SyntaxKind.ExclamationEqualsEqualsToken],
['||', ts.SyntaxKind.BarBarToken],
['&&', ts.SyntaxKind.AmpersandAmpersandToken],
['&', ts.SyntaxKind.AmpersandToken],
['|', ts.SyntaxKind.BarToken],
['??', ts.SyntaxKind.QuestionQuestionToken],
['=', ts.SyntaxKind.EqualsToken],
['+=', ts.SyntaxKind.PlusEqualsToken],
['-=', ts.SyntaxKind.MinusEqualsToken],
['*=', ts.SyntaxKind.AsteriskEqualsToken],
['/=', ts.SyntaxKind.SlashEqualsToken],
['%=', ts.SyntaxKind.PercentEqualsToken],
['**=', ts.SyntaxKind.AsteriskAsteriskEqualsToken],
['&&=', ts.SyntaxKind.AmpersandAmpersandEqualsToken],
['||=', ts.SyntaxKind.BarBarEqualsToken],
['??=', ts.SyntaxKind.QuestionQuestionEqualsToken],
['in', ts.SyntaxKind.InKeyword],
['instanceof', ts.SyntaxKind.InstanceOfKeyword],
]);
class TcbExprTranslator implements AstVisitor {
constructor(
private maybeResolve: (ast: AST) => ts.Expression | null,
private maybeResolve: (ast: AST) => TcbExpr | 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.
translate(ast: AST): TcbExpr {
if (ast instanceof ASTWithSource) {
ast = ast.ast;
}
@ -141,37 +75,31 @@ class AstTranslator implements AstVisitor {
return ast.visit(this);
}
visitUnary(ast: Unary): ts.Expression {
visitUnary(ast: Unary): TcbExpr {
const expr = this.translate(ast.expr);
const op = this.UNARY_OPS.get(ast.operator);
if (op === undefined) {
throw new Error(`Unsupported Unary.operator: ${ast.operator}`);
}
const node = wrapForDiagnostics(ts.factory.createPrefixUnaryExpression(op, expr));
addParseSpanInfo(node, ast.sourceSpan);
const node = new TcbExpr(`${ast.operator}${expr.print()}`);
return node.wrapForTypeChecker().addParseSpanInfo(ast.sourceSpan);
}
visitBinary(ast: Binary): TcbExpr {
const lhs = this.translate(ast.left);
const rhs = this.translate(ast.right);
lhs.wrapForTypeChecker();
rhs.wrapForTypeChecker();
const node = new TcbExpr(`${lhs.print()} ${ast.operation} ${rhs.print()}`);
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitBinary(ast: Binary): ts.Expression {
const lhs = wrapForDiagnostics(this.translate(ast.left));
const rhs = wrapForDiagnostics(this.translate(ast.right));
const op = this.BINARY_OPS.get(ast.operation);
if (op === undefined) {
throw new Error(`Unsupported Binary.operation: ${ast.operation}`);
}
const node = ts.factory.createBinaryExpression(lhs, op, rhs);
addParseSpanInfo(node, ast.sourceSpan);
visitChain(ast: Chain): TcbExpr {
const elements = ast.expressions.map((expr) => this.translate(expr).print());
const node = new TcbExpr(elements.join(', '));
node.wrapForTypeChecker();
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitChain(ast: Chain): ts.Expression {
const elements = ast.expressions.map((expr) => this.translate(expr));
const node = wrapForDiagnostics(ts.factory.createCommaListExpression(elements));
addParseSpanInfo(node, ast.sourceSpan);
return node;
}
visitConditional(ast: Conditional): ts.Expression {
visitConditional(ast: Conditional): TcbExpr {
const condExpr = this.translate(ast.condition);
const trueExpr = this.translate(ast.trueExp);
// Wrap `falseExpr` in parens so that the trailing parse span info is not attributed to the
@ -181,11 +109,10 @@ class AstTranslator implements AstVisitor {
// is immediately before it:
// `conditional /*1,2*/ ? trueExpr /*3,4*/ : falseExpr /*5,6*/`
// This should be instead be `conditional /*1,2*/ ? trueExpr /*3,4*/ : (falseExpr /*5,6*/)`
const falseExpr = wrapForTypeChecker(this.translate(ast.falseExp));
const node = ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(condExpr, undefined, trueExpr, undefined, falseExpr),
);
addParseSpanInfo(node, ast.sourceSpan);
const falseExpr = this.translate(ast.falseExp).wrapForTypeChecker();
const node = new TcbExpr(
`(${condExpr.print()} ? ${trueExpr.print()} : ${falseExpr.print()})`,
).addParseSpanInfo(ast.sourceSpan);
return node;
}
@ -197,213 +124,176 @@ class AstTranslator implements AstVisitor {
throw new Error('Method not implemented.');
}
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
return wrapForTypeChecker(
ts.factory.createRegularExpressionLiteral(`/${ast.body}/${ast.flags ?? ''}`),
);
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any): TcbExpr {
const node = new TcbExpr(`/${ast.body}/${ast.flags ?? ''}`);
node.wrapForTypeChecker();
return node;
}
visitInterpolation(ast: Interpolation): ts.Expression {
visitInterpolation(ast: Interpolation): TcbExpr {
// Build up a chain of binary + operations to simulate the string concatenation of the
// interpolation's expressions. The chain is started using an actual string literal to ensure
// the type is inferred as 'string'.
return ast.expressions.reduce(
(lhs: ts.Expression, ast: AST) =>
ts.factory.createBinaryExpression(
lhs,
ts.SyntaxKind.PlusToken,
wrapForTypeChecker(this.translate(ast)),
),
ts.factory.createStringLiteral(''),
);
const exprs = ast.expressions.map((e) => {
const node = this.translate(e);
node.wrapForTypeChecker();
return node.print();
});
return new TcbExpr(`"" + ${exprs.join(' + ')}`);
}
visitKeyedRead(ast: KeyedRead): ts.Expression {
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
visitKeyedRead(ast: KeyedRead): TcbExpr {
const receiver = this.translate(ast.receiver).wrapForTypeChecker();
const key = this.translate(ast.key);
const node = ts.factory.createElementAccessExpression(receiver, key);
addParseSpanInfo(node, ast.sourceSpan);
return node;
return new TcbExpr(`${receiver.print()}[${key.print()}]`).addParseSpanInfo(ast.sourceSpan);
}
visitLiteralArray(ast: LiteralArray): ts.Expression {
visitLiteralArray(ast: LiteralArray): TcbExpr {
const elements = ast.expressions.map((expr) => this.translate(expr));
const literal = ts.factory.createArrayLiteralExpression(elements);
let content = `[${elements.map((el) => el.print()).join(', ')}]`;
// If strictLiteralTypes is disabled, array literals are cast to `any`.
const node = this.config.strictLiteralTypes ? literal : tsCastToAny(literal);
addParseSpanInfo(node, ast.sourceSpan);
return node;
if (!this.config.strictLiteralTypes) {
content += ' as any';
}
return new TcbExpr(content).addParseSpanInfo(ast.sourceSpan);
}
visitLiteralMap(ast: LiteralMap): ts.Expression {
visitLiteralMap(ast: LiteralMap): TcbExpr {
const properties = ast.keys.map((key, idx) => {
const value = this.translate(ast.values[idx]);
if (key.kind === 'property') {
const keyNode = ts.factory.createStringLiteral(key.key);
addParseSpanInfo(keyNode, key.sourceSpan);
return ts.factory.createPropertyAssignment(keyNode, value);
const keyNode = new TcbExpr(`"${key.key}"`).addParseSpanInfo(key.sourceSpan);
return `${keyNode.print()}: ${value.print()}`;
} else {
return ts.factory.createSpreadAssignment(value);
return `...${value.print()}`;
}
});
const literal = ts.factory.createObjectLiteralExpression(properties, true);
// If strictLiteralTypes is disabled, object literals are cast to `any`.
const node = this.config.strictLiteralTypes ? literal : tsCastToAny(literal);
addParseSpanInfo(node, ast.sourceSpan);
return node;
return new TcbExpr(
`{ ${properties.join(', ')} }${this.config.strictLiteralTypes ? '' : ' as any'}`,
).addParseSpanInfo(ast.sourceSpan);
}
visitLiteralPrimitive(ast: LiteralPrimitive): ts.Expression {
let node: ts.Expression;
visitLiteralPrimitive(ast: LiteralPrimitive): TcbExpr {
let node: TcbExpr;
if (ast.value === undefined) {
node = ts.factory.createIdentifier('undefined');
node = new TcbExpr('undefined');
} else if (ast.value === null) {
node = ts.factory.createNull();
node = new TcbExpr('null');
} else if (typeof ast.value === 'string') {
node = ts.factory.createStringLiteral(ast.value);
node = new TcbExpr(JSON.stringify(ast.value));
} else if (typeof ast.value === 'number') {
node = tsNumericExpression(ast.value);
if (Number.isNaN(ast.value)) {
node = new TcbExpr('NaN');
} else if (!Number.isFinite(ast.value)) {
node = new TcbExpr(ast.value > 0 ? 'Infinity' : '-Infinity');
} else {
node = new TcbExpr(ast.value.toString());
}
} else if (typeof ast.value === 'boolean') {
node = ast.value ? ts.factory.createTrue() : ts.factory.createFalse();
node = new TcbExpr(ast.value + '');
} else {
throw Error(`Unsupported AST value of type ${typeof ast.value}`);
}
addParseSpanInfo(node, ast.sourceSpan);
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitNonNullAssert(ast: NonNullAssert): ts.Expression {
const expr = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createNonNullExpression(expr);
addParseSpanInfo(node, ast.sourceSpan);
return node;
visitNonNullAssert(ast: NonNullAssert): TcbExpr {
const expr = this.translate(ast.expression).wrapForTypeChecker();
return new TcbExpr(`${expr.print()}!`).addParseSpanInfo(ast.sourceSpan);
}
visitPipe(ast: BindingPipe): never {
throw new Error('Method not implemented.');
}
visitPrefixNot(ast: PrefixNot): ts.Expression {
const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createLogicalNot(expression);
addParseSpanInfo(node, ast.sourceSpan);
return node;
visitPrefixNot(ast: PrefixNot): TcbExpr {
const expression = this.translate(ast.expression).wrapForTypeChecker();
return new TcbExpr(`!${expression.print()}`).addParseSpanInfo(ast.sourceSpan);
}
visitTypeofExpression(ast: TypeofExpression): ts.Expression {
const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createTypeOfExpression(expression);
addParseSpanInfo(node, ast.sourceSpan);
return node;
visitTypeofExpression(ast: TypeofExpression): TcbExpr {
const expression = this.translate(ast.expression).wrapForTypeChecker();
return new TcbExpr(`typeof ${expression.print()}`).addParseSpanInfo(ast.sourceSpan);
}
visitVoidExpression(ast: VoidExpression): ts.Expression {
const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createVoidExpression(expression);
addParseSpanInfo(node, ast.sourceSpan);
return node;
visitVoidExpression(ast: VoidExpression): TcbExpr {
const expression = this.translate(ast.expression).wrapForTypeChecker();
return new TcbExpr(`void ${expression.print()}`).addParseSpanInfo(ast.sourceSpan);
}
visitPropertyRead(ast: PropertyRead): ts.Expression {
visitPropertyRead(ast: PropertyRead): TcbExpr {
// This is a normal property read - convert the receiver to an expression and emit the correct
// TypeScript expression to read the property.
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const name = ts.factory.createPropertyAccessExpression(receiver, ast.name);
addParseSpanInfo(name, ast.nameSpan);
const node = wrapForDiagnostics(name);
addParseSpanInfo(node, ast.sourceSpan);
return node;
const receiver = this.translate(ast.receiver).wrapForTypeChecker();
return new TcbExpr(`${receiver.print()}.${ast.name}`)
.addParseSpanInfo(ast.nameSpan)
.wrapForTypeChecker()
.addParseSpanInfo(ast.sourceSpan);
}
visitSafePropertyRead(ast: SafePropertyRead): ts.Expression {
let node: ts.Expression;
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
visitSafePropertyRead(ast: SafePropertyRead): TcbExpr {
let node: TcbExpr;
const receiver = this.translate(ast.receiver).wrapForTypeChecker();
const name = new TcbExpr(ast.name).addParseSpanInfo(ast.nameSpan);
// The form of safe property reads depends on whether strictness is in use.
if (this.config.strictSafeNavigationTypes) {
// Basically, the return here is either the type of the complete expression with a null-safe
// property read, or `undefined`. So a ternary is used to create an "or" type:
// "a?.b" becomes (0 as any ? a!.b : undefined)
// The type of this expression is (typeof a!.b) | undefined, which is exactly as desired.
const expr = ts.factory.createPropertyAccessExpression(
ts.factory.createNonNullExpression(receiver),
ast.name,
);
addParseSpanInfo(expr, ast.nameSpan);
node = ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(
getAnyExpression(),
undefined,
expr,
undefined,
this.UNDEFINED,
),
);
node = new TcbExpr(`(0 as any ? ${receiver.print()}!.${name.print()} : undefined)`);
} else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// Emulate a View Engine bug where 'any' is inferred for the left-hand side of the safe
// navigation operation. With this bug, the type of the left-hand side is regarded as any.
// Therefore, the left-hand side only needs repeating in the output (to validate it), and then
// 'any' is used for the rest of the expression. This is done using a comma operator:
// "a?.b" becomes (a as any).b, which will of course have type 'any'.
node = ts.factory.createPropertyAccessExpression(tsCastToAny(receiver), ast.name);
node = new TcbExpr(`(${receiver.print()} as any).${name.print()}`);
} else {
// The View Engine bug isn't active, so check the entire type of the expression, but the final
// result is still inferred as `any`.
// "a?.b" becomes (a!.b as any)
const expr = ts.factory.createPropertyAccessExpression(
ts.factory.createNonNullExpression(receiver),
ast.name,
);
addParseSpanInfo(expr, ast.nameSpan);
node = tsCastToAny(expr);
node = new TcbExpr(`(${receiver.print()}!.${name.print()} as any)`);
}
addParseSpanInfo(node, ast.sourceSpan);
return node;
return node.addParseSpanInfo(ast.sourceSpan);
}
visitSafeKeyedRead(ast: SafeKeyedRead): ts.Expression {
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
visitSafeKeyedRead(ast: SafeKeyedRead): TcbExpr {
const receiver = this.translate(ast.receiver).wrapForTypeChecker();
const key = this.translate(ast.key);
let node: ts.Expression;
let node: TcbExpr;
// The form of safe property reads depends on whether strictness is in use.
if (this.config.strictSafeNavigationTypes) {
// "a?.[...]" becomes (0 as any ? a![...] : undefined)
const expr = ts.factory.createElementAccessExpression(
ts.factory.createNonNullExpression(receiver),
key,
);
addParseSpanInfo(expr, ast.sourceSpan);
node = ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(
getAnyExpression(),
undefined,
expr,
undefined,
this.UNDEFINED,
),
const elementAccess = new TcbExpr(`${receiver.print()}![${key.print()}]`).addParseSpanInfo(
ast.sourceSpan,
);
node = new TcbExpr(`(0 as any ? ${elementAccess.print()} : undefined)`);
} else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// "a?.[...]" becomes (a as any)[...]
node = ts.factory.createElementAccessExpression(tsCastToAny(receiver), key);
node = new TcbExpr(`(${receiver.print()} as any)[${key.print()}]`);
} else {
// "a?.[...]" becomes (a!.[...] as any)
const expr = ts.factory.createElementAccessExpression(
ts.factory.createNonNullExpression(receiver),
key,
const elementAccess = new TcbExpr(`${receiver.print()}![${key.print()}]`).addParseSpanInfo(
ast.sourceSpan,
);
addParseSpanInfo(expr, ast.sourceSpan);
node = tsCastToAny(expr);
node = new TcbExpr(`(${elementAccess.print()} as any)`);
}
addParseSpanInfo(node, ast.sourceSpan);
return node;
return node.addParseSpanInfo(ast.sourceSpan);
}
visitCall(ast: Call): ts.Expression {
visitCall(ast: Call): TcbExpr {
const args = ast.args.map((expr) => this.translate(expr));
let expr: ts.Expression;
const receiver = ast.receiver;
let expr: TcbExpr;
// For calls that have a property read as receiver, we have to special-case their emit to avoid
// inserting superfluous parenthesis as they prevent TypeScript from applying a narrowing effect
@ -413,101 +303,90 @@ class AstTranslator implements AstVisitor {
if (resolved !== null) {
expr = resolved;
} else {
const propertyReceiver = wrapForDiagnostics(this.translate(receiver.receiver));
expr = ts.factory.createPropertyAccessExpression(propertyReceiver, receiver.name);
addParseSpanInfo(expr, receiver.nameSpan);
const propertyReceiver = this.translate(receiver.receiver).wrapForTypeChecker();
expr = new TcbExpr(`${propertyReceiver.print()}.${receiver.name}`).addParseSpanInfo(
receiver.nameSpan,
);
}
} else {
expr = this.translate(receiver);
}
let node: ts.Expression;
let node: TcbExpr;
// Safe property/keyed reads will produce a ternary whose value is nullable.
// We have to generate a similar ternary around the call.
if (ast.receiver instanceof SafePropertyRead || ast.receiver instanceof SafeKeyedRead) {
node = this.convertToSafeCall(ast, expr, args);
} else {
node = ts.factory.createCallExpression(expr, undefined, args);
node = new TcbExpr(`${expr.print()}(${args.map((arg) => arg.print()).join(', ')})`);
}
addParseSpanInfo(node, ast.sourceSpan);
return node;
return node.addParseSpanInfo(ast.sourceSpan);
}
visitSafeCall(ast: SafeCall): ts.Expression {
visitSafeCall(ast: SafeCall): TcbExpr {
const args = ast.args.map((expr) => this.translate(expr));
const expr = wrapForDiagnostics(this.translate(ast.receiver));
const node = this.convertToSafeCall(ast, expr, args);
addParseSpanInfo(node, ast.sourceSpan);
return node;
const expr = this.translate(ast.receiver).wrapForTypeChecker();
return this.convertToSafeCall(ast, expr, args).addParseSpanInfo(ast.sourceSpan);
}
visitTemplateLiteral(ast: TemplateLiteral): ts.TemplateLiteral {
visitTemplateLiteral(ast: TemplateLiteral): TcbExpr {
const length = ast.elements.length;
const head = ast.elements[0];
let result: ts.TemplateLiteral;
let result: string;
if (length === 1) {
result = ts.factory.createNoSubstitutionTemplateLiteral(head.text);
result = `\`${head.text}\``;
} else {
const spans: ts.TemplateSpan[] = [];
let parts = [`\`${head.text}`];
const tailIndex = length - 1;
for (let i = 1; i < tailIndex; i++) {
const middle = ts.factory.createTemplateMiddle(ast.elements[i].text);
spans.push(ts.factory.createTemplateSpan(this.translate(ast.expressions[i - 1]), middle));
const expr = this.translate(ast.expressions[i - 1]);
parts.push(`\${${expr.print()}}${ast.elements[i].text}`);
}
const resolvedExpression = this.translate(ast.expressions[tailIndex - 1]);
const templateTail = ts.factory.createTemplateTail(ast.elements[tailIndex].text);
spans.push(ts.factory.createTemplateSpan(resolvedExpression, templateTail));
result = ts.factory.createTemplateExpression(ts.factory.createTemplateHead(head.text), spans);
parts.push(`\${${resolvedExpression.print()}}${ast.elements[tailIndex].text}\``);
result = parts.join('');
}
return result;
return new TcbExpr(result);
}
visitTemplateLiteralElement(ast: TemplateLiteralElement, context: any) {
visitTemplateLiteralElement() {
throw new Error('Method not implemented');
}
visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral): ts.TaggedTemplateExpression {
return ts.factory.createTaggedTemplateExpression(
this.translate(ast.tag),
undefined,
this.visitTemplateLiteral(ast.template),
);
visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral): TcbExpr {
const tag = this.translate(ast.tag);
const template = this.visitTemplateLiteral(ast.template);
return new TcbExpr(`${tag.print()}${template.print()}`);
}
visitParenthesizedExpression(ast: ParenthesizedExpression): ts.ParenthesizedExpression {
return ts.factory.createParenthesizedExpression(this.translate(ast.expression));
visitParenthesizedExpression(ast: ParenthesizedExpression): TcbExpr {
const expr = this.translate(ast.expression);
return new TcbExpr(`(${expr.print()})`);
}
visitSpreadElement(ast: SpreadElement) {
const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createSpreadElement(expression);
addParseSpanInfo(node, ast.sourceSpan);
const expression = this.translate(ast.expression);
expression.wrapForTypeChecker();
const node = new TcbExpr(`...${expression.print()}`);
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitEmptyExpr(ast: EmptyExpr, context: any) {
const node = ts.factory.createIdentifier('undefined');
addParseSpanInfo(node, ast.sourceSpan);
visitEmptyExpr(ast: EmptyExpr) {
const node = new TcbExpr('undefined');
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitArrowFunction(ast: ArrowFunction): ts.ArrowFunction {
const params = ast.parameters.map((param) => {
const paramNode = ts.factory.createParameterDeclaration(undefined, undefined, param.name);
// Ignore diagnostics on the node to skip diagnostics from `noImplicitAny` since
// users aren't able to set types on the parameters. Note that this is preferable
// to setting their types to `any`, because it allows us to infer the types when
// the arrow function is passed as a callback.
markIgnoreDiagnostics(paramNode);
return paramNode;
});
const body = astToTypescript(
visitArrowFunction(ast: ArrowFunction): TcbExpr {
const params = ast.parameters
.map((param) => new TcbExpr(param.name).markIgnoreDiagnostics().print())
.join(', ');
const body = astToTcbExpr(
ast.body,
(innerAst) => {
if (
@ -521,8 +400,8 @@ class AstTranslator implements AstVisitor {
const correspondingParam = ast.parameters.find((arg) => arg.name === innerAst.name);
if (correspondingParam) {
const node = ts.factory.createIdentifier(innerAst.name);
addParseSpanInfo(node, innerAst.sourceSpan);
const node = new TcbExpr(innerAst.name);
node.addParseSpanInfo(innerAst.sourceSpan);
return node;
}
@ -531,41 +410,27 @@ class AstTranslator implements AstVisitor {
this.config,
);
return ts.factory.createArrowFunction(undefined, undefined, params, undefined, undefined, body);
return new TcbExpr(
`${ast.parameters.length === 1 ? params : `(${params})`} => ${body.print()}`,
);
}
private convertToSafeCall(
ast: Call | SafeCall,
expr: ts.Expression,
args: ts.Expression[],
): ts.Expression {
private convertToSafeCall(ast: Call | SafeCall, exprNode: TcbExpr, argNodes: TcbExpr[]): TcbExpr {
const expr = exprNode.print();
const args = argNodes.map((node) => node.print()).join(', ');
if (this.config.strictSafeNavigationTypes) {
// "a?.method(...)" becomes (0 as any ? a!.method(...) : undefined)
const call = ts.factory.createCallExpression(
ts.factory.createNonNullExpression(expr),
undefined,
args,
);
return ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(
getAnyExpression(),
undefined,
call,
undefined,
this.UNDEFINED,
),
);
// (0 as any ? a!.method(...) : undefined)
return new TcbExpr(`(0 as any ? ${expr}!(${args}) : undefined)`);
}
if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// "a?.method(...)" becomes (a as any).method(...)
return ts.factory.createCallExpression(tsCastToAny(expr), undefined, args);
// (a as any).method(...)
return new TcbExpr(`(${expr} as any)(${args})`);
}
// "a?.method(...)" becomes (a!.method(...) as any)
return tsCastToAny(
ts.factory.createCallExpression(ts.factory.createNonNullExpression(expr), undefined, args),
);
// (a!.method(...) as any)
return new TcbExpr(`(${expr}!(${args}) as any)`);
}
}

View file

@ -129,17 +129,12 @@ export function createHostElement(
* Creates an AST node that can be used as a guard in `if` statements to distinguish TypeScript
* nodes used for checking host bindings from ones used for checking templates.
*/
export function createHostBindingsBlockGuard(): ts.Expression {
export function createHostBindingsBlockGuard(): string {
// Note that the comment text is quite generic. This doesn't really matter, because it is
// used only inside a TCB and there's no way for users to produce a comment there.
// `true /*hostBindings*/`.
const trueExpr = ts.addSyntheticTrailingComment(
ts.factory.createTrue(),
ts.SyntaxKind.MultiLineCommentTrivia,
GUARD_COMMENT_TEXT,
);
// `true /*hostBindingsBlockGuard*/`.
// Wrap the expression in parentheses to ensure that the comment is attached to the correct node.
return ts.factory.createParenthesizedExpression(trueExpr);
return `(true /*${GUARD_COMMENT_TEXT}*/)`;
}
/**

View file

@ -5,7 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import ts from 'typescript';
import {TcbExpr} from './codegen';
/**
* A code generation operation that's involved in the construction of a Type Check Block.
@ -32,7 +33,7 @@ export abstract class TcbOp {
*/
abstract readonly optional: boolean;
abstract execute(): ts.Expression | null;
abstract execute(): TcbExpr | null;
/**
* Replacement value or operation used while this `TcbOp` is executing (i.e. to resolve circular
@ -42,13 +43,13 @@ export abstract class TcbOp {
* `TcbOp` can be returned in cases where additional code generation is necessary to deal with
* circular references.
*/
circularFallback(): TcbOp | ts.Expression {
circularFallback(): TcbOp | TcbExpr {
// Value used to break a circular reference between `TcbOp`s.
//
// This value is returned whenever `TcbOp`s have a circular dependency. The
// expression is a non-null assertion of the null value (in TypeScript, the
// expression `null!`). This construction will infer the least narrow type
// for whatever it's assigned to.
return ts.factory.createNonNullExpression(ts.factory.createNull());
return new TcbExpr('null!');
}
}

View file

@ -9,6 +9,8 @@
import {
AST,
BindingType,
LiteralArray,
LiteralMap,
ParseSourceSpan,
TmplAstBoundAttribute,
TmplAstBoundEvent,
@ -23,7 +25,7 @@ import {TypeCheckableDirectiveMeta} from '../../api';
import {ClassPropertyName} from '../../../metadata';
import {Reference} from '../../../imports';
import {Context} from './context';
import {tsCastToAny} from '../ts_util';
import {TcbExpr} from './codegen';
export interface TcbBoundAttribute {
value: AST | string;
@ -50,9 +52,14 @@ export interface TcbDirectiveBoundInput {
field: string;
/**
* The `ts.Expression` corresponding with the input binding expression.
* The `TcbExpr` corresponding with the input binding expression.
*/
expression: ts.Expression;
expression: TcbExpr;
/**
* The input's original value expression.
*/
originalExpression: AST | string;
/**
* The source span of the full attribute binding.
@ -177,13 +184,13 @@ export function checkSplitTwoWayBinding(
/**
* Potentially widens the type of `expr` according to the type-checking configuration.
*/
export function widenBinding(expr: ts.Expression, tcb: Context): ts.Expression {
export function widenBinding(expr: TcbExpr, tcb: Context, originalValue: string | AST): TcbExpr {
if (!tcb.env.config.checkTypeOfInputBindings) {
// If checking the type of bindings is disabled, cast the resulting expression to 'any'
// before the assignment.
return tsCastToAny(expr);
return new TcbExpr(`(${expr.print()} as any)`);
} else if (!tcb.env.config.strictNullInputBindings) {
if (ts.isObjectLiteralExpression(expr) || ts.isArrayLiteralExpression(expr)) {
if (originalValue instanceof LiteralMap || originalValue instanceof LiteralArray) {
// Object literals and array literals should not be wrapped in non-null assertions as that
// would cause literals to be prematurely widened, resulting in type errors when assigning
// into a literal type.
@ -191,7 +198,7 @@ export function widenBinding(expr: ts.Expression, tcb: Context): ts.Expression {
} else {
// If strict null checks are disabled, erase `null` and `undefined` from the type by
// wrapping the expression in a non-null assertion.
return ts.factory.createNonNullExpression(expr);
return new TcbExpr(`${expr.print()}!`);
}
} else {
// No widening is requested, use the expression as is.

View file

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.dev/license
*/
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Scope} from './scope';
import {addExpressionIdentifier, ExpressionIdentifier, markIgnoreDiagnostics} from '../comments';
import {ExpressionIdentifier} from '../comments';
/**
* A `TcbOp` which generates a completion point for the component context.
@ -26,11 +26,10 @@ export class TcbComponentContextCompletionOp extends TcbOp {
override readonly optional = false;
override execute(): null {
const ctx = ts.factory.createThis();
const ctxDot = ts.factory.createPropertyAccessExpression(ctx, '');
markIgnoreDiagnostics(ctxDot);
addExpressionIdentifier(ctxDot, ExpressionIdentifier.COMPONENT_COMPLETION);
this.scope.addStatement(ts.factory.createExpressionStatement(ctxDot));
const ctx = new TcbExpr('this.');
ctx.markIgnoreDiagnostics();
ctx.addExpressionIdentifier(ExpressionIdentifier.COMPONENT_COMPLETION);
this.scope.addStatement(ctx);
return null;
}
}

View file

@ -7,7 +7,6 @@
*/
import {BoundTarget, SchemaMetadata} from '@angular/compiler';
import ts from 'typescript';
import {DomSchemaChecker} from '../dom';
import {OutOfBandDiagnosticRecorder} from '../oob';
import {TypeCheckableDirectiveMeta, TypeCheckId} from '../../api';
@ -70,8 +69,8 @@ export class Context {
* Currently this uses a monotonically increasing counter, but in the future the variable name
* might change depending on the type of data being stored.
*/
allocateId(): ts.Identifier {
return ts.factory.createIdentifier(`_t${this.nextId++}`);
allocateId(): string {
return `_t${this.nextId++}`;
}
getPipeByName(name: string): PipeMeta | null {

View file

@ -7,16 +7,13 @@
*/
import {DirectiveOwner, ParseSourceSpan, TmplAstHostElement} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {addExpressionIdentifier, ExpressionIdentifier, markIgnoreDiagnostics} from '../comments';
import {addParseSpanInfo, wrapForDiagnostics} from '../diagnostics';
import {tsCreateVariable} from '../ts_util';
import {ExpressionIdentifier} from '../comments';
import {unwrapWritableSignal} from './expression';
import {getAnyExpression} from '../expression';
import {CustomFormControlType, expandBoundAttributesForField} from './signal_forms';
import {getBoundAttributes, TcbBoundAttribute, TcbDirectiveInput, widenBinding} from './bindings';
import {translateInput} from './inputs';
@ -50,9 +47,9 @@ export class TcbDirectiveCtorOp extends TcbOp {
return true;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const genericInputs = new Map<string, TcbDirectiveInput>();
const id = this.tcb.allocateId();
const id = new TcbExpr(this.tcb.allocateId());
let boundAttrs: TcbBoundAttribute[];
let span: ParseSourceSpan;
@ -77,8 +74,7 @@ export class TcbDirectiveCtorOp extends TcbOp {
}
}
addExpressionIdentifier(id, ExpressionIdentifier.DIRECTIVE);
addParseSpanInfo(id, span);
id.addExpressionIdentifier(ExpressionIdentifier.DIRECTIVE).addParseSpanInfo(span);
for (const attr of boundAttrs) {
// Skip text attributes if configured to do so.
@ -98,6 +94,7 @@ export class TcbDirectiveCtorOp extends TcbOp {
type: 'binding',
field: fieldName,
expression,
originalExpression: attr.value,
sourceSpan: attr.sourceSpan,
isTwoWayBinding,
});
@ -114,8 +111,8 @@ export class TcbDirectiveCtorOp extends TcbOp {
// Call the type constructor of the directive to infer a type, and assign the directive
// instance.
const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, Array.from(genericInputs.values()));
markIgnoreDiagnostics(typeCtor);
this.scope.addStatement(tsCreateVariable(id, typeCtor));
typeCtor.markIgnoreDiagnostics();
this.scope.addStatement(new TcbExpr(`var ${id.print()} = ${typeCtor.print()}`));
return id;
}
@ -151,16 +148,11 @@ export class TcbDirectiveCtorCircularFallbackOp extends TcbOp {
return false;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const id = this.tcb.allocateId();
const typeCtor = this.tcb.env.typeCtorFor(this.dir);
const circularPlaceholder = ts.factory.createCallExpression(
typeCtor,
/* typeArguments */ undefined,
[ts.factory.createNonNullExpression(ts.factory.createNull())],
);
this.scope.addStatement(tsCreateVariable(id, circularPlaceholder));
return id;
this.scope.addStatement(new TcbExpr(`var ${id} = ${typeCtor.print()}(null!)`));
return new TcbExpr(id);
}
}
@ -172,39 +164,39 @@ function tcbCallTypeCtor(
dir: TypeCheckableDirectiveMeta,
tcb: Context,
inputs: TcbDirectiveInput[],
): ts.Expression {
): TcbExpr {
const typeCtor = tcb.env.typeCtorFor(dir);
let literal = '{ ';
// Construct an array of `ts.PropertyAssignment`s for each of the directive's inputs.
const members = inputs.map((input) => {
const propertyName = ts.factory.createStringLiteral(input.field);
// Construct an object literal containing each directive input.
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
const propertyName = input.field;
const isLast = i === inputs.length - 1;
if (input.type === 'binding') {
// For bound inputs, the property is assigned the binding expression.
let expr = widenBinding(input.expression, tcb);
let expr = widenBinding(input.expression, tcb, input.originalExpression);
if (input.isTwoWayBinding && tcb.env.config.allowSignalsInTwoWayBindings) {
expr = unwrapWritableSignal(expr, tcb);
}
const assignment = ts.factory.createPropertyAssignment(
propertyName,
wrapForDiagnostics(expr),
);
addParseSpanInfo(assignment, input.sourceSpan);
return assignment;
const assignment = new TcbExpr(`"${propertyName}": ${expr.wrapForTypeChecker().print()}`);
assignment.addParseSpanInfo(input.sourceSpan);
literal += assignment.print();
} else {
// A type constructor is required to be called with all input properties, so any unset
// inputs are simply assigned a value of type `any` to ignore them.
return ts.factory.createPropertyAssignment(propertyName, getAnyExpression());
literal += `"${propertyName}": 0 as any`;
}
});
literal += `${isLast ? '' : ','} `;
}
literal += '}';
// Call the `ngTypeCtor` method on the directive class, with an object literal argument created
// from the matched inputs.
return ts.factory.createCallExpression(
/* expression */ typeCtor,
/* typeArguments */ undefined,
/* argumentsArray */ [ts.factory.createObjectLiteralExpression(members)],
);
return new TcbExpr(`${typeCtor.print()}(${literal})`);
}

View file

@ -11,12 +11,11 @@ import ts from 'typescript';
import type {Context} from './context';
import type {Scope} from './scope';
import {TcbOp} from './base';
import {declareVariable, TcbExpr, tempPrint} from './codegen';
import {TypeCheckableDirectiveMeta} from '../../api';
import {Reference} from '../../../imports';
import {ClassDeclaration} from '../../../reflection';
import {addExpressionIdentifier, ExpressionIdentifier} from '../comments';
import {addParseSpanInfo} from '../diagnostics';
import {tsDeclareVariable} from '../ts_util';
import {ExpressionIdentifier} from '../comments';
/**
* A `TcbOp` which constructs an instance of a directive. For generic directives, generic
@ -39,15 +38,14 @@ export abstract class TcbDirectiveTypeOpBase extends TcbOp {
return true;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
const rawType = this.tcb.env.referenceType(this.dir.ref);
let type: ts.TypeNode;
let type: TcbExpr;
let span: ParseSourceSpan;
if (this.dir.isGeneric === false || dirRef.node.typeParameters === undefined) {
type = rawType;
type = new TcbExpr(tempPrint(rawType, dirRef.node.getSourceFile()));
} else {
if (!ts.isTypeReferenceNode(rawType)) {
throw new Error(
@ -57,7 +55,13 @@ export abstract class TcbDirectiveTypeOpBase extends TcbOp {
const typeArguments = dirRef.node.typeParameters.map(() =>
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
type = ts.factory.createTypeReferenceNode(rawType.typeName, typeArguments);
type = new TcbExpr(
tempPrint(
ts.factory.createTypeReferenceNode(rawType.typeName, typeArguments),
dirRef.node.getSourceFile(),
),
);
}
if (this.node instanceof TmplAstHostElement) {
@ -66,10 +70,10 @@ export abstract class TcbDirectiveTypeOpBase extends TcbOp {
span = this.node.startSourceSpan || this.node.sourceSpan;
}
const id = this.tcb.allocateId();
addExpressionIdentifier(id, ExpressionIdentifier.DIRECTIVE);
addParseSpanInfo(id, span);
this.scope.addStatement(tsDeclareVariable(id, type));
const id = new TcbExpr(this.tcb.allocateId())
.addExpressionIdentifier(ExpressionIdentifier.DIRECTIVE)
.addParseSpanInfo(span);
this.scope.addStatement(declareVariable(id, type));
return id;
}
}
@ -88,7 +92,7 @@ export class TcbNonGenericDirectiveTypeOp extends TcbDirectiveTypeOpBase {
* Creates a variable declaration for this op's directive of the argument type. Returns the id of
* the newly created variable.
*/
override execute(): ts.Identifier {
override execute(): TcbExpr {
const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
if (this.dir.isGeneric) {
throw new Error(`Assertion Error: expected ${dirRef.debugName} not to be generic.`);
@ -106,7 +110,7 @@ export class TcbNonGenericDirectiveTypeOp extends TcbDirectiveTypeOpBase {
* type parameters set to `any`.
*/
export class TcbGenericDirectiveTypeWithAnyParamsOp extends TcbDirectiveTypeOpBase {
override execute(): ts.Identifier {
override execute(): TcbExpr {
const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
if (dirRef.node.typeParameters === undefined) {
throw new Error(

View file

@ -7,12 +7,10 @@
*/
import {TmplAstElement} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {tsCreateElement, tsCreateVariable} from '../ts_util';
import {addParseSpanInfo} from '../diagnostics';
/**
* A `TcbOp` which creates an expression for a native DOM element (or web component) from a
@ -36,12 +34,17 @@ export class TcbElementOp extends TcbOp {
return true;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const id = this.tcb.allocateId();
const idNode = new TcbExpr(id);
idNode.addParseSpanInfo(this.element.startSourceSpan || this.element.sourceSpan);
// Add the declaration of the element using document.createElement.
const initializer = tsCreateElement(this.element.name);
addParseSpanInfo(initializer, this.element.startSourceSpan || this.element.sourceSpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
return id;
const initializer = new TcbExpr(`document.createElement("${this.element.name}")`);
initializer.addParseSpanInfo(this.element.startSourceSpan || this.element.sourceSpan);
const stmt = new TcbExpr(`var ${idNode.print()} = ${initializer.print()}`);
stmt.addParseSpanInfo(this.element.startSourceSpan || this.element.sourceSpan);
this.scope.addStatement(stmt);
return idNode;
}
}

View file

@ -12,20 +12,17 @@ import {
ImplicitReceiver,
ParsedEventType,
PropertyRead,
ThisReceiver,
TmplAstBoundAttribute,
TmplAstBoundEvent,
TmplAstElement,
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {getStatementsBlock, TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {addParseSpanInfo} from '../diagnostics';
import {TcbExpressionTranslator, unwrapWritableSignal} from './expression';
import {tsCreateVariable} from '../ts_util';
import {addExpressionIdentifier, ExpressionIdentifier} from '../comments';
import {ExpressionIdentifier} from '../comments';
import {checkSplitTwoWayBinding} from './bindings';
import {LocalSymbol} from './references';
@ -44,7 +41,7 @@ const enum EventParamType {
* `ts.Expression`, with special handling of the `$event` variable that can be used within event
* bindings.
*/
export function tcbEventHandlerExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
export function tcbEventHandlerExpression(ast: AST, tcb: Context, scope: Scope): TcbExpr {
const translator = new TcbEventHandlerTranslator(tcb, scope);
return translator.translate(ast);
}
@ -72,7 +69,7 @@ export class TcbDirectiveOutputsOp extends TcbOp {
}
override execute(): null {
let dirId: ts.Expression | null = null;
let dirId: TcbExpr | null = null;
const outputs = this.dir.outputs;
for (const output of this.outputs) {
@ -97,22 +94,18 @@ export class TcbDirectiveOutputsOp extends TcbOp {
if (dirId === null) {
dirId = this.scope.resolve(this.node, this.dir);
}
const outputField = ts.factory.createElementAccessExpression(
dirId,
ts.factory.createStringLiteral(field),
const outputField = new TcbExpr(`${dirId.print()}["${field}"]`).addParseSpanInfo(
output.keySpan,
);
addParseSpanInfo(outputField, output.keySpan);
if (this.tcb.env.config.checkTypeOfOutputEvents) {
// For strict checking of directive events, generate a call to the `subscribe` method
// on the directive's output field to let type information flow into the handler function's
// `$event` parameter.
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
const subscribeFn = ts.factory.createPropertyAccessExpression(outputField, 'subscribe');
const call = ts.factory.createCallExpression(subscribeFn, /* typeArguments */ undefined, [
handler,
]);
addParseSpanInfo(call, output.sourceSpan);
this.scope.addStatement(ts.factory.createExpressionStatement(call));
const call = new TcbExpr(`${outputField.print()}.subscribe(${handler.print()})`);
call.addParseSpanInfo(output.sourceSpan);
this.scope.addStatement(call);
} else {
// If strict checking of directive events is disabled:
//
@ -120,9 +113,9 @@ export class TcbDirectiveOutputsOp extends TcbOp {
// of the `TemplateTypeChecker` can still find the node for the class member for the
// output.
// * Emit a handler function where the `$event` parameter has an explicit `any` type.
this.scope.addStatement(ts.factory.createExpressionStatement(outputField));
this.scope.addStatement(outputField);
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
this.scope.addStatement(handler);
}
}
@ -154,7 +147,7 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
}
override execute(): null {
let elId: ts.Expression | null = null;
let elId: TcbExpr | null = null;
// TODO(alxhub): this could be more efficient.
for (const output of this.outputs) {
@ -178,31 +171,31 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
if (output.type === ParsedEventType.LegacyAnimation) {
// Animation output bindings always have an `$event` parameter of type `AnimationEvent`.
const eventType = this.tcb.env.config.checkTypeOfAnimationEvents
? this.tcb.env.referenceExternalType('@angular/animations', 'AnimationEvent')
? this.tcb.env.referenceExternalSymbol('@angular/animations', 'AnimationEvent').print()
: EventParamType.Any;
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
this.scope.addStatement(handler);
} else if (output.type === ParsedEventType.Animation) {
const eventType = this.tcb.env.referenceExternalType(
const eventType = this.tcb.env.referenceExternalSymbol(
'@angular/core',
'AnimationCallbackEvent',
);
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType.print());
this.scope.addStatement(handler);
} else if (this.tcb.env.config.checkTypeOfDomEvents) {
// If strict checking of DOM events is enabled, generate a call to `addEventListener` on
// the element instance so that TypeScript's type inference for
// `HTMLElement.addEventListener` using `HTMLElementEventMap` to infer an accurate type for
// `$event` depending on the event name. For unknown event names, TypeScript resorts to the
// base `Event` type.
let target: ts.Expression;
let domEventAssertion: ts.Expression | undefined;
let target: TcbExpr;
let domEventAssertion: TcbExpr | undefined;
// Only check for `window` and `document` since in theory any target can be passed.
if (output.target === 'window' || output.target === 'document') {
target = ts.factory.createIdentifier(output.target);
target = new TcbExpr(output.target);
} else if (elId === null) {
target = elId = this.scope.resolve(this.target);
} else {
@ -228,26 +221,17 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
if (
this.target instanceof TmplAstElement &&
this.target.isVoid &&
ts.isIdentifier(target) &&
this.tcb.env.config.allowDomEventAssertion
) {
domEventAssertion = ts.factory.createCallExpression(
this.tcb.env.referenceExternalSymbol('@angular/core', 'ɵassertType'),
[ts.factory.createTypeQueryNode(target)],
[
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier(EVENT_PARAMETER),
'target',
),
],
const assertUtil = this.tcb.env.referenceExternalSymbol('@angular/core', 'ɵassertType');
domEventAssertion = new TcbExpr(
`${assertUtil.print()}<typeof ${target.print()}>(${EVENT_PARAMETER}.target)`,
);
}
const propertyAccess = ts.factory.createPropertyAccessExpression(
target,
'addEventListener',
const propertyAccess = new TcbExpr(`${target.print()}.addEventListener`).addParseSpanInfo(
output.keySpan,
);
addParseSpanInfo(propertyAccess, output.keySpan);
const handler = tcbCreateEventHandler(
output,
this.tcb,
@ -255,18 +239,14 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
EventParamType.Infer,
domEventAssertion,
);
const call = ts.factory.createCallExpression(
/* expression */ propertyAccess,
/* typeArguments */ undefined,
/* arguments */ [ts.factory.createStringLiteral(output.name), handler],
);
addParseSpanInfo(call, output.sourceSpan);
this.scope.addStatement(ts.factory.createExpressionStatement(call));
const call = new TcbExpr(`${propertyAccess.print()}("${output.name}", ${handler.print()})`);
call.addParseSpanInfo(output.sourceSpan);
this.scope.addStatement(call);
} else {
// If strict checking of DOM inputs is disabled, emit a handler function where the `$event`
// parameter has an explicit `any` type.
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
this.scope.addStatement(handler);
}
}
@ -275,7 +255,7 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
}
class TcbEventHandlerTranslator extends TcbExpressionTranslator {
protected override resolve(ast: AST): ts.Expression | null {
protected override resolve(ast: AST): TcbExpr | null {
// Recognize a property read on the implicit receiver corresponding with the event parameter
// that is available in event bindings. Since this variable is a parameter of the handler
// function that the converted expression becomes a child of, just create a reference to the
@ -285,9 +265,7 @@ class TcbEventHandlerTranslator extends TcbExpressionTranslator {
ast.receiver instanceof ImplicitReceiver &&
ast.name === EVENT_PARAMETER
) {
const event = ts.factory.createIdentifier(EVENT_PARAMETER);
addParseSpanInfo(event, ast.nameSpan);
return event;
return new TcbExpr(EVENT_PARAMETER).addParseSpanInfo(ast.nameSpan);
}
return super.resolve(ast);
@ -315,14 +293,14 @@ function tcbCreateEventHandler(
event: TmplAstBoundEvent,
tcb: Context,
scope: Scope,
eventType: EventParamType | ts.TypeNode,
assertionExpression?: ts.Expression,
): ts.Expression {
eventType: EventParamType | string,
assertionExpression?: TcbExpr,
): TcbExpr {
const handler = tcbEventHandlerExpression(event.handler, tcb, scope);
const statements: ts.Statement[] = [];
const statements: TcbExpr[] = [];
if (assertionExpression !== undefined) {
statements.push(ts.factory.createExpressionStatement(assertionExpression));
statements.push(assertionExpression);
}
// TODO(crisbeto): remove the `checkTwoWayBoundEvents` check in v20.
@ -332,28 +310,23 @@ function tcbCreateEventHandler(
// this will already be covered by the corresponding input binding, however it allows us to
// handle the case where the input has a wider type than the output (see #58971).
const target = tcb.allocateId();
const assignment = ts.factory.createBinaryExpression(
target,
ts.SyntaxKind.EqualsToken,
ts.factory.createIdentifier(EVENT_PARAMETER),
);
const initializer = tcb.env.config.allowSignalsInTwoWayBindings
? unwrapWritableSignal(handler, tcb)
: handler;
statements.push(
tsCreateVariable(
target,
tcb.env.config.allowSignalsInTwoWayBindings ? unwrapWritableSignal(handler, tcb) : handler,
),
ts.factory.createExpressionStatement(assignment),
new TcbExpr(`var ${target} = ${initializer.print()}`),
new TcbExpr(`${target} = ${EVENT_PARAMETER}`),
);
} else {
statements.push(ts.factory.createExpressionStatement(handler));
statements.push(handler);
}
let eventParamType: ts.TypeNode | undefined;
let eventParamType: string | undefined;
if (eventType === EventParamType.Infer) {
eventParamType = undefined;
} else if (eventType === EventParamType.Any) {
eventParamType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
eventParamType = 'any';
} else {
eventParamType = eventType;
}
@ -361,29 +334,18 @@ function tcbCreateEventHandler(
// Obtain all guards that have been applied to the scope and its parents, as they have to be
// repeated within the handler function for their narrowing to be in effect within the handler.
const guards = scope.guards();
let body = `{\n${getStatementsBlock(statements)} }`;
let body = ts.factory.createBlock(statements);
if (guards !== null) {
// Wrap the body in an `if` statement containing all guards that have to be applied.
body = ts.factory.createBlock([ts.factory.createIfStatement(guards, body)]);
body = `{ if (${guards.print()}) ${body} }`;
}
const eventParam = ts.factory.createParameterDeclaration(
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ EVENT_PARAMETER,
/* questionToken */ undefined,
/* type */ eventParamType,
const eventParam = new TcbExpr(
`${EVENT_PARAMETER}${eventParamType === undefined ? '' : ': ' + eventParamType}`,
);
addExpressionIdentifier(eventParam, ExpressionIdentifier.EVENT_PARAMETER);
eventParam.addExpressionIdentifier(ExpressionIdentifier.EVENT_PARAMETER);
// Return an arrow function instead of a function expression to preserve the `this` context.
return ts.factory.createArrowFunction(
/* modifiers */ undefined,
/* typeParameters */ undefined,
/* parameters */ [eventParam],
/* type */ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
/* equalsGreaterThanToken */ undefined,
/* body */ body,
);
return new TcbExpr(`(${eventParam.print()}): any => ${body}`);
}

View file

@ -22,11 +22,10 @@ import {
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {astToTypescript, getAnyExpression} from '../expression';
import {addParseSpanInfo, wrapForDiagnostics} from '../diagnostics';
import {markIgnoreDiagnostics} from '../comments';
import {astToTcbExpr} from '../expression';
import {Reference} from '../../../imports';
import {ClassDeclaration} from '../../../reflection';
@ -34,7 +33,7 @@ import {ClassDeclaration} from '../../../reflection';
* Process an `AST` expression and convert it into a `ts.Expression`, generating references to the
* correct identifiers in the current scope.
*/
export function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
export function tcbExpression(ast: AST, tcb: Context, scope: Scope): TcbExpr {
const translator = new TcbExpressionTranslator(tcb, scope);
return translator.translate(ast);
}
@ -42,12 +41,12 @@ export function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expressi
/**
* Wraps an expression in an `unwrapSignal` call which extracts the signal's value.
*/
export function unwrapWritableSignal(expression: ts.Expression, tcb: Context): ts.CallExpression {
export function unwrapWritableSignal(expression: TcbExpr, tcb: Context): TcbExpr {
const unwrapRef = tcb.env.referenceExternalSymbol(
R3Identifiers.unwrapWritableSignal.moduleName,
R3Identifiers.unwrapWritableSignal.name,
);
return ts.factory.createCallExpression(unwrapRef, undefined, [expression]);
return new TcbExpr(`${unwrapRef.print()}(${expression.print()})`);
}
/**
@ -70,7 +69,7 @@ export class TcbExpressionOp extends TcbOp {
override execute(): null {
const expr = tcbExpression(this.expression, this.tcb, this.scope);
this.scope.addStatement(ts.factory.createExpressionStatement(expr));
this.scope.addStatement(expr);
return null;
}
}
@ -98,7 +97,7 @@ export class TcbConditionOp extends TcbOp {
override execute(): null {
const expr = tcbExpression(this.expression, this.tcb, this.scope);
// Wrap in an if-statement to enable TS2774 for uninvoked signals/functions.
this.scope.addStatement(ts.factory.createIfStatement(expr, ts.factory.createBlock([])));
this.scope.addStatement(new TcbExpr(`if (${expr.print()}) {}`));
return null;
}
}
@ -109,11 +108,11 @@ export class TcbExpressionTranslator {
protected scope: Scope,
) {}
translate(ast: AST): ts.Expression {
// `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed
translate(ast: AST): TcbExpr {
// `astToTcbExpr` actually does the conversion. A special resolver `tcbResolve` is passed
// which interprets specific expression nodes that interact with the `ImplicitReceiver`. These
// nodes actually refer to identifiers within the current scope.
return astToTypescript(ast, (ast) => this.resolve(ast), this.tcb.env.config);
return astToTcbExpr(ast, (ast) => this.resolve(ast), this.tcb.env.config);
}
/**
@ -122,7 +121,7 @@ export class TcbExpressionTranslator {
* Some `AST` expressions refer to top-level concepts (references, variables, the component
* context). This method assists in resolving those.
*/
protected resolve(ast: AST): ts.Expression | null {
protected resolve(ast: AST): TcbExpr | null {
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
// Try to resolve a bound target for this expression. If no such target is available, then
// the expression is referencing the top-level component context. In that case, `null` is
@ -139,10 +138,7 @@ export class TcbExpressionTranslator {
// We don't use `markIgnoreForDiagnostics` here, because it won't prevent duplicate
// diagnostics for nested accesses in cases like `@let value = value.foo.bar.baz`.
if (targetExpression !== null) {
return ts.factory.createAsExpression(
targetExpression,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
return new TcbExpr(`${targetExpression.print()} as any`);
}
}
return targetExpression;
@ -160,16 +156,14 @@ export class TcbExpressionTranslator {
const targetExpression = this.getTargetNodeExpression(target, read);
const expr = this.translate(ast.right);
const result = ts.factory.createParenthesizedExpression(
ts.factory.createBinaryExpression(targetExpression, ts.SyntaxKind.EqualsToken, expr),
);
addParseSpanInfo(result, read.sourceSpan);
const result = new TcbExpr(`(${targetExpression.print()} = ${expr.print()})`);
result.addParseSpanInfo(read.sourceSpan);
// Ignore diagnostics from TS produced for writes to `@let` and re-report them using
// our own infrastructure. We can't rely on the TS reporting, because it includes
// the name of the auto-generated TCB variable name.
if (target instanceof TmplAstLetDeclaration) {
markIgnoreDiagnostics(result);
result.markIgnoreDiagnostics();
this.tcb.oobRecorder.illegalWriteToLetDeclaration(this.tcb.id, read, target);
}
@ -187,17 +181,17 @@ export class TcbExpressionTranslator {
// Therefore if `resolve` is called on an `ImplicitReceiver`, it's because no outer
// PropertyRead/Call resolved to a variable or reference, and therefore this is a
// property read or method call on the component context itself.
return ts.factory.createThis();
return new TcbExpr('this');
} else if (ast instanceof BindingPipe) {
const expr = this.translate(ast.exp);
const pipeMeta = this.tcb.getPipeByName(ast.name);
let pipe: ts.Expression | null;
let pipe: TcbExpr | null;
if (pipeMeta === null) {
// No pipe by that name exists in scope. Record this as an error.
this.tcb.oobRecorder.missingPipe(this.tcb.id, ast, this.tcb.hostIsStandalone);
// Use an 'any' value to at least allow the rest of the expression to be checked.
pipe = getAnyExpression();
pipe = new TcbExpr('(0 as any)');
} else if (
pipeMeta.isExplicitlyDeferred &&
this.tcb.boundTarget.getEagerlyUsedPipes().includes(ast.name)
@ -207,33 +201,22 @@ export class TcbExpressionTranslator {
this.tcb.oobRecorder.deferredPipeUsedEagerly(this.tcb.id, ast);
// Use an 'any' value to at least allow the rest of the expression to be checked.
pipe = getAnyExpression();
pipe = new TcbExpr('(0 as any)');
} else {
// Use a variable declared as the pipe's type.
pipe = this.tcb.env.pipeInst(
pipeMeta.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>,
);
}
const args = ast.args.map((arg) => this.translate(arg));
let methodAccess: ts.Expression = ts.factory.createPropertyAccessExpression(
pipe,
'transform',
);
addParseSpanInfo(methodAccess, ast.nameSpan);
const args = ast.args.map((arg) => this.translate(arg).print());
let methodAccess = new TcbExpr(`${pipe.print()}.transform`).addParseSpanInfo(ast.nameSpan);
if (!this.tcb.env.config.checkTypeOfPipes) {
methodAccess = ts.factory.createAsExpression(
methodAccess,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
methodAccess = new TcbExpr(`(${methodAccess.print()} as any)`);
}
const result = ts.factory.createCallExpression(
/* expression */ methodAccess,
/* typeArguments */ undefined,
/* argumentsArray */ [expr, ...args],
);
addParseSpanInfo(result, ast.sourceSpan);
return result;
const result = new TcbExpr(`${methodAccess.print()}(${[expr.print(), ...args].join(', ')})`);
return result.addParseSpanInfo(ast.sourceSpan);
} else if (
(ast instanceof Call || ast instanceof SafeCall) &&
(ast.receiver instanceof PropertyRead || ast.receiver instanceof SafePropertyRead)
@ -246,12 +229,8 @@ export class TcbExpressionTranslator {
ast.args.length === 1
) {
const expr = this.translate(ast.args[0]);
const exprAsAny = ts.factory.createAsExpression(
expr,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
const result = ts.factory.createParenthesizedExpression(exprAsAny);
addParseSpanInfo(result, ast.sourceSpan);
const result = new TcbExpr(`(${expr.print()} as any)`);
result.addParseSpanInfo(ast.sourceSpan);
return result;
}
@ -264,12 +243,11 @@ export class TcbExpressionTranslator {
return null;
}
const receiver = this.getTargetNodeExpression(target, ast);
const method = wrapForDiagnostics(receiver);
addParseSpanInfo(method, ast.receiver.nameSpan);
const args = ast.args.map((arg) => this.translate(arg));
const node = ts.factory.createCallExpression(method, undefined, args);
addParseSpanInfo(node, ast.sourceSpan);
const method = this.getTargetNodeExpression(target, ast);
method.addParseSpanInfo(ast.receiver.nameSpan).wrapForTypeChecker();
const args = ast.args.map((arg) => this.translate(arg).print());
const node = new TcbExpr(`${method.print()}(${args.join(', ')})`);
node.addParseSpanInfo(ast.sourceSpan);
return node;
} else {
// This AST isn't special after all.
@ -277,9 +255,9 @@ export class TcbExpressionTranslator {
}
}
private getTargetNodeExpression(targetNode: TemplateEntity, expressionNode: AST): ts.Expression {
private getTargetNodeExpression(targetNode: TemplateEntity, expressionNode: AST): TcbExpr {
const expr = this.scope.resolve(targetNode);
addParseSpanInfo(expr, expressionNode.sourceSpan);
expr.addParseSpanInfo(expressionNode.sourceSpan);
return expr;
}

View file

@ -14,12 +14,11 @@ import {
TmplAstForLoopBlock,
TmplAstVariable,
} from '@angular/compiler';
import ts from 'typescript';
import {tcbExpression, TcbExpressionTranslator} from './expression';
import type {Context} from './context';
import type {Scope} from './scope';
import {TcbOp} from './base';
import {addParseSpanInfo} from '../diagnostics';
import {getStatementsBlock, TcbExpr} from './codegen';
/**
* A `TcbOp` which renders a `for` block as a TypeScript `for...of` loop.
@ -47,37 +46,20 @@ export class TcbForOfOp extends TcbOp {
null,
);
const initializerId = loopScope.resolve(this.block.item);
if (!ts.isIdentifier(initializerId)) {
throw new Error(
`Could not resolve for loop variable ${this.block.item.name} to an identifier`,
);
}
const initializer = ts.factory.createVariableDeclarationList(
[ts.factory.createVariableDeclaration(initializerId)],
ts.NodeFlags.Const,
);
addParseSpanInfo(initializer, this.block.item.keySpan);
const initializer = new TcbExpr(`const ${initializerId.print()}`);
initializer.addParseSpanInfo(this.block.item.keySpan);
// It's common to have a for loop over a nullable value (e.g. produced by the `async` pipe).
// Add a non-null expression to allow such values to be assigned.
const expression = ts.factory.createNonNullExpression(
tcbExpression(this.block.expression, this.tcb, this.scope),
const expression = new TcbExpr(
`${tcbExpression(this.block.expression, this.tcb, this.scope).print()}!`,
);
const trackTranslator = new TcbForLoopTrackTranslator(this.tcb, loopScope, this.block);
const trackExpression = trackTranslator.translate(this.block.trackBy);
const statements = [
...loopScope.render(),
ts.factory.createExpressionStatement(trackExpression),
];
const block = getStatementsBlock([...loopScope.render(), trackExpression]);
this.scope.addStatement(
ts.factory.createForOfStatement(
undefined,
initializer,
expression,
ts.factory.createBlock(statements),
),
new TcbExpr(`for (${initializer.print()} of ${expression.print()}) {\n${block} }`),
);
return null;
}
}
@ -102,7 +84,7 @@ export class TcbForLoopTrackTranslator extends TcbExpressionTranslator {
}
}
protected override resolve(ast: AST): ts.Expression | null {
protected override resolve(ast: AST): TcbExpr | null {
if (
ast instanceof PropertyRead &&
(ast.receiver instanceof ImplicitReceiver || ast.receiver instanceof ThisReceiver)

View file

@ -7,12 +7,10 @@
*/
import {TmplAstHostElement} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {tsCreateElement, tsCreateVariable} from '../ts_util';
import {addParseSpanInfo} from '../diagnostics';
/**
* A `TcbOp` which creates an expression for a the host element of a directive.
@ -30,11 +28,19 @@ export class TcbHostElementOp extends TcbOp {
super();
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const id = this.tcb.allocateId();
const initializer = tsCreateElement(...this.element.tagNames);
addParseSpanInfo(initializer, this.element.sourceSpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
return id;
let tagNames: string;
if (this.element.tagNames.length === 1) {
tagNames = `"${this.element.tagNames[0]}"`;
} else {
tagNames = `null! as ${this.element.tagNames.map((t) => `"${t}"`).join(' | ')}`;
}
const initializer = new TcbExpr(`document.createElement(${tagNames})`);
initializer.addParseSpanInfo(this.element.sourceSpan);
this.scope.addStatement(new TcbExpr(`var ${id} = ${initializer.print()}`));
return new TcbExpr(id);
}
}

View file

@ -7,12 +7,11 @@
*/
import {TmplAstIfBlock, TmplAstIfBlockBranch} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {getStatementsBlock, TcbExpr} from './codegen';
import type {Scope} from './scope';
import type {Context} from './context';
import {tcbExpression} from './expression';
import {markIgnoreDiagnostics} from '../comments';
/**
* A `TcbOp` which renders an `if` template block as a TypeScript `if` statement.
@ -40,7 +39,7 @@ export class TcbIfOp extends TcbOp {
return null;
}
private generateBranch(index: number): ts.Statement | undefined {
private generateBranch(index: number): TcbExpr | undefined {
const branch = this.block.branches[index];
if (!branch) {
@ -50,7 +49,7 @@ export class TcbIfOp extends TcbOp {
// If the expression is null, it means that it's an `else` statement.
if (branch.expression === null) {
const branchScope = this.getBranchScope(this.scope, branch, index);
return ts.factory.createBlock(branchScope.render());
return new TcbExpr(`{\n${getStatementsBlock(branchScope.render())}}`);
}
// We process the expression first in the parent scope, but create a scope around the block
@ -63,19 +62,15 @@ export class TcbIfOp extends TcbOp {
let expression = tcbExpression(branch.expression, this.tcb, this.scope);
if (branch.expressionAlias !== null) {
expression = ts.factory.createBinaryExpression(
ts.factory.createParenthesizedExpression(expression),
ts.SyntaxKind.AmpersandAmpersandToken,
outerScope.resolve(branch.expressionAlias),
expression = new TcbExpr(
`(${expression.print()}) && ${outerScope.resolve(branch.expressionAlias).print()}`,
);
}
const bodyScope = this.getBranchScope(outerScope, branch, index);
const ifStatement = `if (${expression.print()}) {\n${getStatementsBlock(bodyScope.render())}}`;
const elseBranch = this.generateBranch(index + 1);
return ts.factory.createIfStatement(
expression,
ts.factory.createBlock(bodyScope.render()),
this.generateBranch(index + 1),
);
return new TcbExpr(ifStatement + (elseBranch ? ' else ' + elseBranch.print() : ''));
}
private getBranchScope(parentScope: Scope, branch: TmplAstIfBlockBranch, index: number): Scope {
@ -88,8 +83,8 @@ export class TcbIfOp extends TcbOp {
);
}
private generateBranchGuard(index: number): ts.Expression | null {
let guard: ts.Expression | null = null;
private generateBranchGuard(index: number): TcbExpr | null {
let guard: TcbExpr | null = null;
// Since event listeners are inside callbacks, type narrowing doesn't apply to them anymore.
// To recreate the behavior, we generate an expression that negates all the values of the
@ -111,41 +106,30 @@ export class TcbIfOp extends TcbOp {
}
const expressionScope = this.expressionScopes.get(branch)!;
let expression: ts.Expression;
let expression: TcbExpr;
// We need to recreate the expression and mark it to be ignored for diagnostics,
// because it was already checked as a part of the block's condition and we don't
// want it to produce a duplicate diagnostic.
expression = tcbExpression(branch.expression, this.tcb, expressionScope);
if (branch.expressionAlias !== null) {
expression = ts.factory.createBinaryExpression(
ts.factory.createParenthesizedExpression(expression),
ts.SyntaxKind.AmpersandAmpersandToken,
expressionScope.resolve(branch.expressionAlias),
expression = new TcbExpr(
`(${expression.print()}) && ${expressionScope.resolve(branch.expressionAlias).print()}`,
);
}
markIgnoreDiagnostics(expression);
expression.markIgnoreDiagnostics();
// The expressions of the preceding branches have to be negated
// (e.g. `expr` becomes `!(expr)`) when comparing in the guard, except
// for the branch's own expression which is preserved as is.
const comparisonExpression =
i === index
? expression
: ts.factory.createPrefixUnaryExpression(
ts.SyntaxKind.ExclamationToken,
ts.factory.createParenthesizedExpression(expression),
);
i === index ? expression : new TcbExpr(`!(${expression.print()})`);
// Finally add the expression to the guard with an && operator.
guard =
guard === null
? comparisonExpression
: ts.factory.createBinaryExpression(
guard,
ts.SyntaxKind.AmpersandAmpersandToken,
comparisonExpression,
);
: new TcbExpr(`${guard.print()} && ${comparisonExpression.print()}`);
}
return guard;

View file

@ -22,12 +22,10 @@ import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {TcbOp} from './base';
import {declareVariable, TcbExpr, tempPrint} from './codegen';
import {BindingPropertyName, ClassPropertyName} from '../../../metadata';
import {addParseSpanInfo, wrapForDiagnostics} from '../diagnostics';
import {markIgnoreDiagnostics} from '../comments';
import {REGISTRY} from '../dom';
import {tcbExpression, unwrapWritableSignal} from './expression';
import {tsCreateTypeQueryForCoercedInput, tsDeclareVariable} from '../ts_util';
import {
checkUnsupportedFieldBindings,
CustomFormControlType,
@ -40,10 +38,10 @@ import {LocalSymbol} from './references';
/**
* Translates the given attribute binding to a `ts.Expression`.
*/
export function translateInput(value: AST | string, tcb: Context, scope: Scope): ts.Expression {
export function translateInput(value: AST | string, tcb: Context, scope: Scope): TcbExpr {
if (typeof value === 'string') {
// For regular attributes with a static string value, use the represented string literal.
return ts.factory.createStringLiteral(value);
return new TcbExpr(`"${value}"`);
} else {
// Produce an expression representing the value of the binding.
return tcbExpression(value, tcb, scope);
@ -73,7 +71,7 @@ export class TcbDirectiveInputsOp extends TcbOp {
}
override execute(): null {
let dirId: ts.Expression | null = null;
let dirId: TcbExpr | null = null;
// TODO(joost): report duplicate properties
const seenRequiredInputs = new Set<ClassPropertyName>();
@ -97,12 +95,15 @@ export class TcbDirectiveInputsOp extends TcbOp {
for (const attr of boundAttrs) {
// For bound inputs, the property is assigned the binding expression.
const expr = widenBinding(translateInput(attr.value, this.tcb, this.scope), this.tcb);
let assignment: ts.Expression = wrapForDiagnostics(expr);
let assignment = widenBinding(
translateInput(attr.value, this.tcb, this.scope),
this.tcb,
attr.value,
);
assignment.wrapForTypeChecker();
for (const {fieldName, required, transformType, isSignal, isTwoWayBinding} of attr.inputs) {
let target: ts.LeftHandSideExpression;
let target: TcbExpr;
if (required) {
seenRequiredInputs.add(fieldName);
@ -115,10 +116,13 @@ export class TcbDirectiveInputsOp extends TcbOp {
// setting the `WriteT` of such `InputSignalWithTransform<_, WriteT>`.
if (this.dir.coercedInputFields.has(fieldName)) {
let type: ts.TypeNode;
let type: TcbExpr;
if (transformType !== null) {
type = this.tcb.env.referenceTransplantedType(new TransplantedType(transformType));
const tsType = this.tcb.env.referenceTransplantedType(
new TransplantedType(transformType),
);
type = new TcbExpr(tempPrint(tsType, transformType.node.getSourceFile()));
} else {
// The input has a coercion declaration which should be used instead of assigning the
// expression into the input field directly. To achieve this, a variable is declared
@ -132,11 +136,15 @@ export class TcbDirectiveInputsOp extends TcbOp {
);
}
type = tsCreateTypeQueryForCoercedInput(dirTypeRef.typeName, fieldName);
const typeName = ts.isIdentifier(dirTypeRef.typeName)
? dirTypeRef.typeName.text
: tempPrint(dirTypeRef.typeName, dirTypeRef.typeName.getSourceFile());
type = new TcbExpr(`typeof ${typeName}.ngAcceptInputType_${fieldName}`);
}
const id = this.tcb.allocateId();
this.scope.addStatement(tsDeclareVariable(id, type));
const id = new TcbExpr(this.tcb.allocateId());
this.scope.addStatement(declareVariable(id, type));
target = id;
} else if (this.dir.undeclaredInputFields.has(fieldName)) {
@ -156,18 +164,15 @@ export class TcbDirectiveInputsOp extends TcbOp {
dirId = this.scope.resolve(this.node, this.dir);
}
const id = this.tcb.allocateId();
const id = new TcbExpr(this.tcb.allocateId());
const dirTypeRef = this.tcb.env.referenceType(this.dir.ref);
if (!ts.isTypeReferenceNode(dirTypeRef)) {
throw new Error(
`Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`,
);
}
const type = ts.factory.createIndexedAccessTypeNode(
ts.factory.createTypeQueryNode(dirId as ts.Identifier),
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(fieldName)),
);
const temp = tsDeclareVariable(id, type);
const type = new TcbExpr(`(typeof ${dirId.print()})["${fieldName}"]`);
const temp = declareVariable(id, type);
this.scope.addStatement(temp);
target = id;
} else {
@ -179,14 +184,8 @@ export class TcbDirectiveInputsOp extends TcbOp {
// when possible. String literal fields may not be valid JS identifiers so we use
// literal element access instead for those cases.
target = this.dir.stringLiteralInputFields.has(fieldName)
? ts.factory.createElementAccessExpression(
dirId,
ts.factory.createStringLiteral(fieldName),
)
: ts.factory.createPropertyAccessExpression(
dirId,
ts.factory.createIdentifier(fieldName),
);
? new TcbExpr(`${dirId.print()}["${fieldName}"]`)
: new TcbExpr(`${dirId.print()}.${fieldName}`);
}
// For signal inputs, we unwrap the target `InputSignal`. Note that
@ -199,20 +198,12 @@ export class TcbDirectiveInputsOp extends TcbOp {
R3Identifiers.InputSignalBrandWriteType.moduleName,
R3Identifiers.InputSignalBrandWriteType.name,
);
if (
!ts.isIdentifier(inputSignalBrandWriteSymbol) &&
!ts.isPropertyAccessExpression(inputSignalBrandWriteSymbol)
) {
throw new Error(
`Expected identifier or property access for reference to ${R3Identifiers.InputSignalBrandWriteType.name}`,
);
}
target = ts.factory.createElementAccessExpression(target, inputSignalBrandWriteSymbol);
target = new TcbExpr(`${target.print()}[${inputSignalBrandWriteSymbol.print()}]`);
}
if (attr.keySpan !== null) {
addParseSpanInfo(target, attr.keySpan);
target.addParseSpanInfo(attr.keySpan);
}
// Two-way bindings accept `T | WritableSignal<T>` so we have to unwrap the value.
@ -221,20 +212,17 @@ export class TcbDirectiveInputsOp extends TcbOp {
}
// Finally the assignment is extended by assigning it into the target expression.
assignment = ts.factory.createBinaryExpression(
target,
ts.SyntaxKind.EqualsToken,
assignment,
);
assignment = new TcbExpr(`${target.print()} = ${assignment.print()}`);
}
addParseSpanInfo(assignment, attr.sourceSpan);
assignment.addParseSpanInfo(attr.sourceSpan);
// Ignore diagnostics for text attributes if configured to do so.
if (!this.tcb.env.config.checkTypeOfAttributes && typeof attr.value === 'string') {
markIgnoreDiagnostics(assignment);
assignment.markIgnoreDiagnostics();
}
this.scope.addStatement(ts.factory.createExpressionStatement(assignment));
this.scope.addStatement(assignment);
}
this.checkRequiredInputs(seenRequiredInputs);
@ -291,7 +279,7 @@ export class TcbUnclaimedInputsOp extends TcbOp {
override execute(): null {
// `this.inputs` contains only those bindings not matched by any directive. These bindings go to
// the element itself.
let elId: ts.Expression | null = null;
let elId: TcbExpr | null = null;
// TODO(alxhub): this could be more efficient.
for (const binding of this.inputs) {
@ -303,7 +291,11 @@ export class TcbUnclaimedInputsOp extends TcbOp {
continue;
}
const expr = widenBinding(tcbExpression(binding.value, this.tcb, this.scope), this.tcb);
const expr = widenBinding(
tcbExpression(binding.value, this.tcb, this.scope),
this.tcb,
binding.value,
);
if (this.tcb.env.config.checkTypeOfDomBindings && isPropertyBinding) {
if (binding.name !== 'style' && binding.name !== 'class') {
@ -312,25 +304,18 @@ export class TcbUnclaimedInputsOp extends TcbOp {
}
// A direct binding to a property.
const propertyName = REGISTRY.getMappedPropName(binding.name);
const prop = ts.factory.createElementAccessExpression(
elId,
ts.factory.createStringLiteral(propertyName),
);
const stmt = ts.factory.createBinaryExpression(
prop,
ts.SyntaxKind.EqualsToken,
wrapForDiagnostics(expr),
);
addParseSpanInfo(stmt, binding.sourceSpan);
this.scope.addStatement(ts.factory.createExpressionStatement(stmt));
const stmt = new TcbExpr(
`${elId.print()}["${propertyName}"] = ${expr.wrapForTypeChecker().print()}`,
).addParseSpanInfo(binding.sourceSpan);
this.scope.addStatement(stmt);
} else {
this.scope.addStatement(ts.factory.createExpressionStatement(expr));
this.scope.addStatement(expr);
}
} else {
// A binding to an animation, attribute, class or style. For now, only validate the right-
// hand side of the expression.
// TODO: properly check class and style bindings.
this.scope.addStatement(ts.factory.createExpressionStatement(expr));
this.scope.addStatement(expr);
}
}

View file

@ -7,8 +7,8 @@
*/
import {AST} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {Context} from './context';
import type {Scope} from './scope';
import {tcbExpression} from './expression';
@ -29,14 +29,7 @@ export class TcbIntersectionObserverOp extends TcbOp {
override execute(): null {
const options = tcbExpression(this.options, this.tcb, this.scope);
const callback = ts.factory.createNonNullExpression(ts.factory.createNull());
const expression = ts.factory.createNewExpression(
ts.factory.createIdentifier('IntersectionObserver'),
undefined,
[callback, options],
);
this.scope.addStatement(ts.factory.createExpressionStatement(expression));
this.scope.addStatement(new TcbExpr(`new IntersectionObserver(null!, ${options.print()})`));
return null;
}
}

View file

@ -7,12 +7,10 @@
*/
import {TmplAstLetDeclaration} from '@angular/compiler';
import ts from 'typescript';
import {Context} from './context';
import type {Scope} from './scope';
import {TcbOp} from './base';
import {addParseSpanInfo, wrapForTypeChecker} from '../diagnostics';
import {tsCreateVariable} from '../ts_util';
import {TcbExpr} from './codegen';
import {tcbExpression} from './expression';
/**
@ -35,14 +33,13 @@ export class TcbLetDeclarationOp extends TcbOp {
*/
override readonly optional = false;
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
addParseSpanInfo(id, this.node.nameSpan);
const value = tcbExpression(this.node.value, this.tcb, this.scope);
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId()).addParseSpanInfo(this.node.nameSpan);
const value = tcbExpression(this.node.value, this.tcb, this.scope).wrapForTypeChecker();
// Value needs to be wrapped, because spans for the expressions inside of it can
// be picked up incorrectly as belonging to the full variable declaration.
const varStatement = tsCreateVariable(id, wrapForTypeChecker(value), ts.NodeFlags.Const);
addParseSpanInfo(varStatement.declarationList.declarations[0], this.node.sourceSpan);
const varStatement = new TcbExpr(`const ${id.print()} = ${value.print()}`);
varStatement.addParseSpanInfo(this.node.sourceSpan);
this.scope.addStatement(varStatement);
return id;
}

View file

@ -7,7 +7,6 @@
*/
import {
DYNAMIC_TYPE,
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
@ -17,14 +16,11 @@ import {
TmplAstTemplate,
TmplAstVariable,
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {addParseSpanInfo} from '../diagnostics';
import {tsCreateVariable} from '../ts_util';
import {getAnyExpression} from '../expression';
/** Types that can referenced locally in a template. */
export type LocalSymbol =
@ -72,9 +68,9 @@ export class TcbReferenceOp extends TcbOp {
// so it can map a reference variable in the template directly to a node in the TCB.
override readonly optional = true;
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
let initializer: ts.Expression =
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId());
let initializer: TcbExpr =
this.target instanceof TmplAstTemplate || this.target instanceof TmplAstElement
? this.scope.resolve(this.target)
: this.scope.resolve(this.host, this.target);
@ -88,28 +84,18 @@ export class TcbReferenceOp extends TcbOp {
// References to DOM nodes are pinned to 'any' when `checkTypeOfDomReferences` is `false`.
// References to `TemplateRef`s and directives are pinned to 'any' when
// `checkTypeOfNonDomReferences` is `false`.
initializer = ts.factory.createAsExpression(
initializer,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
initializer = new TcbExpr(`${initializer.print()} as any`);
} else if (this.target instanceof TmplAstTemplate) {
// Direct references to an <ng-template> node simply require a value of type
// `TemplateRef<any>`. To get this, an expression of the form
// `(_t1 as any as TemplateRef<any>)` is constructed.
initializer = ts.factory.createAsExpression(
initializer,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
initializer = ts.factory.createAsExpression(
initializer,
this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]),
);
initializer = ts.factory.createParenthesizedExpression(initializer);
const templateRef = this.tcb.env.referenceExternalSymbol('@angular/core', 'TemplateRef');
initializer = new TcbExpr(`(${initializer.print()} as any as ${templateRef.print()}<any>)`);
}
addParseSpanInfo(initializer, this.node.sourceSpan);
addParseSpanInfo(id, this.node.keySpan);
initializer.addParseSpanInfo(this.node.sourceSpan);
id.addParseSpanInfo(this.node.keySpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
this.scope.addStatement(new TcbExpr(`var ${id.print()} = ${initializer.print()}`));
return id;
}
}
@ -130,9 +116,9 @@ export class TcbInvalidReferenceOp extends TcbOp {
// The declaration of a missing reference is only needed when the reference is resolved.
override readonly optional = true;
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
this.scope.addStatement(tsCreateVariable(id, getAnyExpression()));
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId());
this.scope.addStatement(new TcbExpr(`var ${id.print()} = any`));
return id;
}
}

View file

@ -7,9 +7,9 @@
*/
import {BindingType, TmplAstComponent, TmplAstElement, TmplAstHostElement} from '@angular/compiler';
import ts from 'typescript';
import {REGISTRY} from '../dom';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {Context} from './context';
import {getComponentTagName} from './selectorless';
@ -37,7 +37,7 @@ export class TcbDomSchemaCheckerOp extends TcbOp {
return false;
}
override execute(): ts.Expression | null {
override execute(): TcbExpr | null {
const element = this.element;
const isTemplateElement =
element instanceof TmplAstElement || element instanceof TmplAstComponent;

View file

@ -35,11 +35,11 @@ import {
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {TypeCheckableDirectiveMeta} from '../../api';
import {Context} from './context';
import {TcbTemplateBodyOp, TcbTemplateContextOp} from './template';
import {TcbElementOp} from './element';
import {addParseSpanInfo} from '../diagnostics';
import {tcbExpression, TcbConditionOp, TcbExpressionOp} from './expression';
import {TcbBlockImplicitVariableOp, TcbBlockVariableOp, TcbTemplateVariableOp} from './variables';
import {TcbComponentContextCompletionOp} from './completions';
@ -89,7 +89,7 @@ export class Scope {
/**
* A queue of operations which need to be performed to generate the TCB code for this scope.
*
* This array can contain either a `TcbOp` which has yet to be executed, or a `ts.Expression|null`
* This array can contain either a `TcbOp` which has yet to be executed, or a `TcbExpr|null`
* representing the memoized result of executing the operation. As operations are executed, their
* results are written into the `opQueue`, overwriting the original operation.
*
@ -99,7 +99,7 @@ export class Scope {
* that fits instead. This has the same semantics as TypeScript itself when types are referenced
* circularly.
*/
private opQueue: (TcbOp | ts.Expression | null)[] = [];
private opQueue: (TcbOp | TcbExpr | null)[] = [];
/**
* A map of `TmplAstElement`s to the index of their `TcbElementOp` in the `opQueue`
@ -138,7 +138,7 @@ export class Scope {
* `TmplAstVariable` nodes) to the index of their `TcbVariableOp`s in the `opQueue`, or to
* pre-resolved variable identifiers.
*/
private varMap = new Map<TmplAstVariable, number | ts.Identifier>();
private varMap = new Map<TmplAstVariable, number | TcbExpr>();
/**
* A map of the names of `TmplAstLetDeclaration`s to the index of their op in the `opQueue`.
@ -152,26 +152,26 @@ export class Scope {
*
* Executing the `TcbOp`s in the `opQueue` populates this array.
*/
private statements: ts.Statement[] = [];
private statements: TcbExpr[] = [];
/**
* Gets names of the for loop context variables and their types.
*/
private static getForLoopContextVariableTypes() {
return new Map<string, ts.KeywordTypeSyntaxKind>([
['$first', ts.SyntaxKind.BooleanKeyword],
['$last', ts.SyntaxKind.BooleanKeyword],
['$even', ts.SyntaxKind.BooleanKeyword],
['$odd', ts.SyntaxKind.BooleanKeyword],
['$index', ts.SyntaxKind.NumberKeyword],
['$count', ts.SyntaxKind.NumberKeyword],
return new Map<string, string>([
['$first', 'boolean'],
['$last', 'boolean'],
['$even', 'boolean'],
['$odd', 'boolean'],
['$index', 'number'],
['$count', 'number'],
]);
}
private constructor(
private tcb: Context,
private parent: Scope | null = null,
private guard: ts.Expression | null = null,
private guard: TcbExpr | null = null,
) {}
/**
@ -195,7 +195,7 @@ export class Scope {
| TmplAstHostElement
| null,
children: TmplAstNode[] | null,
guard: ts.Expression | null,
guard: TcbExpr | null,
): Scope {
const scope = new Scope(tcb, parentScope, guard);
@ -237,8 +237,8 @@ export class Scope {
} else if (scopedNode instanceof TmplAstForLoopBlock) {
// Register the variable for the loop so it can be resolved by
// children. It'll be declared once the loop is created.
const loopInitializer = tcb.allocateId();
addParseSpanInfo(loopInitializer, scopedNode.item.sourceSpan);
const loopInitializer = new TcbExpr(tcb.allocateId());
loopInitializer.addParseSpanInfo(scopedNode.item.sourceSpan);
scope.varMap.set(scopedNode.item, loopInitializer);
const forLoopContextVariableTypes = Scope.getForLoopContextVariableTypes();
@ -248,9 +248,7 @@ export class Scope {
throw new Error(`Unrecognized for loop context variable ${variable.name}`);
}
const type = ts.factory.createKeywordTypeNode(
forLoopContextVariableTypes.get(variable.value)!,
);
const type = new TcbExpr(forLoopContextVariableTypes.get(variable.value)!);
Scope.registerVariable(
scope,
variable,
@ -301,34 +299,11 @@ export class Scope {
* @param directive if present, a directive type on a `TmplAstElement` or `TmplAstTemplate` to
* look up instead of the default for an element or template node.
*/
resolve(
node: LocalSymbol,
directive?: TypeCheckableDirectiveMeta,
): ts.Identifier | ts.NonNullExpression {
resolve(node: LocalSymbol, directive?: TypeCheckableDirectiveMeta): TcbExpr {
// Attempt to resolve the operation locally.
const res = this.resolveLocal(node, directive);
if (res !== null) {
// We want to get a clone of the resolved expression and clear the trailing comments
// so they don't continue to appear in every place the expression is used.
// As an example, this would otherwise produce:
// var _t1 /**T:DIR*/ /*1,2*/ = _ctor1();
// _t1 /**T:DIR*/ /*1,2*/.input = 'value';
//
// In addition, returning a clone prevents the consumer of `Scope#resolve` from
// attaching comments at the declaration site.
let clone: ts.Identifier | ts.NonNullExpression;
if (ts.isIdentifier(res)) {
clone = ts.factory.createIdentifier(res.text);
} else if (ts.isNonNullExpression(res)) {
clone = ts.factory.createNonNullExpression(res.expression);
} else {
throw new Error(`Could not resolve ${node} to an Identifier or a NonNullExpression`);
}
ts.setOriginalNode(clone, res);
(clone as any).parent = clone.parent;
return ts.setSyntheticTrailingComments(clone, []);
return res;
} else if (this.parent !== null) {
// Check with the parent.
return this.parent.resolve(node, directive);
@ -340,14 +315,14 @@ export class Scope {
/**
* Add a statement to this scope.
*/
addStatement(stmt: ts.Statement): void {
addStatement(stmt: TcbExpr): void {
this.statements.push(stmt);
}
/**
* Get the statements.
*/
render(): ts.Statement[] {
render(): TcbExpr[] {
for (let i = 0; i < this.opQueue.length; i++) {
// Optional statements cannot be skipped when we are generating the TCB for use
// by the TemplateTypeChecker.
@ -361,8 +336,8 @@ export class Scope {
* Returns an expression of all template guards that apply to this scope, including those of
* parent scopes. If no guards have been applied, null is returned.
*/
guards(): ts.Expression | null {
let parentGuards: ts.Expression | null = null;
guards(): TcbExpr | null {
let parentGuards: TcbExpr | null = null;
if (this.parent !== null) {
// Start with the guards from the parent scope, if present.
parentGuards = this.parent.guards();
@ -374,16 +349,13 @@ export class Scope {
} else if (parentGuards === null) {
// There's no guards from the parent scope, so this scope's guard represents all available
// guards.
return this.guard;
return typeof this.guard === 'string' ? new TcbExpr(this.guard) : this.guard;
} else {
// Both the parent scope and this scope provide a guard, so create a combination of the two.
// It is important that the parent guard is used as left operand, given that it may provide
// narrowing that is required for this scope's guard to be valid.
return ts.factory.createBinaryExpression(
parentGuards,
ts.SyntaxKind.AmpersandAmpersandToken,
this.guard,
);
const guard = typeof this.guard === 'string' ? this.guard : this.guard.print();
return new TcbExpr(`${parentGuards.print()} && ${guard}`);
}
}
@ -418,15 +390,12 @@ export class Scope {
| TmplAstHostElement
| null,
children: TmplAstNode[] | null,
guard: ts.Expression | null,
guard: TcbExpr | null,
): Scope {
return Scope.forNodes(this.tcb, parentScope, scopedNode, children, guard);
}
private resolveLocal(
ref: LocalSymbol,
directive?: TypeCheckableDirectiveMeta,
): ts.Expression | null {
private resolveLocal(ref: LocalSymbol, directive?: TypeCheckableDirectiveMeta): TcbExpr | null {
if (ref instanceof TmplAstReference && this.referenceOpMap.has(ref)) {
return this.resolveOp(this.referenceOpMap.get(ref)!);
} else if (ref instanceof TmplAstLetDeclaration && this.letDeclOpMap.has(ref.name)) {
@ -435,7 +404,9 @@ export class Scope {
// Resolving a context variable for this template.
// Execute the `TcbVariableOp` associated with the `TmplAstVariable`.
const opIndexOrNode = this.varMap.get(ref)!;
return typeof opIndexOrNode === 'number' ? this.resolveOp(opIndexOrNode) : opIndexOrNode;
return typeof opIndexOrNode === 'number'
? this.resolveOp(opIndexOrNode)
: new TcbExpr(opIndexOrNode.print(true /* ignoreComments */));
} else if (
ref instanceof TmplAstTemplate &&
directive === undefined &&
@ -469,9 +440,9 @@ export class Scope {
}
/**
* Like `executeOp`, but assert that the operation actually returned `ts.Expression`.
* Like `executeOp`, but assert that the operation actually returned `TcbExpr`.
*/
private resolveOp(opIndex: number): ts.Expression {
private resolveOp(opIndex: number): TcbExpr {
const res = this.executeOp(opIndex, /* skipOptional */ false);
if (res === null) {
throw new Error(`Error resolving operation, got null`);
@ -486,10 +457,10 @@ export class Scope {
* and also protects against a circular dependency from the operation to itself by temporarily
* setting the operation's result to a special expression.
*/
private executeOp(opIndex: number, skipOptional: boolean): ts.Expression | null {
private executeOp(opIndex: number, skipOptional: boolean): TcbExpr | null {
const op = this.opQueue[opIndex];
if (!(op instanceof TcbOp)) {
return op;
return op === null ? null : new TcbExpr(op.print(true /* ignoreComments */));
}
if (skipOptional && op.optional) {
@ -500,7 +471,10 @@ export class Scope {
// operation results in a circular dependency, this will prevent an infinite loop and allow for
// the resolution of such cycles.
this.opQueue[opIndex] = op.circularFallback();
const res = op.execute();
let res = op.execute();
if (res !== null) {
res = new TcbExpr(res.print(true /* ignoreComments */));
}
// Once the operation has finished executing, it's safe to cache the real result.
this.opQueue[opIndex] = res;
return res;

View file

@ -7,10 +7,8 @@
*/
import {TmplAstComponent} from '@angular/compiler';
import ts from 'typescript';
import {addParseSpanInfo} from '../diagnostics';
import {tsCreateElement, tsCreateVariable} from '../ts_util';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {Context} from './context';
import type {Scope} from './scope';
@ -37,11 +35,13 @@ export class TcbComponentNodeOp extends TcbOp {
super();
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const id = this.tcb.allocateId();
const initializer = tsCreateElement(getComponentTagName(this.component));
addParseSpanInfo(initializer, this.component.startSourceSpan || this.component.sourceSpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
return id;
const initializer = new TcbExpr(
`document.createElement("${getComponentTagName(this.component)}")`,
);
initializer.addParseSpanInfo(this.component.startSourceSpan || this.component.sourceSpan);
this.scope.addStatement(new TcbExpr(`var ${id} = ${initializer.print()}`));
return new TcbExpr(id);
}
}

View file

@ -23,10 +23,8 @@ import {
} from '@angular/compiler';
import ts from 'typescript';
import {TypeCheckableDirectiveMeta} from '../../api';
import {markIgnoreDiagnostics} from '../comments';
import {addParseSpanInfo} from '../diagnostics';
import {tsDeclareVariable} from '../ts_util';
import {TcbOp} from './base';
import {declareVariable, TcbExpr} from './codegen';
import {TcbBoundAttribute} from './bindings';
import type {Context} from './context';
import {tcbExpression} from './expression';
@ -114,23 +112,24 @@ export class TcbNativeFieldOp extends TcbOp {
checkUnsupportedFieldBindings(this.node, this.unsupportedBindingFields, this.tcb);
const expectedType = this.getExpectedTypeFromDomNode(this.node);
const expectedType = new TcbExpr(this.getExpectedTypeFromDomNode(this.node));
const value = extractFieldValue(fieldBinding.value, this.tcb, this.scope);
// Create a variable with the expected type and check that the field value is assignable, e.g.
// var t1 = null! as string | number; t1 = f().value()`.
const id = this.tcb.allocateId();
const assignment = ts.factory.createBinaryExpression(id, ts.SyntaxKind.EqualsToken, value);
addParseSpanInfo(assignment, fieldBinding.valueSpan ?? fieldBinding.sourceSpan);
this.scope.addStatement(tsDeclareVariable(id, expectedType));
this.scope.addStatement(ts.factory.createExpressionStatement(assignment));
const id = new TcbExpr(this.tcb.allocateId());
const assignment = new TcbExpr(`${id.print()} = ${value.print()}`);
assignment.addParseSpanInfo(fieldBinding.valueSpan ?? fieldBinding.sourceSpan);
this.scope.addStatement(declareVariable(id, expectedType));
this.scope.addStatement(assignment);
return null;
}
private getExpectedTypeFromDomNode(node: TmplAstElement): ts.TypeNode {
private getExpectedTypeFromDomNode(node: TmplAstElement): string {
if (node.name === 'textarea' || node.name === 'select') {
// `<textarea>` and `<select>` are always strings.
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
return 'string';
}
if (node.name !== 'input') {
@ -139,27 +138,18 @@ export class TcbNativeFieldOp extends TcbOp {
switch (this.inputType) {
case 'checkbox':
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
return 'boolean';
case 'number':
case 'range':
case 'datetime-local':
return ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.factory.createLiteralTypeNode(ts.factory.createNull()),
]);
return 'string | number | null';
case 'date':
case 'month':
case 'time':
case 'week':
return ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.factory.createTypeReferenceNode('Date'),
ts.factory.createLiteralTypeNode(ts.factory.createNull()),
]);
return 'string | number | Date | null';
}
const hasDynamicType =
@ -172,21 +162,15 @@ export class TcbNativeFieldOp extends TcbOp {
// If the type is dynamic, check it as if it can be any of the types above.
if (hasDynamicType) {
return ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
ts.factory.createTypeReferenceNode('Date'),
ts.factory.createLiteralTypeNode(ts.factory.createNull()),
]);
return 'string | number | boolean | Date | null';
}
// Fall back to string if we couldn't map the type.
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
return 'string';
}
private getUnsupportedType(): ts.TypeNode {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword);
private getUnsupportedType(): string {
return 'never';
}
}
@ -208,14 +192,12 @@ export class TcbNativeRadioButtonFieldOp extends TcbNativeFieldOp {
if (valueBinding !== undefined) {
// Include an additional expression to check that the `value` is a string.
const id = this.tcb.allocateId();
const id = new TcbExpr(this.tcb.allocateId());
const value = tcbExpression(valueBinding.value, this.tcb, this.scope);
const assignment = ts.factory.createBinaryExpression(id, ts.SyntaxKind.EqualsToken, value);
addParseSpanInfo(assignment, valueBinding.sourceSpan);
this.scope.addStatement(
tsDeclareVariable(id, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)),
);
this.scope.addStatement(ts.factory.createExpressionStatement(assignment));
const assignment = new TcbExpr(`${id.print()} = ${value.print()}`);
assignment.addParseSpanInfo(valueBinding.sourceSpan);
this.scope.addStatement(declareVariable(id, new TcbExpr('string')));
this.scope.addStatement(assignment);
}
return null;
@ -430,25 +412,17 @@ export function checkUnsupportedFieldBindings(
/**
* Gets an expression that extracts the value of a field binding.
*/
function extractFieldValue(expression: AST, tcb: Context, scope: Scope): ts.Expression {
function extractFieldValue(expression: AST, tcb: Context, scope: Scope): TcbExpr {
// Unwraps the field, e.g. `[field]="f"` turns into `f()`.
const innerCall = ts.factory.createCallExpression(
tcbExpression(expression, tcb, scope),
undefined,
undefined,
);
const innerCall = new TcbExpr(tcbExpression(expression, tcb, scope).print() + '()');
// Note: we ignore diagnostics on this call, because it might not be callable
// (e.g. `undefined` is passed in). Whether the value conforms to `FieldTree` is
// checked using the common inputs op.
markIgnoreDiagnostics(innerCall);
innerCall.markIgnoreDiagnostics();
// Extract the value from the field, e.g. `f().value()`.
return ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(innerCall, 'value'),
undefined,
undefined,
);
return new TcbExpr(`${innerCall.print()}.value()`);
}
/** Checks whether a directive has a model-like input with a specific name. */

View file

@ -7,9 +7,8 @@
*/
import {TmplAstSwitchBlock, TmplAstSwitchBlockCaseGroup} from '@angular/compiler';
import ts from 'typescript';
import {markIgnoreDiagnostics} from '../comments';
import {TcbOp} from './base';
import {getStatementsBlock, TcbExpr} from './codegen';
import type {Context} from './context';
import {tcbExpression} from './expression';
import type {Scope} from './scope';
@ -34,7 +33,7 @@ export class TcbSwitchOp extends TcbOp {
override execute(): null {
const switchExpression = tcbExpression(this.block.expression, this.tcb, this.scope);
const clauses = this.block.groups.flatMap<ts.CaseOrDefaultClause>((current) => {
const clauses = this.block.groups.flatMap<TcbExpr>((current) => {
const checkBody = this.tcb.env.config.checkControlFlowBodies;
const clauseScope = this.scope.createChildScope(
this.scope,
@ -43,74 +42,60 @@ export class TcbSwitchOp extends TcbOp {
checkBody ? this.generateGuard(current, switchExpression) : null,
);
const statements = [...clauseScope.render(), ts.factory.createBreakStatement()];
const statements = [...clauseScope.render(), new TcbExpr('break')];
return current.cases.map((switchCase, index) => {
const statementsForCase = index === current.cases.length - 1 ? statements : [];
return switchCase.expression === null
? ts.factory.createDefaultClause(statementsForCase)
: ts.factory.createCaseClause(
tcbExpression(switchCase.expression, this.tcb, this.scope),
statementsForCase,
);
const statementsStr = getStatementsBlock(
index === current.cases.length - 1 ? statements : [],
true /* singleLine */,
);
const source =
switchCase.expression === null
? `default: ${statementsStr}`
: `case ${tcbExpression(switchCase.expression, this.tcb, this.scope).print()}: ${statementsStr}`;
return new TcbExpr(source);
});
});
if (this.block.exhaustiveCheck) {
const switchValue = tcbExpression(this.block.expression, this.tcb, this.scope);
const exhaustiveId = this.tcb.allocateId();
clauses.push(
ts.factory.createDefaultClause([
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createUniqueName('tcbExhaustive'),
undefined,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
switchValue,
),
],
ts.NodeFlags.Const,
),
),
]),
new TcbExpr(`default: const tcbExhaustive${exhaustiveId}: never = ${switchValue.print()};`),
);
}
this.scope.addStatement(
ts.factory.createSwitchStatement(switchExpression, ts.factory.createCaseBlock(clauses)),
new TcbExpr(
`switch (${switchExpression.print()}) { ${clauses.map((c) => c.print()).join('\n')} }`,
),
);
return null;
}
private generateGuard(
group: TmplAstSwitchBlockCaseGroup,
switchValue: ts.Expression,
): ts.Expression | null {
private generateGuard(group: TmplAstSwitchBlockCaseGroup, switchValue: TcbExpr): TcbExpr | null {
// For non-default cases, the guard needs to compare against the case value, e.g.
// `switchExpression === caseExpression`.
const hasDefault = group.cases.some((c) => c.expression === null);
if (!hasDefault) {
let guard: ts.Expression | null = null;
let guard: TcbExpr | null = null;
for (const switchCase of group.cases) {
if (switchCase.expression !== null) {
// The expression needs to be ignored for diagnostics since it has been checked already.
const expression = tcbExpression(switchCase.expression, this.tcb, this.scope);
markIgnoreDiagnostics(expression);
const comparison = ts.factory.createBinaryExpression(
switchValue,
ts.SyntaxKind.EqualsEqualsEqualsToken,
expression,
);
expression.markIgnoreDiagnostics();
const comparison = new TcbExpr(`${switchValue.print()} === ${expression.print()}`);
if (guard === null) {
guard = comparison;
} else {
guard = ts.factory.createBinaryExpression(guard, ts.SyntaxKind.BarBarToken, comparison);
guard = new TcbExpr(`${guard.print()} || ${comparison.print()}`);
}
}
}
@ -126,7 +111,7 @@ export class TcbSwitchOp extends TcbOp {
// @default {}
// }
// Will produce the guard `expr !== 1 && expr !== 2`.
let guard: ts.Expression | null = null;
let guard: TcbExpr | null = null;
for (const currentGroup of this.block.groups) {
if (currentGroup === group) {
@ -141,21 +126,13 @@ export class TcbSwitchOp extends TcbOp {
// The expression needs to be ignored for diagnostics since it has been checked already.
const expression = tcbExpression(switchCase.expression, this.tcb, this.scope);
markIgnoreDiagnostics(expression);
const comparison = ts.factory.createBinaryExpression(
switchValue,
ts.SyntaxKind.ExclamationEqualsEqualsToken,
expression,
);
expression.markIgnoreDiagnostics();
const comparison = new TcbExpr(`${switchValue.print()} !== ${expression.print()}`);
if (guard === null) {
guard = comparison;
} else {
guard = ts.factory.createBinaryExpression(
guard,
ts.SyntaxKind.AmpersandAmpersandToken,
comparison,
);
guard = new TcbExpr(`${guard.print()} && ${comparison.print()}`);
}
}
}

View file

@ -7,15 +7,13 @@
*/
import {TmplAstBoundAttribute, TmplAstDirective, TmplAstTemplate} from '@angular/compiler';
import ts from 'typescript';
import {tsCallMethod, tsDeclareVariable} from '../ts_util';
import {addParseSpanInfo} from '../diagnostics';
import {TcbOp} from './base';
import {declareVariable, getStatementsBlock, TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {Reference} from '../../../imports';
import {ClassDeclaration} from '../../../reflection';
import {markIgnoreDiagnostics} from '../comments';
import {tcbExpression} from './expression';
/**
@ -34,12 +32,11 @@ export class TcbTemplateContextOp extends TcbOp {
// The declaration of the context variable is only needed when the context is actually referenced.
override readonly optional = true;
override execute(): ts.Identifier {
override execute(): TcbExpr {
// Allocate a template ctx variable and declare it with an 'any' type. The type of this variable
// may be narrowed as a result of template guard conditions.
const ctx = this.tcb.allocateId();
const type = ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
this.scope.addStatement(tsDeclareVariable(ctx, type));
const ctx = new TcbExpr(this.tcb.allocateId());
this.scope.addStatement(declareVariable(ctx, new TcbExpr('any')));
return ctx;
}
}
@ -76,8 +73,8 @@ export class TcbTemplateBodyOp extends TcbOp {
// Collect these into `guards` by processing the directives.
// By default the guard is simply `true`.
let guard: ts.Expression | null = null;
const directiveGuards: ts.Expression[] = [];
let guard: TcbExpr | null = null;
const directiveGuards: TcbExpr[] = [];
this.addDirectiveGuards(
directiveGuards,
@ -98,8 +95,7 @@ export class TcbTemplateBodyOp extends TcbOp {
// Pop the first value and use it as the initializer to reduce(). This way, a single guard
// will be used on its own, but two or more will be combined into binary AND expressions.
guard = directiveGuards.reduce(
(expr, dirGuard) =>
ts.factory.createBinaryExpression(expr, ts.SyntaxKind.AmpersandAmpersandToken, dirGuard),
(expr, dirGuard) => new TcbExpr(`${expr.print()} && ${dirGuard.print()}`),
directiveGuards.pop()!,
);
}
@ -125,22 +121,19 @@ export class TcbTemplateBodyOp extends TcbOp {
return null;
}
let tmplBlock: ts.Statement = ts.factory.createBlock(statements);
let tmplBlock = `{\n${getStatementsBlock(statements)}}`;
if (guard !== null) {
// The scope has a guard that needs to be applied, so wrap the template block into an `if`
// statement containing the guard expression.
tmplBlock = ts.factory.createIfStatement(
/* expression */ guard,
/* thenStatement */ tmplBlock,
);
tmplBlock = `if (${guard.print()}) ${tmplBlock}`;
}
this.scope.addStatement(tmplBlock);
this.scope.addStatement(new TcbExpr(tmplBlock));
return null;
}
private addDirectiveGuards(
guards: ts.Expression[],
guards: TcbExpr[],
hostNode: TmplAstTemplate | TmplAstDirective,
directives: TypeCheckableDirectiveMeta[] | null,
) {
@ -174,7 +167,7 @@ export class TcbTemplateBodyOp extends TcbOp {
// The expression has already been checked in the type constructor invocation, so
// it should be ignored when used within a template guard.
markIgnoreDiagnostics(expr);
expr.markIgnoreDiagnostics();
if (guard.type === 'binding') {
// Use the binding expression itself as guard.
@ -182,11 +175,11 @@ export class TcbTemplateBodyOp extends TcbOp {
} else {
// Call the guard function on the directive with the directive instance and that
// expression.
const guardInvoke = tsCallMethod(dirId, `ngTemplateGuard_${guard.inputName}`, [
dirInstId,
expr,
]);
addParseSpanInfo(guardInvoke, boundInput.value.sourceSpan);
const guardInvoke = new TcbExpr(
`${dirId.print()}.ngTemplateGuard_${guard.inputName}(${dirInstId.print()}, ${expr.print()})`,
);
guardInvoke.addParseSpanInfo(boundInput.value.sourceSpan);
guards.push(guardInvoke);
}
}
@ -197,9 +190,12 @@ export class TcbTemplateBodyOp extends TcbOp {
if (dir.hasNgTemplateContextGuard) {
if (this.tcb.env.config.applyTemplateContextGuards) {
const ctx = this.scope.resolve(hostNode);
const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]);
markIgnoreDiagnostics(guardInvoke);
addParseSpanInfo(guardInvoke, hostNode.sourceSpan);
const guardInvoke = new TcbExpr(
`${dirId.print()}.ngTemplateContextGuard(${dirInstId.print()}, ${ctx.print()})`,
);
guardInvoke.markIgnoreDiagnostics();
guardInvoke.addParseSpanInfo(hostNode.sourceSpan);
guards.push(guardInvoke);
} else if (
isTemplate &&

View file

@ -7,12 +7,10 @@
*/
import {TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import type {Context} from './context';
import type {Scope} from './scope';
import {addParseSpanInfo, wrapForTypeChecker} from '../diagnostics';
import {tsCreateVariable, tsDeclareVariable} from '../ts_util';
import {declareVariable, TcbExpr} from './codegen';
/**
* A `TcbOp` which renders a variable that is implicitly available within a block (e.g. `$count`
@ -24,7 +22,7 @@ export class TcbBlockImplicitVariableOp extends TcbOp {
constructor(
private tcb: Context,
private scope: Scope,
private type: ts.TypeNode,
private type: TcbExpr,
private variable: TmplAstVariable,
) {
super();
@ -32,11 +30,11 @@ export class TcbBlockImplicitVariableOp extends TcbOp {
override readonly optional = true;
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
addParseSpanInfo(id, this.variable.keySpan);
const variable = tsDeclareVariable(id, this.type);
addParseSpanInfo(variable.declarationList.declarations[0], this.variable.sourceSpan);
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId());
id.addParseSpanInfo(this.variable.keySpan);
const variable = declareVariable(id, this.type);
variable.addParseSpanInfo(this.variable.sourceSpan);
this.scope.addStatement(variable);
return id;
}
@ -62,28 +60,23 @@ export class TcbTemplateVariableOp extends TcbOp {
return false;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
// Look for a context variable for the template.
const ctx = this.scope.resolve(this.template);
// Allocate an identifier for the TmplAstVariable, and initialize it to a read of the variable
// on the template context.
const id = this.tcb.allocateId();
const initializer = ts.factory.createPropertyAccessExpression(
/* expression */ ctx,
/* name */ this.variable.value || '$implicit',
);
addParseSpanInfo(id, this.variable.keySpan);
const id = new TcbExpr(this.tcb.allocateId());
const initializer = new TcbExpr(`${ctx.print()}.${this.variable.value || '$implicit'}`);
id.addParseSpanInfo(this.variable.keySpan);
// Declare the variable, and return its identifier.
let variable: ts.VariableStatement;
if (this.variable.valueSpan !== undefined) {
addParseSpanInfo(initializer, this.variable.valueSpan);
variable = tsCreateVariable(id, wrapForTypeChecker(initializer));
initializer.addParseSpanInfo(this.variable.valueSpan).wrapForTypeChecker();
} else {
variable = tsCreateVariable(id, initializer);
}
addParseSpanInfo(variable.declarationList.declarations[0], this.variable.sourceSpan);
const variable = new TcbExpr(`var ${id.print()} = ${initializer.print()}`);
variable.addParseSpanInfo(this.variable.sourceSpan);
this.scope.addStatement(variable);
return id;
}
@ -98,7 +91,7 @@ export class TcbBlockVariableOp extends TcbOp {
constructor(
private tcb: Context,
private scope: Scope,
private initializer: ts.Expression,
private initializer: TcbExpr,
private variable: TmplAstVariable,
) {
super();
@ -108,11 +101,12 @@ export class TcbBlockVariableOp extends TcbOp {
return false;
}
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
addParseSpanInfo(id, this.variable.keySpan);
const variable = tsCreateVariable(id, wrapForTypeChecker(this.initializer));
addParseSpanInfo(variable.declarationList.declarations[0], this.variable.sourceSpan);
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId());
id.addParseSpanInfo(this.variable.keySpan);
this.initializer.wrapForTypeChecker();
const variable = new TcbExpr(`var ${id.print()} = ${this.initializer.print()}`);
variable.addParseSpanInfo(this.variable.sourceSpan);
this.scope.addStatement(variable);
return id;
}

View file

@ -23,7 +23,8 @@ import {
ReferenceEmitter,
} from '../../imports';
import {ReflectionHost} from '../../reflection';
import {ImportManager, translateExpression, translateType} from '../../translator';
import {ImportManager, translateType} from '../../translator';
import {TcbExpr} from './ops/codegen';
/**
* An environment for a given source file that can be used to emit references.
@ -74,13 +75,20 @@ export class ReferenceEmitEnvironment {
);
}
/**
* Generate a `ts.Expression` that refers to the external symbol. This
* may result in new imports being generated.
*/
referenceExternalSymbol(moduleName: string, name: string): ts.Expression {
const external = new ExternalExpr({moduleName, name});
return translateExpression(this.contextFile, external, this.importManager);
referenceExternalSymbol(moduleName: string, name: string): TcbExpr {
const importResult = this.importManager.addImport({
exportModuleSpecifier: moduleName,
exportSymbolName: name,
requestedFile: this.contextFile,
});
if (ts.isIdentifier(importResult)) {
return new TcbExpr(importResult.text);
} else if (ts.isIdentifier(importResult.expression)) {
return new TcbExpr(`${importResult.expression.text}.${importResult.name.text}`);
}
throw new Error('Unexpected value returned by import manager');
}
/**

View file

@ -12,7 +12,6 @@ import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection';
import {TypeCheckBlockMetadata} from '../api';
import {addTypeCheckId} from './diagnostics';
import {DomSchemaChecker} from './dom';
import {Environment} from './environment';
import {OutOfBandDiagnosticRecorder} from './oob';
@ -20,6 +19,7 @@ import {TypeParameterEmitter} from './type_parameter_emitter';
import {createHostBindingsBlockGuard} from './host_bindings';
import {Context, TcbGenericContextBehavior} from './ops/context';
import {Scope} from './ops/scope';
import {getStatementsBlock, tempPrint} from './ops/codegen';
/**
* Given a `ts.ClassDeclaration` for a component, and metadata regarding that component, compose a
@ -53,7 +53,7 @@ export function generateTypeCheckBlock(
domSchemaChecker: DomSchemaChecker,
oobRecorder: OutOfBandDiagnosticRecorder,
genericContextBehavior: TcbGenericContextBehavior,
): ts.FunctionDeclaration {
): string {
const tcb = new Context(
env,
domSchemaChecker,
@ -104,8 +104,21 @@ export function generateTypeCheckBlock(
}
}
const paramList = [tcbThisParam(ctxRawType.typeName, typeArguments)];
const statements: ts.Statement[] = [];
const sourceFile = env.contextFile;
const typeParamsStr =
typeParameters === undefined || typeParameters.length === 0
? ''
: `<${typeParameters.map((p) => tempPrint(p, sourceFile)).join(', ')}>`;
const typeArgsStr =
typeArguments === undefined || typeArguments.length === 0
? ''
: `<${typeArguments.map((p) => tempPrint(p, sourceFile)).join(', ')}>`;
const typeRef = ts.isIdentifier(ctxRawType.typeName)
? ctxRawType.typeName.text
: tempPrint(ctxRawType.typeName, sourceFile);
const thisParamStr = `this: ${typeRef}${typeArgsStr}`;
const statements: string[] = [];
// Add the template type checking code.
if (tcb.boundTarget.target.template !== undefined) {
@ -117,7 +130,7 @@ export function generateTypeCheckBlock(
/* guard */ null,
);
statements.push(renderBlockStatements(env, templateScope, ts.factory.createTrue()));
statements.push(renderBlockStatements(env, templateScope, 'true'));
}
// Add the host bindings type checking code.
@ -126,48 +139,19 @@ export function generateTypeCheckBlock(
statements.push(renderBlockStatements(env, hostScope, createHostBindingsBlockGuard()));
}
const body = ts.factory.createBlock(statements);
const fnDecl = ts.factory.createFunctionDeclaration(
/* modifiers */ undefined,
/* asteriskToken */ undefined,
/* name */ name,
/* typeParameters */ env.config.useContextGenericType ? typeParameters : undefined,
/* parameters */ paramList,
/* type */ undefined,
/* body */ body,
);
addTypeCheckId(fnDecl, meta.id);
return fnDecl;
const bodyStr = `{\n${statements.join('\n')}\n}`;
const funcDeclStr = `function ${name.text}${typeParamsStr}(${thisParamStr}) ${bodyStr}`;
return `/*${meta.id}*/\n${funcDeclStr}`;
}
function renderBlockStatements(
env: Environment,
scope: Scope,
wrapperExpression: ts.Expression,
): ts.Statement {
function renderBlockStatements(env: Environment, scope: Scope, wrapperExpression: string): string {
// Note: this needs to be called first so that it can populate the prelude statements.
const scopeStatements = scope.render();
const innerBody = ts.factory.createBlock([...env.getPreludeStatements(), ...scopeStatements]);
const statements = getStatementsBlock([...env.getPreludeStatements(), ...scopeStatements]);
// Wrap the body in an if statement. This serves two purposes:
// 1. It allows us to distinguish between the sections of the block (e.g. host or template).
// 2. It allows the `ts.Printer` to produce better-looking output.
return ts.factory.createIfStatement(wrapperExpression, innerBody);
}
/**
* Create the `this` parameter to the top-level TCB function, with the given generic type
* arguments.
*/
function tcbThisParam(
name: ts.EntityName,
typeArguments: ts.TypeNode[] | undefined,
): ts.ParameterDeclaration {
return ts.factory.createParameterDeclaration(
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'this',
/* questionToken */ undefined,
/* type */ ts.factory.createTypeReferenceNode(name, typeArguments),
/* initializer */ undefined,
);
return `if (${wrapperExpression}) {\n${statements}\n}`;
}

View file

@ -19,6 +19,7 @@ import {OutOfBandDiagnosticRecorder} from './oob';
import {ensureTypeCheckFilePreparationImports} from './tcb_util';
import {generateTypeCheckBlock} from './type_check_block';
import {TcbGenericContextBehavior} from './ops/context';
import {getStatementsBlock, TcbExpr} from './ops/codegen';
/**
* An `Environment` representing the single type-checking file into which most (if not all) Type
@ -30,7 +31,7 @@ import {TcbGenericContextBehavior} from './ops/context';
*/
export class TypeCheckFile extends Environment {
private nextTcbId = 1;
private tcbStatements: ts.Statement[] = [];
private tcbStatements: string[] = [];
constructor(
readonly fileName: AbsoluteFsPath,
@ -79,7 +80,7 @@ export class TypeCheckFile extends Environment {
this.tcbStatements.push(fn);
}
render(removeComments: boolean): string {
render(): string {
// NOTE: We are conditionally adding imports whenever we discover signal inputs. This has a
// risk of changing the import graph of the TypeScript program, degrading incremental program
// re-use due to program structure changes. For type check block files, we are ensuring an
@ -93,7 +94,7 @@ export class TypeCheckFile extends Environment {
);
}
const printer = ts.createPrinter({removeComments});
const printer = ts.createPrinter();
let source = '';
const newImports = importChanges.newImports.get(this.contextFile.fileName);
@ -104,15 +105,12 @@ export class TypeCheckFile extends Environment {
}
source += '\n';
for (const stmt of this.pipeInstStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
}
for (const stmt of this.typeCtorStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
}
source += getStatementsBlock(this.pipeInstStatements);
source += getStatementsBlock(this.typeCtorStatements);
source += '\n';
for (const stmt of this.tcbStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
source += stmt + '\n';
}
// Ensure the template type-checking file is an ES module. Otherwise, it's interpreted as some
@ -123,7 +121,7 @@ export class TypeCheckFile extends Environment {
return source;
}
override getPreludeStatements(): ts.Statement[] {
override getPreludeStatements(): TcbExpr[] {
return [];
}
}

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ExpressionType, R3Identifiers, WrappedNodeExpr} from '@angular/compiler';
import {R3Identifiers} from '@angular/compiler';
import ts from 'typescript';
import {ClassDeclaration, ReflectionHost} from '../../reflection';
@ -14,50 +14,37 @@ import {TypeCtorMetadata} from '../api';
import {ReferenceEmitEnvironment} from './reference_emit_environment';
import {checkIfGenericTypeBoundsCanBeEmitted} from './tcb_util';
import {tsCreateTypeQueryForCoercedInput} from './ts_util';
import {TcbExpr, tempPrint} from './ops/codegen';
export function generateTypeCtorDeclarationFn(
env: ReferenceEmitEnvironment,
meta: TypeCtorMetadata,
nodeTypeRef: ts.EntityName,
typeParams: ts.TypeParameterDeclaration[] | undefined,
): ts.Statement {
const rawTypeArgs = typeParams !== undefined ? generateGenericArgs(typeParams) : undefined;
const rawType = ts.factory.createTypeReferenceNode(nodeTypeRef, rawTypeArgs);
const initParam = constructTypeCtorParameter(env, meta, rawType);
): TcbExpr {
const typeArgs = generateGenericArgs(typeParams);
const typeRef = ts.isIdentifier(nodeTypeRef)
? nodeTypeRef.text
: tempPrint(nodeTypeRef, nodeTypeRef.getSourceFile());
const typeRefWithGenerics = `${typeRef}${typeArgs}`;
const initParam = constructTypeCtorParameter(
env,
meta,
nodeTypeRef.getSourceFile(),
typeRef,
typeRefWithGenerics,
);
const typeParameters = typeParametersWithDefaultTypes(typeParams);
let source: string;
if (meta.body) {
const fnType = ts.factory.createFunctionTypeNode(
/* typeParameters */ typeParameters,
/* parameters */ [initParam],
/* type */ rawType,
);
const decl = ts.factory.createVariableDeclaration(
/* name */ meta.fnName,
/* exclamationToken */ undefined,
/* type */ fnType,
/* body */ ts.factory.createNonNullExpression(ts.factory.createNull()),
);
const declList = ts.factory.createVariableDeclarationList([decl], ts.NodeFlags.Const);
return ts.factory.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */ declList,
);
const fnType = `${typeParameters}(${initParam}) => ${typeRefWithGenerics}`;
source = `const ${meta.fnName}: ${fnType} = null!`;
} else {
return ts.factory.createFunctionDeclaration(
/* modifiers */ [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
/* asteriskToken */ undefined,
/* name */ meta.fnName,
/* typeParameters */ typeParameters,
/* parameters */ [initParam],
/* type */ rawType,
/* body */ undefined,
);
source = `declare function ${meta.fnName}${typeParameters}(${initParam}): ${typeRefWithGenerics}`;
}
return new TcbExpr(source);
}
/**
@ -99,44 +86,37 @@ export function generateInlineTypeCtor(
env: ReferenceEmitEnvironment,
node: ClassDeclaration<ts.ClassDeclaration>,
meta: TypeCtorMetadata,
): ts.MethodDeclaration {
): string {
// Build rawType, a `ts.TypeNode` of the class with its generic parameters passed through from
// the definition without any type bounds. For example, if the class is
// `FooDirective<T extends Bar>`, its rawType would be `FooDirective<T>`.
const rawTypeArgs =
node.typeParameters !== undefined ? generateGenericArgs(node.typeParameters) : undefined;
const rawType = ts.factory.createTypeReferenceNode(node.name, rawTypeArgs);
const initParam = constructTypeCtorParameter(env, meta, rawType);
const typeRef = node.name.text;
const typeRefWithGenerics = `${typeRef}${generateGenericArgs(node.typeParameters)}`;
const initParam = constructTypeCtorParameter(
env,
meta,
node.getSourceFile(),
typeRef,
typeRefWithGenerics,
);
// If this constructor is being generated into a .ts file, then it needs a fake body. The body
// is set to a return of `null!`. If the type constructor is being generated into a .d.ts file,
// it needs no body.
let body: ts.Block | undefined = undefined;
if (meta.body) {
body = ts.factory.createBlock([
ts.factory.createReturnStatement(ts.factory.createNonNullExpression(ts.factory.createNull())),
]);
}
const body = `{ return null!; }`;
const typeParams = typeParametersWithDefaultTypes(node.typeParameters);
// Create the type constructor method declaration.
return ts.factory.createMethodDeclaration(
/* modifiers */ [ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)],
/* asteriskToken */ undefined,
/* name */ meta.fnName,
/* questionToken */ undefined,
/* typeParameters */ typeParametersWithDefaultTypes(node.typeParameters),
/* parameters */ [initParam],
/* type */ rawType,
/* body */ body,
);
return `static ${meta.fnName}${typeParams}(${initParam}): ${typeRefWithGenerics} ${body}`;
}
function constructTypeCtorParameter(
env: ReferenceEmitEnvironment,
meta: TypeCtorMetadata,
rawType: ts.TypeReferenceNode,
): ts.ParameterDeclaration {
sourceFile: ts.SourceFile,
typeRef: string,
typeRefWithGenerics: string,
): string {
// initType is the type of 'init', the single argument to the type constructor method.
// If the Directive has any inputs, its initType will be:
//
@ -146,90 +126,68 @@ function constructTypeCtorParameter(
// directive will be inferred.
//
// In the special case there are no inputs, initType is set to {}.
let initType: ts.TypeNode | null = null;
let initType: string | null = null;
const plainKeys: ts.LiteralTypeNode[] = [];
const coercedKeys: ts.PropertySignature[] = [];
const signalInputKeys: ts.LiteralTypeNode[] = [];
const plainKeys: string[] = [];
const coercedKeys: string[] = [];
const signalInputKeys: string[] = [];
for (const {classPropertyName, transform, isSignal} of meta.fields.inputs) {
if (isSignal) {
signalInputKeys.push(
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName)),
);
signalInputKeys.push(`"${classPropertyName}"`);
} else if (!meta.coercedInputFields.has(classPropertyName)) {
plainKeys.push(
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName)),
);
plainKeys.push(`"${classPropertyName}"`);
} else {
const coercionType =
transform != null
? transform.type.node
: tsCreateTypeQueryForCoercedInput(rawType.typeName, classPropertyName);
? tempPrint(transform.type.node, sourceFile)
: `typeof ${typeRef}.ngAcceptInputType_${classPropertyName}`;
coercedKeys.push(
ts.factory.createPropertySignature(
/* modifiers */ undefined,
/* name */ classPropertyName,
/* questionToken */ undefined,
/* type */ coercionType,
),
);
coercedKeys.push(`${classPropertyName}: ${coercionType}`);
}
}
if (plainKeys.length > 0) {
// Construct a union of all the field names.
const keyTypeUnion = ts.factory.createUnionTypeNode(plainKeys);
// Construct the Pick<rawType, keyTypeUnion>.
initType = ts.factory.createTypeReferenceNode('Pick', [rawType, keyTypeUnion]);
initType = `Pick<${typeRefWithGenerics}, ${plainKeys.join(' | ')}>`;
}
if (coercedKeys.length > 0) {
const coercedLiteral = ts.factory.createTypeLiteralNode(coercedKeys);
initType =
initType !== null
? ts.factory.createIntersectionTypeNode([initType, coercedLiteral])
: coercedLiteral;
let coercedLiteral = '{\n';
for (const key of coercedKeys) {
coercedLiteral += `${key};\n`;
}
coercedLiteral += '}';
initType = initType !== null ? `${initType} & ${coercedLiteral}` : coercedLiteral;
}
if (signalInputKeys.length > 0) {
const keyTypeUnion = ts.factory.createUnionTypeNode(signalInputKeys);
const keyTypeUnion = signalInputKeys.join(' | ');
// Construct the UnwrapDirectiveSignalInputs<rawType, keyTypeUnion>.
const unwrapDirectiveSignalInputsExpr = env.referenceExternalType(
const unwrapRef = env.referenceExternalSymbol(
R3Identifiers.UnwrapDirectiveSignalInputs.moduleName,
R3Identifiers.UnwrapDirectiveSignalInputs.name,
[
// TODO:
new ExpressionType(new WrappedNodeExpr(rawType)),
new ExpressionType(new WrappedNodeExpr(keyTypeUnion)),
],
);
initType =
initType !== null
? ts.factory.createIntersectionTypeNode([initType, unwrapDirectiveSignalInputsExpr])
: unwrapDirectiveSignalInputsExpr;
const unwrapExpr = `${unwrapRef.print()}<${typeRefWithGenerics}, ${keyTypeUnion}>`;
initType = initType !== null ? `${initType} & ${unwrapExpr}` : unwrapExpr;
}
if (initType === null) {
// Special case - no inputs, outputs, or other fields which could influence the result type.
initType = ts.factory.createTypeLiteralNode([]);
initType = '{}';
}
// Create the 'init' parameter itself.
return ts.factory.createParameterDeclaration(
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'init',
/* questionToken */ undefined,
/* type */ initType,
/* initializer */ undefined,
);
return `init: ${initType}`;
}
function generateGenericArgs(params: ReadonlyArray<ts.TypeParameterDeclaration>): ts.TypeNode[] {
return params.map((param) => ts.factory.createTypeReferenceNode(param.name, undefined));
function generateGenericArgs(
typeParameters: ReadonlyArray<ts.TypeParameterDeclaration> | undefined,
): string {
if (typeParameters === undefined || typeParameters.length === 0) {
return '';
}
return `<${typeParameters.map((param) => param.name.text).join(', ')}>`;
}
export function requiresInlineTypeCtor(
@ -288,22 +246,21 @@ export function requiresInlineTypeCtor(
*/
function typeParametersWithDefaultTypes(
params: ReadonlyArray<ts.TypeParameterDeclaration> | undefined,
): ts.TypeParameterDeclaration[] | undefined {
if (params === undefined) {
return undefined;
): string {
if (params === undefined || params.length === 0) {
return '';
}
return params.map((param) => {
if (param.default === undefined) {
return ts.factory.updateTypeParameterDeclaration(
param,
param.modifiers,
param.name,
param.constraint,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
} else {
return param;
}
const paramStrings = params.map((param) => {
const constraint = param.constraint
? ` extends ${tempPrint(param.constraint, param.getSourceFile())}`
: '';
const defaultValue = ` = ${
param.default ? tempPrint(param.default, param.getSourceFile()) : 'any'
}`;
return `${param.name.text}${constraint}${defaultValue}`;
});
return `<${paramStrings.join(', ')}>`;
}

View file

@ -201,19 +201,19 @@ describe('type check blocks', () => {
it('should handle template literals', () => {
expect(tcb('{{ `hello world` }}')).toContain('"" + (`hello world`);');
expect(tcb('{{ `hello \\${name}!!!` }}')).toContain('"" + (`hello \\${name}!!!`);');
expect(tcb('{{ `hello ${name}!!!` }}')).toContain('"" + (`hello ${((this).name)}!!!`);');
expect(tcb('{{ `${a} - ${b} - ${c}` }}')).toContain(
'"" + (`${((this).a)} - ${((this).b)} - ${((this).c)}`);',
);
});
it('should handle tagged template literals', () => {
expect(tcb('{{ tag`hello world` }}')).toContain('"" + (((this).tag) `hello world`);');
expect(tcb('{{ tag`hello \\${name}!!!` }}')).toContain(
'"" + (((this).tag) `hello \\${name}!!!`);',
expect(tcb('{{ tag`hello world` }}')).toContain('"" + (((this).tag)`hello world`);');
expect(tcb('{{ tag`hello ${name}!!!` }}')).toContain(
'"" + (((this).tag)`hello ${((this).name)}!!!`);',
);
expect(tcb('{{ tag`${a} - ${b} - ${c}` }}')).toContain(
'"" + (((this).tag) `${((this).a)} - ${((this).b)} - ${((this).c)}`);',
'"" + (((this).tag)`${((this).a)} - ${((this).b)} - ${((this).c)}`);',
);
});
@ -306,7 +306,7 @@ describe('type check blocks', () => {
);
});
it('should generate circular references between two directives correctly', () => {
it('should generate circular references between two generic directives correctly', () => {
const TEMPLATE = `
<div #a="dirA" dir-a [inputA]="b">A</div>
<div #b="dirB" dir-b [inputB]="a">B</div>
@ -339,7 +339,7 @@ describe('type check blocks', () => {
'var _t2 = _ctor2({ "inputB": (_t3) }); ' +
'var _t1 = _t2; ' +
'_t4.inputA = (_t1); ' +
'_t2.inputB = (_t3);',
'_t2.inputB = ((_t3));',
);
});
@ -1226,7 +1226,7 @@ describe('type check blocks', () => {
checkTypeOfInputBindings: false,
};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t1.dirInput = ((((((this).a)) === (((this).b))) as any));');
expect(block).toContain('_t1.dirInput = (((((this).a)) === (((this).b)) as any));');
});
});
@ -1843,7 +1843,7 @@ describe('type check blocks', () => {
}
`;
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) { }');
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) {}');
});
it('should generate `prefetch when` trigger', () => {
@ -1853,7 +1853,7 @@ describe('type check blocks', () => {
}
`;
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) { }');
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) {}');
});
it('should generate `hydrate when` trigger', () => {
@ -1863,7 +1863,7 @@ describe('type check blocks', () => {
}
`;
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) { }');
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) {}');
});
it('should generate options for `viewport` trigger', () => {
@ -1936,7 +1936,7 @@ describe('type check blocks', () => {
}`;
expect(tcb(TEMPLATE)).toContain(
'var _t1 = ((((this).expr)) === (1)); if (((((this).expr)) === (1)) && _t1) { "" + (_t1); } } }',
'var _t1 = ((((this).expr)) === (1)); if (((((this).expr)) === (1)) && _t1) { "" + (_t1); }; } }',
);
});
@ -2114,12 +2114,12 @@ describe('type check blocks', () => {
'var _t1 = null! as any; { var _t2 = (_t1.exp); switch (_t2()) { ' +
'case "one": "" + ((this).one()); break; ' +
'case "two": "" + ((this).two()); break; ' +
'default: "" + ((this).default()); break; } }',
'default: "" + ((this).default()); break; }; }',
);
});
it('should handle an empty switch block', () => {
expect(tcb('@switch (expr) {}')).toContain('if (true) { switch (((this).expr)) { } }');
expect(tcb('@switch (expr) {}')).toContain('if (true) { switch (((this).expr)) { }; }');
});
it('should not generate the body of a switch block if checkControlFlowBodies is disabled', () => {
@ -2159,7 +2159,7 @@ describe('type check blocks', () => {
'switch (((this).expr)) { ' +
'case 1: "" + ((this).one()); break; ' +
'case 2: "" + ((this).two()); break; ' +
'default: const tcbExhaustive_1: never = ((this).expr);',
'default: const tcbExhaustive_t1: never = ((this).expr);',
);
});
@ -2172,7 +2172,7 @@ describe('type check blocks', () => {
`;
const SOURCE = `
export class TestComponent {
expr!: 1|2;
expr!: 1|2;
}
`;
expect(diagnose(TEMPLATE, SOURCE, undefined, [], undefined, {noUnusedLocals: true})).toEqual([
@ -2305,7 +2305,7 @@ describe('type check blocks', () => {
expect(result).toContain('for (const _t1 of ((this).items)!) { var _t2 = null! as number;');
expect(result).toContain('"" + (_t1) + (_t2)');
expect(result).toContain('for (const _t3 of ((_t1).items)!) { var _t4 = null! as number;');
expect(result).toContain('"" + (_t1) + (_t2) + (_t3) + (_t4)');
expect(result).toContain('"" + (_t1) + ((_t2)) + (_t3) + (_t4)');
});
it('should generate the tracking expression of a for loop', () => {
@ -2543,7 +2543,7 @@ describe('type check blocks', () => {
const block = selectorlessTcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('var _t1 = null! as i0.Dir;');
expect(block).toContain('_t1.someInput = (((this).value));');
expect(block).toContain('if (((this).value)) { "" + (((this).value)); } }');
expect(block).toContain('if (((this).value)) { "" + (((this).value)); }; }');
});
it('should generate bindings for unclaimed component inputs', () => {

View file

@ -64,7 +64,7 @@ runInEachFileSystem(() => {
/* reflector */ null!,
host,
);
const sf = file.render(false /* removeComments */);
const sf = file.render();
expect(sf).toContain('export const IS_A_MODULE = true;');
});

View file

@ -297,24 +297,23 @@ export const ALL_ENABLED_CONFIG: Readonly<TypeCheckingConfig> = {
};
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
export interface TestDirective
extends Partial<
Pick<
TypeCheckableDirectiveMeta,
Exclude<
keyof TypeCheckableDirectiveMeta,
| 'ref'
| 'coercedInputFields'
| 'restrictedInputFields'
| 'stringLiteralInputFields'
| 'undeclaredInputFields'
| 'publicMethods'
| 'inputs'
| 'outputs'
| 'hostDirectives'
>
export interface TestDirective extends Partial<
Pick<
TypeCheckableDirectiveMeta,
Exclude<
keyof TypeCheckableDirectiveMeta,
| 'ref'
| 'coercedInputFields'
| 'restrictedInputFields'
| 'stringLiteralInputFields'
| 'undeclaredInputFields'
| 'publicMethods'
| 'inputs'
| 'outputs'
| 'hostDirectives'
>
> {
>
> {
selector: string | null;
name: string;
file?: AbsoluteFsPath;
@ -464,7 +463,12 @@ export function tcb(
TcbGenericContextBehavior.UseEmitter,
);
const rendered = env.render(!options.emitSpans /* removeComments */);
let rendered = env.render();
if (!options.emitSpans) {
rendered = rendered.replace(/\s+\/\*[\s\S]*?\*\//g, '');
}
return rendered.replace(/\s+/g, ' ');
}