diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a850b6f5a..bc82f2e24d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ ## Unreleased +* Transform private names ([#47](https://github.com/evanw/esbuild/issues/47)) + + Private names are an access control mechanism for classes. They begin with a `#` and are not accessible outside of the class they are declared in. Support for parsing this syntax was added in esbuild version 0.4.9 but the syntax was passed through unmodified, meaning it didn't work in older browsers. + + This release adds support for transforming private fields and private methods for older browsers that don't support this syntax. This transform uses `WeakMap` and `WeakSet` to preserve the privacy properties of this feature, similar to the corresponding transforms in the Babel and TypeScript compilers. + + This code: + + ```js + class Counter { + #count = 1 + get value() { return this.#count } + increment() { ++this.#count } + } + ``` + + is transformed to this code when using `--target=es2020`: + + ```js + var _count; + class Counter { + constructor() { _count.set(this, 1); } + get value() { return __privateGet(this, _count); } + increment() { __privateSet(this, _count, +__privateGet(this, _count) + 1); } + } + _count = new WeakMap(); + ``` + + Note that as far as I know, most modern JavaScript engines (V8, JavaScriptCore, and SpiderMonkey but not ChakraCore) may not have good performance characteristics for large `WeakMap` and `WeakSet` objects. Creating many instances of classes with private fields or private methods with this syntax transform active may cause a lot of overhead for the garbage collector. This is because engines other than ChakraCore store weak values in an actual map object instead of as hidden properties on the keys themselves, and large map objects can cause performance issues with garbage collection. + * Fix re-exports when bundling This is similar to the fix for re-exports in version 0.5.6 except that it applies when bundling, instead of just when transforming. It needed to be fixed differently because of how cross-file linking works when bundling. diff --git a/README.md b/README.md index bd2026a5bd2..580e5f8a337 100644 --- a/README.md +++ b/README.md @@ -97,16 +97,20 @@ These syntax features are always transformed for older browsers: These syntax features are conditionally transformed for older browsers depending on the configured language target: -| Syntax transform | Transformed when `--target` is below | Example | -|-------------------------------------------------------------------------------------|--------------------------------------|----------------------| -| [Exponentiation operator](https://github.com/tc39/proposal-exponentiation-operator) | `es2016` | `a ** b` | -| [Spread properties](https://github.com/tc39/proposal-object-rest-spread) | `es2018` | `let x = {...y}` | -| [Optional catch binding](https://github.com/tc39/proposal-optional-catch-binding) | `es2019` | `try {} catch {}` | -| [Optional chaining](https://github.com/tc39/proposal-optional-chaining) | `es2020` | `a?.b` | -| [Nullish coalescing](https://github.com/tc39/proposal-nullish-coalescing) | `es2020` | `a ?? b` | -| [Class instance fields](https://github.com/tc39/proposal-class-fields) | `esnext` | `class { x }` | -| [Static class fields](https://github.com/tc39/proposal-static-class-features) | `esnext` | `class { static x }` | -| [Logical assignment operators](https://github.com/tc39/proposal-logical-assignment) | `esnext` | `a ??= b` | +| Syntax transform | Transformed when `--target` is below | Example | +|-------------------------------------------------------------------------------------|--------------------------------------|----------------------------| +| [Exponentiation operator](https://github.com/tc39/proposal-exponentiation-operator) | `es2016` | `a ** b` | +| [Spread properties](https://github.com/tc39/proposal-object-rest-spread) | `es2018` | `let x = {...y}` | +| [Optional catch binding](https://github.com/tc39/proposal-optional-catch-binding) | `es2019` | `try {} catch {}` | +| [Optional chaining](https://github.com/tc39/proposal-optional-chaining) | `es2020` | `a?.b` | +| [Nullish coalescing](https://github.com/tc39/proposal-nullish-coalescing) | `es2020` | `a ?? b` | +| [Class instance fields](https://github.com/tc39/proposal-class-fields) | `esnext` | `class { x }` | +| [Static class fields](https://github.com/tc39/proposal-static-class-features) | `esnext` | `class { static x }` | +| [Private instance methods](https://github.com/tc39/proposal-private-methods) | `esnext` | `class { #x() {} }` | +| [Private instance fields](https://github.com/tc39/proposal-class-fields) | `esnext` | `class { #x }` | +| [Private static methods](https://github.com/tc39/proposal-static-class-features) | `esnext` | `class { static #x() {} }` | +| [Private static fields](https://github.com/tc39/proposal-static-class-features) | `esnext` | `class { static #x }` | +| [Logical assignment operators](https://github.com/tc39/proposal-logical-assignment) | `esnext` | `a ??= b` | These syntax features are currently always passed through un-transformed: @@ -118,10 +122,6 @@ These syntax features are currently always passed through un-transformed: | [Async generators](https://github.com/tc39/proposal-async-iteration) | `es2018` | `async function* foo() {}` | | [BigInt](https://github.com/tc39/proposal-bigint) | `es2020` | `123n` | | [Hashbang grammar](https://github.com/tc39/proposal-hashbang) | `esnext` | `#!/usr/bin/env node` | -| [Private instance methods](https://github.com/tc39/proposal-private-methods) | `esnext` | `class { #x() {} }` | -| [Private instance fields](https://github.com/tc39/proposal-class-fields) | `esnext` | `class { #x }` | -| [Private static methods](https://github.com/tc39/proposal-static-class-features) | `esnext` | `class { static #x() {} }` | -| [Private static fields](https://github.com/tc39/proposal-static-class-features) | `esnext` | `class { static #x }` | These syntax features are not yet supported, and currently cannot be parsed: diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 03d0dc5f0dd..015dfdf1844 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -909,7 +909,8 @@ const ( SymbolClass // A class-private identifier (i.e. "#foo"). - SymbolPrivate + SymbolPrivateField + SymbolPrivateMethod SymbolPrivateGet SymbolPrivateSet SymbolPrivateGetSetPair @@ -931,7 +932,7 @@ const ( ) func (kind SymbolKind) IsPrivate() bool { - return kind >= SymbolPrivate && kind <= SymbolPrivateGetSetPair + return kind >= SymbolPrivateField && kind <= SymbolPrivateGetSetPair } func (kind SymbolKind) IsHoisted() bool { diff --git a/internal/bundler/bundler_lower_test.go b/internal/bundler/bundler_lower_test.go index 8fc5a3548d9..9f70d276020 100644 --- a/internal/bundler/bundler_lower_test.go +++ b/internal/bundler/bundler_lower_test.go @@ -182,3 +182,1367 @@ let tests = { }, }) } + +func TestLowerPrivateFieldAssignments2015NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #x + unary() { + this.#x++ + this.#x-- + ++this.#x + --this.#x + } + binary() { + this.#x = 1 + this.#x += 1 + this.#x -= 1 + this.#x *= 1 + this.#x /= 1 + this.#x %= 1 + this.#x **= 1 + this.#x <<= 1 + this.#x >>= 1 + this.#x >>>= 1 + this.#x &= 1 + this.#x |= 1 + this.#x ^= 1 + this.#x &&= 1 + this.#x ||= 1 + this.#x ??= 1 + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2015, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _x; +class Foo { + constructor() { + _x.set(this, void 0); + } + unary() { + var _a, _b; + __privateSet(this, _x, (_a = +__privateGet(this, _x)) + 1), _a; + __privateSet(this, _x, (_b = +__privateGet(this, _x)) - 1), _b; + __privateSet(this, _x, +__privateGet(this, _x) + 1); + __privateSet(this, _x, +__privateGet(this, _x) - 1); + } + binary() { + var _a; + __privateSet(this, _x, 1); + __privateSet(this, _x, __privateGet(this, _x) + 1); + __privateSet(this, _x, __privateGet(this, _x) - 1); + __privateSet(this, _x, __privateGet(this, _x) * 1); + __privateSet(this, _x, __privateGet(this, _x) / 1); + __privateSet(this, _x, __privateGet(this, _x) % 1); + __privateSet(this, _x, __pow(__privateGet(this, _x), 1)); + __privateSet(this, _x, __privateGet(this, _x) << 1); + __privateSet(this, _x, __privateGet(this, _x) >> 1); + __privateSet(this, _x, __privateGet(this, _x) >>> 1); + __privateSet(this, _x, __privateGet(this, _x) & 1); + __privateSet(this, _x, __privateGet(this, _x) | 1); + __privateSet(this, _x, __privateGet(this, _x) ^ 1); + __privateGet(this, _x) && __privateSet(this, _x, 1); + __privateGet(this, _x) || __privateSet(this, _x, 1); + (_a = __privateGet(this, _x)) != null ? _a : __privateSet(this, _x, 1); + } +} +_x = new WeakMap(); +`, + }, + }) +} + +func TestLowerPrivateFieldAssignments2019NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #x + unary() { + this.#x++ + this.#x-- + ++this.#x + --this.#x + } + binary() { + this.#x = 1 + this.#x += 1 + this.#x -= 1 + this.#x *= 1 + this.#x /= 1 + this.#x %= 1 + this.#x **= 1 + this.#x <<= 1 + this.#x >>= 1 + this.#x >>>= 1 + this.#x &= 1 + this.#x |= 1 + this.#x ^= 1 + this.#x &&= 1 + this.#x ||= 1 + this.#x ??= 1 + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2019, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _x; +class Foo { + constructor() { + _x.set(this, void 0); + } + unary() { + var _a, _b; + __privateSet(this, _x, (_a = +__privateGet(this, _x)) + 1), _a; + __privateSet(this, _x, (_b = +__privateGet(this, _x)) - 1), _b; + __privateSet(this, _x, +__privateGet(this, _x) + 1); + __privateSet(this, _x, +__privateGet(this, _x) - 1); + } + binary() { + var _a; + __privateSet(this, _x, 1); + __privateSet(this, _x, __privateGet(this, _x) + 1); + __privateSet(this, _x, __privateGet(this, _x) - 1); + __privateSet(this, _x, __privateGet(this, _x) * 1); + __privateSet(this, _x, __privateGet(this, _x) / 1); + __privateSet(this, _x, __privateGet(this, _x) % 1); + __privateSet(this, _x, __privateGet(this, _x) ** 1); + __privateSet(this, _x, __privateGet(this, _x) << 1); + __privateSet(this, _x, __privateGet(this, _x) >> 1); + __privateSet(this, _x, __privateGet(this, _x) >>> 1); + __privateSet(this, _x, __privateGet(this, _x) & 1); + __privateSet(this, _x, __privateGet(this, _x) | 1); + __privateSet(this, _x, __privateGet(this, _x) ^ 1); + __privateGet(this, _x) && __privateSet(this, _x, 1); + __privateGet(this, _x) || __privateSet(this, _x, 1); + (_a = __privateGet(this, _x)) != null ? _a : __privateSet(this, _x, 1); + } +} +_x = new WeakMap(); +`, + }, + }) +} + +func TestLowerPrivateFieldAssignments2020NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #x + unary() { + this.#x++ + this.#x-- + ++this.#x + --this.#x + } + binary() { + this.#x = 1 + this.#x += 1 + this.#x -= 1 + this.#x *= 1 + this.#x /= 1 + this.#x %= 1 + this.#x **= 1 + this.#x <<= 1 + this.#x >>= 1 + this.#x >>>= 1 + this.#x &= 1 + this.#x |= 1 + this.#x ^= 1 + this.#x &&= 1 + this.#x ||= 1 + this.#x ??= 1 + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2020, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _x; +class Foo { + constructor() { + _x.set(this, void 0); + } + unary() { + var _a, _b; + __privateSet(this, _x, (_a = +__privateGet(this, _x)) + 1), _a; + __privateSet(this, _x, (_b = +__privateGet(this, _x)) - 1), _b; + __privateSet(this, _x, +__privateGet(this, _x) + 1); + __privateSet(this, _x, +__privateGet(this, _x) - 1); + } + binary() { + __privateSet(this, _x, 1); + __privateSet(this, _x, __privateGet(this, _x) + 1); + __privateSet(this, _x, __privateGet(this, _x) - 1); + __privateSet(this, _x, __privateGet(this, _x) * 1); + __privateSet(this, _x, __privateGet(this, _x) / 1); + __privateSet(this, _x, __privateGet(this, _x) % 1); + __privateSet(this, _x, __privateGet(this, _x) ** 1); + __privateSet(this, _x, __privateGet(this, _x) << 1); + __privateSet(this, _x, __privateGet(this, _x) >> 1); + __privateSet(this, _x, __privateGet(this, _x) >>> 1); + __privateSet(this, _x, __privateGet(this, _x) & 1); + __privateSet(this, _x, __privateGet(this, _x) | 1); + __privateSet(this, _x, __privateGet(this, _x) ^ 1); + __privateGet(this, _x) && __privateSet(this, _x, 1); + __privateGet(this, _x) || __privateSet(this, _x, 1); + __privateGet(this, _x) ?? __privateSet(this, _x, 1); + } +} +_x = new WeakMap(); +`, + }, + }) +} + +func TestLowerPrivateFieldAssignmentsNextNoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #x + unary() { + this.#x++ + this.#x-- + ++this.#x + --this.#x + } + binary() { + this.#x = 1 + this.#x += 1 + this.#x -= 1 + this.#x *= 1 + this.#x /= 1 + this.#x %= 1 + this.#x **= 1 + this.#x <<= 1 + this.#x >>= 1 + this.#x >>>= 1 + this.#x &= 1 + this.#x |= 1 + this.#x ^= 1 + this.#x &&= 1 + this.#x ||= 1 + this.#x ??= 1 + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `class Foo { + #x; + unary() { + this.#x++; + this.#x--; + ++this.#x; + --this.#x; + } + binary() { + this.#x = 1; + this.#x += 1; + this.#x -= 1; + this.#x *= 1; + this.#x /= 1; + this.#x %= 1; + this.#x **= 1; + this.#x <<= 1; + this.#x >>= 1; + this.#x >>>= 1; + this.#x &= 1; + this.#x |= 1; + this.#x ^= 1; + this.#x &&= 1; + this.#x ||= 1; + this.#x ??= 1; + } +} +`, + }, + }) +} + +func TestLowerPrivateFieldOptionalChain2019NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #x + foo() { + this?.#x.y + this?.y.#x + this.#x?.y + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2019, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _x; +class Foo { + constructor() { + _x.set(this, void 0); + } + foo() { + var _a; + this == null ? void 0 : __privateGet(this, _x).y; + this == null ? void 0 : __privateGet(this.y, _x); + (_a = __privateGet(this, _x)) == null ? void 0 : _a.y; + } +} +_x = new WeakMap(); +`, + }, + }) +} + +func TestLowerPrivateFieldOptionalChain2020NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #x + foo() { + this?.#x.y + this?.y.#x + this.#x?.y + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2020, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _x; +class Foo { + constructor() { + _x.set(this, void 0); + } + foo() { + this == null ? void 0 : __privateGet(this, _x).y; + this == null ? void 0 : __privateGet(this.y, _x); + __privateGet(this, _x)?.y; + } +} +_x = new WeakMap(); +`, + }, + }) +} + +func TestLowerPrivateFieldOptionalChainNextNoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #x + foo() { + this?.#x.y + this?.y.#x + this.#x?.y + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `class Foo { + #x; + foo() { + this?.#x.y; + this?.y.#x; + this.#x?.y; + } +} +`, + }, + }) +} + +func TestTSLowerPrivateFieldOptionalChain2015NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.ts": ` + class Foo { + #x + foo() { + this?.#x.y + this?.y.#x + this.#x?.y + } + } + `, + }, + entryPaths: []string{"/entry.ts"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2015, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _x; +class Foo { + constructor() { + _x.set(this, void 0); + } + foo() { + var _a; + this == null ? void 0 : __privateGet(this, _x).y; + this == null ? void 0 : __privateGet(this.y, _x); + (_a = __privateGet(this, _x)) == null ? void 0 : _a.y; + } +} +_x = new WeakMap(); +`, + }, + }) +} + +func TestTSLowerPrivateStaticMembers2015NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.ts": ` + class Foo { + static #x + static get #y() {} + static set #y() {} + static #z() {} + foo() { + Foo.#x += 1 + Foo.#y += 1 + Foo.#z() + } + } + `, + }, + entryPaths: []string{"/entry.ts"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2015, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _x, _y, y_get, y_set, _z, z_fn; +class Foo { + foo() { + __privateSet(Foo, _x, __privateGet(Foo, _x) + 1); + __privateSet(Foo, _y, __privateGet(Foo, _y, y_get) + 1, y_set); + __privateMethod(Foo, _z, z_fn).call(Foo); + } +} +_x = new WeakMap(); +_y = new WeakSet(); +y_get = function() { +}; +y_set = function() { +}; +_z = new WeakSet(); +z_fn = function() { +}; +_x.set(Foo, void 0); +_y.add(Foo); +_z.add(Foo); +`, + }, + }) +} + +func TestTSLowerPrivateFieldAndMethodAvoidNameCollision2015(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.ts": ` + class WeakMap { + #x + } + class WeakSet { + #y() {} + } + `, + }, + entryPaths: []string{"/entry.ts"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + Target: parser.ES2015, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.ts +var _x; +class WeakMap2 { + constructor() { + _x.set(this, void 0); + } +} +_x = new WeakMap(); +var _y, y_fn; +class WeakSet2 { + constructor() { + _y.add(this); + } +} +_y = new WeakSet(); +y_fn = function() { +}; +`, + }, + }) +} + +func TestLowerPrivateGetterSetter2015(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + get #foo() { return this.foo } + set #bar(val) { this.bar = val } + get #prop() { return this.prop } + set #prop(val) { this.prop = val } + foo(fn) { + fn().#foo + fn().#bar = 1 + fn().#prop + fn().#prop = 2 + } + unary(fn) { + fn().#prop++; + fn().#prop--; + ++fn().#prop; + --fn().#prop; + } + binary(fn) { + fn().#prop = 1; + fn().#prop += 1; + fn().#prop -= 1; + fn().#prop *= 1; + fn().#prop /= 1; + fn().#prop %= 1; + fn().#prop **= 1; + fn().#prop <<= 1; + fn().#prop >>= 1; + fn().#prop >>>= 1; + fn().#prop &= 1; + fn().#prop |= 1; + fn().#prop ^= 1; + fn().#prop &&= 1; + fn().#prop ||= 1; + fn().#prop ??= 1; + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + Target: parser.ES2015, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +var _foo, foo_get, _bar, bar_set, _prop, prop_get, prop_set; +class Foo { + constructor() { + _foo.add(this); + _bar.add(this); + _prop.add(this); + } + foo(fn) { + __privateGet(fn(), _foo, foo_get); + __privateSet(fn(), _bar, 1, bar_set); + __privateGet(fn(), _prop, prop_get); + __privateSet(fn(), _prop, 2, prop_set); + } + unary(fn) { + var _a, _b, _c, _d, _e, _f; + __privateSet(_a = fn(), _prop, (_b = +__privateGet(_a, _prop, prop_get)) + 1, prop_set), _b; + __privateSet(_c = fn(), _prop, (_d = +__privateGet(_c, _prop, prop_get)) - 1, prop_set), _d; + __privateSet(_e = fn(), _prop, +__privateGet(_e, _prop, prop_get) + 1, prop_set); + __privateSet(_f = fn(), _prop, +__privateGet(_f, _prop, prop_get) - 1, prop_set); + } + binary(fn) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p; + __privateSet(fn(), _prop, 1, prop_set); + __privateSet(_a = fn(), _prop, __privateGet(_a, _prop, prop_get) + 1, prop_set); + __privateSet(_b = fn(), _prop, __privateGet(_b, _prop, prop_get) - 1, prop_set); + __privateSet(_c = fn(), _prop, __privateGet(_c, _prop, prop_get) * 1, prop_set); + __privateSet(_d = fn(), _prop, __privateGet(_d, _prop, prop_get) / 1, prop_set); + __privateSet(_e = fn(), _prop, __privateGet(_e, _prop, prop_get) % 1, prop_set); + __privateSet(_f = fn(), _prop, __pow(__privateGet(_f, _prop, prop_get), 1), prop_set); + __privateSet(_g = fn(), _prop, __privateGet(_g, _prop, prop_get) << 1, prop_set); + __privateSet(_h = fn(), _prop, __privateGet(_h, _prop, prop_get) >> 1, prop_set); + __privateSet(_i = fn(), _prop, __privateGet(_i, _prop, prop_get) >>> 1, prop_set); + __privateSet(_j = fn(), _prop, __privateGet(_j, _prop, prop_get) & 1, prop_set); + __privateSet(_k = fn(), _prop, __privateGet(_k, _prop, prop_get) | 1, prop_set); + __privateSet(_l = fn(), _prop, __privateGet(_l, _prop, prop_get) ^ 1, prop_set); + __privateGet(_m = fn(), _prop, prop_get) && __privateSet(_m, _prop, 1, prop_set); + __privateGet(_n = fn(), _prop, prop_get) || __privateSet(_n, _prop, 1, prop_set); + (_p = __privateGet(_o = fn(), _prop, prop_get)) != null ? _p : __privateSet(_o, _prop, 1, prop_set); + } +} +_foo = new WeakSet(); +foo_get = function() { + return this.foo; +}; +_bar = new WeakSet(); +bar_set = function(val) { + this.bar = val; +}; +_prop = new WeakSet(); +prop_get = function() { + return this.prop; +}; +prop_set = function(val) { + this.prop = val; +}; +`, + }, + }) +} + +func TestLowerPrivateGetterSetter2019(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + get #foo() { return this.foo } + set #bar(val) { this.bar = val } + get #prop() { return this.prop } + set #prop(val) { this.prop = val } + foo(fn) { + fn().#foo + fn().#bar = 1 + fn().#prop + fn().#prop = 2 + } + unary(fn) { + fn().#prop++; + fn().#prop--; + ++fn().#prop; + --fn().#prop; + } + binary(fn) { + fn().#prop = 1; + fn().#prop += 1; + fn().#prop -= 1; + fn().#prop *= 1; + fn().#prop /= 1; + fn().#prop %= 1; + fn().#prop **= 1; + fn().#prop <<= 1; + fn().#prop >>= 1; + fn().#prop >>>= 1; + fn().#prop &= 1; + fn().#prop |= 1; + fn().#prop ^= 1; + fn().#prop &&= 1; + fn().#prop ||= 1; + fn().#prop ??= 1; + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + Target: parser.ES2019, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +var _foo, foo_get, _bar, bar_set, _prop, prop_get, prop_set; +class Foo { + constructor() { + _foo.add(this); + _bar.add(this); + _prop.add(this); + } + foo(fn) { + __privateGet(fn(), _foo, foo_get); + __privateSet(fn(), _bar, 1, bar_set); + __privateGet(fn(), _prop, prop_get); + __privateSet(fn(), _prop, 2, prop_set); + } + unary(fn) { + var _a, _b, _c, _d, _e, _f; + __privateSet(_a = fn(), _prop, (_b = +__privateGet(_a, _prop, prop_get)) + 1, prop_set), _b; + __privateSet(_c = fn(), _prop, (_d = +__privateGet(_c, _prop, prop_get)) - 1, prop_set), _d; + __privateSet(_e = fn(), _prop, +__privateGet(_e, _prop, prop_get) + 1, prop_set); + __privateSet(_f = fn(), _prop, +__privateGet(_f, _prop, prop_get) - 1, prop_set); + } + binary(fn) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p; + __privateSet(fn(), _prop, 1, prop_set); + __privateSet(_a = fn(), _prop, __privateGet(_a, _prop, prop_get) + 1, prop_set); + __privateSet(_b = fn(), _prop, __privateGet(_b, _prop, prop_get) - 1, prop_set); + __privateSet(_c = fn(), _prop, __privateGet(_c, _prop, prop_get) * 1, prop_set); + __privateSet(_d = fn(), _prop, __privateGet(_d, _prop, prop_get) / 1, prop_set); + __privateSet(_e = fn(), _prop, __privateGet(_e, _prop, prop_get) % 1, prop_set); + __privateSet(_f = fn(), _prop, __privateGet(_f, _prop, prop_get) ** 1, prop_set); + __privateSet(_g = fn(), _prop, __privateGet(_g, _prop, prop_get) << 1, prop_set); + __privateSet(_h = fn(), _prop, __privateGet(_h, _prop, prop_get) >> 1, prop_set); + __privateSet(_i = fn(), _prop, __privateGet(_i, _prop, prop_get) >>> 1, prop_set); + __privateSet(_j = fn(), _prop, __privateGet(_j, _prop, prop_get) & 1, prop_set); + __privateSet(_k = fn(), _prop, __privateGet(_k, _prop, prop_get) | 1, prop_set); + __privateSet(_l = fn(), _prop, __privateGet(_l, _prop, prop_get) ^ 1, prop_set); + __privateGet(_m = fn(), _prop, prop_get) && __privateSet(_m, _prop, 1, prop_set); + __privateGet(_n = fn(), _prop, prop_get) || __privateSet(_n, _prop, 1, prop_set); + (_p = __privateGet(_o = fn(), _prop, prop_get)) != null ? _p : __privateSet(_o, _prop, 1, prop_set); + } +} +_foo = new WeakSet(); +foo_get = function() { + return this.foo; +}; +_bar = new WeakSet(); +bar_set = function(val) { + this.bar = val; +}; +_prop = new WeakSet(); +prop_get = function() { + return this.prop; +}; +prop_set = function(val) { + this.prop = val; +}; +`, + }, + }) +} + +func TestLowerPrivateGetterSetter2020(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + get #foo() { return this.foo } + set #bar(val) { this.bar = val } + get #prop() { return this.prop } + set #prop(val) { this.prop = val } + foo(fn) { + fn().#foo + fn().#bar = 1 + fn().#prop + fn().#prop = 2 + } + unary(fn) { + fn().#prop++; + fn().#prop--; + ++fn().#prop; + --fn().#prop; + } + binary(fn) { + fn().#prop = 1; + fn().#prop += 1; + fn().#prop -= 1; + fn().#prop *= 1; + fn().#prop /= 1; + fn().#prop %= 1; + fn().#prop **= 1; + fn().#prop <<= 1; + fn().#prop >>= 1; + fn().#prop >>>= 1; + fn().#prop &= 1; + fn().#prop |= 1; + fn().#prop ^= 1; + fn().#prop &&= 1; + fn().#prop ||= 1; + fn().#prop ??= 1; + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + Target: parser.ES2020, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +var _foo, foo_get, _bar, bar_set, _prop, prop_get, prop_set; +class Foo { + constructor() { + _foo.add(this); + _bar.add(this); + _prop.add(this); + } + foo(fn) { + __privateGet(fn(), _foo, foo_get); + __privateSet(fn(), _bar, 1, bar_set); + __privateGet(fn(), _prop, prop_get); + __privateSet(fn(), _prop, 2, prop_set); + } + unary(fn) { + var _a, _b, _c, _d, _e, _f; + __privateSet(_a = fn(), _prop, (_b = +__privateGet(_a, _prop, prop_get)) + 1, prop_set), _b; + __privateSet(_c = fn(), _prop, (_d = +__privateGet(_c, _prop, prop_get)) - 1, prop_set), _d; + __privateSet(_e = fn(), _prop, +__privateGet(_e, _prop, prop_get) + 1, prop_set); + __privateSet(_f = fn(), _prop, +__privateGet(_f, _prop, prop_get) - 1, prop_set); + } + binary(fn) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o; + __privateSet(fn(), _prop, 1, prop_set); + __privateSet(_a = fn(), _prop, __privateGet(_a, _prop, prop_get) + 1, prop_set); + __privateSet(_b = fn(), _prop, __privateGet(_b, _prop, prop_get) - 1, prop_set); + __privateSet(_c = fn(), _prop, __privateGet(_c, _prop, prop_get) * 1, prop_set); + __privateSet(_d = fn(), _prop, __privateGet(_d, _prop, prop_get) / 1, prop_set); + __privateSet(_e = fn(), _prop, __privateGet(_e, _prop, prop_get) % 1, prop_set); + __privateSet(_f = fn(), _prop, __privateGet(_f, _prop, prop_get) ** 1, prop_set); + __privateSet(_g = fn(), _prop, __privateGet(_g, _prop, prop_get) << 1, prop_set); + __privateSet(_h = fn(), _prop, __privateGet(_h, _prop, prop_get) >> 1, prop_set); + __privateSet(_i = fn(), _prop, __privateGet(_i, _prop, prop_get) >>> 1, prop_set); + __privateSet(_j = fn(), _prop, __privateGet(_j, _prop, prop_get) & 1, prop_set); + __privateSet(_k = fn(), _prop, __privateGet(_k, _prop, prop_get) | 1, prop_set); + __privateSet(_l = fn(), _prop, __privateGet(_l, _prop, prop_get) ^ 1, prop_set); + __privateGet(_m = fn(), _prop, prop_get) && __privateSet(_m, _prop, 1, prop_set); + __privateGet(_n = fn(), _prop, prop_get) || __privateSet(_n, _prop, 1, prop_set); + __privateGet(_o = fn(), _prop, prop_get) ?? __privateSet(_o, _prop, 1, prop_set); + } +} +_foo = new WeakSet(); +foo_get = function() { + return this.foo; +}; +_bar = new WeakSet(); +bar_set = function(val) { + this.bar = val; +}; +_prop = new WeakSet(); +prop_get = function() { + return this.prop; +}; +prop_set = function(val) { + this.prop = val; +}; +`, + }, + }) +} + +func TestLowerPrivateGetterSetterNext(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + get #foo() { return this.foo } + set #bar(val) { this.bar = val } + get #prop() { return this.prop } + set #prop(val) { this.prop = val } + foo(fn) { + fn().#foo + fn().#bar = 1 + fn().#prop + fn().#prop = 2 + } + unary(fn) { + fn().#prop++; + fn().#prop--; + ++fn().#prop; + --fn().#prop; + } + binary(fn) { + fn().#prop = 1; + fn().#prop += 1; + fn().#prop -= 1; + fn().#prop *= 1; + fn().#prop /= 1; + fn().#prop %= 1; + fn().#prop **= 1; + fn().#prop <<= 1; + fn().#prop >>= 1; + fn().#prop >>>= 1; + fn().#prop &= 1; + fn().#prop |= 1; + fn().#prop ^= 1; + fn().#prop &&= 1; + fn().#prop ||= 1; + fn().#prop ??= 1; + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +class Foo { + get #foo() { + return this.foo; + } + set #bar(val) { + this.bar = val; + } + get #prop() { + return this.prop; + } + set #prop(val) { + this.prop = val; + } + foo(fn) { + fn().#foo; + fn().#bar = 1; + fn().#prop; + fn().#prop = 2; + } + unary(fn) { + fn().#prop++; + fn().#prop--; + ++fn().#prop; + --fn().#prop; + } + binary(fn) { + fn().#prop = 1; + fn().#prop += 1; + fn().#prop -= 1; + fn().#prop *= 1; + fn().#prop /= 1; + fn().#prop %= 1; + fn().#prop **= 1; + fn().#prop <<= 1; + fn().#prop >>= 1; + fn().#prop >>>= 1; + fn().#prop &= 1; + fn().#prop |= 1; + fn().#prop ^= 1; + fn().#prop &&= 1; + fn().#prop ||= 1; + fn().#prop ??= 1; + } +} +`, + }, + }) +} + +func TestLowerPrivateMethod2019(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #field + #method() {} + baseline() { + a().foo + b().foo(x) + c()?.foo(x) + d().foo?.(x) + e()?.foo?.(x) + } + privateField() { + a().#field + b().#field(x) + c()?.#field(x) + d().#field?.(x) + e()?.#field?.(x) + f()?.foo.#field(x).bar() + } + privateMethod() { + a().#method + b().#method(x) + c()?.#method(x) + d().#method?.(x) + e()?.#method?.(x) + f()?.foo.#method(x).bar() + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + Target: parser.ES2019, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +var _field, _method, method_fn; +class Foo { + constructor() { + _field.set(this, void 0); + _method.add(this); + } + baseline() { + var _a, _b, _c, _d, _e; + a().foo; + b().foo(x); + (_a = c()) == null ? void 0 : _a.foo(x); + (_c = (_b = d()).foo) == null ? void 0 : _c.call(_b, x); + (_e = (_d = e()) == null ? void 0 : _d.foo) == null ? void 0 : _e.call(_d, x); + } + privateField() { + var _a, _b, _c, _d, _e, _f, _g, _h; + __privateGet(a(), _field); + __privateGet(_a = b(), _field).call(_a, x); + (_b = c()) == null ? void 0 : __privateGet(_b, _field).call(_b, x); + (_d = __privateGet(_c = d(), _field)) == null ? void 0 : _d.call(_c, x); + (_f = (_e = e()) == null ? void 0 : __privateGet(_e, _field)) == null ? void 0 : _f.call(_e, x); + (_g = f()) == null ? void 0 : __privateGet(_h = _g.foo, _field).call(_h, x).bar(); + } + privateMethod() { + var _a, _b, _c, _d, _e, _f, _g, _h; + __privateMethod(a(), _method, method_fn); + __privateMethod(_a = b(), _method, method_fn).call(_a, x); + (_b = c()) == null ? void 0 : __privateMethod(_b, _method, method_fn).call(_b, x); + (_d = __privateMethod(_c = d(), _method, method_fn)) == null ? void 0 : _d.call(_c, x); + (_f = (_e = e()) == null ? void 0 : __privateMethod(_e, _method, method_fn)) == null ? void 0 : _f.call(_e, x); + (_g = f()) == null ? void 0 : __privateMethod(_h = _g.foo, _method, method_fn).call(_h, x).bar(); + } +} +_field = new WeakMap(); +_method = new WeakSet(); +method_fn = function() { +}; +`, + }, + }) +} + +func TestLowerPrivateMethod2020(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #field + #method() {} + baseline() { + a().foo + b().foo(x) + c()?.foo(x) + d().foo?.(x) + e()?.foo?.(x) + } + privateField() { + a().#field + b().#field(x) + c()?.#field(x) + d().#field?.(x) + e()?.#field?.(x) + f()?.foo.#field(x).bar() + } + privateMethod() { + a().#method + b().#method(x) + c()?.#method(x) + d().#method?.(x) + e()?.#method?.(x) + f()?.foo.#method(x).bar() + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + Target: parser.ES2020, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +var _field, _method, method_fn; +class Foo { + constructor() { + _field.set(this, void 0); + _method.add(this); + } + baseline() { + a().foo; + b().foo(x); + c()?.foo(x); + d().foo?.(x); + e()?.foo?.(x); + } + privateField() { + var _a, _b, _c, _d, _e, _f, _g; + __privateGet(a(), _field); + __privateGet(_a = b(), _field).call(_a, x); + (_b = c()) == null ? void 0 : __privateGet(_b, _field).call(_b, x); + (_d = __privateGet(_c = d(), _field)) == null ? void 0 : _d.call(_c, x); + ((_e = e()) == null ? void 0 : __privateGet(_e, _field))?.(x); + (_f = f()) == null ? void 0 : __privateGet(_g = _f.foo, _field).call(_g, x).bar(); + } + privateMethod() { + var _a, _b, _c, _d, _e, _f, _g; + __privateMethod(a(), _method, method_fn); + __privateMethod(_a = b(), _method, method_fn).call(_a, x); + (_b = c()) == null ? void 0 : __privateMethod(_b, _method, method_fn).call(_b, x); + (_d = __privateMethod(_c = d(), _method, method_fn)) == null ? void 0 : _d.call(_c, x); + ((_e = e()) == null ? void 0 : __privateMethod(_e, _method, method_fn))?.(x); + (_f = f()) == null ? void 0 : __privateMethod(_g = _f.foo, _method, method_fn).call(_g, x).bar(); + } +} +_field = new WeakMap(); +_method = new WeakSet(); +method_fn = function() { +}; +`, + }, + }) +} + +func TestLowerPrivateMethodNext(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + #field + #method() {} + baseline() { + a().foo + b().foo(x) + c()?.foo(x) + d().foo?.(x) + e()?.foo?.(x) + } + privateField() { + a().#field + b().#field(x) + c()?.#field(x) + d().#field?.(x) + e()?.#field?.(x) + f()?.foo.#field(x).bar() + } + privateMethod() { + a().#method + b().#method(x) + c()?.#method(x) + d().#method?.(x) + e()?.#method?.(x) + f()?.foo.#method(x).bar() + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +class Foo { + #field; + #method() { + } + baseline() { + a().foo; + b().foo(x); + c()?.foo(x); + d().foo?.(x); + e()?.foo?.(x); + } + privateField() { + a().#field; + b().#field(x); + c()?.#field(x); + d().#field?.(x); + e()?.#field?.(x); + f()?.foo.#field(x).bar(); + } + privateMethod() { + a().#method; + b().#method(x); + c()?.#method(x); + d().#method?.(x); + e()?.#method?.(x); + f()?.foo.#method(x).bar(); + } +} +`, + }, + }) +} + +func TestLowerPrivateClassExpr2020NoBundle(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + export let Foo = class { + #field + #method() {} + static #staticField + static #staticMethod() {} + foo() { + this.#field = this.#method() + Foo.#staticField = Foo.#staticMethod() + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: false, + Target: parser.ES2020, + }, + bundleOptions: BundleOptions{ + IsBundling: false, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `var _field, _method, method_fn, _a, _staticField, _staticMethod, staticMethod_fn; +export let Foo = (_a = class { + constructor() { + _field.set(this, void 0); + _method.add(this); + } + foo() { + __privateSet(this, _field, __privateMethod(this, _method, method_fn).call(this)); + __privateSet(Foo, _staticField, __privateMethod(Foo, _staticMethod, staticMethod_fn).call(Foo)); + } +}, _field = new WeakMap(), _method = new WeakSet(), method_fn = function() { +}, _staticField = new WeakMap(), _staticMethod = new WeakSet(), staticMethod_fn = function() { +}, _staticField.set(_a, void 0), _staticMethod.add(_a), _a); +`, + }, + }) +} + +func TestLowerPrivateMethodWithModifiers2020(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + class Foo { + *#g() {} + async #a() {} + async *#ag() {} + + static *#sg() {} + static async #sa() {} + static async *#sag() {} + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + Target: parser.ES2020, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /entry.js +var _g, g_fn, _a, a_fn, _ag, ag_fn, _sg, sg_fn, _sa, sa_fn, _sag, sag_fn; +class Foo { + constructor() { + _g.add(this); + _a.add(this); + _ag.add(this); + } +} +_g = new WeakSet(); +g_fn = function* () { +}; +_a = new WeakSet(); +a_fn = async function() { +}; +_ag = new WeakSet(); +ag_fn = async function* () { +}; +_sg = new WeakSet(); +sg_fn = function* () { +}; +_sa = new WeakSet(); +sa_fn = async function() { +}; +_sag = new WeakSet(); +sag_fn = async function* () { +}; +_sg.add(Foo); +_sa.add(Foo); +_sag.add(Foo); +`, + }, + }) +} diff --git a/internal/bundler/bundler_test.go b/internal/bundler/bundler_test.go index 212d2c7aab5..e27683009a1 100644 --- a/internal/bundler/bundler_test.go +++ b/internal/bundler/bundler_test.go @@ -3767,7 +3767,6 @@ func TestRuntimeNameCollisionNoBundle(t *testing.T) { entryPaths: []string{"/entry.js"}, parseOptions: parser.ParseOptions{ IsBundling: false, - Target: parser.ES2018, }, bundleOptions: BundleOptions{ IsBundling: false, diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 24ccdd7318f..f05ca138395 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -52,6 +52,12 @@ type parser struct { declaredSymbols []ast.DeclaredSymbol runtimeImports map[string]ast.Ref + // For lowering private methods + weakMapRef ast.Ref + weakSetRef ast.Ref + privateGetters map[ast.Ref]ast.Ref + privateSetters map[ast.Ref]ast.Ref + // These are for TypeScript shouldFoldNumericConstants bool enclosingNamespaceRef *ast.Ref @@ -1474,7 +1480,6 @@ func (p *parser) parseProperty( if !opts.isClass || len(opts.tsDecorators) > 0 { p.lexer.Expected(lexer.TIdentifier) } - p.markFutureSyntax(futureSyntaxPrivateName, p.lexer.Range()) key = ast.Expr{p.lexer.Loc(), &ast.EPrivateIdentifier{p.storeNameInRef(p.lexer.Identifier)}} p.lexer.Next() @@ -1635,7 +1640,7 @@ func (p *parser) parseProperty( if name == "#constructor" { p.log.AddRangeError(&p.source, keyRange, fmt.Sprintf("Invalid field name %q", name)) } - private.Ref = p.declareSymbol(ast.SymbolPrivate, key.Loc, name) + private.Ref = p.declareSymbol(ast.SymbolPrivateField, key.Loc, name) } p.lexer.ExpectOrInsertSemicolon() @@ -1702,19 +1707,31 @@ func (p *parser) parseProperty( // Special-case private identifiers if private, ok := key.Data.(*ast.EPrivateIdentifier); ok { var declare ast.SymbolKind + var suffix string switch kind { case ast.PropertyGet: declare = ast.SymbolPrivateGet + suffix = "_get" case ast.PropertySet: declare = ast.SymbolPrivateSet + suffix = "_set" default: - declare = ast.SymbolPrivate + declare = ast.SymbolPrivateMethod + suffix = "_fn" } name := p.loadNameFromRef(private.Ref) if name == "#constructor" { p.log.AddRangeError(&p.source, keyRange, fmt.Sprintf("Invalid method name %q", name)) } private.Ref = p.declareSymbol(declare, key.Loc, name) + if p.Target < privateNameTarget { + methodRef := p.newSymbol(ast.SymbolOther, name[1:]+suffix) + if kind == ast.PropertySet { + p.privateSetters[private.Ref] = methodRef + } else { + p.privateGetters[private.Ref] = methodRef + } + } } return ast.Property{ @@ -5543,13 +5560,16 @@ const ( tempRefNoDeclare ) -func (p *parser) generateTempRef(declare generateTempRefArg) ast.Ref { +func (p *parser) generateTempRef(declare generateTempRefArg, optionalName string) ast.Ref { scope := p.currentScope for !scope.Kind.StopsHoisting() { scope = scope.Parent } - ref := p.newSymbol(ast.SymbolOther, "_"+lexer.NumberToMinifiedName(p.tempRefCount)) - p.tempRefCount++ + if optionalName == "" { + optionalName = "_" + lexer.NumberToMinifiedName(p.tempRefCount) + p.tempRefCount++ + } + ref := p.newSymbol(ast.SymbolOther, optionalName) if declare == tempRefNeedsDeclare { p.tempRefsToDeclare = append(p.tempRefsToDeclare, ref) } @@ -6951,7 +6971,7 @@ func (p *parser) captureValueWithPossibleSideEffects( if p.currentScope.Kind == ast.ScopeFunctionArgs { return func() ast.Expr { if tempRef == ast.InvalidRef { - tempRef = p.generateTempRef(tempRefNoDeclare) + tempRef = p.generateTempRef(tempRefNoDeclare, "") // Assign inline so the order of side effects remains the same p.recordUsage(tempRef) @@ -6983,7 +7003,7 @@ func (p *parser) captureValueWithPossibleSideEffects( return func() ast.Expr { if tempRef == ast.InvalidRef { - tempRef = p.generateTempRef(tempRefNeedsDeclare) + tempRef = p.generateTempRef(tempRefNeedsDeclare, "") p.recordUsage(tempRef) return ast.Expr{loc, &ast.EBinary{ ast.BinOpAssign, @@ -7059,7 +7079,7 @@ func (p *parser) exprForExportedBindingInNamespace(binding ast.Binding, value as // Handle default values if item.DefaultValue != nil { - tempRef := p.generateTempRef(tempRefNeedsDeclare) + tempRef := p.generateTempRef(tempRefNeedsDeclare, "") expr = maybeJoinWithComma(expr, ast.Expr{loc, &ast.EBinary{ ast.BinOpAssign, ast.Expr{loc, &ast.EIdentifier{tempRef}}, @@ -7139,7 +7159,7 @@ func (p *parser) exprForExportedBindingInNamespace(binding ast.Binding, value as keysForSpread = append(keysForSpread, symbolOrString(key.Loc, k.Ref)) default: - tempRef := p.generateTempRef(tempRefNeedsDeclare) + tempRef := p.generateTempRef(tempRefNeedsDeclare, "") key = ast.Expr{loc, &ast.EBinary{ ast.BinOpAssign, ast.Expr{loc, &ast.EIdentifier{tempRef}}, @@ -7170,7 +7190,7 @@ func (p *parser) exprForExportedBindingInNamespace(binding ast.Binding, value as // Handle default values if property.DefaultValue != nil { - tempRef := p.generateTempRef(tempRefNeedsDeclare) + tempRef := p.generateTempRef(tempRefNeedsDeclare, "") expr = maybeJoinWithComma(expr, ast.Expr{loc, &ast.EBinary{ ast.BinOpAssign, ast.Expr{loc, &ast.EIdentifier{tempRef}}, @@ -7688,7 +7708,7 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { e.Right = p.visitExpr(e.Right) - // Fold constants + // Post-process the binary expression switch e.Op { case ast.BinOpLooseEq: if result, ok := checkEqualityIfNoSideEffects(e.Left.Data, e.Right.Data); ok { @@ -7831,27 +7851,6 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { return p.callRuntime(expr.Loc, "__pow", []ast.Expr{e.Left, e.Right}), exprOut{} } - case ast.BinOpPowAssign: - // Lower the exponentiation operator for browsers that don't support it - if p.Target < ES2016 { - return p.lowerExponentiationAssignmentOperator(expr.Loc, e), exprOut{} - } - - case ast.BinOpNullishCoalescingAssign: - if p.Target < ESNext { - return p.lowerNullishCoalescingAssignmentOperator(expr.Loc, e), exprOut{} - } - - case ast.BinOpLogicalAndAssign: - if p.Target < ESNext { - return p.lowerLogicalAssignmentOperator(expr.Loc, e, ast.BinOpLogicalAnd), exprOut{} - } - - case ast.BinOpLogicalOrAssign: - if p.Target < ESNext { - return p.lowerLogicalAssignmentOperator(expr.Loc, e, ast.BinOpLogicalOr), exprOut{} - } - case ast.BinOpShl: if p.shouldFoldNumericConstants { if left, right, ok := extractNumericValues(e.Left, e.Right); ok { @@ -7893,9 +7892,98 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { return ast.Expr{expr.Loc, &ast.ENumber{float64(toInt32(left) ^ toInt32(right))}}, exprOut{} } } + + //////////////////////////////////////////////////////////////////////////////// + // All assignment operators below here + + case ast.BinOpAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSet(target, loc, private, e.Right), exprOut{} + } + + case ast.BinOpAddAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpAdd, e.Right), exprOut{} + } + + case ast.BinOpSubAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpSub, e.Right), exprOut{} + } + + case ast.BinOpMulAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpMul, e.Right), exprOut{} + } + + case ast.BinOpDivAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpDiv, e.Right), exprOut{} + } + + case ast.BinOpRemAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpRem, e.Right), exprOut{} + } + + case ast.BinOpPowAssign: + // Lower the exponentiation operator for browsers that don't support it + if p.Target < ES2016 { + return p.lowerExponentiationAssignmentOperator(expr.Loc, e), exprOut{} + } + + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpPow, e.Right), exprOut{} + } + + case ast.BinOpShlAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpShl, e.Right), exprOut{} + } + + case ast.BinOpShrAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpShr, e.Right), exprOut{} + } + + case ast.BinOpUShrAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpUShr, e.Right), exprOut{} + } + + case ast.BinOpBitwiseOrAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpBitwiseOr, e.Right), exprOut{} + } + + case ast.BinOpBitwiseAndAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpBitwiseAnd, e.Right), exprOut{} + } + + case ast.BinOpBitwiseXorAssign: + if target, loc, private := p.extractPrivateIndex(e.Left); private != nil { + return p.lowerPrivateSetBinOp(target, loc, private, ast.BinOpBitwiseXor, e.Right), exprOut{} + } + + case ast.BinOpNullishCoalescingAssign: + if p.Target < ESNext { + return p.lowerNullishCoalescingAssignmentOperator(expr.Loc, e), exprOut{} + } + + case ast.BinOpLogicalAndAssign: + if p.Target < ESNext { + return p.lowerLogicalAssignmentOperator(expr.Loc, e, ast.BinOpLogicalAnd), exprOut{} + } + + case ast.BinOpLogicalOrAssign: + if p.Target < ESNext { + return p.lowerLogicalAssignmentOperator(expr.Loc, e, ast.BinOpLogicalOr), exprOut{} + } } case *ast.EIndex: + isCallTarget := e == p.callTarget target, out := p.visitExprInOut(e.Target, exprIn{ hasChainParent: e.OptionalChain == ast.OptionalChainContinue, }) @@ -7910,7 +7998,15 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { // Unlike regular identifiers, there are no unbound private identifiers if !p.symbols[result.ref.InnerIndex].Kind.IsPrivate() { r := ast.Range{e.Index.Loc, int32(len(name))} - p.log.AddRangeError(&p.source, r, fmt.Sprintf("Private name %q is not available here", name)) + p.log.AddRangeError(&p.source, r, fmt.Sprintf("Private name %q must be declared in an enclosing class", name)) + } + + // Lower private member access only if we're sure the target isn't needed + // for the value of "this" for a call expression. All other cases will be + // taken care of by the enclosing call expression. + if p.Target < privateNameTarget && e.OptionalChain == ast.OptionalChainNone && !in.isAssignTarget && !isCallTarget { + // "foo.#bar" => "__privateGet(foo, #bar)" + return p.lowerPrivateGet(e.Target, e.Index.Loc, private), exprOut{} } } else { e.Index = p.visitExpr(e.Index) @@ -7972,7 +8068,7 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { e.Value, _ = p.visitExprInOut(e.Value, exprIn{isAssignTarget: e.Op.IsUnaryUpdate()}) - // Fold constants + // Post-process the binary expression switch e.Op { case ast.UnOpNot: if boolean, ok := toBooleanWithoutSideEffects(e.Value.Data); ok { @@ -8005,6 +8101,29 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { if number, ok := toNumberWithoutSideEffects(e.Value.Data); ok { return ast.Expr{expr.Loc, &ast.ENumber{-number}}, exprOut{} } + + //////////////////////////////////////////////////////////////////////////////// + // All assignment operators below here + + case ast.UnOpPreDec: + if target, loc, private := p.extractPrivateIndex(e.Value); private != nil { + return p.lowerPrivateSetUnOp(target, loc, private, ast.BinOpSub, false), exprOut{} + } + + case ast.UnOpPreInc: + if target, loc, private := p.extractPrivateIndex(e.Value); private != nil { + return p.lowerPrivateSetUnOp(target, loc, private, ast.BinOpAdd, false), exprOut{} + } + + case ast.UnOpPostDec: + if target, loc, private := p.extractPrivateIndex(e.Value); private != nil { + return p.lowerPrivateSetUnOp(target, loc, private, ast.BinOpSub, true), exprOut{} + } + + case ast.UnOpPostInc: + if target, loc, private := p.extractPrivateIndex(e.Value); private != nil { + return p.lowerPrivateSetUnOp(target, loc, private, ast.BinOpAdd, true), exprOut{} + } } case *ast.EDot: @@ -8169,6 +8288,23 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { return result, out } + // If this is a plain call expression (instead of an optional chain), lower + // private member access in the call target now if there is one + if !containsOptionalChain { + if target, loc, private := p.extractPrivateIndex(e.Target); private != nil { + // "foo.#bar(123)" => "__privateGet(foo, #bar).call(foo, 123)" + targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(target.Loc, 2, target) + return targetWrapFunc(ast.Expr{target.Loc, &ast.ECall{ + Target: ast.Expr{target.Loc, &ast.EDot{ + Target: p.lowerPrivateGet(targetFunc(), loc, private), + Name: "call", + NameLoc: target.Loc, + }}, + Args: append([]ast.Expr{targetFunc()}, e.Args...), + }}), exprOut{} + } + } + // Track calls to require() so we can use them while bundling if id, ok := e.Target.Data.(*ast.EIdentifier); ok && id.Ref == p.requireRef && p.IsBundling { // There must be one argument @@ -8827,6 +8963,12 @@ func newParser(log logging.Log, source logging.Source, lexer lexer.Lexer, option useCountEstimates: make(map[ast.Ref]uint32), runtimeImports: make(map[string]ast.Ref), + // For lowering private methods + weakMapRef: ast.InvalidRef, + weakSetRef: ast.InvalidRef, + privateGetters: make(map[ast.Ref]ast.Ref), + privateSetters: make(map[ast.Ref]ast.Ref), + // These are for TypeScript emittedNamespaceVars: make(map[ast.Ref]bool), isExportedInsideNamespace: make(map[ast.Ref]ast.Ref), diff --git a/internal/parser/parser_lower.go b/internal/parser/parser_lower.go index 94ee8698790..f6b2de06fb2 100644 --- a/internal/parser/parser_lower.go +++ b/internal/parser/parser_lower.go @@ -11,6 +11,8 @@ import ( "github.com/evanw/esbuild/internal/lexer" ) +const privateNameTarget = ESNext + type futureSyntax uint8 const ( @@ -20,7 +22,6 @@ const ( futureSyntaxForAwait futureSyntaxBigInteger futureSyntaxNonIdentifierArrayRest - futureSyntaxPrivateName ) func (p *parser) markFutureSyntax(syntax futureSyntax, r ast.Range) { @@ -39,8 +40,6 @@ func (p *parser) markFutureSyntax(syntax futureSyntax, r ast.Range) { target = ES2020 case futureSyntaxNonIdentifierArrayRest: target = ES2016 - case futureSyntaxPrivateName: - target = ESNext } if p.Target < target { @@ -61,8 +60,6 @@ func (p *parser) markFutureSyntax(syntax futureSyntax, r ast.Range) { yet = "" // This will never be supported case futureSyntaxNonIdentifierArrayRest: name = "Non-identifier array rest patterns" - case futureSyntaxPrivateName: - name = "Private names" } p.log.AddRangeError(&p.source, r, @@ -74,6 +71,7 @@ func (p *parser) markFutureSyntax(syntax futureSyntax, r ast.Range) { func (p *parser) lowerOptionalChain(expr ast.Expr, in exprIn, out exprOut, thisArgFunc func() ast.Expr) (ast.Expr, exprOut) { valueWhenUndefined := ast.Expr{expr.Loc, &ast.EUndefined{}} endsWithPropertyAccess := false + containsPrivateName := false startsWithCall := false originalExpr := expr chain := []ast.Expr{} @@ -100,6 +98,17 @@ flatten: if len(chain) == 1 { endsWithPropertyAccess = true } + + // If this is a private name that needs to be lowered, the entire chain + // itself will have to be lowered even if the language target supports + // optional chaining. This is because there's no way to use our shim + // function for private names with optional chaining syntax. + if p.Target < privateNameTarget { + if _, ok := e.Index.Data.(*ast.EPrivateIdentifier); ok { + containsPrivateName = true + } + } + if e.OptionalChain == ast.OptionalChainStart { break flatten } @@ -127,10 +136,18 @@ flatten: return valueWhenUndefined, exprOut{} } + // We need to lower this if this is an optional call off of a private name + // such as "foo.#bar?.()" because the value of "this" must be captured. + if p.Target < privateNameTarget { + if _, _, private := p.extractPrivateIndex(expr); private != nil { + containsPrivateName = true + } + } + // Don't lower this if we don't need to. This check must be done here instead // of earlier so we can do the dead code elimination above when the target is // null or undefined. - if p.Target >= ES2020 { + if p.Target >= ES2020 && !containsPrivateName { return originalExpr, exprOut{} } @@ -162,12 +179,24 @@ flatten: case *ast.EIndex: targetFunc, wrapFunc := p.captureValueWithPossibleSideEffects(loc, 2, e.Target) + targetWrapFunc = wrapFunc + + // Capture the value of "this" if the target of the starting call + // expression is a private property access + if p.Target < privateNameTarget { + if private, ok := e.Index.Data.(*ast.EPrivateIdentifier); ok { + // "foo().#bar?.()" must capture "foo()" for "this" + expr = p.lowerPrivateGet(targetFunc(), e.Index.Loc, private) + thisArg = targetFunc() + break + } + } + expr = ast.Expr{loc, &ast.EIndex{ Target: targetFunc(), Index: e.Index, }} thisArg = targetFunc() - targetWrapFunc = wrapFunc } } } @@ -183,6 +212,8 @@ flatten: // Step 4: Wrap the starting value by each expression in the chain. We // traverse the chain in reverse because we want to go from the inside out // and the chain was built from the outside in. + var privateThisFunc func() ast.Expr + var privateThisWrapFunc func(ast.Expr) ast.Expr for i := len(chain) - 1; i >= 0; i-- { // Save a reference to the value of "this" for our parent ECall if i == 0 && in.storeThisArgForParentOptionalChain != nil && endsWithPropertyAccess { @@ -198,6 +229,22 @@ flatten: }} case *ast.EIndex: + if private, ok := e.Index.Data.(*ast.EPrivateIdentifier); ok && p.Target < privateNameTarget { + // If this is private name property access inside a call expression and + // the call expression is part of this chain, then the call expression + // is going to need a copy of the property access target as the value + // for "this" for the call. Example for this case: "foo.#bar?.()" + if i > 0 { + if _, ok := chain[i-1].Data.(*ast.ECall); ok { + privateThisFunc, privateThisWrapFunc = p.captureValueWithPossibleSideEffects(loc, 2, result) + result = privateThisFunc() + } + } + + result = p.lowerPrivateGet(result, e.Index.Loc, private) + continue + } + result = ast.Expr{loc, &ast.EIndex{ Target: result, Index: e.Index, @@ -219,6 +266,23 @@ flatten: break } + // If the target of this call expression is a private name property + // access that's also part of this chain, then we must use the copy of + // the property access target that was stashed away earlier as the value + // for "this" for the call. Example for this case: "foo.#bar?.()" + if privateThisFunc != nil { + result = privateThisWrapFunc(ast.Expr{loc, &ast.ECall{ + Target: ast.Expr{loc, &ast.EDot{ + Target: result, + Name: "call", + NameLoc: loc, + }}, + Args: append([]ast.Expr{privateThisFunc()}, e.Args...), + }}) + privateThisFunc = nil + break + } + result = ast.Expr{loc, &ast.ECall{ Target: result, Args: e.Args, @@ -306,6 +370,16 @@ func (p *parser) lowerAssignmentOperator(value ast.Expr, callback func(ast.Expr, } func (p *parser) lowerExponentiationAssignmentOperator(loc ast.Loc, e *ast.EBinary) ast.Expr { + if target, privateLoc, private := p.extractPrivateIndex(e.Left); private != nil { + // "a.#b **= c" => "__privateSet(a, #b, __pow(__privateGet(a, #b), c))" + targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(loc, 2, target) + return targetWrapFunc(p.lowerPrivateSet(targetFunc(), privateLoc, private, + p.callRuntime(loc, "__pow", []ast.Expr{ + p.lowerPrivateGet(targetFunc(), privateLoc, private), + e.Right, + }))) + } + return p.lowerAssignmentOperator(e.Left, func(a ast.Expr, b ast.Expr) ast.Expr { // "a **= b" => "a = __pow(a, b)" return ast.Expr{loc, &ast.EBinary{ @@ -317,6 +391,32 @@ func (p *parser) lowerExponentiationAssignmentOperator(loc ast.Loc, e *ast.EBina } func (p *parser) lowerNullishCoalescingAssignmentOperator(loc ast.Loc, e *ast.EBinary) ast.Expr { + if target, privateLoc, private := p.extractPrivateIndex(e.Left); private != nil { + if p.Target < ES2020 { + // "a.#b ??= c" => "(_a = __privateGet(a, #b)) != null ? _a : __privateSet(a, #b, c)" + targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(loc, 2, target) + testFunc, testWrapFunc := p.captureValueWithPossibleSideEffects(loc, 2, + p.lowerPrivateGet(targetFunc(), privateLoc, private)) + return testWrapFunc(targetWrapFunc(ast.Expr{loc, &ast.EIf{ + Test: ast.Expr{loc, &ast.EBinary{ + Op: ast.BinOpLooseNe, + Left: testFunc(), + Right: ast.Expr{loc, &ast.ENull{}}, + }}, + Yes: testFunc(), + No: p.lowerPrivateSet(targetFunc(), privateLoc, private, e.Right), + }})) + } + + // "a.#b ??= c" => "__privateGet(a, #b) ?? __privateSet(a, #b, c)" + targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(loc, 2, target) + return targetWrapFunc(ast.Expr{loc, &ast.EBinary{ + Op: ast.BinOpNullishCoalescing, + Left: p.lowerPrivateGet(targetFunc(), privateLoc, private), + Right: p.lowerPrivateSet(targetFunc(), privateLoc, private, e.Right), + }}) + } + return p.lowerAssignmentOperator(e.Left, func(a ast.Expr, b ast.Expr) ast.Expr { if p.Target < ES2020 { // "a ??= b" => "(_a = a) != null ? _a : a = b" @@ -342,6 +442,17 @@ func (p *parser) lowerNullishCoalescingAssignmentOperator(loc ast.Loc, e *ast.EB } func (p *parser) lowerLogicalAssignmentOperator(loc ast.Loc, e *ast.EBinary, op ast.OpCode) ast.Expr { + if target, privateLoc, private := p.extractPrivateIndex(e.Left); private != nil { + // "a.#b &&= c" => "__privateGet(a, #b) && __privateSet(a, #b, c)" + // "a.#b ||= c" => "__privateGet(a, #b) || __privateSet(a, #b, c)" + targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(loc, 2, target) + return targetWrapFunc(ast.Expr{loc, &ast.EBinary{ + Op: op, + Left: p.lowerPrivateGet(targetFunc(), privateLoc, private), + Right: p.lowerPrivateSet(targetFunc(), privateLoc, private, e.Right), + }}) + } + return p.lowerAssignmentOperator(e.Left, func(a ast.Expr, b ast.Expr) ast.Expr { // "a &&= b" => "a && (a = b)" // "a ||= b" => "a || (a = b)" @@ -435,6 +546,113 @@ func (p *parser) lowerObjectSpread(loc ast.Loc, e *ast.EObject) ast.Expr { return result } +func (p *parser) lowerPrivateGet(target ast.Expr, loc ast.Loc, private *ast.EPrivateIdentifier) ast.Expr { + switch p.symbols[private.Ref.InnerIndex].Kind { + case ast.SymbolPrivateMethod: + // "this.#method" => "__privateMethod(this, #method, method_fn)" + return p.callRuntime(target.Loc, "__privateMethod", []ast.Expr{ + target, + ast.Expr{loc, &ast.EIdentifier{private.Ref}}, + ast.Expr{loc, &ast.EIdentifier{p.privateGetters[private.Ref]}}, + }) + + case ast.SymbolPrivateGet, ast.SymbolPrivateGetSetPair: + // "this.#getter" => "__privateGet(this, #getter, getter_get)" + return p.callRuntime(target.Loc, "__privateGet", []ast.Expr{ + target, + ast.Expr{loc, &ast.EIdentifier{private.Ref}}, + ast.Expr{loc, &ast.EIdentifier{p.privateGetters[private.Ref]}}, + }) + + default: + // "this.#field" => "__privateGet(this, #field)" + return p.callRuntime(target.Loc, "__privateGet", []ast.Expr{ + target, + ast.Expr{loc, &ast.EIdentifier{private.Ref}}, + }) + } +} + +func (p *parser) lowerPrivateSet( + target ast.Expr, + loc ast.Loc, + private *ast.EPrivateIdentifier, + value ast.Expr, +) ast.Expr { + switch p.symbols[private.Ref.InnerIndex].Kind { + case ast.SymbolPrivateSet, ast.SymbolPrivateGetSetPair: + // "this.#setter = 123" => "__privateSet(this, #setter, 123, setter_set)" + return p.callRuntime(target.Loc, "__privateSet", []ast.Expr{ + target, + ast.Expr{loc, &ast.EIdentifier{private.Ref}}, + value, + ast.Expr{loc, &ast.EIdentifier{p.privateSetters[private.Ref]}}, + }) + + default: + // "this.#field = 123" => "__privateSet(this, #field, 123)" + return p.callRuntime(target.Loc, "__privateSet", []ast.Expr{ + target, + ast.Expr{loc, &ast.EIdentifier{private.Ref}}, + value, + }) + } +} + +func (p *parser) lowerPrivateSetUnOp(target ast.Expr, loc ast.Loc, private *ast.EPrivateIdentifier, op ast.OpCode, isSuffix bool) ast.Expr { + targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(target.Loc, 2, target) + target = targetFunc() + + // Load the private field and then use the unary "+" operator to force it to + // be a number. Otherwise the binary "+" operator may cause string + // concatenation instead of addition if one of the operands is not a number. + value := ast.Expr{target.Loc, &ast.EUnary{ + Op: ast.UnOpPos, + Value: p.lowerPrivateGet(targetFunc(), loc, private), + }} + + if isSuffix { + // "target.#private++" => "__privateSet(target, #private, _a = +__privateGet(target, #private) + 1), _a" + valueFunc, valueWrapFunc := p.captureValueWithPossibleSideEffects(value.Loc, 2, value) + assign := valueWrapFunc(targetWrapFunc(p.lowerPrivateSet(target, loc, private, ast.Expr{target.Loc, &ast.EBinary{ + Op: op, + Left: valueFunc(), + Right: ast.Expr{target.Loc, &ast.ENumber{1}}, + }}))) + return ast.JoinWithComma(assign, valueFunc()) + } + + // "++target.#private" => "__privateSet(target, #private, +__privateGet(target, #private) + 1)" + return targetWrapFunc(p.lowerPrivateSet(target, loc, private, ast.Expr{target.Loc, &ast.EBinary{ + Op: op, + Left: value, + Right: ast.Expr{target.Loc, &ast.ENumber{1}}, + }})) +} + +func (p *parser) lowerPrivateSetBinOp(target ast.Expr, loc ast.Loc, private *ast.EPrivateIdentifier, op ast.OpCode, value ast.Expr) ast.Expr { + // "target.#private += 123" => "__privateSet(target, #private, __privateGet(target, #private) + 123)" + targetFunc, targetWrapFunc := p.captureValueWithPossibleSideEffects(target.Loc, 2, target) + return targetWrapFunc(p.lowerPrivateSet(targetFunc(), loc, private, ast.Expr{value.Loc, &ast.EBinary{ + Op: op, + Left: p.lowerPrivateGet(targetFunc(), loc, private), + Right: value, + }})) +} + +// Returns valid data if target is an expression of the form "foo.#bar" and if +// the language target is such that private members must be lowered +func (p *parser) extractPrivateIndex(target ast.Expr) (ast.Expr, ast.Loc, *ast.EPrivateIdentifier) { + if p.Target < privateNameTarget { + if index, ok := target.Data.(*ast.EIndex); ok { + if private, ok := index.Index.Data.(*ast.EPrivateIdentifier); ok { + return index.Target, index.Index.Loc, private + } + } + } + return ast.Expr{}, ast.Loc{}, nil +} + // Lower class fields for environments that don't support them. This either // takes a statement or an expression. func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) { @@ -483,18 +701,20 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) var ctor *ast.EFunction var parameterFields []ast.Stmt - var instanceFields []ast.Stmt + var instanceMembers []ast.Stmt end := 0 // These expressions are generated after the class body, in this order var computedPropertyCache ast.Expr - var staticFields []ast.Expr + var privateMembers []ast.Expr + var staticMembers []ast.Expr var instanceDecorators []ast.Expr var staticDecorators []ast.Expr // These are only for class expressions that need to be captured var nameFunc func() ast.Expr var wrapFunc func(ast.Expr) ast.Expr + didCaptureClassExpr := false // Class statements can be missing a name if they are in an // "export default" statement: @@ -503,13 +723,23 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) // static foo = 123 // } // - if kind != classKindExpr { - nameFunc = func() ast.Expr { + nameFunc = func() ast.Expr { + if kind == classKindExpr { + // If this is a class expression, capture and store it. We have to + // do this even if it has a name since the name isn't exposed + // outside the class body. + classExpr := &ast.EClass{Class: *class} + class = &classExpr.Class + nameFunc, wrapFunc = p.captureValueWithPossibleSideEffects(classLoc, 2, ast.Expr{classLoc, classExpr}) + expr = nameFunc() + didCaptureClassExpr = true + return nameFunc() + } else { if class.Name == nil { if kind == classKindExportDefaultStmt { class.Name = &defaultName } else { - class.Name = &ast.LocRef{classLoc, p.generateTempRef(tempRefNoDeclare)} + class.Name = &ast.LocRef{classLoc, p.generateTempRef(tempRefNoDeclare, "")} } } p.recordUsage(class.Name.Ref) @@ -554,7 +784,7 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) computedPropertyCache = maybeJoinWithComma(computedPropertyCache, prop.Key) } else { // Store the key in a temporary so we can assign to it later - ref := p.generateTempRef(tempRefNeedsDeclare) + ref := p.generateTempRef(tempRefNeedsDeclare, "") computedPropertyCache = maybeJoinWithComma(computedPropertyCache, ast.Expr{prop.Key.Loc, &ast.EBinary{ Op: ast.BinOpAssign, Left: ast.Expr{prop.Key.Loc, &ast.EIdentifier{ref}}, @@ -620,41 +850,20 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) // Instance and static fields are a JavaScript feature if (p.TS.Parse || p.Target < ESNext) && !prop.IsMethod && (prop.IsStatic || prop.Value == nil) { - _, isPrivateField := prop.Key.Data.(*ast.EPrivateIdentifier) + privateField, _ := prop.Key.Data.(*ast.EPrivateIdentifier) // The TypeScript compiler doesn't follow the JavaScript spec for // uninitialized fields. They are supposed to be set to undefined but the // TypeScript compiler just omits them entirely. - if !p.TS.Parse || prop.Initializer != nil || prop.Value != nil { + if !p.TS.Parse || prop.Initializer != nil || prop.Value != nil || (privateField != nil && p.Target < privateNameTarget) { + loc := prop.Key.Loc + // Determine where to store the field var target ast.Expr if prop.IsStatic { - if nameFunc == nil { - // If this is a class expression, capture and store it. We have to - // do this even if it has a name since the name isn't exposed - // outside the class body. - classExpr := &ast.EClass{Class: *class} - class = &classExpr.Class - nameFunc, wrapFunc = p.captureValueWithPossibleSideEffects(classLoc, 2, ast.Expr{classLoc, classExpr}) - expr = nameFunc() - } target = nameFunc() } else { - target = ast.Expr{prop.Key.Loc, &ast.EThis{}} - } - - // Generate the assignment target - if key, ok := prop.Key.Data.(*ast.EString); ok && !prop.IsComputed { - target = ast.Expr{prop.Key.Loc, &ast.EDot{ - Target: target, - Name: lexer.UTF16ToString(key.Value), - NameLoc: prop.Key.Loc, - }} - } else { - target = ast.Expr{prop.Key.Loc, &ast.EIndex{ - Target: target, - Index: prop.Key, - }} + target = ast.Expr{loc, &ast.EThis{}} } // Generate the assignment initializer @@ -664,20 +873,68 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) } else if prop.Value != nil { init = *prop.Value } else { - init = ast.Expr{prop.Key.Loc, &ast.EUndefined{}} + init = ast.Expr{loc, &ast.EUndefined{}} + } + + // Generate the assignment target + var expr ast.Expr + if privateField != nil && p.Target < privateNameTarget { + // Generate a new symbol for this private field + ref := p.generateTempRef(tempRefNeedsDeclare, "_"+p.symbols[privateField.Ref.InnerIndex].Name[1:]) + p.symbols[privateField.Ref.InnerIndex].Link = ref + + // Initialize the private field to a new WeakMap + if p.weakMapRef == ast.InvalidRef { + p.weakMapRef = p.newSymbol(ast.SymbolUnbound, "WeakMap") + p.moduleScope.Generated = append(p.moduleScope.Generated, p.weakMapRef) + } + privateMembers = append(privateMembers, ast.Expr{loc, &ast.EBinary{ + Op: ast.BinOpAssign, + Left: ast.Expr{loc, &ast.EIdentifier{ref}}, + Right: ast.Expr{loc, &ast.ENew{ + Target: ast.Expr{loc, &ast.EIdentifier{p.weakMapRef}}, + }}, + }}) + + // Add every newly-constructed instance into this map + expr = ast.Expr{loc, &ast.ECall{ + Target: ast.Expr{loc, &ast.EDot{ + Target: ast.Expr{loc, &ast.EIdentifier{ref}}, + Name: "set", + NameLoc: loc, + }}, + Args: []ast.Expr{ + target, + init, + }, + }} + } else { + if key, ok := prop.Key.Data.(*ast.EString); ok && !prop.IsComputed { + target = ast.Expr{loc, &ast.EDot{ + Target: target, + Name: lexer.UTF16ToString(key.Value), + NameLoc: loc, + }} + } else { + target = ast.Expr{loc, &ast.EIndex{ + Target: target, + Index: prop.Key, + }} + } + + expr = ast.Expr{loc, &ast.EBinary{ast.BinOpAssign, target, init}} } - expr := ast.Expr{prop.Key.Loc, &ast.EBinary{ast.BinOpAssign, target, init}} if prop.IsStatic { // Move this property to an assignment after the class ends - staticFields = append(staticFields, expr) + staticMembers = append(staticMembers, expr) } else { // Move this property to an assignment inside the class constructor - instanceFields = append(instanceFields, ast.Stmt{prop.Key.Loc, &ast.SExpr{expr}}) + instanceMembers = append(instanceMembers, ast.Stmt{loc, &ast.SExpr{expr}}) } } - if isPrivateField { + if privateField != nil && p.Target >= privateNameTarget { // Keep the private field but remove the initializer prop.Initializer = nil } else { @@ -688,24 +945,95 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) // Remember where the constructor is for later if prop.IsMethod && prop.Value != nil { - if str, ok := prop.Key.Data.(*ast.EString); ok && lexer.UTF16EqualsString(str.Value, "constructor") { - if fn, ok := prop.Value.Data.(*ast.EFunction); ok { - ctor = fn - - // Initialize TypeScript constructor parameter fields - if p.TS.Parse { - for _, arg := range ctor.Fn.Args { - if arg.IsTypeScriptCtorField { - if id, ok := arg.Binding.Data.(*ast.BIdentifier); ok { - parameterFields = append(parameterFields, ast.Stmt{arg.Binding.Loc, &ast.SExpr{ast.Expr{arg.Binding.Loc, &ast.EBinary{ - ast.BinOpAssign, - ast.Expr{arg.Binding.Loc, &ast.EDot{ - Target: ast.Expr{arg.Binding.Loc, &ast.EThis{}}, - Name: p.symbols[id.Ref.InnerIndex].Name, - NameLoc: arg.Binding.Loc, - }}, - ast.Expr{arg.Binding.Loc, &ast.EIdentifier{id.Ref}}, - }}}}) + switch key := prop.Key.Data.(type) { + case *ast.EPrivateIdentifier: + if p.Target >= privateNameTarget { + break + } + loc := prop.Key.Loc + + // Don't generate a symbol for a getter/setter pair twice + if p.symbols[key.Ref.InnerIndex].Link == ast.InvalidRef { + // Generate a new symbol for this private method + ref := p.generateTempRef(tempRefNeedsDeclare, "_"+p.symbols[key.Ref.InnerIndex].Name[1:]) + p.symbols[key.Ref.InnerIndex].Link = ref + + // Initialize the private method to a new WeakSet + if p.weakSetRef == ast.InvalidRef { + p.weakSetRef = p.newSymbol(ast.SymbolUnbound, "WeakSet") + p.moduleScope.Generated = append(p.moduleScope.Generated, p.weakSetRef) + } + privateMembers = append(privateMembers, ast.Expr{loc, &ast.EBinary{ + Op: ast.BinOpAssign, + Left: ast.Expr{loc, &ast.EIdentifier{ref}}, + Right: ast.Expr{loc, &ast.ENew{ + Target: ast.Expr{loc, &ast.EIdentifier{p.weakSetRef}}, + }}, + }}) + + // Determine where to store the private method + var target ast.Expr + if prop.IsStatic { + target = nameFunc() + } else { + target = ast.Expr{loc, &ast.EThis{}} + } + + // Add every newly-constructed instance into this map + expr := ast.Expr{loc, &ast.ECall{ + Target: ast.Expr{loc, &ast.EDot{ + Target: ast.Expr{loc, &ast.EIdentifier{ref}}, + Name: "add", + NameLoc: loc, + }}, + Args: []ast.Expr{ + target, + }, + }} + + if prop.IsStatic { + // Move this property to an assignment after the class ends + staticMembers = append(staticMembers, expr) + } else { + // Move this property to an assignment inside the class constructor + instanceMembers = append(instanceMembers, ast.Stmt{loc, &ast.SExpr{expr}}) + } + } + + // Move the method definition outside the class body + methodRef := p.generateTempRef(tempRefNeedsDeclare, "_") + if prop.Kind == ast.PropertySet { + p.symbols[methodRef.InnerIndex].Link = p.privateSetters[key.Ref] + } else { + p.symbols[methodRef.InnerIndex].Link = p.privateGetters[key.Ref] + } + privateMembers = append(privateMembers, ast.Expr{loc, &ast.EBinary{ + Op: ast.BinOpAssign, + Left: ast.Expr{loc, &ast.EIdentifier{methodRef}}, + Right: *prop.Value, + }}) + continue + + case *ast.EString: + if lexer.UTF16EqualsString(key.Value, "constructor") { + if fn, ok := prop.Value.Data.(*ast.EFunction); ok { + ctor = fn + + // Initialize TypeScript constructor parameter fields + if p.TS.Parse { + for _, arg := range ctor.Fn.Args { + if arg.IsTypeScriptCtorField { + if id, ok := arg.Binding.Data.(*ast.BIdentifier); ok { + parameterFields = append(parameterFields, ast.Stmt{arg.Binding.Loc, &ast.SExpr{ast.Expr{arg.Binding.Loc, &ast.EBinary{ + ast.BinOpAssign, + ast.Expr{arg.Binding.Loc, &ast.EDot{ + Target: ast.Expr{arg.Binding.Loc, &ast.EThis{}}, + Name: p.symbols[id.Ref.InnerIndex].Name, + NameLoc: arg.Binding.Loc, + }}, + ast.Expr{arg.Binding.Loc, &ast.EIdentifier{id.Ref}}, + }}}}) + } } } } @@ -723,7 +1051,7 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) class.Properties = class.Properties[:end] // Insert instance field initializers into the constructor - if len(instanceFields) > 0 || len(parameterFields) > 0 { + if len(instanceMembers) > 0 || len(parameterFields) > 0 { // Create a constructor if one doesn't already exist if ctor == nil { ctor = &ast.EFunction{} @@ -756,7 +1084,7 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) stmtsFrom = stmtsFrom[1:] } stmtsTo = append(stmtsTo, parameterFields...) - stmtsTo = append(stmtsTo, instanceFields...) + stmtsTo = append(stmtsTo, instanceMembers...) ctor.Fn.Body.Stmts = append(stmtsTo, stmtsFrom...) // Sort the constructor first to match the TypeScript compiler's output @@ -775,21 +1103,31 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) // Pack the class back into an expression. We don't need to handle TypeScript // decorators for class expressions because TypeScript doesn't support them. if kind == classKindExpr { - // Initialize any remaining computed properties immediately after the end - // of the class body + // Calling "nameFunc" will replace "expr", so make sure to do that first + // before joining "expr" with any other expressions + var nameToJoin ast.Expr + if didCaptureClassExpr || computedPropertyCache.Data != nil || + len(privateMembers) > 0 || len(staticMembers) > 0 { + nameToJoin = nameFunc() + } + + // Then join "expr" with any other expressions that apply if computedPropertyCache.Data != nil { expr = ast.JoinWithComma(expr, computedPropertyCache) } + for _, value := range privateMembers { + expr = ast.JoinWithComma(expr, value) + } + for _, value := range staticMembers { + expr = ast.JoinWithComma(expr, value) + } - // Join the static field initializers if this is a class expression - if len(staticFields) > 0 { - for _, initializer := range staticFields { - expr = ast.JoinWithComma(expr, initializer) - } - expr = ast.JoinWithComma(expr, nameFunc()) - if wrapFunc != nil { - expr = wrapFunc(expr) - } + // Finally join "expr" with the variable that holds the class object + if nameToJoin.Data != nil { + expr = ast.JoinWithComma(expr, nameToJoin) + } + if wrapFunc != nil { + expr = wrapFunc(expr) } return nil, expr } @@ -829,7 +1167,10 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) if computedPropertyCache.Data != nil { stmts = append(stmts, ast.Stmt{expr.Loc, &ast.SExpr{computedPropertyCache}}) } - for _, expr := range staticFields { + for _, expr := range privateMembers { + stmts = append(stmts, ast.Stmt{expr.Loc, &ast.SExpr{expr}}) + } + for _, expr := range staticMembers { stmts = append(stmts, ast.Stmt{expr.Loc, &ast.SExpr{expr}}) } for _, expr := range instanceDecorators { @@ -853,8 +1194,7 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) // we don't want it to accidentally use the same variable as the class and // cause a name collision. nameFromPath := ast.GenerateNonUniqueNameFromPath(p.source.AbsolutePath) + "_default" - defaultRef := p.generateTempRef(tempRefNoDeclare) - p.symbols[defaultRef.InnerIndex].Name = nameFromPath + defaultRef := p.generateTempRef(tempRefNoDeclare, nameFromPath) p.namedExports["default"] = defaultRef p.recordDeclaredSymbol(defaultRef) diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index ba2aa918a69..e142d433cfb 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -1623,8 +1623,8 @@ func TestLowerClassInstance(t *testing.T) { expectPrintedTarget(t, ES2015, "(class { foo = null })", "(class {\n constructor() {\n this.foo = null;\n }\n});\n") expectPrintedTarget(t, ES2015, "(class { 123 })", "(class {\n constructor() {\n this[123] = void 0;\n }\n});\n") expectPrintedTarget(t, ES2015, "(class { 123 = null })", "(class {\n constructor() {\n this[123] = null;\n }\n});\n") - expectPrintedTarget(t, ES2015, "(class { [foo] })", "var _a;\n(class {\n constructor() {\n this[_a] = void 0;\n }\n}), _a = foo;\n") - expectPrintedTarget(t, ES2015, "(class { [foo] = null })", "var _a;\n(class {\n constructor() {\n this[_a] = null;\n }\n}), _a = foo;\n") + expectPrintedTarget(t, ES2015, "(class { [foo] })", "var _a, _b;\n_b = class {\n constructor() {\n this[_a] = void 0;\n }\n}, _a = foo, _b;\n") + expectPrintedTarget(t, ES2015, "(class { [foo] = null })", "var _a, _b;\n_b = class {\n constructor() {\n this[_a] = null;\n }\n}, _a = foo, _b;\n") expectPrintedTarget(t, ES2015, "class Foo extends Bar {}", `class Foo extends Bar { } @@ -1937,9 +1937,9 @@ func TestPrivateIdentifiers(t *testing.T) { expectPrinted(t, "class Foo { foo = this.#foo; #foo }", "class Foo {\n foo = this.#foo;\n #foo;\n}\n") expectPrinted(t, "class Foo { foo = this?.#foo; #foo }", "class Foo {\n foo = this?.#foo;\n #foo;\n}\n") expectParseError(t, "class Foo { #foo } class Bar { foo = this.#foo }", - ": error: Private name \"#foo\" is not available here\n") + ": error: Private name \"#foo\" must be declared in an enclosing class\n") expectParseError(t, "class Foo { #foo } class Bar { foo = this?.#foo }", - ": error: Private name \"#foo\" is not available here\n") + ": error: Private name \"#foo\" must be declared in an enclosing class\n") expectPrinted(t, `class Foo { #if diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index e56c3932728..f236c36dd16 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -71,6 +71,21 @@ const Code = ` return result } export let __param = (index, decorator) => (target, key) => decorator(target, key, index) + + // For class private members + export let __privateGet = (obj, member, getter) => { + if (!member.has(obj)) throw new TypeError('Cannot read from private field') + return getter ? getter.call(obj) : member.get(obj) + } + export let __privateSet = (obj, member, value, setter) => { + if (!member.has(obj)) throw new TypeError('Cannot write to private field') + setter ? setter.call(obj, value) : member.set(obj, value) + return value + } + export let __privateMethod = (obj, member, method) => { + if (!member.has(obj)) throw new TypeError('Cannot access private method') + return method + } ` // The TypeScript decorator transform behaves similar to the official diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index ef18e4c6056..5faed94d379 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -436,6 +436,52 @@ }), ) + // Class lowering tests + tests.push( + test(['in.js', '--outfile=node.js', '--target=es6'], { + 'in.js': ` + class Foo { + foo = 123 + self = this + #method() { + if (this.foo !== 123) throw 'fail' + } + bar() { + let that = () => this + that().#method() + that().#method?.() + that()?.#method() + that()?.#method?.() + that().self.#method() + that().self.#method?.() + that().self?.#method() + that().self?.#method?.() + that()?.self.#method() + that()?.self.#method?.() + that()?.self?.#method() + that()?.self?.#method?.() + } + } + new Foo().bar() + `, + }), + test(['in.js', '--outfile=node.js', '--target=es6'], { + 'in.js': ` + class Foo { + foo = 123 + get #bar() { return this.foo } + set #bar(x) { this.foo = x } + bar() { + let that = () => this + that().#bar **= 2 + if (this.foo !== 15129) throw 'fail' + } + } + new Foo().bar() + `, + }), + ) + // Test writing to stdout tests.push( // These should succeed diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 6a6e3a38e90..8e537b57b52 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -264,14 +264,6 @@ let transformTests = { bigInt: ({ service }) => futureSyntax(service, '123n', 'es2019', 'es2020'), objRest: ({ service }) => futureSyntax(service, 'let {...x} = y', 'es2017', 'es2018'), nonIdArrayRest: ({ service }) => futureSyntax(service, 'let [...[x]] = y', 'es2015', 'es2016'), - privateField: ({ service }) => futureSyntax(service, 'class Foo { #foo }', 'es2020', 'esnext'), - privateMethod: ({ service }) => futureSyntax(service, 'class Foo { #foo() {} }', 'es2020', 'esnext'), - privateGetter: ({ service }) => futureSyntax(service, 'class Foo { get #foo() {} }', 'es2020', 'esnext'), - privateSetter: ({ service }) => futureSyntax(service, 'class Foo { set #foo() {} }', 'es2020', 'esnext'), - privateStaticField: ({ service }) => futureSyntax(service, 'class Foo { static #foo }', 'es2020', 'esnext'), - privateStaticMethod: ({ service }) => futureSyntax(service, 'class Foo { static #foo() {} }', 'es2020', 'esnext'), - privateStaticGetter: ({ service }) => futureSyntax(service, 'class Foo { static get #foo() {} }', 'es2020', 'esnext'), - privateStaticSetter: ({ service }) => futureSyntax(service, 'class Foo { static set #foo() {} }', 'es2020', 'esnext'), // Future syntax: async functions asyncFnStmt: ({ service }) => futureSyntax(service, 'async function foo() {}', 'es2016', 'es2017'),