diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index 516f2260796..d06cdaab370 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -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; -} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts index 06ab4f2e017..da52b4fa52e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts @@ -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(); - protected typeCtorStatements: ts.Statement[] = []; + private typeCtors = new Map(); + protected typeCtorStatements: TcbExpr[] = []; - private pipeInsts = new Map(); - protected pipeInstStatements: ts.Statement[] = []; + private pipeInsts = new Map(); + 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>; 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>): ts.Expression { + pipeInst(ref: Reference>): 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>): ts.Expression { + reference(ref: Reference>): 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]; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index e360bfcec9d..2303fdfe6ad 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -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([ - ['+', ts.SyntaxKind.PlusToken], - ['-', ts.SyntaxKind.MinusToken], - ]); - - private readonly BINARY_OPS = new Map([ - ['+', 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)`); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/host_bindings.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/host_bindings.ts index 17262ba862a..f2e85520e4c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/host_bindings.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/host_bindings.ts @@ -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}*/)`; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/base.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/base.ts index 1a99262d6c4..cbd0d7b1b4f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/base.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/base.ts @@ -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!'); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/bindings.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/bindings.ts index 85d4c9f5760..f08094ceb6f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/bindings.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/bindings.ts @@ -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. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/completions.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/completions.ts index 37e44a0e242..cad99aab6ad 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/completions.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/completions.ts @@ -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; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/context.ts index ea319533c94..7997c1d7dc9 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/context.ts @@ -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 { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/directive_constructor.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/directive_constructor.ts index 6d8ff142f41..556a6f1d5aa 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/directive_constructor.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/directive_constructor.ts @@ -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(); - 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})`); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/directive_type.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/directive_type.ts index 16a197069a5..81860fff553 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/directive_type.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/directive_type.ts @@ -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>; - 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>; 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>; if (dirRef.node.typeParameters === undefined) { throw new Error( diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/element.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/element.ts index ea13281e4fa..a47a37da527 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/element.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/element.ts @@ -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; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/events.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/events.ts index 94f763ea812..208d6d11b95 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/events.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/events.ts @@ -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()}(${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}`); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/expression.ts index a3048868fa1..cdfbdf4f094 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/expression.ts @@ -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>, ); } - 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; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/for_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/for_block.ts index 6bd96c9dd53..7c3dffb971c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/for_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/for_block.ts @@ -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) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/host.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/host.ts index 55857954c5a..1628f40c609 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/host.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/host.ts @@ -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); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/if_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/if_block.ts index da5ae476d38..f03b837976c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/if_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/if_block.ts @@ -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; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/inputs.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/inputs.ts index 6b3f6a268bc..4852be4ed6f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/inputs.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/inputs.ts @@ -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(); @@ -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` 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); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/intersection_observer.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/intersection_observer.ts index 658c5650118..e555ab6ec9d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/intersection_observer.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/intersection_observer.ts @@ -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; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/let.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/let.ts index 7cc9ef8b2ca..255bc3e8f40 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/let.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/let.ts @@ -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; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/references.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/references.ts index 63a7c699f4c..945aeb772ed 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/references.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/references.ts @@ -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 node simply require a value of type // `TemplateRef`. To get this, an expression of the form // `(_t1 as any as TemplateRef)` 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()})`); } - 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; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/schema.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/schema.ts index 9ba2a326dc3..14a89784522 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/schema.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/schema.ts @@ -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; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts index 4844b946069..0c38aefd97f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts @@ -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(); + private varMap = new Map(); /** * 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([ - ['$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([ + ['$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; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/selectorless.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/selectorless.ts index 3e152cd5288..7614f5b9061 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/selectorless.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/selectorless.ts @@ -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); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts index 0fabdb6ef77..f046afa2699 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts @@ -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') { // `