diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2e8deeeb5d770..ed9d7f9b323bc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -167,6 +167,8 @@ import { equateValues, escapeLeadingUnderscores, escapeString, + EvaluatorResult, + evaluatorResult, every, EvolvingArrayType, ExclamationToken, @@ -723,7 +725,6 @@ import { isStringOrNumericLiteralLike, isSuperCall, isSuperProperty, - isSyntacticallyString, isTaggedTemplateExpression, isTemplateSpan, isThisContainerOrFunctionBlock, @@ -12896,7 +12897,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { for (const member of (declaration as EnumDeclaration).members) { if (hasBindableName(member)) { const memberSymbol = getSymbolOfDeclaration(member); - const value = getEnumMemberValue(member); + const value = getEnumMemberValue(member).value; const memberType = getFreshTypeOfLiteralType( value !== undefined ? getEnumLiteralType(value, getSymbolId(symbol), memberSymbol) : @@ -21335,8 +21336,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } return false; } - const sourceValue = getEnumMemberValue(getDeclarationOfKind(sourceProperty, SyntaxKind.EnumMember)!); - const targetValue = getEnumMemberValue(getDeclarationOfKind(targetProperty, SyntaxKind.EnumMember)!); + const sourceValue = getEnumMemberValue(getDeclarationOfKind(sourceProperty, SyntaxKind.EnumMember)!).value; + const targetValue = getEnumMemberValue(getDeclarationOfKind(targetProperty, SyntaxKind.EnumMember)!).value; if (sourceValue !== targetValue) { const sourceIsString = typeof sourceValue === "string"; const targetIsString = typeof targetValue === "string"; @@ -39494,7 +39495,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (isConstContext(node) || isTemplateLiteralContext(node) || someType(getContextualType(node, /*contextFlags*/ undefined) || unknownType, isTemplateLiteralContextualType)) { return getTemplateLiteralType(texts, types); } - const evaluated = node.parent.kind !== SyntaxKind.TaggedTemplateExpression && evaluate(node); + const evaluated = node.parent.kind !== SyntaxKind.TaggedTemplateExpression && evaluate(node).value; return evaluated ? getFreshTypeOfLiteralType(getStringLiteralType(evaluated)) : stringType; } @@ -45903,15 +45904,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let autoValue: number | undefined = 0; let previous: EnumMember | undefined; for (const member of node.members) { - const value = computeMemberValue(member, autoValue, previous); - getNodeLinks(member).enumMemberValue = value; - autoValue = typeof value === "number" ? value + 1 : undefined; + const result = computeEnumMemberValue(member, autoValue, previous); + getNodeLinks(member).enumMemberValue = result; + autoValue = typeof result.value === "number" ? result.value + 1 : undefined; previous = member; } } } - function computeMemberValue(member: EnumMember, autoValue: number | undefined, previous: EnumMember | undefined) { + function computeEnumMemberValue(member: EnumMember, autoValue: number | undefined, previous: EnumMember | undefined): EvaluatorResult { if (isComputedNonLiteralName(member.name)) { error(member.name, Diagnostics.Computed_property_names_are_not_allowed_in_enums); } @@ -45922,12 +45923,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } if (member.initializer) { - return computeConstantValue(member); + return computeConstantEnumMemberValue(member); } // In ambient non-const numeric enum declarations, enum members without initializers are // considered computed members (as opposed to having auto-incremented values). if (member.parent.flags & NodeFlags.Ambient && !isEnumConst(member.parent)) { - return undefined; + return evaluatorResult(/*value*/ undefined); } // If the member declaration specifies no value, the member is considered a constant enum member. // If the member is the first member in the enum declaration, it is assigned the value zero. @@ -45935,31 +45936,34 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // occurs if the immediately preceding member is not a constant enum member. if (autoValue === undefined) { error(member.name, Diagnostics.Enum_member_must_have_initializer); - return undefined; + return evaluatorResult(/*value*/ undefined); } - if (getIsolatedModules(compilerOptions) && previous?.initializer && !isSyntacticallyNumericConstant(previous.initializer)) { - error( - member.name, - Diagnostics.Enum_member_following_a_non_literal_numeric_member_must_have_an_initializer_when_isolatedModules_is_enabled, - ); + if (getIsolatedModules(compilerOptions) && previous?.initializer) { + const prevValue = getEnumMemberValue(previous); + if (!(typeof prevValue.value === "number" && !prevValue.resolvedOtherFiles)) { + error( + member.name, + Diagnostics.Enum_member_following_a_non_literal_numeric_member_must_have_an_initializer_when_isolatedModules_is_enabled, + ); + } } - return autoValue; + return evaluatorResult(autoValue); } - function computeConstantValue(member: EnumMember): string | number | undefined { + function computeConstantEnumMemberValue(member: EnumMember): EvaluatorResult { const isConstEnum = isEnumConst(member.parent); const initializer = member.initializer!; - const value = evaluate(initializer, member); - if (value !== undefined) { - if (isConstEnum && typeof value === "number" && !isFinite(value)) { + const result = evaluate(initializer, member); + if (result.value !== undefined) { + if (isConstEnum && typeof result.value === "number" && !isFinite(result.value)) { error( initializer, - isNaN(value) ? + isNaN(result.value) ? Diagnostics.const_enum_member_initializer_was_evaluated_to_disallowed_value_NaN : Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value, ); } - else if (getIsolatedModules(compilerOptions) && typeof value === "string" && !isSyntacticallyString(initializer)) { + else if (getIsolatedModules(compilerOptions) && typeof result.value === "string" && !result.isSyntacticallyString) { error( initializer, Diagnostics._0_has_a_string_type_but_must_have_syntactically_recognizable_string_syntax_when_isolatedModules_is_enabled, @@ -45976,30 +45980,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { else { checkTypeAssignableTo(checkExpression(initializer), numberType, initializer, Diagnostics.Type_0_is_not_assignable_to_type_1_as_required_for_computed_enum_member_values); } - return value; - } - - function isSyntacticallyNumericConstant(expr: Expression): boolean { - expr = skipOuterExpressions(expr); - switch (expr.kind) { - case SyntaxKind.PrefixUnaryExpression: - return isSyntacticallyNumericConstant((expr as PrefixUnaryExpression).operand); - case SyntaxKind.BinaryExpression: - return isSyntacticallyNumericConstant((expr as BinaryExpression).left) && isSyntacticallyNumericConstant((expr as BinaryExpression).right); - case SyntaxKind.NumericLiteral: - return true; - } - return false; + return result; } function evaluateEntityNameExpression(expr: EntityNameExpression, location?: Declaration) { const symbol = resolveEntityName(expr, SymbolFlags.Value, /*ignoreErrors*/ true); - if (!symbol) return undefined; + if (!symbol) return evaluatorResult(/*value*/ undefined); if (expr.kind === SyntaxKind.Identifier) { const identifier = expr; if (isInfinityOrNaNString(identifier.escapedText) && (symbol === getGlobalSymbol(identifier.escapedText, SymbolFlags.Value, /*diagnostic*/ undefined))) { - return +(identifier.escapedText); + // Technically we resolved a global lib file here, but the decision to treat this as numeric + // is more predicated on the fact that the single-file resolution *didn't* resolve to a + // different meaning of `Infinity` or `NaN`. Transpilers handle this no problem. + return evaluatorResult(+(identifier.escapedText), /*isSyntacticallyString*/ false); } } @@ -46009,9 +46003,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (isConstantVariable(symbol)) { const declaration = symbol.valueDeclaration; if (declaration && isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && (!location || declaration !== location && isBlockScopedNameDeclaredBeforeUse(declaration, location))) { - return evaluate(declaration.initializer, declaration); + const result = evaluate(declaration.initializer, declaration); + if (location && getSourceFileOfNode(location) !== getSourceFileOfNode(declaration)) { + return evaluatorResult( + result.value, + /*isSyntacticallyString*/ false, + /*resolvedOtherFiles*/ true, + ); + } + return result; } } + return evaluatorResult(/*value*/ undefined); } function evaluateElementAccessExpression(expr: ElementAccessExpression, location?: Declaration) { @@ -46022,21 +46025,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const name = escapeLeadingUnderscores(expr.argumentExpression.text); const member = rootSymbol.exports!.get(name); if (member) { + Debug.assert(getSourceFileOfNode(member.valueDeclaration) === getSourceFileOfNode(rootSymbol.valueDeclaration)); return location ? evaluateEnumMember(expr, member, location) : getEnumMemberValue(member.valueDeclaration as EnumMember); } } } + return evaluatorResult(/*value*/ undefined); } function evaluateEnumMember(expr: Expression, symbol: Symbol, location: Declaration) { const declaration = symbol.valueDeclaration; if (!declaration || declaration === location) { error(expr, Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(symbol)); - return undefined; + return evaluatorResult(/*value*/ undefined); } if (!isBlockScopedNameDeclaredBeforeUse(declaration, location)) { error(expr, Diagnostics.A_member_initializer_in_a_enum_declaration_cannot_reference_members_declared_after_it_including_members_defined_in_other_enums); - return 0; + return evaluatorResult(/*value*/ 0); } return getEnumMemberValue(declaration as EnumMember); } @@ -48668,9 +48673,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return nodeLinks[nodeId]?.flags || 0; } - function getEnumMemberValue(node: EnumMember): string | number | undefined { + function getEnumMemberValue(node: EnumMember): EvaluatorResult { computeEnumMemberValues(node.parent); - return getNodeLinks(node).enumMemberValue; + return getNodeLinks(node).enumMemberValue ?? evaluatorResult(/*value*/ undefined); } function canHaveConstantValue(node: Node): node is EnumMember | AccessExpression { @@ -48685,7 +48690,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function getConstantValue(node: EnumMember | AccessExpression): string | number | undefined { if (node.kind === SyntaxKind.EnumMember) { - return getEnumMemberValue(node); + return getEnumMemberValue(node).value; } const symbol = getNodeLinks(node).resolvedSymbol; @@ -48693,7 +48698,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // inline property\index accesses only for const enums const member = symbol.valueDeclaration as EnumMember; if (isEnumConst(member.parent)) { - return getEnumMemberValue(member); + return getEnumMemberValue(member).value; } } @@ -49096,6 +49101,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const node = getParseTreeNode(nodeIn, canHaveConstantValue); return node ? getConstantValue(node) : undefined; }, + getEnumMemberValue: nodeIn => { + const node = getParseTreeNode(nodeIn, isEnumMember); + return node ? getEnumMemberValue(node) : undefined; + }, collectLinkedAliases, getReferencedValueDeclaration, getReferencedValueDeclarations, diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 021b4e24881e6..77a089585e6fc 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1103,6 +1103,7 @@ export const notImplementedResolver: EmitResolver = { isEntityNameVisible: notImplemented, // Returns the constant value this property access resolves to: notImplemented, or 'undefined' for a non-constant getConstantValue: notImplemented, + getEnumMemberValue: notImplemented, getReferencedValueDeclaration: notImplemented, getReferencedValueDeclarations: notImplemented, getTypeReferenceSerializationKind: notImplemented, diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index f7af3ceb8eb04..0f9bc54418004 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -122,7 +122,6 @@ import { isSimpleInlineableExpression, isSourceFile, isStatement, - isSyntacticallyString, isTemplateLiteral, isTryStatement, JsxOpeningElement, @@ -1915,7 +1914,8 @@ export function transformTypeScript(context: TransformationContext) { // we pass false as 'generateNameForComputedPropertyName' for a backward compatibility purposes // old emitter always generate 'expression' part of the name as-is. const name = getExpressionForPropertyName(member, /*generateNameForComputedPropertyName*/ false); - const valueExpression = transformEnumMemberDeclarationValue(member); + const evaluated = resolver.getEnumMemberValue(member); + const valueExpression = transformEnumMemberDeclarationValue(member, evaluated?.value); const innerAssignment = factory.createAssignment( factory.createElementAccessExpression( currentNamespaceContainerName, @@ -1923,7 +1923,7 @@ export function transformTypeScript(context: TransformationContext) { ), valueExpression, ); - const outerAssignment = isSyntacticallyString(valueExpression) ? + const outerAssignment = typeof evaluated?.value === "string" || evaluated?.isSyntacticallyString ? innerAssignment : factory.createAssignment( factory.createElementAccessExpression( @@ -1948,12 +1948,11 @@ export function transformTypeScript(context: TransformationContext) { * * @param member The enum member node. */ - function transformEnumMemberDeclarationValue(member: EnumMember): Expression { - const value = resolver.getConstantValue(member); - if (value !== undefined) { - return typeof value === "string" ? factory.createStringLiteral(value) : - value < 0 ? factory.createPrefixUnaryExpression(SyntaxKind.MinusToken, factory.createNumericLiteral(-value)) : - factory.createNumericLiteral(value); + function transformEnumMemberDeclarationValue(member: EnumMember, constantValue: string | number | undefined): Expression { + if (constantValue !== undefined) { + return typeof constantValue === "string" ? factory.createStringLiteral(constantValue) : + constantValue < 0 ? factory.createPrefixUnaryExpression(SyntaxKind.MinusToken, factory.createNumericLiteral(-constantValue)) : + factory.createNumericLiteral(constantValue); } else { enableSubstitutionForNonQualifiedEnumMembers(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 05ec88e3f62d2..f3ac579029fc3 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5638,6 +5638,7 @@ export interface EmitResolver { isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult; // Returns the constant value this property access resolves to, or 'undefined' for a non-constant getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined; + getEnumMemberValue(node: EnumMember): EvaluatorResult | undefined; getReferencedValueDeclaration(reference: Identifier): Declaration | undefined; getReferencedValueDeclarations(reference: Identifier): Declaration[] | undefined; getTypeReferenceSerializationKind(typeName: EntityName, location?: Node): TypeReferenceSerializationKind; @@ -5969,6 +5970,13 @@ export const enum NodeCheckFlags { InCheckIdentifier = 1 << 22, } +/** @internal */ +export interface EvaluatorResult { + value: T; + isSyntacticallyString: boolean; + resolvedOtherFiles: boolean; +} + // dprint-ignore /** @internal */ export interface NodeLinks { @@ -5979,7 +5987,7 @@ export interface NodeLinks { resolvedSymbol?: Symbol; // Cached name resolution result resolvedIndexInfo?: IndexInfo; // Cached indexing info resolution result effectsSignature?: Signature; // Signature with possible control flow effects - enumMemberValue?: string | number; // Constant value of enum member + enumMemberValue?: EvaluatorResult; // Constant value of enum member isVisible?: boolean; // Is this node visible containsArgumentsReference?: boolean; // Whether a function-like declaration contains an 'arguments' reference hasReportedStatementInAmbientContext?: boolean; // Cache boolean if we report statements in ambient context @@ -10086,6 +10094,6 @@ export interface Queue { /** @internal */ export interface EvaluationResolver { - evaluateEntityNameExpression(expr: EntityNameExpression, location: Declaration | undefined): string | number | undefined; - evaluateElementAccessExpression(expr: ElementAccessExpression, location: Declaration | undefined): string | number | undefined; + evaluateEntityNameExpression(expr: EntityNameExpression, location: Declaration | undefined): EvaluatorResult; + evaluateElementAccessExpression(expr: ElementAccessExpression, location: Declaration | undefined): EvaluatorResult; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index ddcb5c258dd37..6657db6af7812 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -120,6 +120,7 @@ import { equateValues, escapeLeadingUnderscores, EvaluationResolver, + EvaluatorResult, every, ExportAssignment, ExportDeclaration, @@ -10649,91 +10650,99 @@ export function getNameFromImportAttribute(node: ImportAttribute) { } /** @internal */ -export function isSyntacticallyString(expr: Expression): boolean { - expr = skipOuterExpressions(expr); - switch (expr.kind) { - case SyntaxKind.BinaryExpression: - const left = (expr as BinaryExpression).left; - const right = (expr as BinaryExpression).right; - return ( - (expr as BinaryExpression).operatorToken.kind === SyntaxKind.PlusToken && - (isSyntacticallyString(left) || isSyntacticallyString(right)) - ); - case SyntaxKind.TemplateExpression: - case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - return true; - } - return false; +export function evaluatorResult(value: T, isSyntacticallyString = false, resolvedOtherFiles = false): EvaluatorResult { + return { value, isSyntacticallyString, resolvedOtherFiles }; } /** @internal */ export function createEvaluator({ evaluateElementAccessExpression, evaluateEntityNameExpression }: EvaluationResolver) { - function evaluate(expr: TemplateExpression, location?: Declaration): string; - function evaluate(expr: Expression, location?: Declaration): string | number | undefined; - function evaluate(expr: Expression, location?: Declaration): string | number | undefined { + function evaluate(expr: TemplateExpression, location?: Declaration): EvaluatorResult; + function evaluate(expr: Expression, location?: Declaration): EvaluatorResult; + function evaluate(expr: Expression, location?: Declaration): EvaluatorResult { + let isSyntacticallyString = false; + let resolvedOtherFiles = false; + // It's unclear when/whether we should consider skipping other kinds of outer expressions. + // Type assertions intentionally break evaluation when evaluating literal types, such as: + // type T = `one ${"two" as any} three`; // string + // But it's less clear whether such an assertion should break enum member evaluation: + // enum E { + // A = "one" as any + // } + // SatisfiesExpressions and non-null assertions seem to have even less reason to break + // emitting enum members as literals. However, these expressions also break Babel's + // evaluation (but not esbuild's), and the isolatedModules errors we give depend on + // our evaluation results, so we're currently being conservative so as to issue errors + // on code that might break Babel. + expr = skipParentheses(expr); switch (expr.kind) { case SyntaxKind.PrefixUnaryExpression: - const value = evaluate((expr as PrefixUnaryExpression).operand, location); - if (typeof value === "number") { + const result = evaluate((expr as PrefixUnaryExpression).operand, location); + resolvedOtherFiles = result.resolvedOtherFiles; + if (typeof result.value === "number") { switch ((expr as PrefixUnaryExpression).operator) { case SyntaxKind.PlusToken: - return value; + return evaluatorResult(result.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.MinusToken: - return -value; + return evaluatorResult(-result.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.TildeToken: - return ~value; + return evaluatorResult(~result.value, isSyntacticallyString, resolvedOtherFiles); } } break; - case SyntaxKind.BinaryExpression: + case SyntaxKind.BinaryExpression: { const left = evaluate((expr as BinaryExpression).left, location); const right = evaluate((expr as BinaryExpression).right, location); - if (typeof left === "number" && typeof right === "number") { + isSyntacticallyString = (left.isSyntacticallyString || right.isSyntacticallyString) && (expr as BinaryExpression).operatorToken.kind === SyntaxKind.PlusToken; + resolvedOtherFiles = left.resolvedOtherFiles || right.resolvedOtherFiles; + if (typeof left.value === "number" && typeof right.value === "number") { switch ((expr as BinaryExpression).operatorToken.kind) { case SyntaxKind.BarToken: - return left | right; + return evaluatorResult(left.value | right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.AmpersandToken: - return left & right; + return evaluatorResult(left.value & right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.GreaterThanGreaterThanToken: - return left >> right; + return evaluatorResult(left.value >> right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - return left >>> right; + return evaluatorResult(left.value >>> right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.LessThanLessThanToken: - return left << right; + return evaluatorResult(left.value << right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.CaretToken: - return left ^ right; + return evaluatorResult(left.value ^ right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.AsteriskToken: - return left * right; + return evaluatorResult(left.value * right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.SlashToken: - return left / right; + return evaluatorResult(left.value / right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.PlusToken: - return left + right; + return evaluatorResult(left.value + right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.MinusToken: - return left - right; + return evaluatorResult(left.value - right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.PercentToken: - return left % right; + return evaluatorResult(left.value % right.value, isSyntacticallyString, resolvedOtherFiles); case SyntaxKind.AsteriskAsteriskToken: - return left ** right; + return evaluatorResult(left.value ** right.value, isSyntacticallyString, resolvedOtherFiles); } } else if ( - (typeof left === "string" || typeof left === "number") && - (typeof right === "string" || typeof right === "number") && + (typeof left.value === "string" || typeof left.value === "number") && + (typeof right.value === "string" || typeof right.value === "number") && (expr as BinaryExpression).operatorToken.kind === SyntaxKind.PlusToken ) { - return "" + left + right; + return evaluatorResult( + "" + left.value + right.value, + isSyntacticallyString, + resolvedOtherFiles, + ); } + break; + } case SyntaxKind.StringLiteral: case SyntaxKind.NoSubstitutionTemplateLiteral: - return (expr as StringLiteralLike).text; + return evaluatorResult((expr as StringLiteralLike).text, /*isSyntacticallyString*/ true); case SyntaxKind.TemplateExpression: return evaluateTemplateExpression(expr as TemplateExpression, location); case SyntaxKind.NumericLiteral: - return +(expr as NumericLiteral).text; - case SyntaxKind.ParenthesizedExpression: - return evaluate((expr as ParenthesizedExpression).expression, location); + return evaluatorResult(+(expr as NumericLiteral).text); case SyntaxKind.Identifier: return evaluateEntityNameExpression(expr as Identifier, location); case SyntaxKind.PropertyAccessExpression: @@ -10744,20 +10753,26 @@ export function createEvaluator({ evaluateElementAccessExpression, evaluateEntit case SyntaxKind.ElementAccessExpression: return evaluateElementAccessExpression(expr as ElementAccessExpression, location); } - return undefined; + return evaluatorResult(/*value*/ undefined, isSyntacticallyString, resolvedOtherFiles); } - function evaluateTemplateExpression(expr: TemplateExpression, location?: Declaration) { + function evaluateTemplateExpression(expr: TemplateExpression, location?: Declaration): EvaluatorResult { let result = expr.head.text; + let resolvedOtherFiles = false; for (const span of expr.templateSpans) { - const value = evaluate(span.expression, location); - if (value === undefined) { - return undefined; + const spanResult = evaluate(span.expression, location); + if (spanResult.value === undefined) { + return evaluatorResult(/*value*/ undefined, /*isSyntacticallyString*/ true); } - result += value; + result += spanResult.value; result += span.literal.text; + resolvedOtherFiles ||= spanResult.resolvedOtherFiles; } - return result; + return evaluatorResult( + result, + /*isSyntacticallyString*/ true, + resolvedOtherFiles, + ); } return evaluate; } diff --git a/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=false).errors.txt b/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=false).errors.txt index 94d81a5a16ac2..741f89a635f7c 100644 --- a/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=false).errors.txt +++ b/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=false).errors.txt @@ -1,11 +1,12 @@ computedEnumMemberSyntacticallyString.ts(4,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. computedEnumMemberSyntacticallyString.ts(5,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. computedEnumMemberSyntacticallyString.ts(6,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. -computedEnumMemberSyntacticallyString.ts(7,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. computedEnumMemberSyntacticallyString.ts(8,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. +computedEnumMemberSyntacticallyString.ts(9,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. +computedEnumMemberSyntacticallyString.ts(12,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. -==== computedEnumMemberSyntacticallyString.ts (5 errors) ==== +==== computedEnumMemberSyntacticallyString.ts (6 errors) ==== const BAR = 2..toFixed(0); enum Foo { @@ -18,11 +19,18 @@ computedEnumMemberSyntacticallyString.ts(8,9): error TS18033: Type 'string' is n C = (`${BAR}`), ~~~~~~~~~~ !!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. - D = (`${BAR}}`) as string, - ~~~~~~~~~~~~~~~~~~~~~ + + F = BAR, + ~~~ !!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. - E = `${BAR}`!, - ~~~~~~~~~ + G = 2 + BAR, + ~~~~~~~ +!!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + + H = A, + I = H + BAR, + ~~~~~~~ !!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + J = H } \ No newline at end of file diff --git a/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=false).js b/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=false).js index a8cc14cca3ad4..38c360bdf0c83 100644 --- a/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=false).js +++ b/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=false).js @@ -7,8 +7,13 @@ enum Foo { A = `${BAR}`, B = "2" + BAR, C = (`${BAR}`), - D = (`${BAR}}`) as string, - E = `${BAR}`!, + + F = BAR, + G = 2 + BAR, + + H = A, + I = H + BAR, + J = H } @@ -19,6 +24,9 @@ var Foo; Foo["A"] = `${BAR}`; Foo["B"] = "2" + BAR; Foo["C"] = (`${BAR}`); - Foo["D"] = (`${BAR}}`); - Foo["E"] = `${BAR}`; + Foo[Foo["F"] = BAR] = "F"; + Foo[Foo["G"] = 2 + BAR] = "G"; + Foo["H"] = Foo.A; + Foo["I"] = Foo.H + BAR; + Foo["J"] = Foo.H; })(Foo || (Foo = {})); diff --git a/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=true).errors.txt b/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=true).errors.txt index 94d81a5a16ac2..741f89a635f7c 100644 --- a/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=true).errors.txt +++ b/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=true).errors.txt @@ -1,11 +1,12 @@ computedEnumMemberSyntacticallyString.ts(4,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. computedEnumMemberSyntacticallyString.ts(5,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. computedEnumMemberSyntacticallyString.ts(6,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. -computedEnumMemberSyntacticallyString.ts(7,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. computedEnumMemberSyntacticallyString.ts(8,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. +computedEnumMemberSyntacticallyString.ts(9,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. +computedEnumMemberSyntacticallyString.ts(12,9): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. -==== computedEnumMemberSyntacticallyString.ts (5 errors) ==== +==== computedEnumMemberSyntacticallyString.ts (6 errors) ==== const BAR = 2..toFixed(0); enum Foo { @@ -18,11 +19,18 @@ computedEnumMemberSyntacticallyString.ts(8,9): error TS18033: Type 'string' is n C = (`${BAR}`), ~~~~~~~~~~ !!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. - D = (`${BAR}}`) as string, - ~~~~~~~~~~~~~~~~~~~~~ + + F = BAR, + ~~~ !!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. - E = `${BAR}`!, - ~~~~~~~~~ + G = 2 + BAR, + ~~~~~~~ +!!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + + H = A, + I = H + BAR, + ~~~~~~~ !!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + J = H } \ No newline at end of file diff --git a/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=true).js b/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=true).js index a8cc14cca3ad4..38c360bdf0c83 100644 --- a/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=true).js +++ b/tests/baselines/reference/computedEnumMemberSyntacticallyString(isolatedmodules=true).js @@ -7,8 +7,13 @@ enum Foo { A = `${BAR}`, B = "2" + BAR, C = (`${BAR}`), - D = (`${BAR}}`) as string, - E = `${BAR}`!, + + F = BAR, + G = 2 + BAR, + + H = A, + I = H + BAR, + J = H } @@ -19,6 +24,9 @@ var Foo; Foo["A"] = `${BAR}`; Foo["B"] = "2" + BAR; Foo["C"] = (`${BAR}`); - Foo["D"] = (`${BAR}}`); - Foo["E"] = `${BAR}`; + Foo[Foo["F"] = BAR] = "F"; + Foo[Foo["G"] = 2 + BAR] = "G"; + Foo["H"] = Foo.A; + Foo["I"] = Foo.H + BAR; + Foo["J"] = Foo.H; })(Foo || (Foo = {})); diff --git a/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=false).errors.txt b/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=false).errors.txt new file mode 100644 index 0000000000000..2be83179ada19 --- /dev/null +++ b/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=false).errors.txt @@ -0,0 +1,32 @@ +foo.ts(11,8): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. +foo.ts(12,8): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + + +==== ./foo.ts (2 errors) ==== + import { BAR } from './bar'; + const LOCAL = 'LOCAL'; + + enum Foo { + A = `${BAR}`, + + B = LOCAL, + C = B, + D = C + 'BAR', + + E1 = (`${BAR}`) as string, // We could recognize these, + ~~~~~~~~~~~~~~~~~~~~ +!!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + E2 = `${BAR}`!, // but Babel doesn't + ~~~~~~~~~ +!!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + + F = BAR, + G = 2 + BAR, + + H = A, + I = H + BAR, + J = H + } + +==== ./bar.ts (0 errors) ==== + export const BAR = 'bar'; \ No newline at end of file diff --git a/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=false).js b/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=false).js index 0a0bf9e0a8a24..132fda679153f 100644 --- a/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=false).js +++ b/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=false).js @@ -2,7 +2,25 @@ //// [foo.ts] import { BAR } from './bar'; -enum Foo { A = `${BAR}` } +const LOCAL = 'LOCAL'; + +enum Foo { + A = `${BAR}`, + + B = LOCAL, + C = B, + D = C + 'BAR', + + E1 = (`${BAR}`) as string, // We could recognize these, + E2 = `${BAR}`!, // but Babel doesn't + + F = BAR, + G = 2 + BAR, + + H = A, + I = H + BAR, + J = H +} //// [bar.ts] export const BAR = 'bar'; @@ -11,7 +29,18 @@ export const BAR = 'bar'; export const BAR = 'bar'; //// [foo.js] import { BAR } from './bar'; +const LOCAL = 'LOCAL'; var Foo; (function (Foo) { Foo["A"] = "bar"; + Foo["B"] = "LOCAL"; + Foo["C"] = "LOCAL"; + Foo["D"] = "LOCALBAR"; + Foo[Foo["E1"] = (`${BAR}`)] = "E1"; + Foo[Foo["E2"] = `${BAR}`] = "E2"; + Foo["F"] = "bar"; + Foo["G"] = "2bar"; + Foo["H"] = "bar"; + Foo["I"] = "barbar"; + Foo["J"] = "bar"; })(Foo || (Foo = {})); diff --git a/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=true).errors.txt b/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=true).errors.txt new file mode 100644 index 0000000000000..66b0352943649 --- /dev/null +++ b/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=true).errors.txt @@ -0,0 +1,38 @@ +foo.ts(11,8): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. +foo.ts(12,8): error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. +foo.ts(14,7): error TS18055: 'Foo.F' has a string type, but must have syntactically recognizable string syntax when 'isolatedModules' is enabled. +foo.ts(15,7): error TS18055: 'Foo.G' has a string type, but must have syntactically recognizable string syntax when 'isolatedModules' is enabled. + + +==== ./foo.ts (4 errors) ==== + import { BAR } from './bar'; + const LOCAL = 'LOCAL'; + + enum Foo { + A = `${BAR}`, + + B = LOCAL, + C = B, + D = C + 'BAR', + + E1 = (`${BAR}`) as string, // We could recognize these, + ~~~~~~~~~~~~~~~~~~~~ +!!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + E2 = `${BAR}`!, // but Babel doesn't + ~~~~~~~~~ +!!! error TS18033: Type 'string' is not assignable to type 'number' as required for computed enum member values. + + F = BAR, + ~~~ +!!! error TS18055: 'Foo.F' has a string type, but must have syntactically recognizable string syntax when 'isolatedModules' is enabled. + G = 2 + BAR, + ~~~~~~~ +!!! error TS18055: 'Foo.G' has a string type, but must have syntactically recognizable string syntax when 'isolatedModules' is enabled. + + H = A, + I = H + BAR, + J = H + } + +==== ./bar.ts (0 errors) ==== + export const BAR = 'bar'; \ No newline at end of file diff --git a/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=true).js b/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=true).js index 0a0bf9e0a8a24..132fda679153f 100644 --- a/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=true).js +++ b/tests/baselines/reference/computedEnumMemberSyntacticallyString2(isolatedmodules=true).js @@ -2,7 +2,25 @@ //// [foo.ts] import { BAR } from './bar'; -enum Foo { A = `${BAR}` } +const LOCAL = 'LOCAL'; + +enum Foo { + A = `${BAR}`, + + B = LOCAL, + C = B, + D = C + 'BAR', + + E1 = (`${BAR}`) as string, // We could recognize these, + E2 = `${BAR}`!, // but Babel doesn't + + F = BAR, + G = 2 + BAR, + + H = A, + I = H + BAR, + J = H +} //// [bar.ts] export const BAR = 'bar'; @@ -11,7 +29,18 @@ export const BAR = 'bar'; export const BAR = 'bar'; //// [foo.js] import { BAR } from './bar'; +const LOCAL = 'LOCAL'; var Foo; (function (Foo) { Foo["A"] = "bar"; + Foo["B"] = "LOCAL"; + Foo["C"] = "LOCAL"; + Foo["D"] = "LOCALBAR"; + Foo[Foo["E1"] = (`${BAR}`)] = "E1"; + Foo[Foo["E2"] = `${BAR}`] = "E2"; + Foo["F"] = "bar"; + Foo["G"] = "2bar"; + Foo["H"] = "bar"; + Foo["I"] = "barbar"; + Foo["J"] = "bar"; })(Foo || (Foo = {})); diff --git a/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.errors.txt b/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.errors.txt index 2999b769c9699..3b137187d1478 100644 --- a/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.errors.txt +++ b/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.errors.txt @@ -1,16 +1,22 @@ bad.ts(4,5): error TS18056: Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled. +bad.ts(7,5): error TS1061: Enum member must have initializer. ==== ./helpers.ts (0 errors) ==== export const foo = 2; -==== ./bad.ts (1 errors) ==== +==== ./bad.ts (2 errors) ==== import { foo } from "./helpers"; enum A { a = foo, b, ~ !!! error TS18056: Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled. + c = 10, + d = (c)! satisfies number as any, + e, + ~ +!!! error TS1061: Enum member must have initializer. } ==== ./good.ts (0 errors) ==== @@ -31,4 +37,12 @@ bad.ts(4,5): error TS18056: Enum member following a non-literal numeric member m a = (2), b, } + enum E { + a, + b, + c = a, + d, + e = d | b, + f, + } \ No newline at end of file diff --git a/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.js b/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.js index 1b24257e04f7c..bc21c531709a1 100644 --- a/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.js +++ b/tests/baselines/reference/enumNoInitializerFollowsNonLiteralInitializer.js @@ -8,6 +8,9 @@ import { foo } from "./helpers"; enum A { a = foo, b, + c = 10, + d = (c)! satisfies number as any, + e, } //// [good.ts] @@ -28,6 +31,14 @@ enum D { a = (2), b, } +enum E { + a, + b, + c = a, + d, + e = d | b, + f, +} //// [helpers.js] @@ -43,6 +54,9 @@ var A; (function (A) { A[A["a"] = 2] = "a"; A[A["b"] = 3] = "b"; + A[A["c"] = 10] = "c"; + A[A["d"] = (A.c)] = "d"; + A[A["e"] = void 0] = "e"; })(A || (A = {})); //// [good.js] "use strict"; @@ -68,3 +82,12 @@ var D; D[D["a"] = 2] = "a"; D[D["b"] = 3] = "b"; })(D || (D = {})); +var E; +(function (E) { + E[E["a"] = 0] = "a"; + E[E["b"] = 1] = "b"; + E[E["c"] = 0] = "c"; + E[E["d"] = 1] = "d"; + E[E["e"] = 1] = "e"; + E[E["f"] = 2] = "f"; +})(E || (E = {})); diff --git a/tests/cases/compiler/computedEnumMemberSyntacticallyString.ts b/tests/cases/compiler/computedEnumMemberSyntacticallyString.ts index 15af31ebc6fb8..ae16dbcc0a7df 100644 --- a/tests/cases/compiler/computedEnumMemberSyntacticallyString.ts +++ b/tests/cases/compiler/computedEnumMemberSyntacticallyString.ts @@ -8,6 +8,11 @@ enum Foo { A = `${BAR}`, B = "2" + BAR, C = (`${BAR}`), - D = (`${BAR}}`) as string, - E = `${BAR}`!, + + F = BAR, + G = 2 + BAR, + + H = A, + I = H + BAR, + J = H } diff --git a/tests/cases/compiler/computedEnumMemberSyntacticallyString2.ts b/tests/cases/compiler/computedEnumMemberSyntacticallyString2.ts index e163858b13c4a..cd364ff4261a0 100644 --- a/tests/cases/compiler/computedEnumMemberSyntacticallyString2.ts +++ b/tests/cases/compiler/computedEnumMemberSyntacticallyString2.ts @@ -4,7 +4,25 @@ // @filename: ./foo.ts import { BAR } from './bar'; -enum Foo { A = `${BAR}` } +const LOCAL = 'LOCAL'; + +enum Foo { + A = `${BAR}`, + + B = LOCAL, + C = B, + D = C + 'BAR', + + E1 = (`${BAR}`) as string, // We could recognize these, + E2 = `${BAR}`!, // but Babel doesn't + + F = BAR, + G = 2 + BAR, + + H = A, + I = H + BAR, + J = H +} // @filename: ./bar.ts export const BAR = 'bar'; \ No newline at end of file diff --git a/tests/cases/compiler/enumNoInitializerFollowsNonLiteralInitializer.ts b/tests/cases/compiler/enumNoInitializerFollowsNonLiteralInitializer.ts index b4274dbd53385..72cd4af5e8ac5 100644 --- a/tests/cases/compiler/enumNoInitializerFollowsNonLiteralInitializer.ts +++ b/tests/cases/compiler/enumNoInitializerFollowsNonLiteralInitializer.ts @@ -9,6 +9,9 @@ import { foo } from "./helpers"; enum A { a = foo, b, + c = 10, + d = (c)! satisfies number as any, + e, } // @filename: ./good.ts @@ -29,3 +32,11 @@ enum D { a = (2), b, } +enum E { + a, + b, + c = a, + d, + e = d | b, + f, +}