Skip to content

Commit 9f8a231

Browse files
authored
Use evaluator for isolatedModules enum restrictions (#57966)
1 parent 83e3d6a commit 9f8a231

18 files changed

+400
-137
lines changed

src/compiler/checker.ts

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ import {
167167
equateValues,
168168
escapeLeadingUnderscores,
169169
escapeString,
170+
EvaluatorResult,
171+
evaluatorResult,
170172
every,
171173
EvolvingArrayType,
172174
ExclamationToken,
@@ -723,7 +725,6 @@ import {
723725
isStringOrNumericLiteralLike,
724726
isSuperCall,
725727
isSuperProperty,
726-
isSyntacticallyString,
727728
isTaggedTemplateExpression,
728729
isTemplateSpan,
729730
isThisContainerOrFunctionBlock,
@@ -12896,7 +12897,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1289612897
for (const member of (declaration as EnumDeclaration).members) {
1289712898
if (hasBindableName(member)) {
1289812899
const memberSymbol = getSymbolOfDeclaration(member);
12899-
const value = getEnumMemberValue(member);
12900+
const value = getEnumMemberValue(member).value;
1290012901
const memberType = getFreshTypeOfLiteralType(
1290112902
value !== undefined ?
1290212903
getEnumLiteralType(value, getSymbolId(symbol), memberSymbol) :
@@ -21335,8 +21336,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2133521336
}
2133621337
return false;
2133721338
}
21338-
const sourceValue = getEnumMemberValue(getDeclarationOfKind(sourceProperty, SyntaxKind.EnumMember)!);
21339-
const targetValue = getEnumMemberValue(getDeclarationOfKind(targetProperty, SyntaxKind.EnumMember)!);
21339+
const sourceValue = getEnumMemberValue(getDeclarationOfKind(sourceProperty, SyntaxKind.EnumMember)!).value;
21340+
const targetValue = getEnumMemberValue(getDeclarationOfKind(targetProperty, SyntaxKind.EnumMember)!).value;
2134021341
if (sourceValue !== targetValue) {
2134121342
const sourceIsString = typeof sourceValue === "string";
2134221343
const targetIsString = typeof targetValue === "string";
@@ -39494,7 +39495,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3949439495
if (isConstContext(node) || isTemplateLiteralContext(node) || someType(getContextualType(node, /*contextFlags*/ undefined) || unknownType, isTemplateLiteralContextualType)) {
3949539496
return getTemplateLiteralType(texts, types);
3949639497
}
39497-
const evaluated = node.parent.kind !== SyntaxKind.TaggedTemplateExpression && evaluate(node);
39498+
const evaluated = node.parent.kind !== SyntaxKind.TaggedTemplateExpression && evaluate(node).value;
3949839499
return evaluated ? getFreshTypeOfLiteralType(getStringLiteralType(evaluated)) : stringType;
3949939500
}
3950039501

@@ -45903,15 +45904,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4590345904
let autoValue: number | undefined = 0;
4590445905
let previous: EnumMember | undefined;
4590545906
for (const member of node.members) {
45906-
const value = computeMemberValue(member, autoValue, previous);
45907-
getNodeLinks(member).enumMemberValue = value;
45908-
autoValue = typeof value === "number" ? value + 1 : undefined;
45907+
const result = computeEnumMemberValue(member, autoValue, previous);
45908+
getNodeLinks(member).enumMemberValue = result;
45909+
autoValue = typeof result.value === "number" ? result.value + 1 : undefined;
4590945910
previous = member;
4591045911
}
4591145912
}
4591245913
}
4591345914

45914-
function computeMemberValue(member: EnumMember, autoValue: number | undefined, previous: EnumMember | undefined) {
45915+
function computeEnumMemberValue(member: EnumMember, autoValue: number | undefined, previous: EnumMember | undefined): EvaluatorResult {
4591545916
if (isComputedNonLiteralName(member.name)) {
4591645917
error(member.name, Diagnostics.Computed_property_names_are_not_allowed_in_enums);
4591745918
}
@@ -45922,44 +45923,47 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4592245923
}
4592345924
}
4592445925
if (member.initializer) {
45925-
return computeConstantValue(member);
45926+
return computeConstantEnumMemberValue(member);
4592645927
}
4592745928
// In ambient non-const numeric enum declarations, enum members without initializers are
4592845929
// considered computed members (as opposed to having auto-incremented values).
4592945930
if (member.parent.flags & NodeFlags.Ambient && !isEnumConst(member.parent)) {
45930-
return undefined;
45931+
return evaluatorResult(/*value*/ undefined);
4593145932
}
4593245933
// If the member declaration specifies no value, the member is considered a constant enum member.
4593345934
// If the member is the first member in the enum declaration, it is assigned the value zero.
4593445935
// Otherwise, it is assigned the value of the immediately preceding member plus one, and an error
4593545936
// occurs if the immediately preceding member is not a constant enum member.
4593645937
if (autoValue === undefined) {
4593745938
error(member.name, Diagnostics.Enum_member_must_have_initializer);
45938-
return undefined;
45939+
return evaluatorResult(/*value*/ undefined);
4593945940
}
45940-
if (getIsolatedModules(compilerOptions) && previous?.initializer && !isSyntacticallyNumericConstant(previous.initializer)) {
45941-
error(
45942-
member.name,
45943-
Diagnostics.Enum_member_following_a_non_literal_numeric_member_must_have_an_initializer_when_isolatedModules_is_enabled,
45944-
);
45941+
if (getIsolatedModules(compilerOptions) && previous?.initializer) {
45942+
const prevValue = getEnumMemberValue(previous);
45943+
if (!(typeof prevValue.value === "number" && !prevValue.resolvedOtherFiles)) {
45944+
error(
45945+
member.name,
45946+
Diagnostics.Enum_member_following_a_non_literal_numeric_member_must_have_an_initializer_when_isolatedModules_is_enabled,
45947+
);
45948+
}
4594545949
}
45946-
return autoValue;
45950+
return evaluatorResult(autoValue);
4594745951
}
4594845952

45949-
function computeConstantValue(member: EnumMember): string | number | undefined {
45953+
function computeConstantEnumMemberValue(member: EnumMember): EvaluatorResult {
4595045954
const isConstEnum = isEnumConst(member.parent);
4595145955
const initializer = member.initializer!;
45952-
const value = evaluate(initializer, member);
45953-
if (value !== undefined) {
45954-
if (isConstEnum && typeof value === "number" && !isFinite(value)) {
45956+
const result = evaluate(initializer, member);
45957+
if (result.value !== undefined) {
45958+
if (isConstEnum && typeof result.value === "number" && !isFinite(result.value)) {
4595545959
error(
4595645960
initializer,
45957-
isNaN(value) ?
45961+
isNaN(result.value) ?
4595845962
Diagnostics.const_enum_member_initializer_was_evaluated_to_disallowed_value_NaN :
4595945963
Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value,
4596045964
);
4596145965
}
45962-
else if (getIsolatedModules(compilerOptions) && typeof value === "string" && !isSyntacticallyString(initializer)) {
45966+
else if (getIsolatedModules(compilerOptions) && typeof result.value === "string" && !result.isSyntacticallyString) {
4596345967
error(
4596445968
initializer,
4596545969
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 {
4597645980
else {
4597745981
checkTypeAssignableTo(checkExpression(initializer), numberType, initializer, Diagnostics.Type_0_is_not_assignable_to_type_1_as_required_for_computed_enum_member_values);
4597845982
}
45979-
return value;
45980-
}
45981-
45982-
function isSyntacticallyNumericConstant(expr: Expression): boolean {
45983-
expr = skipOuterExpressions(expr);
45984-
switch (expr.kind) {
45985-
case SyntaxKind.PrefixUnaryExpression:
45986-
return isSyntacticallyNumericConstant((expr as PrefixUnaryExpression).operand);
45987-
case SyntaxKind.BinaryExpression:
45988-
return isSyntacticallyNumericConstant((expr as BinaryExpression).left) && isSyntacticallyNumericConstant((expr as BinaryExpression).right);
45989-
case SyntaxKind.NumericLiteral:
45990-
return true;
45991-
}
45992-
return false;
45983+
return result;
4599345984
}
4599445985

4599545986
function evaluateEntityNameExpression(expr: EntityNameExpression, location?: Declaration) {
4599645987
const symbol = resolveEntityName(expr, SymbolFlags.Value, /*ignoreErrors*/ true);
45997-
if (!symbol) return undefined;
45988+
if (!symbol) return evaluatorResult(/*value*/ undefined);
4599845989

4599945990
if (expr.kind === SyntaxKind.Identifier) {
4600045991
const identifier = expr;
4600145992
if (isInfinityOrNaNString(identifier.escapedText) && (symbol === getGlobalSymbol(identifier.escapedText, SymbolFlags.Value, /*diagnostic*/ undefined))) {
46002-
return +(identifier.escapedText);
45993+
// Technically we resolved a global lib file here, but the decision to treat this as numeric
45994+
// is more predicated on the fact that the single-file resolution *didn't* resolve to a
45995+
// different meaning of `Infinity` or `NaN`. Transpilers handle this no problem.
45996+
return evaluatorResult(+(identifier.escapedText), /*isSyntacticallyString*/ false);
4600345997
}
4600445998
}
4600545999

@@ -46009,9 +46003,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4600946003
if (isConstantVariable(symbol)) {
4601046004
const declaration = symbol.valueDeclaration;
4601146005
if (declaration && isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && (!location || declaration !== location && isBlockScopedNameDeclaredBeforeUse(declaration, location))) {
46012-
return evaluate(declaration.initializer, declaration);
46006+
const result = evaluate(declaration.initializer, declaration);
46007+
if (location && getSourceFileOfNode(location) !== getSourceFileOfNode(declaration)) {
46008+
return evaluatorResult(
46009+
result.value,
46010+
/*isSyntacticallyString*/ false,
46011+
/*resolvedOtherFiles*/ true,
46012+
);
46013+
}
46014+
return result;
4601346015
}
4601446016
}
46017+
return evaluatorResult(/*value*/ undefined);
4601546018
}
4601646019

4601746020
function evaluateElementAccessExpression(expr: ElementAccessExpression, location?: Declaration) {
@@ -46022,21 +46025,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4602246025
const name = escapeLeadingUnderscores(expr.argumentExpression.text);
4602346026
const member = rootSymbol.exports!.get(name);
4602446027
if (member) {
46028+
Debug.assert(getSourceFileOfNode(member.valueDeclaration) === getSourceFileOfNode(rootSymbol.valueDeclaration));
4602546029
return location ? evaluateEnumMember(expr, member, location) : getEnumMemberValue(member.valueDeclaration as EnumMember);
4602646030
}
4602746031
}
4602846032
}
46033+
return evaluatorResult(/*value*/ undefined);
4602946034
}
4603046035

4603146036
function evaluateEnumMember(expr: Expression, symbol: Symbol, location: Declaration) {
4603246037
const declaration = symbol.valueDeclaration;
4603346038
if (!declaration || declaration === location) {
4603446039
error(expr, Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(symbol));
46035-
return undefined;
46040+
return evaluatorResult(/*value*/ undefined);
4603646041
}
4603746042
if (!isBlockScopedNameDeclaredBeforeUse(declaration, location)) {
4603846043
error(expr, Diagnostics.A_member_initializer_in_a_enum_declaration_cannot_reference_members_declared_after_it_including_members_defined_in_other_enums);
46039-
return 0;
46044+
return evaluatorResult(/*value*/ 0);
4604046045
}
4604146046
return getEnumMemberValue(declaration as EnumMember);
4604246047
}
@@ -48668,9 +48673,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4866848673
return nodeLinks[nodeId]?.flags || 0;
4866948674
}
4867048675

48671-
function getEnumMemberValue(node: EnumMember): string | number | undefined {
48676+
function getEnumMemberValue(node: EnumMember): EvaluatorResult {
4867248677
computeEnumMemberValues(node.parent);
48673-
return getNodeLinks(node).enumMemberValue;
48678+
return getNodeLinks(node).enumMemberValue ?? evaluatorResult(/*value*/ undefined);
4867448679
}
4867548680

4867648681
function canHaveConstantValue(node: Node): node is EnumMember | AccessExpression {
@@ -48685,15 +48690,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4868548690

4868648691
function getConstantValue(node: EnumMember | AccessExpression): string | number | undefined {
4868748692
if (node.kind === SyntaxKind.EnumMember) {
48688-
return getEnumMemberValue(node);
48693+
return getEnumMemberValue(node).value;
4868948694
}
4869048695

4869148696
const symbol = getNodeLinks(node).resolvedSymbol;
4869248697
if (symbol && (symbol.flags & SymbolFlags.EnumMember)) {
4869348698
// inline property\index accesses only for const enums
4869448699
const member = symbol.valueDeclaration as EnumMember;
4869548700
if (isEnumConst(member.parent)) {
48696-
return getEnumMemberValue(member);
48701+
return getEnumMemberValue(member).value;
4869748702
}
4869848703
}
4869948704

@@ -49096,6 +49101,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4909649101
const node = getParseTreeNode(nodeIn, canHaveConstantValue);
4909749102
return node ? getConstantValue(node) : undefined;
4909849103
},
49104+
getEnumMemberValue: nodeIn => {
49105+
const node = getParseTreeNode(nodeIn, isEnumMember);
49106+
return node ? getEnumMemberValue(node) : undefined;
49107+
},
4909949108
collectLinkedAliases,
4910049109
getReferencedValueDeclaration,
4910149110
getReferencedValueDeclarations,

src/compiler/emitter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,7 @@ export const notImplementedResolver: EmitResolver = {
11031103
isEntityNameVisible: notImplemented,
11041104
// Returns the constant value this property access resolves to: notImplemented, or 'undefined' for a non-constant
11051105
getConstantValue: notImplemented,
1106+
getEnumMemberValue: notImplemented,
11061107
getReferencedValueDeclaration: notImplemented,
11071108
getReferencedValueDeclarations: notImplemented,
11081109
getTypeReferenceSerializationKind: notImplemented,

src/compiler/transformers/ts.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ import {
122122
isSimpleInlineableExpression,
123123
isSourceFile,
124124
isStatement,
125-
isSyntacticallyString,
126125
isTemplateLiteral,
127126
isTryStatement,
128127
JsxOpeningElement,
@@ -1915,15 +1914,16 @@ export function transformTypeScript(context: TransformationContext) {
19151914
// we pass false as 'generateNameForComputedPropertyName' for a backward compatibility purposes
19161915
// old emitter always generate 'expression' part of the name as-is.
19171916
const name = getExpressionForPropertyName(member, /*generateNameForComputedPropertyName*/ false);
1918-
const valueExpression = transformEnumMemberDeclarationValue(member);
1917+
const evaluated = resolver.getEnumMemberValue(member);
1918+
const valueExpression = transformEnumMemberDeclarationValue(member, evaluated?.value);
19191919
const innerAssignment = factory.createAssignment(
19201920
factory.createElementAccessExpression(
19211921
currentNamespaceContainerName,
19221922
name,
19231923
),
19241924
valueExpression,
19251925
);
1926-
const outerAssignment = isSyntacticallyString(valueExpression) ?
1926+
const outerAssignment = typeof evaluated?.value === "string" || evaluated?.isSyntacticallyString ?
19271927
innerAssignment :
19281928
factory.createAssignment(
19291929
factory.createElementAccessExpression(
@@ -1948,12 +1948,11 @@ export function transformTypeScript(context: TransformationContext) {
19481948
*
19491949
* @param member The enum member node.
19501950
*/
1951-
function transformEnumMemberDeclarationValue(member: EnumMember): Expression {
1952-
const value = resolver.getConstantValue(member);
1953-
if (value !== undefined) {
1954-
return typeof value === "string" ? factory.createStringLiteral(value) :
1955-
value < 0 ? factory.createPrefixUnaryExpression(SyntaxKind.MinusToken, factory.createNumericLiteral(-value)) :
1956-
factory.createNumericLiteral(value);
1951+
function transformEnumMemberDeclarationValue(member: EnumMember, constantValue: string | number | undefined): Expression {
1952+
if (constantValue !== undefined) {
1953+
return typeof constantValue === "string" ? factory.createStringLiteral(constantValue) :
1954+
constantValue < 0 ? factory.createPrefixUnaryExpression(SyntaxKind.MinusToken, factory.createNumericLiteral(-constantValue)) :
1955+
factory.createNumericLiteral(constantValue);
19571956
}
19581957
else {
19591958
enableSubstitutionForNonQualifiedEnumMembers();

src/compiler/types.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5638,6 +5638,7 @@ export interface EmitResolver {
56385638
isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult;
56395639
// Returns the constant value this property access resolves to, or 'undefined' for a non-constant
56405640
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined;
5641+
getEnumMemberValue(node: EnumMember): EvaluatorResult | undefined;
56415642
getReferencedValueDeclaration(reference: Identifier): Declaration | undefined;
56425643
getReferencedValueDeclarations(reference: Identifier): Declaration[] | undefined;
56435644
getTypeReferenceSerializationKind(typeName: EntityName, location?: Node): TypeReferenceSerializationKind;
@@ -5969,6 +5970,13 @@ export const enum NodeCheckFlags {
59695970
InCheckIdentifier = 1 << 22,
59705971
}
59715972

5973+
/** @internal */
5974+
export interface EvaluatorResult<T extends string | number | undefined = string | number | undefined> {
5975+
value: T;
5976+
isSyntacticallyString: boolean;
5977+
resolvedOtherFiles: boolean;
5978+
}
5979+
59725980
// dprint-ignore
59735981
/** @internal */
59745982
export interface NodeLinks {
@@ -5979,7 +5987,7 @@ export interface NodeLinks {
59795987
resolvedSymbol?: Symbol; // Cached name resolution result
59805988
resolvedIndexInfo?: IndexInfo; // Cached indexing info resolution result
59815989
effectsSignature?: Signature; // Signature with possible control flow effects
5982-
enumMemberValue?: string | number; // Constant value of enum member
5990+
enumMemberValue?: EvaluatorResult; // Constant value of enum member
59835991
isVisible?: boolean; // Is this node visible
59845992
containsArgumentsReference?: boolean; // Whether a function-like declaration contains an 'arguments' reference
59855993
hasReportedStatementInAmbientContext?: boolean; // Cache boolean if we report statements in ambient context
@@ -10086,6 +10094,6 @@ export interface Queue<T> {
1008610094

1008710095
/** @internal */
1008810096
export interface EvaluationResolver {
10089-
evaluateEntityNameExpression(expr: EntityNameExpression, location: Declaration | undefined): string | number | undefined;
10090-
evaluateElementAccessExpression(expr: ElementAccessExpression, location: Declaration | undefined): string | number | undefined;
10097+
evaluateEntityNameExpression(expr: EntityNameExpression, location: Declaration | undefined): EvaluatorResult;
10098+
evaluateElementAccessExpression(expr: ElementAccessExpression, location: Declaration | undefined): EvaluatorResult;
1009110099
}

0 commit comments

Comments
 (0)