Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

isolatedModules errors for non-literal enum initializers #56736

Merged
merged 11 commits into from
Mar 20, 2024

Conversation

frigus02
Copy link
Contributor

@frigus02 frigus02 commented Dec 11, 2023

Fixes #56153

Adds 2 new errors under isolatedModules only:

  • (1) When an enum initializer expression that isn't a string literal has a string type
  • (2) When an enum member without an initialization expression follows an enum member with an initialization expression that isn't a numeric literal

Ran the new checks across our code base. I wanted to check if we need to support computed values based on local identifiers. But we have so few errors, I don't think this is necessary:

Error Percent of TS projects failing with the new error
(1) identifier imported from another file 0.0059%
(1) identifier local to the same file 0.0025%
(2) 0.0002%
sum 0.0085%

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Dec 11, 2023
@frigus02 frigus02 marked this pull request as ready for review December 11, 2023 18:50
@jakebailey
Copy link
Member

@typescript-bot test top200
@typescript-bot user test this
@typescript-bot run dt

@typescript-bot perf test this
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 11, 2023

Heya @jakebailey, I've started to run the regular perf test suite on this PR at c51645a. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 11, 2023

Heya @jakebailey, I've started to run the diff-based top-repos suite on this PR at c51645a. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 11, 2023

Heya @jakebailey, I've started to run the parallelized Definitely Typed test suite on this PR at c51645a. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 11, 2023

Heya @jakebailey, I've started to run the diff-based user code test suite on this PR at c51645a. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 11, 2023

Heya @jakebailey, I've started to run the tarball bundle task on this PR at c51645a. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 11, 2023

Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/159054/artifacts?artifactName=tgz&fileId=FA18007074EDA659A8573C547724ABF39560091EFF9B4B80CE963603F3AF048F02&fileName=/typescript-5.4.0-insiders.20231211.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the user test suite comparing main and refs/pull/56736/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Package install failed"

Otherwise...

Something interesting changed - please have a look.

Details

puppeteer

packages/browsers/test/src/tsconfig.json

@typescript-bot
Copy link
Collaborator

Hey @jakebailey, the results of running the DT tests are ready.
Everything looks the same!
You can check the log here.

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the top-repos suite comparing main and refs/pull/56736/merge:

Everything looks good!

@jakebailey
Copy link
Member

@typescript-bot perf test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 11, 2023

Heya @jakebailey, I've started to run the regular perf test suite on this PR at c51645a. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

@jakebailey
The results of the perf run you requested are in!

Here they are:

Compiler

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Angular - node (v18.15.0, x64)
Memory used 295,432k (± 0.01%) 295,389k (± 0.00%) -43k (- 0.01%) 295,372k 295,409k p=0.031 n=6
Parse Time 2.64s (± 0.37%) 2.65s (± 0.21%) ~ 2.64s 2.65s p=0.201 n=6
Bind Time 0.82s (± 0.63%) 0.82s (± 0.00%) ~ 0.82s 0.82s p=0.174 n=6
Check Time 8.13s (± 0.20%) 8.15s (± 0.41%) ~ 8.11s 8.20s p=0.292 n=6
Emit Time 7.08s (± 0.34%) 7.09s (± 0.32%) ~ 7.05s 7.11s p=0.288 n=6
Total Time 18.67s (± 0.13%) 18.71s (± 0.25%) ~ 18.63s 18.77s p=0.143 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used 192,845k (± 1.24%) 194,751k (± 1.49%) ~ 191,354k 197,322k p=0.688 n=6
Parse Time 1.35s (± 1.11%) 1.36s (± 1.11%) ~ 1.34s 1.38s p=0.737 n=6
Bind Time 0.72s (± 0.00%) 0.72s (± 0.00%) ~ 0.72s 0.72s p=1.000 n=6
Check Time 9.26s (± 0.30%) 9.27s (± 0.37%) ~ 9.21s 9.30s p=0.332 n=6
Emit Time 2.63s (± 0.99%) 2.62s (± 0.20%) ~ 2.61s 2.62s p=0.368 n=6
Total Time 13.96s (± 0.36%) 13.97s (± 0.27%) ~ 13.90s 14.00s p=0.747 n=6
Monaco - node (v18.15.0, x64)
Memory used 347,374k (± 0.00%) 347,382k (± 0.00%) ~ 347,366k 347,393k p=0.335 n=6
Parse Time 2.46s (± 0.76%) 2.46s (± 0.48%) ~ 2.44s 2.47s p=1.000 n=6
Bind Time 0.92s (± 0.56%) 0.93s (± 0.88%) ~ 0.92s 0.94s p=0.523 n=6
Check Time 6.91s (± 0.53%) 6.90s (± 0.24%) ~ 6.87s 6.91s p=1.000 n=6
Emit Time 4.05s (± 0.46%) 4.05s (± 0.56%) ~ 4.03s 4.09s p=0.459 n=6
Total Time 14.34s (± 0.43%) 14.34s (± 0.14%) ~ 14.31s 14.36s p=0.462 n=6
TFS - node (v18.15.0, x64)
Memory used 302,636k (± 0.00%) 302,647k (± 0.00%) ~ 302,627k 302,666k p=0.230 n=6
Parse Time 1.99s (± 1.02%) 2.00s (± 1.84%) ~ 1.94s 2.03s p=0.625 n=6
Bind Time 1.01s (± 1.25%) 1.01s (± 1.53%) ~ 1.00s 1.04s p=0.741 n=6
Check Time 6.27s (± 0.22%) 6.27s (± 0.55%) ~ 6.24s 6.33s p=0.292 n=6
Emit Time 3.57s (± 0.42%) 3.57s (± 0.51%) ~ 3.55s 3.60s p=0.805 n=6
Total Time 12.84s (± 0.17%) 12.85s (± 0.40%) ~ 12.77s 12.91s p=0.686 n=6
material-ui - node (v18.15.0, x64)
Memory used 506,784k (± 0.01%) 506,797k (± 0.01%) ~ 506,753k 506,839k p=0.471 n=6
Parse Time 2.58s (± 0.68%) 2.58s (± 0.38%) ~ 2.57s 2.59s p=0.548 n=6
Bind Time 0.99s (± 1.18%) 0.99s (± 0.76%) ~ 0.98s 1.00s p=0.734 n=6
Check Time 16.97s (± 0.26%) 16.89s (± 0.36%) -0.08s (- 0.45%) 16.84s 17.01s p=0.045 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 20.54s (± 0.24%) 20.46s (± 0.30%) -0.08s (- 0.39%) 20.40s 20.57s p=0.045 n=6
xstate - node (v18.15.0, x64)
Memory used 512,765k (± 0.01%) 512,820k (± 0.01%) ~ 512,740k 512,914k p=0.230 n=6
Parse Time 3.28s (± 0.32%) 3.27s (± 0.17%) ~ 3.27s 3.28s p=1.000 n=6
Bind Time 1.53s (± 0.27%) 1.54s (± 0.41%) +0.01s (+ 0.54%) 1.53s 1.55s p=0.033 n=6
Check Time 2.80s (± 0.18%) 2.81s (± 0.30%) +0.01s (+ 0.42%) 2.81s 2.83s p=0.020 n=6
Emit Time 0.07s (± 0.00%) 0.07s (± 0.00%) ~ 0.07s 0.07s p=1.000 n=6
Total Time 7.68s (± 0.23%) 7.70s (± 0.14%) +0.02s (+ 0.28%) 7.69s 7.72s p=0.027 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Angular - node (v18.15.0, x64)
  • Compiler-Unions - node (v18.15.0, x64)
  • Monaco - node (v18.15.0, x64)
  • TFS - node (v18.15.0, x64)
  • material-ui - node (v18.15.0, x64)
  • xstate - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

tsserver

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,340ms (± 0.91%) 2,343ms (± 0.78%) ~ 2,319ms 2,363ms p=0.810 n=6
Req 2 - geterr 5,407ms (± 1.63%) 5,416ms (± 1.53%) ~ 5,328ms 5,530ms p=0.809 n=6
Req 3 - references 326ms (± 1.61%) 328ms (± 1.35%) ~ 320ms 333ms p=0.809 n=6
Req 4 - navto 277ms (± 1.19%) 277ms (± 1.20%) ~ 273ms 280ms p=1.000 n=6
Req 5 - completionInfo count 1,356 (± 0.00%) 1,356 (± 0.00%) ~ 1,356 1,356 p=1.000 n=6
Req 5 - completionInfo 87ms (± 6.13%) 87ms (± 6.29%) ~ 82ms 93ms p=0.934 n=6
CompilerTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,476ms (± 0.98%) 2,468ms (± 0.66%) ~ 2,442ms 2,490ms p=0.369 n=6
Req 2 - geterr 4,133ms (± 1.99%) 4,161ms (± 1.80%) ~ 4,059ms 4,219ms p=0.575 n=6
Req 3 - references 340ms (± 1.65%) 339ms (± 1.64%) ~ 334ms 348ms p=0.624 n=6
Req 4 - navto 286ms (± 1.16%) 286ms (± 1.32%) ~ 283ms 293ms p=0.803 n=6
Req 5 - completionInfo count 1,518 (± 0.00%) 1,518 (± 0.00%) ~ 1,518 1,518 p=1.000 n=6
Req 5 - completionInfo 83ms (± 7.26%) 81ms (± 7.31%) ~ 77ms 89ms p=0.676 n=6
xstateTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,597ms (± 0.52%) 2,600ms (± 0.58%) ~ 2,575ms 2,616ms p=0.575 n=6
Req 2 - geterr 1,707ms (± 1.93%) 1,672ms (± 2.35%) -35ms (- 2.04%) 1,639ms 1,723ms p=0.045 n=6
Req 3 - references 103ms (± 0.87%) 119ms (± 6.32%) 🔻+16ms (+15.86%) 104ms 123ms p=0.007 n=6
Req 4 - navto 365ms (± 0.21%) 364ms (± 0.22%) ~ 363ms 365ms p=0.383 n=6
Req 5 - completionInfo count 2,073 (± 0.00%) 2,073 (± 0.00%) ~ 2,073 2,073 p=1.000 n=6
Req 5 - completionInfo 307ms (± 1.39%) 305ms (± 2.10%) ~ 295ms 313ms p=0.688 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • CompilerTSServer - node (v18.15.0, x64)
  • Compiler-UnionsTSServer - node (v18.15.0, x64)
  • xstateTSServer - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
tsc-startup - node (v18.15.0, x64)
Execution time 152.96ms (± 0.22%) 153.06ms (± 0.20%) +0.10ms (+ 0.06%) 151.94ms 157.10ms p=0.001 n=600
tsserver-startup - node (v18.15.0, x64)
Execution time 228.19ms (± 0.17%) 227.90ms (± 0.14%) -0.29ms (- 0.13%) 226.63ms 231.83ms p=0.000 n=600
tsserverlibrary-startup - node (v18.15.0, x64)
Execution time 229.68ms (± 0.18%) 229.68ms (± 0.27%) ~ 228.11ms 249.88ms p=0.242 n=600
typescript-startup - node (v18.15.0, x64)
Execution time 229.71ms (± 0.17%) 229.67ms (± 0.18%) ~ 227.99ms 234.66ms p=0.132 n=600
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • tsc-startup - node (v18.15.0, x64)
  • tsserver-startup - node (v18.15.0, x64)
  • tsserverlibrary-startup - node (v18.15.0, x64)
  • typescript-startup - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

Copy link
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @frigus02!

Whatever we do in evaluatesToStringLiteral and evaluatesToNumericLiteral here will become the minimum requirement for all third-party compilers to implement to guide their own emit. I think what you have is pretty reasonable, but we should get the team, and other compiler maintainers, to weigh in. @nicolo-ribaudo @evanw @kdy1

src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
@jakebailey
Copy link
Member

At least for esbuild, my understanding is that it doesn't really care about enum stuff that violates isolatedModules; for example, TS itself uses exported const enums and to do so disables isolatedModules and esbuild will happily inline them (which is good).

The new test cases don't fail currently in esbuild, so I'm not sure there's actually any work to be done outside of TS if this PR just intends to add more errors.

Copy link
Contributor Author

@frigus02 frigus02 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, Andrew and Jake. I addressed the comments.

This implementation is quite thorough. I wanted to avoid too many new errors in our code base when turning on isolatedModules, so I tried to support as much as possible. I'm happy to simplify it if you like, though.

src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/diagnosticMessages.json Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
@frigus02
Copy link
Contributor Author

It's probably too late to get this into 5.4 now, is it? That's alright.

Rather than introducing 2 new functions, which duplicate some of the `evaluate` logic, we could also extend `evaluate` to return whether the evaluation was purely syntactic, e.g.:
diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index b0ea7ea9ff..78cab2d428 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -38687,7 +38687,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 && evaluateTemplateExpression(node);
+        const evaluated = node.parent.kind !== SyntaxKind.TaggedTemplateExpression && evaluateTemplateExpression(node).value;
         return evaluated ? getFreshTypeOfLiteralType(getStringLiteralType(evaluated)) : stringType;
     }
 
@@ -45010,15 +45010,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
         if (!(nodeLinks.flags & NodeCheckFlags.EnumValuesComputed)) {
             nodeLinks.flags |= NodeCheckFlags.EnumValuesComputed;
             let autoValue: number | undefined = 0;
+            let previous: EnumMember | undefined;
             for (const member of node.members) {
-                const value = computeMemberValue(member, autoValue);
+                const value = computeMemberValue(member, autoValue, previous);
                 getNodeLinks(member).enumMemberValue = value;
                 autoValue = typeof value === "number" ? value + 1 : undefined;
+                previous = member;
             }
         }
     }
 
-    function computeMemberValue(member: EnumMember, autoValue: number | undefined) {
+    function computeMemberValue(member: EnumMember, autoValue: number | undefined, previous: EnumMember | undefined) {
         if (isComputedNonLiteralName(member.name)) {
             error(member.name, Diagnostics.Computed_property_names_are_not_allowed_in_enums);
         }
@@ -45040,26 +45042,41 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
         // 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) {
-            return autoValue;
-        }
+        if (autoValue === undefined) {
             error(member.name, Diagnostics.Enum_member_must_have_initializer);
             return undefined;
         }
+        if (getIsolatedModules(compilerOptions) && previous?.initializer) {
+            const previousResult = evaluate(previous.initializer);
+            if (typeof previousResult.value !== "number" || !previousResult.syntactic) {
+                error(
+                    member.name,
+                    Diagnostics.Enum_member_following_a_non_literal_numeric_member_must_have_an_initializer_when_isolatedModules_is_enabled,
+                );
+            }
+        }
+        return autoValue;
+    }
 
     function computeConstantValue(member: EnumMember): string | number | undefined {
         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 result.value === "string" && !result.syntactic) {
+                error(
+                    initializer,
+                    Diagnostics.A_string_member_initializer_in_a_enum_declaration_can_only_use_constant_expressions_when_isolatedModules_is_enabled,
+                );
+            }
         }
         else if (isConstEnum) {
             error(initializer, Diagnostics.const_enum_member_initializers_must_be_constant_expressions);
@@ -45070,13 +45087,23 @@ 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;
+        return result.value;
+    }
+
+    interface EvaluationResult<T> {
+        value: T;
+        syntactic: boolean;
     }
 
-    function evaluate(expr: Expression, location?: Declaration): string | number | undefined {
+    function evaluate(expr: Expression, location?: Declaration): EvaluationResult<string | number | undefined> {
+        let syntactic = true;
+        let value = evaluateInternal(expr, location);
+        return {value, syntactic};
+
+        function evaluateInternal(expr: Expression, location?: Declaration): string | number | undefined {
             switch (expr.kind) {
                 case SyntaxKind.PrefixUnaryExpression:
-                const value = evaluate((expr as PrefixUnaryExpression).operand, location);
+                    const value = evaluateInternal((expr as PrefixUnaryExpression).operand, location);
                     if (typeof value === "number") {
                         switch ((expr as PrefixUnaryExpression).operator) {
                             case SyntaxKind.PlusToken:
@@ -45089,8 +45116,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
                     }
                     break;
                 case SyntaxKind.BinaryExpression:
-                const left = evaluate((expr as BinaryExpression).left, location);
-                const right = evaluate((expr as BinaryExpression).right, location);
+                    const left = evaluateInternal((expr as BinaryExpression).left, location);
+                    const right = evaluateInternal((expr as BinaryExpression).right, location);
                     if (typeof left === "number" && typeof right === "number") {
                         switch ((expr as BinaryExpression).operatorToken.kind) {
                             case SyntaxKind.BarToken:
@@ -45131,13 +45158,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
                 case SyntaxKind.NoSubstitutionTemplateLiteral:
                     return (expr as StringLiteralLike).text;
                 case SyntaxKind.TemplateExpression:
-                return evaluateTemplateExpression(expr as TemplateExpression, location);
+                    const templateResult = evaluateTemplateExpression(expr as TemplateExpression, location);
+                    syntactic &&= templateResult.syntactic;
+                    return templateResult.value;
                 case SyntaxKind.NumericLiteral:
                     checkGrammarNumericLiteral(expr as NumericLiteral);
                     return +(expr as NumericLiteral).text;
                 case SyntaxKind.ParenthesizedExpression:
-                return evaluate((expr as ParenthesizedExpression).expression, location);
+                    return evaluateInternal((expr as ParenthesizedExpression).expression, location);
                 case SyntaxKind.Identifier: {
+                    syntactic = false;
                     const identifier = expr as Identifier;
                     if (isInfinityOrNaNString(identifier.escapedText) && (resolveEntityName(identifier, SymbolFlags.Value, /*ignoreErrors*/ true) === getGlobalSymbol(identifier.escapedText, SymbolFlags.Value, /*diagnostic*/ undefined))) {
                         return +(identifier.escapedText);
@@ -45145,6 +45175,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
                     // falls through
                 }
                 case SyntaxKind.PropertyAccessExpression:
+                    syntactic = false;
                     if (isEntityNameExpression(expr)) {
                         const symbol = resolveEntityName(expr, SymbolFlags.Value, /*ignoreErrors*/ true);
                         if (symbol) {
@@ -45154,13 +45185,14 @@ 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);
+                                    return evaluateInternal(declaration.initializer, declaration);
                                 }
                             }
                         }
                     }
                     break;
                 case SyntaxKind.ElementAccessExpression:
+                    syntactic = false;
                     const root = (expr as ElementAccessExpression).expression;
                     if (isEntityNameExpression(root) && isStringLiteralLike((expr as ElementAccessExpression).argumentExpression)) {
                         const rootSymbol = resolveEntityName(root, SymbolFlags.Value, /*ignoreErrors*/ true);
@@ -45176,6 +45208,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
             }
             return undefined;
         }
+    }
 
     function evaluateEnumMember(expr: Expression, symbol: Symbol, location: Declaration) {
         const declaration = symbol.valueDeclaration;
@@ -45190,17 +45223,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
         return getEnumMemberValue(declaration as EnumMember);
     }
 
-    function evaluateTemplateExpression(expr: TemplateExpression, location?: Declaration) {
-        let result = expr.head.text;
+    function evaluateTemplateExpression(expr: TemplateExpression, location?: Declaration): EvaluationResult<string | undefined> {
+        let value = expr.head.text;
+        let syntactic = true;
         for (const span of expr.templateSpans) {
-            const value = evaluate(span.expression, location);
-            if (value === undefined) {
-                return undefined;
+            const evaluateResult = evaluate(span.expression, location);
+            syntactic &&= evaluateResult.syntactic;
+            if (evaluateResult.value === undefined) {
+                return {value: undefined, syntactic};
             }
-            result += value;
-            result += span.literal.text;
+            value += evaluateResult.value;
+            value += span.literal.text;
         }
-        return result;
+        return {value, syntactic};
     }
 
     function checkEnumDeclaration(node: EnumDeclaration) {

@andrewbranch
Copy link
Member

Yeah, sorry for the delay. I was holding off close to the 5.4 beta when I noticed that there would be significant merge conflicts with #53463, but it turned out that didn’t go into the beta either.

@frigus02
Copy link
Contributor Author

frigus02 commented Mar 7, 2024

Hi @andrewbranch. Congrats to the TS 5.4 release! Do you think we can merge some version of this PR in time for 5.5? 😊

@frigus02
Copy link
Contributor Author

Thank you for #57686, Andrew. I update the PR and it's much smaller now.

@@ -7976,5 +7976,13 @@
"'await using' statements cannot be used inside a class static block.": {
"category": "Error",
"code": 18054
},
"A string member initializer in a enum declaration can only use constant expressions when 'isolatedModules' is enabled.": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paraphrasing a suggestion from @DanielRosenwasser:

Suggested change
"A string member initializer in a enum declaration can only use constant expressions when 'isolatedModules' is enabled.": {
"'{0}' has a string type, but must have syntactically recognizable string syntax when 'isolatedModules' is enabled.": {

We could totally do a quick fix to wrap it in a template literal but I think this error is going to be so niche that I’m not sure it’s worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the suggested error message. But what do I substitute {0} with? The initializer can be a relatively complex expression. Is there a utility function to stringify that? Or should I use the enum member name or even the computed value?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to include that; Daniel’s suggestion was 'EnumName.MemberName'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Do we want to handle member names that are not valid identifier? E.g.:

import {bar} from './bar';
enum Foo { ['not an identifier'] = bar }
// 'Foo.not an identifier' has a string type, ...
// 'Foo["not an identifier"]' has a string type, ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the error message and ignored the non-identifier case for now, because it seems like an extremely rare case. That means it would print 'Foo.not an identifier' in the error message.

@andrewbranch
Copy link
Member

@typescript-bot test top200

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 19, 2024

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top200 ✅ Started ✅ Results

@typescript-bot
Copy link
Collaborator

@andrewbranch Here are the results of running the top-repos suite comparing main and refs/pull/56736/merge:

Everything looks good!

Copy link
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your patience @frigus02!

@andrewbranch andrewbranch merged commit f70b068 into microsoft:main Mar 20, 2024
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Certain resolution-dependent enum emit isn't correctly flagged as an error under isolatedModules
4 participants