Skip to content

Commit

Permalink
Use evaluator for isolatedModules enum restrictions (#57966)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbranch authored Apr 4, 2024
1 parent 83e3d6a commit 9f8a231
Show file tree
Hide file tree
Showing 18 changed files with 400 additions and 137 deletions.
103 changes: 56 additions & 47 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ import {
equateValues,
escapeLeadingUnderscores,
escapeString,
EvaluatorResult,
evaluatorResult,
every,
EvolvingArrayType,
ExclamationToken,
Expand Down Expand Up @@ -723,7 +725,6 @@ import {
isStringOrNumericLiteralLike,
isSuperCall,
isSuperProperty,
isSyntacticallyString,
isTaggedTemplateExpression,
isTemplateSpan,
isThisContainerOrFunctionBlock,
Expand Down Expand Up @@ -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) :
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -45922,44 +45923,47 @@ 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.
// Otherwise, it is assigned the value of the immediately preceding member plus one, and an error
// 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,
Expand All @@ -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);
}
}

Expand All @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand All @@ -48685,15 +48690,15 @@ 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;
if (symbol && (symbol.flags & SymbolFlags.EnumMember)) {
// 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;
}
}

Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 8 additions & 9 deletions src/compiler/transformers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ import {
isSimpleInlineableExpression,
isSourceFile,
isStatement,
isSyntacticallyString,
isTemplateLiteral,
isTryStatement,
JsxOpeningElement,
Expand Down Expand Up @@ -1915,15 +1914,16 @@ 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,
name,
),
valueExpression,
);
const outerAssignment = isSyntacticallyString(valueExpression) ?
const outerAssignment = typeof evaluated?.value === "string" || evaluated?.isSyntacticallyString ?
innerAssignment :
factory.createAssignment(
factory.createElementAccessExpression(
Expand All @@ -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();
Expand Down
14 changes: 11 additions & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Loading

0 comments on commit 9f8a231

Please sign in to comment.