mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
3828ef1917
commit
befbae0dcf
34 changed files with 806 additions and 1234 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}*/)`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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!');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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})`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(', ')}>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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;');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ' ');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue