Skip to content

Commit

Permalink
update enum constant folding for TypeScript 5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 27, 2023
1 parent 73523d9 commit be94d37
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 21 deletions.
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,64 @@
Foo = class <out T> { };
```

* Update `enum` constant folding for TypeScript 5.0

TypeScript 5.0 contains an [updated definition of what it considers a constant expression](https://github.com/microsoft/TypeScript/pull/50528):

> An expression is considered a *constant expression* if it is
>
> * a number or string literal,
> * a unary `+`, `-`, or `~` applied to a numeric constant expression,
> * a binary `+`, `-`, `*`, `/`, `%`, `**`, `<<`, `>>`, `>>>`, `|`, `&`, `^` applied to two numeric constant expressions,
> * a binary `+` applied to two constant expressions whereof at least one is a string,
> * a template expression where each substitution expression is a constant expression,
> * a parenthesized constant expression,
> * a dotted name (e.g. `x.y.z`) that references a `const` variable with a constant expression initializer and no type annotation,
> * a dotted name that references an enum member with an enum literal type, or
> * a dotted name indexed by a string literal (e.g. `x.y["z"]`) that references an enum member with an enum literal type.
This impacts esbuild's implementation of TypeScript's `const enum` feature. With this release, esbuild will now attempt to follow these new rules. For example, you can now initialize an `enum` member with a template literal expression that contains a numeric constant:

```ts
// Original input
const enum Example {
COUNT = 100,
ERROR = `Expected ${COUNT} items`,
}
console.log(
Example.COUNT,
Example.ERROR,
)

// Old output (with --tree-shaking=true)
var Example = /* @__PURE__ */ ((Example2) => {
Example2[Example2["COUNT"] = 100] = "COUNT";
Example2[Example2["ERROR"] = `Expected ${100 /* COUNT */} items`] = "ERROR";
return Example2;
})(Example || {});
console.log(
100 /* COUNT */,
Example.ERROR
);

// New output (with --tree-shaking=true)
console.log(
100 /* COUNT */,
"Expected 100 items" /* ERROR */
);
```

These rules are not followed exactly due to esbuild's limitations. The rule about dotted references to `const` variables is not followed both because esbuild's enum processing is done in an isolated module setting and because doing so would potentially require esbuild to use a type system, which it doesn't have. For example:

```ts
// The TypeScript compiler inlines this but esbuild doesn't:
declare const x = 'foo'
const enum Foo { X = x }
console.log(Foo.X)
```

Also, the rule that requires converting numbers to a string currently only followed for 32-bit signed integers and non-finite numbers. This is done to avoid accidentally introducing a bug if esbuild's number-to-string operation doesn't exactly match the behavior of a real JavaScript VM. Currently esbuild's number-to-string constant folding is conservative for safety.

* Forbid definite assignment assertion operators on class methods

In TypeScript, class methods can use the `?` optional property operator but not the `!` definite assignment assertion operator (while class fields can use both):
Expand Down
174 changes: 174 additions & 0 deletions internal/bundler_tests/bundler_ts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2085,3 +2085,177 @@ NOTE: Node's package format requires that CommonJS files in a "type": "module" p
`,
})
}

func TestEnumRulesFrom_TypeScript_5_0(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
"/supported.ts": `
// From https://github.com/microsoft/TypeScript/pull/50528:
// "An expression is considered a constant expression if it is
const enum Foo {
// a number or string literal,
X0 = 123,
X1 = 'x',
// a unary +, -, or ~ applied to a numeric constant expression,
X2 = +1,
X3 = -2,
X4 = ~3,
// a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions,
X5 = 1 + 2,
X6 = 1 - 2,
X7 = 2 * 3,
X8 = 1 / 2,
X9 = 3 % 2,
X10 = 2 ** 3,
X11 = 1 << 2,
X12 = -9 >> 1,
X13 = -9 >>> 1,
X14 = 5 | 12,
X15 = 5 & 12,
X16 = 5 ^ 12,
// a binary + applied to two constant expressions whereof at least one is a string,
X17 = 'x' + 0,
X18 = 0 + 'x',
X19 = 'x' + 'y',
X20 = '' + NaN,
X21 = '' + Infinity,
X22 = '' + -Infinity,
X23 = '' + -0,
// a template expression where each substitution expression is a constant expression,
X24 = ` + "`A${0}B${'x'}C${1 + 3 - 4 / 2 * 5 ** 6}D`" + `,
// a parenthesized constant expression,
X25 = (321),
// a dotted name (e.g. x.y.z) that references a const variable with a constant expression initializer and no type annotation,
/* (we don't implement this one) */
// a dotted name that references an enum member with an enum literal type, or
X26 = X0,
X27 = X0 + 'x',
X28 = 'x' + X0,
X29 = ` + "`a${X0}b`" + `,
X30 = Foo.X0,
X31 = Foo.X0 + 'x',
X32 = 'x' + Foo.X0,
X33 = ` + "`a${Foo.X0}b`" + `,
// a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type."
X34 = X1,
X35 = X1 + 'y',
X36 = 'y' + X1,
X37 = ` + "`a${X1}b`" + `,
X38 = Foo['X1'],
X39 = Foo['X1'] + 'y',
X40 = 'y' + Foo['X1'],
X41 = ` + "`a${Foo['X1']}b`" + `,
}
console.log(
// a number or string literal,
Foo.X0,
Foo.X1,
// a unary +, -, or ~ applied to a numeric constant expression,
Foo.X2,
Foo.X3,
Foo.X4,
// a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions,
Foo.X5,
Foo.X6,
Foo.X7,
Foo.X8,
Foo.X9,
Foo.X10,
Foo.X11,
Foo.X12,
Foo.X13,
Foo.X14,
Foo.X15,
Foo.X16,
// a template expression where each substitution expression is a constant expression,
Foo.X17,
Foo.X18,
Foo.X19,
Foo.X20,
Foo.X21,
Foo.X22,
Foo.X23,
// a template expression where each substitution expression is a constant expression,
Foo.X24,
// a parenthesized constant expression,
Foo.X25,
// a dotted name that references an enum member with an enum literal type, or
Foo.X26,
Foo.X27,
Foo.X28,
Foo.X29,
Foo.X30,
Foo.X31,
Foo.X32,
Foo.X33,
// a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type."
Foo.X34,
Foo.X35,
Foo.X36,
Foo.X37,
Foo.X38,
Foo.X39,
Foo.X40,
Foo.X41,
)
`,
"/not-supported.ts": `
const enum NonIntegerNumberToString {
SUPPORTED = '' + 1,
UNSUPPORTED = '' + 1.5,
}
console.log(
NonIntegerNumberToString.SUPPORTED,
NonIntegerNumberToString.UNSUPPORTED,
)
const enum OutOfBoundsNumberToString {
SUPPORTED = '' + 1_000_000_000,
UNSUPPORTED = '' + 1_000_000_000_000,
}
console.log(
OutOfBoundsNumberToString.SUPPORTED,
OutOfBoundsNumberToString.UNSUPPORTED,
)
const enum TemplateExpressions {
// TypeScript enums don't handle any of these
NULL = '' + null,
TRUE = '' + true,
FALSE = '' + false,
BIGINT = '' + 123n,
}
console.log(
TemplateExpressions.NULL,
TemplateExpressions.TRUE,
TemplateExpressions.FALSE,
TemplateExpressions.BIGINT,
)
`,
},
entryPaths: []string{
"/supported.ts",
"/not-supported.ts",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
91 changes: 91 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_ts.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,94 @@
TestEnumRulesFrom_TypeScript_5_0
---------- /out/supported.js ----------
// supported.ts
console.log(
// a number or string literal,
123 /* X0 */,
"x" /* X1 */,
// a unary +, -, or ~ applied to a numeric constant expression,
1 /* X2 */,
-2 /* X3 */,
-4 /* X4 */,
// a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions,
3 /* X5 */,
-1 /* X6 */,
6 /* X7 */,
0.5 /* X8 */,
1 /* X9 */,
8 /* X10 */,
4 /* X11 */,
-5 /* X12 */,
2147483643 /* X13 */,
13 /* X14 */,
4 /* X15 */,
9 /* X16 */,
// a template expression where each substitution expression is a constant expression,
"x0" /* X17 */,
"0x" /* X18 */,
"xy" /* X19 */,
"NaN" /* X20 */,
"Infinity" /* X21 */,
"-Infinity" /* X22 */,
"0" /* X23 */,
// a template expression where each substitution expression is a constant expression,
"A0BxC-31246D" /* X24 */,
// a parenthesized constant expression,
321 /* X25 */,
// a dotted name that references an enum member with an enum literal type, or
123 /* X26 */,
"123x" /* X27 */,
"x123" /* X28 */,
"a123b" /* X29 */,
123 /* X30 */,
"123x" /* X31 */,
"x123" /* X32 */,
"a123b" /* X33 */,
// a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type."
"x" /* X34 */,
"xy" /* X35 */,
"yx" /* X36 */,
"axb" /* X37 */,
"x" /* X38 */,
"xy" /* X39 */,
"yx" /* X40 */,
"axb" /* X41 */
);

---------- /out/not-supported.js ----------
// not-supported.ts
var NonIntegerNumberToString = ((NonIntegerNumberToString2) => {
NonIntegerNumberToString2["SUPPORTED"] = "1";
NonIntegerNumberToString2[NonIntegerNumberToString2["UNSUPPORTED"] = "" + 1.5] = "UNSUPPORTED";
return NonIntegerNumberToString2;
})(NonIntegerNumberToString || {});
console.log(
"1" /* SUPPORTED */,
NonIntegerNumberToString.UNSUPPORTED
);
var OutOfBoundsNumberToString = ((OutOfBoundsNumberToString2) => {
OutOfBoundsNumberToString2["SUPPORTED"] = "1000000000";
OutOfBoundsNumberToString2[OutOfBoundsNumberToString2["UNSUPPORTED"] = "" + 1e12] = "UNSUPPORTED";
return OutOfBoundsNumberToString2;
})(OutOfBoundsNumberToString || {});
console.log(
"1000000000" /* SUPPORTED */,
OutOfBoundsNumberToString.UNSUPPORTED
);
var TemplateExpressions = ((TemplateExpressions2) => {
TemplateExpressions2[TemplateExpressions2["NULL"] = "" + null] = "NULL";
TemplateExpressions2[TemplateExpressions2["TRUE"] = "" + true] = "TRUE";
TemplateExpressions2[TemplateExpressions2["FALSE"] = "" + false] = "FALSE";
TemplateExpressions2[TemplateExpressions2["BIGINT"] = "" + 123n] = "BIGINT";
return TemplateExpressions2;
})(TemplateExpressions || {});
console.log(
TemplateExpressions.NULL,
TemplateExpressions.TRUE,
TemplateExpressions.FALSE,
TemplateExpressions.BIGINT
);

================================================================================
TestExportTypeIssue379
---------- /out.js ----------
// a.ts
Expand Down
Loading

0 comments on commit be94d37

Please sign in to comment.