diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index 2303fdfe6ad..cebaeedef0b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -39,7 +39,7 @@ import { Unary, VoidExpression, } from '@angular/compiler'; -import {TcbExpr} from './ops/codegen'; +import {quoteAndEscape, TcbExpr} from './ops/codegen'; import {TypeCheckingConfig} from '../api'; /** @@ -165,7 +165,8 @@ class TcbExprTranslator implements AstVisitor { const value = this.translate(ast.values[idx]); if (key.kind === 'property') { - const keyNode = new TcbExpr(`"${key.key}"`).addParseSpanInfo(key.sourceSpan); + const keyNode = new TcbExpr(quoteAndEscape(key.key)); + keyNode.addParseSpanInfo(key.sourceSpan); return `${keyNode.print()}: ${value.print()}`; } else { return `...${value.print()}`; @@ -185,7 +186,7 @@ class TcbExprTranslator implements AstVisitor { } else if (ast.value === null) { node = new TcbExpr('null'); } else if (typeof ast.value === 'string') { - node = new TcbExpr(JSON.stringify(ast.value)); + node = new TcbExpr(quoteAndEscape(ast.value)); } else if (typeof ast.value === 'number') { if (Number.isNaN(ast.value)) { node = new TcbExpr('NaN'); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/codegen.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/codegen.ts index ede7168f762..91f1baba836 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/codegen.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/codegen.ts @@ -129,6 +129,13 @@ export function getStatementsBlock(expressions: TcbExpr[], singleLine = false): return result; } +/** Wraps a string value in quotes and escapes relevant characters. */ +export function quoteAndEscape(value: string): string { + // Passing the value through `JSON.stringify` automatically + // escapes quotes and allows us to handle line breaks. + return JSON.stringify(value); +} + let tempPrinter: ts.Printer | null = 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 556a6f1d5aa..f699fd902df 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 @@ -8,7 +8,7 @@ import {DirectiveOwner, ParseSourceSpan, TmplAstHostElement} from '@angular/compiler'; import {TcbOp} from './base'; -import {TcbExpr} from './codegen'; +import {quoteAndEscape, TcbExpr} from './codegen'; import {Context} from './context'; import type {Scope} from './scope'; import {TypeCheckableDirectiveMeta} from '../../api'; @@ -171,7 +171,7 @@ function tcbCallTypeCtor( // 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 propertyName = quoteAndEscape(input.field); const isLast = i === inputs.length - 1; if (input.type === 'binding') { @@ -182,13 +182,13 @@ function tcbCallTypeCtor( expr = unwrapWritableSignal(expr, tcb); } - const assignment = new TcbExpr(`"${propertyName}": ${expr.wrapForTypeChecker().print()}`); + 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. - literal += `"${propertyName}": 0 as any`; + literal += `${propertyName}: 0 as any`; } literal += `${isLast ? '' : ','} `; 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 208d6d11b95..6d1385169e1 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/events.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/events.ts @@ -17,7 +17,7 @@ import { TmplAstElement, } from '@angular/compiler'; import {TcbOp} from './base'; -import {getStatementsBlock, TcbExpr} from './codegen'; +import {quoteAndEscape, getStatementsBlock, TcbExpr} from './codegen'; import type {Context} from './context'; import type {Scope} from './scope'; import {TypeCheckableDirectiveMeta} from '../../api'; @@ -94,9 +94,8 @@ export class TcbDirectiveOutputsOp extends TcbOp { if (dirId === null) { dirId = this.scope.resolve(this.node, this.dir); } - const outputField = new TcbExpr(`${dirId.print()}["${field}"]`).addParseSpanInfo( - output.keySpan, - ); + const outputField = new TcbExpr(`${dirId.print()}[${quoteAndEscape(field)}]`); + outputField.addParseSpanInfo(output.keySpan); if (this.tcb.env.config.checkTypeOfOutputEvents) { // For strict checking of directive events, generate a call to the `subscribe` method @@ -239,7 +238,9 @@ export class TcbUnclaimedOutputsOp extends TcbOp { EventParamType.Infer, domEventAssertion, ); - const call = new TcbExpr(`${propertyAccess.print()}("${output.name}", ${handler.print()})`); + const call = new TcbExpr( + `${propertyAccess.print()}(${quoteAndEscape(output.name)}, ${handler.print()})`, + ); call.addParseSpanInfo(output.sourceSpan); this.scope.addStatement(call); } else { 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 4852be4ed6f..0e23dc9a914 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/inputs.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/inputs.ts @@ -22,7 +22,7 @@ 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 {declareVariable, quoteAndEscape, TcbExpr, tempPrint} from './codegen'; import {BindingPropertyName, ClassPropertyName} from '../../../metadata'; import {REGISTRY} from '../dom'; import {tcbExpression, unwrapWritableSignal} from './expression'; @@ -41,7 +41,7 @@ import {LocalSymbol} from './references'; 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 new TcbExpr(`"${value}"`); + return new TcbExpr(quoteAndEscape(value)); } else { // Produce an expression representing the value of the binding. return tcbExpression(value, tcb, scope); @@ -171,7 +171,7 @@ export class TcbDirectiveInputsOp extends TcbOp { `Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`, ); } - const type = new TcbExpr(`(typeof ${dirId.print()})["${fieldName}"]`); + const type = new TcbExpr(`(typeof ${dirId.print()})[${quoteAndEscape(fieldName)}]`); const temp = declareVariable(id, type); this.scope.addStatement(temp); target = id; @@ -184,7 +184,7 @@ 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) - ? new TcbExpr(`${dirId.print()}["${fieldName}"]`) + ? new TcbExpr(`${dirId.print()}[${quoteAndEscape(fieldName)}]`) : new TcbExpr(`${dirId.print()}.${fieldName}`); } @@ -305,7 +305,7 @@ export class TcbUnclaimedInputsOp extends TcbOp { // A direct binding to a property. const propertyName = REGISTRY.getMappedPropName(binding.name); const stmt = new TcbExpr( - `${elId.print()}["${propertyName}"] = ${expr.wrapForTypeChecker().print()}`, + `${elId.print()}[${quoteAndEscape(propertyName)}] = ${expr.wrapForTypeChecker().print()}`, ).addParseSpanInfo(binding.sourceSpan); this.scope.addStatement(stmt); } else { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts index 56806867b6b..d554ce21306 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts @@ -14,7 +14,7 @@ import {TypeCtorMetadata} from '../api'; import {ReferenceEmitEnvironment} from './reference_emit_environment'; import {checkIfGenericTypeBoundsCanBeEmitted} from './tcb_util'; -import {TcbExpr, tempPrint} from './ops/codegen'; +import {quoteAndEscape, TcbExpr, tempPrint} from './ops/codegen'; export function generateTypeCtorDeclarationFn( env: ReferenceEmitEnvironment, @@ -134,9 +134,9 @@ function constructTypeCtorParameter( for (const {classPropertyName, transform, isSignal} of meta.fields.inputs) { if (isSignal) { - signalInputKeys.push(`"${classPropertyName}"`); + signalInputKeys.push(quoteAndEscape(classPropertyName)); } else if (!meta.coercedInputFields.has(classPropertyName)) { - plainKeys.push(`"${classPropertyName}"`); + plainKeys.push(quoteAndEscape(classPropertyName)); } else { const coercionType = transform != null