Use evaluator for isolatedModules enum restrictions (#57966)

This commit is contained in:
Andrew Branch 2024-04-04 09:56:23 -07:00 committed by GitHub
parent 83e3d6ae59
commit 9f8a231270
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 400 additions and 137 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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();

View File

@ -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<T extends string | number | undefined = string | number | undefined> {
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<T> {
/** @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;
}

View File

@ -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<T extends string | number | undefined>(value: T, isSyntacticallyString = false, resolvedOtherFiles = false): EvaluatorResult<T> {
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<string | undefined>;
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<string | undefined> {
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;
}

View File

@ -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
}

View File

@ -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 = {}));

View File

@ -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
}

View File

@ -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 = {}));

View File

@ -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';

View File

@ -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 = {}));

View File

@ -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';

View File

@ -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 = {}));

View File

@ -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,
}

View File

@ -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 = {}));

View File

@ -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
}

View File

@ -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';

View File

@ -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,
}