/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/compiler/src/output/output_ast.ts
2 119 строк
61 KB
Matthieu Riegler
feat(compiler): Angular expressions with optional chaining returns `undefined`
29 апр 2026, 01:26
Не верифицирован
29 апр 2026, 01:26
2896c93
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * 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 {computeMsgId} from '../i18n/digest'; import {Message} from '../i18n/i18n_ast'; import {ParseSourceSpan} from '../parse_util'; import type {I18nMeta} from '../render3/view/i18n/meta'; //// Types export enum TypeModifier { None = 0, Const = 1 << 0, } export abstract class Type { constructor(public modifiers: TypeModifier = TypeModifier.None) {} abstract visitType(visitor: TypeVisitor, context: any): any; hasModifier(modifier: TypeModifier): boolean { return (this.modifiers & modifier) !== 0; } } export enum BuiltinTypeName { Dynamic, Bool, String, Int, Number, Function, Inferred, None, } export class BuiltinType extends Type { constructor( public name: BuiltinTypeName, modifiers?: TypeModifier, ) { super(modifiers); } override visitType(visitor: TypeVisitor, context: any): any { return visitor.visitBuiltinType(this, context); } } export class ExpressionType extends Type { constructor( public value: Expression, modifiers?: TypeModifier, public typeParams: Type[] | null = null, ) { super(modifiers); } override visitType(visitor: TypeVisitor, context: any): any { return visitor.visitExpressionType(this, context); } } export class ArrayType extends Type { constructor( public of: Type, modifiers?: TypeModifier, ) { super(modifiers); } override visitType(visitor: TypeVisitor, context: any): any { return visitor.visitArrayType(this, context); } } export class MapType extends Type { public valueType: Type | null; constructor(valueType: Type | null | undefined, modifiers?: TypeModifier) { super(modifiers); this.valueType = valueType || null; } override visitType(visitor: TypeVisitor, context: any): any { return visitor.visitMapType(this, context); } } export class TransplantedType<T> extends Type { constructor( readonly type: T, modifiers?: TypeModifier, ) { super(modifiers); } override visitType(visitor: TypeVisitor, context: any): any { return visitor.visitTransplantedType(this, context); } } export const DYNAMIC_TYPE = new BuiltinType(BuiltinTypeName.Dynamic); export const INFERRED_TYPE = new BuiltinType(BuiltinTypeName.Inferred); export const BOOL_TYPE = new BuiltinType(BuiltinTypeName.Bool); export const INT_TYPE = new BuiltinType(BuiltinTypeName.Int); export const NUMBER_TYPE = new BuiltinType(BuiltinTypeName.Number); export const STRING_TYPE = new BuiltinType(BuiltinTypeName.String); export const FUNCTION_TYPE = new BuiltinType(BuiltinTypeName.Function); export const NONE_TYPE = new BuiltinType(BuiltinTypeName.None); export interface TypeVisitor { visitBuiltinType(type: BuiltinType, context: any): any; visitExpressionType(type: ExpressionType, context: any): any; visitArrayType(type: ArrayType, context: any): any; visitMapType(type: MapType, context: any): any; visitTransplantedType(type: TransplantedType<unknown>, context: any): any; } ///// Expressions export enum UnaryOperator { Minus, Plus, } export enum BinaryOperator { Equals, NotEquals, Assign, Identical, NotIdentical, Minus, Plus, Divide, Multiply, Modulo, And, Or, BitwiseOr, BitwiseAnd, Lower, LowerEquals, Bigger, BiggerEquals, NullishCoalesce, Exponentiation, In, InstanceOf, AdditionAssignment, SubtractionAssignment, MultiplicationAssignment, DivisionAssignment, RemainderAssignment, ExponentiationAssignment, AndAssignment, OrAssignment, NullishCoalesceAssignment, } export function nullSafeIsEquivalent<T extends {isEquivalent(other: T): boolean}>( base: T | null, other: T | null, ) { if (base == null || other == null) { return base == other; } return base.isEquivalent(other); } function areAllEquivalentPredicate<T>( base: T[], other: T[], equivalentPredicate: (baseElement: T, otherElement: T) => boolean, ) { const len = base.length; if (len !== other.length) { return false; } for (let i = 0; i < len; i++) { if (!equivalentPredicate(base[i], other[i])) { return false; } } return true; } export function areAllEquivalent<T extends {isEquivalent(other: T): boolean}>( base: T[], other: T[], ) { return areAllEquivalentPredicate(base, other, (baseElement: T, otherElement: T) => baseElement.isEquivalent(otherElement), ); } export abstract class Expression { public type: Type | null; public sourceSpan: ParseSourceSpan | null; constructor( type: Type | null | undefined, sourceSpan?: ParseSourceSpan | null, public leadingComments?: LeadingComment[], ) { this.type = type || null; this.sourceSpan = sourceSpan || null; } abstract visitExpression(visitor: ExpressionVisitor, context: any): any; /** * Calculates whether this expression produces the same value as the given expression. * Note: We don't check Types nor ParseSourceSpans nor function arguments. */ abstract isEquivalent(e: Expression): boolean; /** * Return true if the expression is constant. */ abstract isConstant(): boolean; abstract clone(): Expression; prop(name: string, sourceSpan?: ParseSourceSpan | null): ReadPropExpr { return new ReadPropExpr(this, name, null, sourceSpan); } key(index: Expression, type?: Type | null, sourceSpan?: ParseSourceSpan | null): ReadKeyExpr { return new ReadKeyExpr(this, index, type, sourceSpan); } callFn( params: Expression[], sourceSpan?: ParseSourceSpan | null, pure?: boolean, leadingComments?: LeadingComment[], ): InvokeFunctionExpr { return new InvokeFunctionExpr(this, params, null, sourceSpan, pure, leadingComments); } instantiate( params: Expression[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ): InstantiateExpr { return new InstantiateExpr(this, params, type, sourceSpan); } conditional( trueCase: Expression, falseCase: Expression | null = null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ): ConditionalExpr { return new ConditionalExpr(this, trueCase, falseCase, null, sourceSpan); } equals(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Equals, this, rhs, null, sourceSpan); } notEquals(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.NotEquals, this, rhs, null, sourceSpan); } identical(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Identical, this, rhs, null, sourceSpan); } notIdentical(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.NotIdentical, this, rhs, null, sourceSpan); } minus(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Minus, this, rhs, null, sourceSpan); } plus(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Plus, this, rhs, null, sourceSpan); } divide(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Divide, this, rhs, null, sourceSpan); } multiply(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Multiply, this, rhs, null, sourceSpan); } modulo(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Modulo, this, rhs, null, sourceSpan); } power(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Exponentiation, this, rhs, null, sourceSpan); } and(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.And, this, rhs, null, sourceSpan); } bitwiseOr(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.BitwiseOr, this, rhs, null, sourceSpan); } bitwiseAnd(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.BitwiseAnd, this, rhs, null, sourceSpan); } or(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Or, this, rhs, null, sourceSpan); } lower(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Lower, this, rhs, null, sourceSpan); } lowerEquals(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.LowerEquals, this, rhs, null, sourceSpan); } bigger(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Bigger, this, rhs, null, sourceSpan); } biggerEquals(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.BiggerEquals, this, rhs, null, sourceSpan); } isBlank(sourceSpan?: ParseSourceSpan | null): Expression { // Note: We use equals by purpose here to compare to null and undefined in JS. // We use the typed null to allow strictNullChecks to narrow types. return this.equals(TYPED_NULL_EXPR, sourceSpan); } nullishCoalesce(rhs: Expression, sourceSpan?: ParseSourceSpan | null): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.NullishCoalesce, this, rhs, null, sourceSpan); } toStmt(leadingComments?: LeadingComment[]): Statement { return new ExpressionStatement(this, null, leadingComments); } } export class ReadVarExpr extends Expression { constructor( public name: string, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return e instanceof ReadVarExpr && this.name === e.name; } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadVarExpr(this, context); } override clone(): ReadVarExpr { return new ReadVarExpr(this.name, this.type, this.sourceSpan); } set(value: Expression): BinaryOperatorExpr { return new BinaryOperatorExpr(BinaryOperator.Assign, this, value, null, this.sourceSpan); } } export class TypeofExpr extends Expression { constructor( public expr: Expression, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override visitExpression(visitor: ExpressionVisitor, context: any) { return visitor.visitTypeofExpr(this, context); } override isEquivalent(e: Expression): boolean { return e instanceof TypeofExpr && e.expr.isEquivalent(this.expr); } override isConstant(): boolean { return this.expr.isConstant(); } override clone(): TypeofExpr { return new TypeofExpr(this.expr.clone()); } } export class VoidExpr extends Expression { constructor( public expr: Expression, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override visitExpression(visitor: ExpressionVisitor, context: any) { return visitor.visitVoidExpr(this, context); } override isEquivalent(e: Expression): boolean { return e instanceof VoidExpr && e.expr.isEquivalent(this.expr); } override isConstant(): boolean { return this.expr.isConstant(); } override clone(): VoidExpr { return new VoidExpr(this.expr.clone()); } } export class WrappedNodeExpr<T> extends Expression { constructor( public node: T, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return e instanceof WrappedNodeExpr && this.node === e.node; } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitWrappedNodeExpr(this, context); } override clone(): WrappedNodeExpr<T> { return new WrappedNodeExpr(this.node, this.type, this.sourceSpan); } } export class InvokeFunctionExpr extends Expression { constructor( public fn: Expression, public args: Expression[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, public pure = false, leadingComments?: LeadingComment[], public isOptional = false, ) { super(type, sourceSpan, leadingComments); } // An alias for fn, which allows other logic to handle calls and property reads together. get receiver(): Expression { return this.fn; } override isEquivalent(e: Expression): boolean { return ( e instanceof InvokeFunctionExpr && this.fn.isEquivalent(e.fn) && areAllEquivalent(this.args, e.args) && this.pure === e.pure ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitInvokeFunctionExpr(this, context); } override clone(): InvokeFunctionExpr { return new InvokeFunctionExpr( this.fn.clone(), this.args.map((arg) => arg.clone()), this.type, this.sourceSpan, this.pure, [], this.isOptional, ); } } export class TaggedTemplateLiteralExpr extends Expression { constructor( public tag: Expression, public template: TemplateLiteralExpr, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return ( e instanceof TaggedTemplateLiteralExpr && this.tag.isEquivalent(e.tag) && this.template.isEquivalent(e.template) ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitTaggedTemplateLiteralExpr(this, context); } override clone(): TaggedTemplateLiteralExpr { return new TaggedTemplateLiteralExpr( this.tag.clone(), this.template.clone(), this.type, this.sourceSpan, ); } } export class InstantiateExpr extends Expression { constructor( public classExpr: Expression, public args: Expression[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return ( e instanceof InstantiateExpr && this.classExpr.isEquivalent(e.classExpr) && areAllEquivalent(this.args, e.args) ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitInstantiateExpr(this, context); } override clone(): InstantiateExpr { return new InstantiateExpr( this.classExpr.clone(), this.args.map((arg) => arg.clone()), this.type, this.sourceSpan, ); } } export class RegularExpressionLiteralExpr extends Expression { constructor( public body: string, public flags: string | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(null, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return ( e instanceof RegularExpressionLiteralExpr && this.body === e.body && this.flags === e.flags ); } override isConstant() { return true; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitRegularExpressionLiteral(this, context); } override clone(): RegularExpressionLiteralExpr { return new RegularExpressionLiteralExpr(this.body, this.flags, this.sourceSpan); } } export class LiteralExpr extends Expression { constructor( public value: number | string | boolean | null | undefined, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return e instanceof LiteralExpr && this.value === e.value; } override isConstant() { return true; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLiteralExpr(this, context); } override clone(): LiteralExpr { return new LiteralExpr(this.value, this.type, this.sourceSpan); } } export class TemplateLiteralExpr extends Expression { constructor( public elements: TemplateLiteralElementExpr[], public expressions: Expression[], sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(null, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return ( e instanceof TemplateLiteralExpr && areAllEquivalentPredicate(this.elements, e.elements, (a, b) => a.text === b.text) && areAllEquivalent(this.expressions, e.expressions) ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitTemplateLiteralExpr(this, context); } override clone(): TemplateLiteralExpr { return new TemplateLiteralExpr( this.elements.map((el) => el.clone()), this.expressions.map((expr) => expr.clone()), ); } } export class TemplateLiteralElementExpr extends Expression { readonly rawText: string; constructor( readonly text: string, sourceSpan?: ParseSourceSpan | null, rawText?: string, leadingComments?: LeadingComment[], ) { super(STRING_TYPE, sourceSpan, leadingComments); // If `rawText` is not provided, "fake" the raw string by escaping the following sequences: // - "\" would otherwise indicate that the next character is a control character. // - "`" and "${" are template string control sequences that would otherwise prematurely // indicate the end of the template literal element. // Note that we can't rely on the `sourceSpan` here, because it may be incorrect (see // https://github.com/angular/angular/pull/60267#discussion_r1986402524). this.rawText = rawText ?? escapeForTemplateLiteral(escapeSlashes(text)); } override visitExpression(visitor: ExpressionVisitor, context: any) { return visitor.visitTemplateLiteralElementExpr(this, context); } override isEquivalent(e: Expression): boolean { return ( e instanceof TemplateLiteralElementExpr && e.text === this.text && e.rawText === this.rawText ); } override isConstant(): boolean { return true; } override clone(): TemplateLiteralElementExpr { return new TemplateLiteralElementExpr(this.text, this.sourceSpan, this.rawText); } } export class LiteralPiece { constructor( public text: string, public sourceSpan: ParseSourceSpan, ) {} } export class PlaceholderPiece { /** * Create a new instance of a `PlaceholderPiece`. * * @param text the name of this placeholder (e.g. `PH_1`). * @param sourceSpan the location of this placeholder in its localized message the source code. * @param associatedMessage reference to another message that this placeholder is associated with. * The `associatedMessage` is mainly used to provide a relationship to an ICU message that has * been extracted out from the message containing the placeholder. */ constructor( public text: string, public sourceSpan: ParseSourceSpan, public associatedMessage?: Message, ) {} } export type MessagePiece = LiteralPiece | PlaceholderPiece; const MEANING_SEPARATOR = '|'; const ID_SEPARATOR = '@@'; const LEGACY_ID_INDICATOR = '␟'; export class LocalizedString extends Expression { constructor( readonly metaBlock: I18nMeta, readonly messageParts: LiteralPiece[], readonly placeHolderNames: PlaceholderPiece[], readonly expressions: Expression[], sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(STRING_TYPE, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { // return e instanceof LocalizedString && this.message === e.message; return false; } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLocalizedString(this, context); } override clone(): LocalizedString { return new LocalizedString( this.metaBlock, this.messageParts, this.placeHolderNames, this.expressions.map((expr) => expr.clone()), this.sourceSpan, ); } /** * Serialize the given `meta` and `messagePart` into "cooked" and "raw" strings that can be used * in a `$localize` tagged string. The format of the metadata is the same as that parsed by * `parseI18nMeta()`. * * @param meta The metadata to serialize * @param messagePart The first part of the tagged string */ serializeI18nHead(): CookedRawString { let metaBlock = this.metaBlock.description || ''; if (this.metaBlock.meaning) { metaBlock = `${this.metaBlock.meaning}${MEANING_SEPARATOR}${metaBlock}`; } if (this.metaBlock.customId) { metaBlock = `${metaBlock}${ID_SEPARATOR}${this.metaBlock.customId}`; } if (this.metaBlock.legacyIds) { this.metaBlock.legacyIds.forEach((legacyId) => { metaBlock = `${metaBlock}${LEGACY_ID_INDICATOR}${legacyId}`; }); } return createCookedRawString( metaBlock, this.messageParts[0].text, this.getMessagePartSourceSpan(0), ); } getMessagePartSourceSpan(i: number): ParseSourceSpan | null { return this.messageParts[i]?.sourceSpan ?? this.sourceSpan; } getPlaceholderSourceSpan(i: number): ParseSourceSpan { return ( this.placeHolderNames[i]?.sourceSpan ?? this.expressions[i]?.sourceSpan ?? this.sourceSpan ); } /** * Serialize the given `placeholderName` and `messagePart` into "cooked" and "raw" strings that * can be used in a `$localize` tagged string. * * The format is `:<placeholder-name>[@@<associated-id>]:`. * * The `associated-id` is the message id of the (usually an ICU) message to which this placeholder * refers. * * @param partIndex The index of the message part to serialize. */ serializeI18nTemplatePart(partIndex: number): CookedRawString { const placeholder = this.placeHolderNames[partIndex - 1]; const messagePart = this.messageParts[partIndex]; let metaBlock = placeholder.text; if (placeholder.associatedMessage?.legacyIds.length === 0) { metaBlock += `${ID_SEPARATOR}${computeMsgId( placeholder.associatedMessage.messageString, placeholder.associatedMessage.meaning, )}`; } return createCookedRawString( metaBlock, messagePart.text, this.getMessagePartSourceSpan(partIndex), ); } } /** * A structure to hold the cooked and raw strings of a template literal element, along with its * source-span range. */ export interface CookedRawString { cooked: string; raw: string; range: ParseSourceSpan | null; } const escapeSlashes = (str: string): string => str.replace(/\\/g, '\\\\'); const escapeStartingColon = (str: string): string => str.replace(/^:/, '\\:'); const escapeColons = (str: string): string => str.replace(/:/g, '\\:'); const escapeForTemplateLiteral = (str: string): string => str.replace(/`/g, '\\`').replace(/\${/g, '$\\{'); /** * Creates a `{cooked, raw}` object from the `metaBlock` and `messagePart`. * * The `raw` text must have various character sequences escaped: * * "\" would otherwise indicate that the next character is a control character. * * "`" and "${" are template string control sequences that would otherwise prematurely indicate * the end of a message part. * * ":" inside a metablock would prematurely indicate the end of the metablock. * * ":" at the start of a messagePart with no metablock would erroneously indicate the start of a * metablock. * * @param metaBlock Any metadata that should be prepended to the string * @param messagePart The message part of the string */ function createCookedRawString( metaBlock: string, messagePart: string, range: ParseSourceSpan | null, ): CookedRawString { if (metaBlock === '') { return { cooked: messagePart, raw: escapeForTemplateLiteral(escapeStartingColon(escapeSlashes(messagePart))), range, }; } else { return { cooked: `:${metaBlock}:${messagePart}`, raw: escapeForTemplateLiteral( `:${escapeColons(escapeSlashes(metaBlock))}:${escapeSlashes(messagePart)}`, ), range, }; } } export class ExternalExpr extends Expression { constructor( public value: ExternalReference, type?: Type | null, public typeParams: Type[] | null = null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return ( e instanceof ExternalExpr && this.value.name === e.value.name && this.value.moduleName === e.value.moduleName ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitExternalExpr(this, context); } override clone(): ExternalExpr { return new ExternalExpr(this.value, this.type, this.typeParams, this.sourceSpan); } } export class ExternalReference { constructor( public moduleName: string | null, public name: string | null, ) {} // Note: no isEquivalent method here as we use this as an interface too. } export class ConditionalExpr extends Expression { public trueCase: Expression; constructor( public condition: Expression, trueCase: Expression, public falseCase: Expression | null = null, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type || trueCase.type, sourceSpan, leadingComments); this.trueCase = trueCase; } override isEquivalent(e: Expression): boolean { return ( e instanceof ConditionalExpr && this.condition.isEquivalent(e.condition) && this.trueCase.isEquivalent(e.trueCase) && nullSafeIsEquivalent(this.falseCase, e.falseCase) ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitConditionalExpr(this, context); } override clone(): ConditionalExpr { return new ConditionalExpr( this.condition.clone(), this.trueCase.clone(), this.falseCase?.clone(), this.type, this.sourceSpan, ); } } export class DynamicImportExpr extends Expression { constructor( public url: string | Expression, sourceSpan?: ParseSourceSpan | null, public urlComment?: string, leadingComments?: LeadingComment[], ) { super(null, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return e instanceof DynamicImportExpr && this.url === e.url && this.urlComment === e.urlComment; } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitDynamicImportExpr(this, context); } override clone(): DynamicImportExpr { return new DynamicImportExpr( typeof this.url === 'string' ? this.url : this.url.clone(), this.sourceSpan, this.urlComment, ); } } export class NotExpr extends Expression { constructor( public condition: Expression, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(BOOL_TYPE, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return e instanceof NotExpr && this.condition.isEquivalent(e.condition); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitNotExpr(this, context); } override clone(): NotExpr { return new NotExpr(this.condition.clone(), this.sourceSpan); } } export class FnParam { constructor( public name: string, public type: Type | null = null, ) {} isEquivalent(param: FnParam): boolean { return this.name === param.name; } clone(): FnParam { return new FnParam(this.name, this.type); } } export class FunctionExpr extends Expression { constructor( public params: FnParam[], public statements: Statement[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, public name?: string | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression | Statement): boolean { return ( (e instanceof FunctionExpr || e instanceof DeclareFunctionStmt) && areAllEquivalent(this.params, e.params) && areAllEquivalent(this.statements, e.statements) ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitFunctionExpr(this, context); } toDeclStmt(name: string, modifiers?: StmtModifier): DeclareFunctionStmt { return new DeclareFunctionStmt( name, this.params, this.statements, this.type, modifiers, this.sourceSpan, ); } override clone(): FunctionExpr { // TODO: Should we deep clone statements? return new FunctionExpr( this.params.map((p) => p.clone()), this.statements, this.type, this.sourceSpan, this.name, ); } } export class ArrowFunctionExpr extends Expression { // Note that `body: Expression` represents `() => expr` whereas // `body: Statement[]` represents `() => { expr }`. constructor( public params: FnParam[], public body: Expression | Statement[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { if (!(e instanceof ArrowFunctionExpr) || !areAllEquivalent(this.params, e.params)) { return false; } if (this.body instanceof Expression && e.body instanceof Expression) { return this.body.isEquivalent(e.body); } if (Array.isArray(this.body) && Array.isArray(e.body)) { return areAllEquivalent(this.body, e.body); } return false; } override isConstant(): boolean { return false; } override visitExpression(visitor: ExpressionVisitor, context: any) { return visitor.visitArrowFunctionExpr(this, context); } override clone(): Expression { // TODO: Should we deep clone statements? return new ArrowFunctionExpr( this.params.map((p) => p.clone()), Array.isArray(this.body) ? this.body : this.body.clone(), this.type, this.sourceSpan, ); } toDeclStmt(name: string, modifiers?: StmtModifier): DeclareVarStmt { return new DeclareVarStmt(name, this, INFERRED_TYPE, modifiers, this.sourceSpan); } } export class UnaryOperatorExpr extends Expression { constructor( public operator: UnaryOperator, public expr: Expression, type?: Type | null, sourceSpan?: ParseSourceSpan | null, public parens: boolean = true, leadingComments?: LeadingComment[], ) { super(type || NUMBER_TYPE, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return ( e instanceof UnaryOperatorExpr && this.operator === e.operator && this.expr.isEquivalent(e.expr) ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitUnaryOperatorExpr(this, context); } override clone(): UnaryOperatorExpr { return new UnaryOperatorExpr( this.operator, this.expr.clone(), this.type, this.sourceSpan, this.parens, ); } } export class ParenthesizedExpr extends Expression { constructor( public expr: Expression, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); } override visitExpression(visitor: ExpressionVisitor, context: any) { return visitor.visitParenthesizedExpr(this, context); } override isEquivalent(e: Expression): boolean { // TODO: should this ignore paren depth? i.e. is `(1)` equivalent to `1`? return e instanceof ParenthesizedExpr && e.expr.isEquivalent(this.expr); } override isConstant(): boolean { return this.expr.isConstant(); } override clone(): ParenthesizedExpr { return new ParenthesizedExpr(this.expr.clone()); } } export class BinaryOperatorExpr extends Expression { public lhs: Expression; constructor( public operator: BinaryOperator, lhs: Expression, public rhs: Expression, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type || lhs.type, sourceSpan, leadingComments); this.lhs = lhs; } override isEquivalent(e: Expression): boolean { return ( e instanceof BinaryOperatorExpr && this.operator === e.operator && this.lhs.isEquivalent(e.lhs) && this.rhs.isEquivalent(e.rhs) ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitBinaryOperatorExpr(this, context); } override clone(): BinaryOperatorExpr { return new BinaryOperatorExpr( this.operator, this.lhs.clone(), this.rhs.clone(), this.type, this.sourceSpan, ); } isAssignment(): boolean { const op = this.operator; return ( op === BinaryOperator.Assign || op === BinaryOperator.AdditionAssignment || op === BinaryOperator.SubtractionAssignment || op === BinaryOperator.MultiplicationAssignment || op === BinaryOperator.DivisionAssignment || op === BinaryOperator.RemainderAssignment || op === BinaryOperator.ExponentiationAssignment || op === BinaryOperator.AndAssignment || op === BinaryOperator.OrAssignment || op === BinaryOperator.NullishCoalesceAssignment ); } } export class ReadPropExpr extends Expression { constructor( public receiver: Expression, public name: string, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], /** * Whether the property access uses the optional-chaining operator (`?.`). */ public isOptional = false, ) { super(type, sourceSpan, leadingComments); } // An alias for name, which allows other logic to handle property reads and keyed reads together. get index() { return this.name; } override isEquivalent(e: Expression): boolean { return ( e instanceof ReadPropExpr && this.receiver.isEquivalent(e.receiver) && this.name === e.name && this.isOptional === e.isOptional ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadPropExpr(this, context); } set(value: Expression): BinaryOperatorExpr { return new BinaryOperatorExpr( BinaryOperator.Assign, this.receiver.prop(this.name), value, null, this.sourceSpan, ); } override clone(): ReadPropExpr { return new ReadPropExpr( this.receiver.clone(), this.name, this.type, this.sourceSpan, [], this.isOptional, ); } } export class ReadKeyExpr extends Expression { constructor( public receiver: Expression, public index: Expression, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], /** * Whether the property access uses the optional-chaining operator (`?.[`). */ public isOptional = false, ) { super(type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return ( e instanceof ReadKeyExpr && this.receiver.isEquivalent(e.receiver) && this.index.isEquivalent(e.index) && this.isOptional === e.isOptional ); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadKeyExpr(this, context); } set(value: Expression): BinaryOperatorExpr { return new BinaryOperatorExpr( BinaryOperator.Assign, this.receiver.key(this.index), value, null, this.sourceSpan, ); } override clone(): ReadKeyExpr { return new ReadKeyExpr( this.receiver.clone(), this.index.clone(), this.type, this.sourceSpan, [], this.isOptional, ); } } export class LiteralArrayExpr extends Expression { public entries: Expression[]; constructor( entries: Expression[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); this.entries = entries; } override isConstant() { return this.entries.every((e) => e.isConstant()); } override isEquivalent(e: Expression): boolean { return e instanceof LiteralArrayExpr && areAllEquivalent(this.entries, e.entries); } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLiteralArrayExpr(this, context); } override clone(): LiteralArrayExpr { return new LiteralArrayExpr( this.entries.map((e) => e.clone()), this.type, this.sourceSpan, ); } } export class LiteralMapPropertyAssignment { constructor( public key: string, public value: Expression, public quoted: boolean, ) {} isEquivalent(e: LiteralMapPropertyAssignment): boolean { return this.key === e.key && this.value.isEquivalent(e.value); } clone(): LiteralMapPropertyAssignment { return new LiteralMapPropertyAssignment(this.key, this.value.clone(), this.quoted); } isConstant() { return this.value.isConstant(); } } export class LiteralMapSpreadAssignment { constructor(public expression: Expression) {} isEquivalent(e: LiteralMapSpreadAssignment): boolean { return e instanceof LiteralMapSpreadAssignment && this.expression.isEquivalent(e.expression); } clone(): LiteralMapSpreadAssignment { return new LiteralMapSpreadAssignment(this.expression.clone()); } isConstant() { return this.expression.isConstant(); } } export type LiteralMapEntry = LiteralMapPropertyAssignment | LiteralMapSpreadAssignment; export class LiteralMapExpr extends Expression { public valueType: Type | null = null; constructor( public entries: LiteralMapEntry[], type?: MapType | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(type, sourceSpan, leadingComments); if (type) { this.valueType = type.valueType; } } override isEquivalent(e: Expression): boolean { return e instanceof LiteralMapExpr && areAllEquivalent(this.entries, e.entries); } override isConstant() { return this.entries.every((e) => e.isConstant()); } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLiteralMapExpr(this, context); } override clone(): LiteralMapExpr { const entriesClone = this.entries.map((entry) => entry.clone()); return new LiteralMapExpr(entriesClone, this.type as MapType | null, this.sourceSpan); } } export class CommaExpr extends Expression { constructor( public parts: Expression[], sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(parts[parts.length - 1].type, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return e instanceof CommaExpr && areAllEquivalent(this.parts, e.parts); } override isConstant() { return false; } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitCommaExpr(this, context); } override clone(): CommaExpr { return new CommaExpr(this.parts.map((p) => p.clone())); } } export class SpreadElementExpr extends Expression { constructor( public expression: Expression, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(null, sourceSpan, leadingComments); } override isEquivalent(e: Expression): boolean { return e instanceof SpreadElementExpr && this.expression.isEquivalent(e.expression); } override isConstant() { return this.expression.isConstant(); } override visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitSpreadElementExpr(this, context); } override clone(): SpreadElementExpr { return new SpreadElementExpr(this.expression.clone(), this.sourceSpan); } } export interface ExpressionVisitor { visitReadVarExpr(ast: ReadVarExpr, context: any): any; visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any; visitTaggedTemplateLiteralExpr(ast: TaggedTemplateLiteralExpr, context: any): any; visitTemplateLiteralExpr(ast: TemplateLiteralExpr, context: any): any; visitTemplateLiteralElementExpr(ast: TemplateLiteralElementExpr, context: any): any; visitInstantiateExpr(ast: InstantiateExpr, context: any): any; visitLiteralExpr(ast: LiteralExpr, context: any): any; visitLocalizedString(ast: LocalizedString, context: any): any; visitExternalExpr(ast: ExternalExpr, context: any): any; visitConditionalExpr(ast: ConditionalExpr, context: any): any; visitDynamicImportExpr(ast: DynamicImportExpr, context: any): any; visitNotExpr(ast: NotExpr, context: any): any; visitFunctionExpr(ast: FunctionExpr, context: any): any; visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: any): any; visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any; visitReadPropExpr(ast: ReadPropExpr, context: any): any; visitReadKeyExpr(ast: ReadKeyExpr, context: any): any; visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any; visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any; visitCommaExpr(ast: CommaExpr, context: any): any; visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any): any; visitTypeofExpr(ast: TypeofExpr, context: any): any; visitVoidExpr(ast: VoidExpr, context: any): any; visitArrowFunctionExpr(ast: ArrowFunctionExpr, context: any): any; visitParenthesizedExpr(ast: ParenthesizedExpr, context: any): any; visitRegularExpressionLiteral(ast: RegularExpressionLiteralExpr, context: any): any; visitSpreadElementExpr(ast: SpreadElementExpr, context: any): any; } export const NULL_EXPR = new LiteralExpr(null, null, null); export const TYPED_NULL_EXPR = new LiteralExpr(null, INFERRED_TYPE, null); //// Statements export enum StmtModifier { None = 0, Final = 1 << 0, Private = 1 << 1, Exported = 1 << 2, Static = 1 << 3, } export class LeadingComment { constructor( public text: string, public multiline: boolean, public trailingNewline: boolean, ) {} toString() { return this.multiline ? ` ${this.text} ` : this.text; } } export class JSDocComment extends LeadingComment { constructor(public tags: JSDocTag[]) { super('', /* multiline */ true, /* trailingNewline */ true); } override toString(): string { return serializeTags(this.tags); } } export abstract class Statement { constructor( public modifiers: StmtModifier = StmtModifier.None, public sourceSpan: ParseSourceSpan | null = null, public leadingComments?: LeadingComment[], ) {} /** * Calculates whether this statement produces the same value as the given statement. * Note: We don't check Types nor ParseSourceSpans nor function arguments. */ abstract isEquivalent(stmt: Statement): boolean; abstract visitStatement(visitor: StatementVisitor, context: any): any; hasModifier(modifier: StmtModifier): boolean { return (this.modifiers & modifier) !== 0; } addLeadingComment(leadingComment: LeadingComment): void { this.leadingComments = this.leadingComments ?? []; this.leadingComments.push(leadingComment); } } export class DeclareVarStmt extends Statement { public type: Type | null; constructor( public name: string, public value?: Expression, type?: Type | null, modifiers?: StmtModifier, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(modifiers, sourceSpan, leadingComments); this.type = type || (value && value.type) || null; } override isEquivalent(stmt: Statement): boolean { return ( stmt instanceof DeclareVarStmt && this.name === stmt.name && (this.value ? !!stmt.value && this.value.isEquivalent(stmt.value) : !stmt.value) ); } override visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitDeclareVarStmt(this, context); } } export class DeclareFunctionStmt extends Statement { public type: Type | null; constructor( public name: string, public params: FnParam[], public statements: Statement[], type?: Type | null, modifiers?: StmtModifier, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(modifiers, sourceSpan, leadingComments); this.type = type || null; } override isEquivalent(stmt: Statement): boolean { return ( stmt instanceof DeclareFunctionStmt && areAllEquivalent(this.params, stmt.params) && areAllEquivalent(this.statements, stmt.statements) ); } override visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitDeclareFunctionStmt(this, context); } } export class ExpressionStatement extends Statement { constructor( public expr: Expression, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(StmtModifier.None, sourceSpan, leadingComments); } override isEquivalent(stmt: Statement): boolean { return stmt instanceof ExpressionStatement && this.expr.isEquivalent(stmt.expr); } override visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitExpressionStmt(this, context); } } export class ReturnStatement extends Statement { constructor( public value: Expression, sourceSpan: ParseSourceSpan | null = null, leadingComments?: LeadingComment[], ) { super(StmtModifier.None, sourceSpan, leadingComments); } override isEquivalent(stmt: Statement): boolean { return stmt instanceof ReturnStatement && this.value.isEquivalent(stmt.value); } override visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitReturnStmt(this, context); } } export class IfStmt extends Statement { constructor( public condition: Expression, public trueCase: Statement[], public falseCase: Statement[] = [], sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ) { super(StmtModifier.None, sourceSpan, leadingComments); } override isEquivalent(stmt: Statement): boolean { return ( stmt instanceof IfStmt && this.condition.isEquivalent(stmt.condition) && areAllEquivalent(this.trueCase, stmt.trueCase) && areAllEquivalent(this.falseCase, stmt.falseCase) ); } override visitStatement(visitor: StatementVisitor, context: any): any { return visitor.visitIfStmt(this, context); } } export interface StatementVisitor { visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any; visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any; visitExpressionStmt(stmt: ExpressionStatement, context: any): any; visitReturnStmt(stmt: ReturnStatement, context: any): any; visitIfStmt(stmt: IfStmt, context: any): any; } export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor { visitType(ast: Type, context: any): any { return ast; } visitExpression(ast: Expression, context: any): any { if (ast.type) { ast.type.visitType(this, context); } return ast; } visitBuiltinType(type: BuiltinType, context: any): any { return this.visitType(type, context); } visitExpressionType(type: ExpressionType, context: any): any { type.value.visitExpression(this, context); if (type.typeParams !== null) { type.typeParams.forEach((param) => this.visitType(param, context)); } return this.visitType(type, context); } visitArrayType(type: ArrayType, context: any): any { return this.visitType(type, context); } visitMapType(type: MapType, context: any): any { return this.visitType(type, context); } visitTransplantedType(type: TransplantedType<unknown>, context: any): any { return type; } visitWrappedNodeExpr(ast: WrappedNodeExpr<any>, context: any): any { return ast; } visitReadVarExpr(ast: ReadVarExpr, context: any): any { return this.visitExpression(ast, context); } visitDynamicImportExpr(ast: DynamicImportExpr, context: any) { return this.visitExpression(ast, context); } visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any { ast.fn.visitExpression(this, context); this.visitAllExpressions(ast.args, context); return this.visitExpression(ast, context); } visitTaggedTemplateLiteralExpr(ast: TaggedTemplateLiteralExpr, context: any): any { ast.tag.visitExpression(this, context); ast.template.visitExpression(this, context); return this.visitExpression(ast, context); } visitInstantiateExpr(ast: InstantiateExpr, context: any): any { ast.classExpr.visitExpression(this, context); this.visitAllExpressions(ast.args, context); return this.visitExpression(ast, context); } visitLiteralExpr(ast: LiteralExpr, context: any): any { return this.visitExpression(ast, context); } visitRegularExpressionLiteral(ast: RegularExpressionLiteralExpr, context: any): any { return this.visitExpression(ast, context); } visitLocalizedString(ast: LocalizedString, context: any): any { return this.visitExpression(ast, context); } visitExternalExpr(ast: ExternalExpr, context: any): any { if (ast.typeParams) { ast.typeParams.forEach((type) => type.visitType(this, context)); } return this.visitExpression(ast, context); } visitConditionalExpr(ast: ConditionalExpr, context: any): any { ast.condition.visitExpression(this, context); ast.trueCase.visitExpression(this, context); ast.falseCase!.visitExpression(this, context); return this.visitExpression(ast, context); } visitNotExpr(ast: NotExpr, context: any): any { ast.condition.visitExpression(this, context); return this.visitExpression(ast, context); } visitFunctionExpr(ast: FunctionExpr, context: any): any { this.visitAllStatements(ast.statements, context); return this.visitExpression(ast, context); } visitArrowFunctionExpr(ast: ArrowFunctionExpr, context: any): any { if (Array.isArray(ast.body)) { this.visitAllStatements(ast.body, context); } else { // Note: `body.visitExpression`, rather than `this.visitExpressiont(body)`, // because the latter won't recurse into the sub-expressions. ast.body.visitExpression(this, context); } return this.visitExpression(ast, context); } visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: any): any { ast.expr.visitExpression(this, context); return this.visitExpression(ast, context); } visitTypeofExpr(ast: TypeofExpr, context: any): any { ast.expr.visitExpression(this, context); return this.visitExpression(ast, context); } visitVoidExpr(ast: VoidExpr, context: any) { ast.expr.visitExpression(this, context); return this.visitExpression(ast, context); } visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any { ast.lhs.visitExpression(this, context); ast.rhs.visitExpression(this, context); return this.visitExpression(ast, context); } visitReadPropExpr(ast: ReadPropExpr, context: any): any { ast.receiver.visitExpression(this, context); return this.visitExpression(ast, context); } visitReadKeyExpr(ast: ReadKeyExpr, context: any): any { ast.receiver.visitExpression(this, context); ast.index.visitExpression(this, context); return this.visitExpression(ast, context); } visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any { this.visitAllExpressions(ast.entries, context); return this.visitExpression(ast, context); } visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any { ast.entries.forEach((entry) => { if (entry instanceof LiteralMapSpreadAssignment) { entry.expression.visitExpression(this, context); } else { entry.value.visitExpression(this, context); } }); return this.visitExpression(ast, context); } visitCommaExpr(ast: CommaExpr, context: any): any { this.visitAllExpressions(ast.parts, context); return this.visitExpression(ast, context); } visitTemplateLiteralExpr(ast: TemplateLiteralExpr, context: any) { this.visitAllExpressions(ast.elements, context); this.visitAllExpressions(ast.expressions, context); return this.visitExpression(ast, context); } visitTemplateLiteralElementExpr(ast: TemplateLiteralElementExpr, context: any) { return this.visitExpression(ast, context); } visitParenthesizedExpr(ast: ParenthesizedExpr, context: any) { ast.expr.visitExpression(this, context); return this.visitExpression(ast, context); } visitSpreadElementExpr(ast: SpreadElementExpr, context: any): any { ast.expression.visitExpression(this, context); return this.visitExpression(ast, context); } visitAllExpressions(exprs: Expression[], context: any): void { exprs.forEach((expr) => expr.visitExpression(this, context)); } visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any { if (stmt.value) { stmt.value.visitExpression(this, context); } if (stmt.type) { stmt.type.visitType(this, context); } return stmt; } visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any { this.visitAllStatements(stmt.statements, context); if (stmt.type) { stmt.type.visitType(this, context); } return stmt; } visitExpressionStmt(stmt: ExpressionStatement, context: any): any { stmt.expr.visitExpression(this, context); return stmt; } visitReturnStmt(stmt: ReturnStatement, context: any): any { stmt.value.visitExpression(this, context); return stmt; } visitIfStmt(stmt: IfStmt, context: any): any { stmt.condition.visitExpression(this, context); this.visitAllStatements(stmt.trueCase, context); this.visitAllStatements(stmt.falseCase, context); return stmt; } visitAllStatements(stmts: Statement[], context: any): void { stmts.forEach((stmt) => stmt.visitStatement(this, context)); } } export function leadingComment( text: string, multiline: boolean = false, trailingNewline: boolean = true, ): LeadingComment { return new LeadingComment(text, multiline, trailingNewline); } export function jsDocComment(tags: JSDocTag[] = []): JSDocComment { return new JSDocComment(tags); } export function variable( name: string, type?: Type | null, sourceSpan?: ParseSourceSpan | null, leadingComments?: LeadingComment[], ): ReadVarExpr { return new ReadVarExpr(name, type, sourceSpan, leadingComments); } export function importExpr( id: ExternalReference, typeParams: Type[] | null = null, sourceSpan?: ParseSourceSpan | null, ): ExternalExpr { return new ExternalExpr(id, null, typeParams, sourceSpan); } export function importType( id: ExternalReference, typeParams?: Type[] | null, typeModifiers?: TypeModifier, ): ExpressionType | null { return id != null ? expressionType(importExpr(id, typeParams, null), typeModifiers) : null; } export function expressionType( expr: Expression, typeModifiers?: TypeModifier, typeParams?: Type[] | null, ): ExpressionType { return new ExpressionType(expr, typeModifiers, typeParams); } export function transplantedType<T>(type: T, typeModifiers?: TypeModifier): TransplantedType<T> { return new TransplantedType(type, typeModifiers); } export function typeofExpr(expr: Expression) { return new TypeofExpr(expr); } export function literalArr( values: Expression[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, ): LiteralArrayExpr { return new LiteralArrayExpr(values, type, sourceSpan); } export function literalMap( values: {key: string; quoted: boolean; value: Expression}[], type: MapType | null = null, ): LiteralMapExpr { return new LiteralMapExpr( values.map((e) => new LiteralMapPropertyAssignment(e.key, e.value, e.quoted)), type, null, ); } export function unary( operator: UnaryOperator, expr: Expression, type?: Type, sourceSpan?: ParseSourceSpan | null, ): UnaryOperatorExpr { return new UnaryOperatorExpr(operator, expr, type, sourceSpan); } export function not(expr: Expression, sourceSpan?: ParseSourceSpan | null): NotExpr { return new NotExpr(expr, sourceSpan); } export function fn( params: FnParam[], body: Statement[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, name?: string | null, ): FunctionExpr { return new FunctionExpr(params, body, type, sourceSpan, name); } export function arrowFn( params: FnParam[], body: Expression | Statement[], type?: Type | null, sourceSpan?: ParseSourceSpan | null, ) { return new ArrowFunctionExpr(params, body, type, sourceSpan); } export function ifStmt( condition: Expression, thenClause: Statement[], elseClause?: Statement[], sourceSpan?: ParseSourceSpan, leadingComments?: LeadingComment[], ) { return new IfStmt(condition, thenClause, elseClause, sourceSpan, leadingComments); } export function taggedTemplate( tag: Expression, template: TemplateLiteralExpr, type?: Type | null, sourceSpan?: ParseSourceSpan | null, ): TaggedTemplateLiteralExpr { return new TaggedTemplateLiteralExpr(tag, template, type, sourceSpan); } export function literal( value: any, type?: Type | null, sourceSpan?: ParseSourceSpan | null, ): LiteralExpr { return new LiteralExpr(value, type, sourceSpan); } export function localizedString( metaBlock: I18nMeta, messageParts: LiteralPiece[], placeholderNames: PlaceholderPiece[], expressions: Expression[], sourceSpan?: ParseSourceSpan | null, ): LocalizedString { return new LocalizedString(metaBlock, messageParts, placeholderNames, expressions, sourceSpan); } export function isNull(exp: Expression): boolean { return exp instanceof LiteralExpr && exp.value === null; } // The list of JSDoc tags that we currently support. Extend it if needed. export const enum JSDocTagName { Desc = 'desc', Id = 'id', Meaning = 'meaning', Suppress = 'suppress', } /* * TypeScript has an API for JSDoc already, but it's not exposed. * https://github.com/Microsoft/TypeScript/issues/7393 * For now we create types that are similar to theirs so that migrating * to their API will be easier. See e.g. `ts.JSDocTag` and `ts.JSDocComment`. */ export type JSDocTag = | { // `tagName` is e.g. "param" in an `@param` declaration tagName: JSDocTagName | string; // Any remaining text on the tag, e.g. the description text?: string; } | { // no `tagName` for plain text documentation that occurs before any `@param` lines tagName?: undefined; text: string; }; /* * Serializes a `Tag` into a string. * Returns a string like " @foo {bar} baz" (note the leading whitespace before `@foo`). */ function tagToString(tag: JSDocTag): string { let out = ''; if (tag.tagName) { out += ` @${tag.tagName}`; } if (tag.text) { if (tag.text.match(/\/\*|\*\//)) { throw new Error('JSDoc text cannot contain "/*" and "*/"'); } out += ' ' + tag.text.replace(/@/g, '\\@'); } return out; } function serializeTags(tags: JSDocTag[]): string { if (tags.length === 0) return ''; if (tags.length === 1 && tags[0].tagName && !tags[0].text) { // The JSDOC comment is a single simple tag: e.g `/** @tagname */`. return `*${tagToString(tags[0])} `; } let out = '*\n'; for (const tag of tags) { out += ' *'; // If the tagToString is multi-line, insert " * " prefixes on lines. out += tagToString(tag).replace(/\n/g, '\n * '); out += '\n'; } out += ' '; return out; }