diff --git a/packages/babel-helper-create-class-features-plugin/src/fields.ts b/packages/babel-helper-create-class-features-plugin/src/fields.ts index 695979cdd1..0f500852c8 100644 --- a/packages/babel-helper-create-class-features-plugin/src/fields.ts +++ b/packages/babel-helper-create-class-features-plugin/src/fields.ts @@ -1,6 +1,6 @@ import { template, traverse, types as t } from "@babel/core"; import type { File } from "@babel/core"; -import type { NodePath, Visitor } from "@babel/traverse"; +import type { NodePath, Visitor, Scope } from "@babel/traverse"; import ReplaceSupers, { environmentVisitor, } from "@babel/helper-replace-supers"; @@ -58,7 +58,7 @@ export function buildPrivateNamesMap(props: PropPath[]) { export function buildPrivateNamesNodes( privateNamesMap: PrivateNamesMap, privateFieldsAsProperties: boolean, - state, + state: File, ) { const initNodes: t.Statement[] = []; @@ -165,6 +165,7 @@ interface PrivateNameState { classRef: t.Identifier; file: File; noDocumentAll: boolean; + innerBinding?: t.Identifier; } const privateNameVisitor = privateNameVisitorFactory< @@ -188,9 +189,28 @@ const privateNameVisitor = privateNameVisitorFactory< }, }); +// rename all bindings that shadows innerBinding +function unshadow( + name: string, + scope: Scope, + innerBinding: t.Identifier | undefined, +) { + // in some cases, scope.getBinding(name) === undefined + // so we check hasBinding to avoid keeping looping + // see: https://github.com/babel/babel/pull/13656#discussion_r686030715 + while ( + scope?.hasBinding(name) && + !scope.bindingIdentifierEquals(name, innerBinding) + ) { + scope.rename(name); + scope = scope.parent; + } +} + const privateInVisitor = privateNameVisitorFactory<{ classRef: t.Identifier; file: File; + innerBinding?: t.Identifier; }>({ BinaryExpression(path) { const { operator, left, right } = path.node; @@ -204,6 +224,10 @@ const privateInVisitor = privateNameVisitorFactory<{ if (!privateNamesMap.has(name)) return; if (redeclared && redeclared.includes(name)) return; + // if there are any local variable shadowing classRef, unshadow it + // see #12960 + unshadow(this.classRef.name, path.scope, this.innerBinding); + if (privateFieldsAsProperties) { const { id } = privateNamesMap.get(name); path.replaceWith(template.expression.ast` @@ -255,7 +279,7 @@ const privateNameHandlerSpec: Handler & Receiver = }, get(member) { - const { classRef, privateNamesMap, file } = this; + const { classRef, privateNamesMap, file, innerBinding } = this; const { name } = (member.node.property as t.PrivateName).id; const { id, @@ -273,6 +297,10 @@ const privateNameHandlerSpec: Handler & Receiver = ? "classStaticPrivateMethodGet" : "classStaticPrivateFieldSpecGet"; + // if there are any local variable shadowing classRef, unshadow it + // see #12960 + unshadow(classRef.name, member.scope, innerBinding); + return t.callExpression(file.addHelper(helperName), [ this.receiver(member), t.cloneNode(classRef), @@ -463,8 +491,8 @@ export function transformPrivateNamesUsage( ref: t.Identifier, path: NodePath, privateNamesMap: PrivateNamesMap, - { privateFieldsAsProperties, noDocumentAll }, - state, + { privateFieldsAsProperties, noDocumentAll, innerBinding }, + state: File, ) { if (!privateNamesMap.size) return; @@ -479,12 +507,14 @@ export function transformPrivateNamesUsage( file: state, ...handler, noDocumentAll, + innerBinding, }); body.traverse(privateInVisitor, { privateNamesMap, classRef: ref, file: state, privateFieldsAsProperties, + innerBinding, }); } @@ -770,7 +800,7 @@ const thisContextVisitor = traverse.visitors.merge([ ]); const innerReferencesVisitor = { - ReferencedIdentifier(path, state) { + ReferencedIdentifier(path: NodePath, state) { if ( path.scope.bindingIdentifierEquals(path.node.name, state.innerBinding) ) { @@ -831,7 +861,7 @@ export function buildFieldsInitNodes( superRef: t.Expression | undefined, props: PropPath[], privateNamesMap: PrivateNamesMap, - state, + state: File, setPublicClassFields: boolean, privateFieldsAsProperties: boolean, constantSuper: boolean, diff --git a/packages/babel-helper-create-class-features-plugin/src/index.ts b/packages/babel-helper-create-class-features-plugin/src/index.ts index 24546834a1..57c7595a15 100644 --- a/packages/babel-helper-create-class-features-plugin/src/index.ts +++ b/packages/babel-helper-create-class-features-plugin/src/index.ts @@ -1,4 +1,5 @@ import { types as t } from "@babel/core"; +import type { File } from "@babel/core"; import type { NodePath } from "@babel/traverse"; import nameFunction from "@babel/helper-function-name"; import splitExportDeclaration from "@babel/helper-split-export-declaration"; @@ -92,7 +93,7 @@ export function createClassFeaturePlugin({ }, visitor: { - Class(path: NodePath, state) { + Class(path: NodePath, state: File) { if (this.file.get(versionKey) !== version) return; verifyUsedFeatures(path, this.file); @@ -198,6 +199,7 @@ export function createClassFeaturePlugin({ { privateFieldsAsProperties: privateFieldsAsProperties ?? loose, noDocumentAll, + innerBinding, }, state, ); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/exec.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/exec.js new file mode 100644 index 0000000000..101547f13c --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/exec.js @@ -0,0 +1,15 @@ +class Test { + + static #x = 1 + + static method() { + const Test = 2; + const func = () => { + const Test = 3; + return this.#x + Test; + } + return func() + Test; + } +} + +expect(Test.method()).toBe(6) diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/input.js new file mode 100644 index 0000000000..5f5ffa0b23 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/input.js @@ -0,0 +1,13 @@ +class Test { + + static #x = 1 + + static method() { + const Test = 2; + const func = () => { + const Test = 3; + return this.#x + Test; + } + return func() + Test; + } +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/options.json new file mode 100644 index 0000000000..19ed5174f5 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["proposal-class-properties"] +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/output.js new file mode 100644 index 0000000000..8b03caa83f --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/static-shadow/output.js @@ -0,0 +1,18 @@ +class Test { + static method() { + const _Test2 = 2; + + const func = () => { + const _Test = 3; + return babelHelpers.classStaticPrivateFieldSpecGet(this, Test, _x) + _Test; + }; + + return func() + _Test2; + } + +} + +var _x = { + writable: true, + value: 1 +}; diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/exec.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/exec.js new file mode 100644 index 0000000000..101547f13c --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/exec.js @@ -0,0 +1,15 @@ +class Test { + + static #x = 1 + + static method() { + const Test = 2; + const func = () => { + const Test = 3; + return this.#x + Test; + } + return func() + Test; + } +} + +expect(Test.method()).toBe(6) diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/input.js new file mode 100644 index 0000000000..5f5ffa0b23 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/input.js @@ -0,0 +1,13 @@ +class Test { + + static #x = 1 + + static method() { + const Test = 2; + const func = () => { + const Test = 3; + return this.#x + Test; + } + return func() + Test; + } +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/options.json new file mode 100644 index 0000000000..19ed5174f5 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["proposal-class-properties"] +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/output.js new file mode 100644 index 0000000000..8b03caa83f --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-shadow/output.js @@ -0,0 +1,18 @@ +class Test { + static method() { + const _Test2 = 2; + + const func = () => { + const _Test = 3; + return babelHelpers.classStaticPrivateFieldSpecGet(this, Test, _x) + _Test; + }; + + return func() + _Test2; + } + +} + +var _x = { + writable: true, + value: 1 +}; diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/exec.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/exec.js new file mode 100644 index 0000000000..673b1cced3 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/exec.js @@ -0,0 +1,17 @@ +class Test { + + static #x = 1 + + method(other) { + const Test = 2; + const func = () => { + const Test = 3; + return #x in other && Test; + } + return func() + Test; + } +} + +const t = new Test(); +const t2 = new Test(); +expect(t.method(Test)).toBe(5) diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/input.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/input.js new file mode 100644 index 0000000000..faa4ce3d10 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/input.js @@ -0,0 +1,13 @@ +class Test { + + static #x = 1 + + method(other) { + const Test = 2; + const func = () => { + const Test = 3; + return #x in other && Test; + } + return func() + Test; + } +} diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/options.json b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/options.json new file mode 100644 index 0000000000..19ed5174f5 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["proposal-class-properties"] +} diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/output.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/output.js new file mode 100644 index 0000000000..898d6a8ddc --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private-loose/static-shadow/output.js @@ -0,0 +1,18 @@ +class Test { + method(other) { + const _Test2 = 2; + + const func = () => { + const _Test = 3; + return other === Test && _Test; + }; + + return func() + _Test2; + } + +} + +var _x = { + writable: true, + value: 1 +}; diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/exec.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/exec.js new file mode 100644 index 0000000000..673b1cced3 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/exec.js @@ -0,0 +1,17 @@ +class Test { + + static #x = 1 + + method(other) { + const Test = 2; + const func = () => { + const Test = 3; + return #x in other && Test; + } + return func() + Test; + } +} + +const t = new Test(); +const t2 = new Test(); +expect(t.method(Test)).toBe(5) diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/input.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/input.js new file mode 100644 index 0000000000..faa4ce3d10 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/input.js @@ -0,0 +1,13 @@ +class Test { + + static #x = 1 + + method(other) { + const Test = 2; + const func = () => { + const Test = 3; + return #x in other && Test; + } + return func() + Test; + } +} diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/options.json b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/options.json new file mode 100644 index 0000000000..19ed5174f5 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["proposal-class-properties"] +} diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/output.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/output.js new file mode 100644 index 0000000000..898d6a8ddc --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/private/static-shadow/output.js @@ -0,0 +1,18 @@ +class Test { + method(other) { + const _Test2 = 2; + + const func = () => { + const _Test = 3; + return other === Test && _Test; + }; + + return func() + _Test2; + } + +} + +var _x = { + writable: true, + value: 1 +}; diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/exec.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/exec.js new file mode 100644 index 0000000000..673b1cced3 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/exec.js @@ -0,0 +1,17 @@ +class Test { + + static #x = 1 + + method(other) { + const Test = 2; + const func = () => { + const Test = 3; + return #x in other && Test; + } + return func() + Test; + } +} + +const t = new Test(); +const t2 = new Test(); +expect(t.method(Test)).toBe(5) diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/input.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/input.js new file mode 100644 index 0000000000..faa4ce3d10 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/input.js @@ -0,0 +1,13 @@ +class Test { + + static #x = 1 + + method(other) { + const Test = 2; + const func = () => { + const Test = 3; + return #x in other && Test; + } + return func() + Test; + } +} diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/options.json b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/options.json new file mode 100644 index 0000000000..19ed5174f5 --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["proposal-class-properties"] +} diff --git a/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/output.js b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/output.js new file mode 100644 index 0000000000..898d6a8ddc --- /dev/null +++ b/packages/babel-plugin-proposal-private-property-in-object/test/fixtures/to-native-fields/static-shadow/output.js @@ -0,0 +1,18 @@ +class Test { + method(other) { + const _Test2 = 2; + + const func = () => { + const _Test = 3; + return other === Test && _Test; + }; + + return func() + _Test2; + } + +} + +var _x = { + writable: true, + value: 1 +};