fix: fix static private field shadowed by local variable (#13656)

* fix: fix static private field shadowed by local variable

currently throw an error, maybe we could generate correct code

fix #12960

* feat: rename local variable and add test cases

* feat: add unshadow to privateIn visitor

also add test cases

* test: add reference to shadowed variable

* refactor: apply suggested changes

simplify logic and add comments
This commit is contained in:
王清雨 2021-08-30 18:03:47 +08:00 committed by GitHub
parent a54f041440
commit 313ecb579d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 291 additions and 8 deletions

View File

@ -1,6 +1,6 @@
import { template, traverse, types as t } from "@babel/core"; import { template, traverse, types as t } from "@babel/core";
import type { File } 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, { import ReplaceSupers, {
environmentVisitor, environmentVisitor,
} from "@babel/helper-replace-supers"; } from "@babel/helper-replace-supers";
@ -58,7 +58,7 @@ export function buildPrivateNamesMap(props: PropPath[]) {
export function buildPrivateNamesNodes( export function buildPrivateNamesNodes(
privateNamesMap: PrivateNamesMap, privateNamesMap: PrivateNamesMap,
privateFieldsAsProperties: boolean, privateFieldsAsProperties: boolean,
state, state: File,
) { ) {
const initNodes: t.Statement[] = []; const initNodes: t.Statement[] = [];
@ -165,6 +165,7 @@ interface PrivateNameState {
classRef: t.Identifier; classRef: t.Identifier;
file: File; file: File;
noDocumentAll: boolean; noDocumentAll: boolean;
innerBinding?: t.Identifier;
} }
const privateNameVisitor = privateNameVisitorFactory< 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<{ const privateInVisitor = privateNameVisitorFactory<{
classRef: t.Identifier; classRef: t.Identifier;
file: File; file: File;
innerBinding?: t.Identifier;
}>({ }>({
BinaryExpression(path) { BinaryExpression(path) {
const { operator, left, right } = path.node; const { operator, left, right } = path.node;
@ -204,6 +224,10 @@ const privateInVisitor = privateNameVisitorFactory<{
if (!privateNamesMap.has(name)) return; if (!privateNamesMap.has(name)) return;
if (redeclared && redeclared.includes(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) { if (privateFieldsAsProperties) {
const { id } = privateNamesMap.get(name); const { id } = privateNamesMap.get(name);
path.replaceWith(template.expression.ast` path.replaceWith(template.expression.ast`
@ -255,7 +279,7 @@ const privateNameHandlerSpec: Handler<PrivateNameState & Receiver> & Receiver =
}, },
get(member) { get(member) {
const { classRef, privateNamesMap, file } = this; const { classRef, privateNamesMap, file, innerBinding } = this;
const { name } = (member.node.property as t.PrivateName).id; const { name } = (member.node.property as t.PrivateName).id;
const { const {
id, id,
@ -273,6 +297,10 @@ const privateNameHandlerSpec: Handler<PrivateNameState & Receiver> & Receiver =
? "classStaticPrivateMethodGet" ? "classStaticPrivateMethodGet"
: "classStaticPrivateFieldSpecGet"; : "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), [ return t.callExpression(file.addHelper(helperName), [
this.receiver(member), this.receiver(member),
t.cloneNode(classRef), t.cloneNode(classRef),
@ -463,8 +491,8 @@ export function transformPrivateNamesUsage(
ref: t.Identifier, ref: t.Identifier,
path: NodePath<t.Class>, path: NodePath<t.Class>,
privateNamesMap: PrivateNamesMap, privateNamesMap: PrivateNamesMap,
{ privateFieldsAsProperties, noDocumentAll }, { privateFieldsAsProperties, noDocumentAll, innerBinding },
state, state: File,
) { ) {
if (!privateNamesMap.size) return; if (!privateNamesMap.size) return;
@ -479,12 +507,14 @@ export function transformPrivateNamesUsage(
file: state, file: state,
...handler, ...handler,
noDocumentAll, noDocumentAll,
innerBinding,
}); });
body.traverse(privateInVisitor, { body.traverse(privateInVisitor, {
privateNamesMap, privateNamesMap,
classRef: ref, classRef: ref,
file: state, file: state,
privateFieldsAsProperties, privateFieldsAsProperties,
innerBinding,
}); });
} }
@ -770,7 +800,7 @@ const thisContextVisitor = traverse.visitors.merge([
]); ]);
const innerReferencesVisitor = { const innerReferencesVisitor = {
ReferencedIdentifier(path, state) { ReferencedIdentifier(path: NodePath<t.Identifier>, state) {
if ( if (
path.scope.bindingIdentifierEquals(path.node.name, state.innerBinding) path.scope.bindingIdentifierEquals(path.node.name, state.innerBinding)
) { ) {
@ -831,7 +861,7 @@ export function buildFieldsInitNodes(
superRef: t.Expression | undefined, superRef: t.Expression | undefined,
props: PropPath[], props: PropPath[],
privateNamesMap: PrivateNamesMap, privateNamesMap: PrivateNamesMap,
state, state: File,
setPublicClassFields: boolean, setPublicClassFields: boolean,
privateFieldsAsProperties: boolean, privateFieldsAsProperties: boolean,
constantSuper: boolean, constantSuper: boolean,

View File

@ -1,4 +1,5 @@
import { types as t } from "@babel/core"; import { types as t } from "@babel/core";
import type { File } from "@babel/core";
import type { NodePath } from "@babel/traverse"; import type { NodePath } from "@babel/traverse";
import nameFunction from "@babel/helper-function-name"; import nameFunction from "@babel/helper-function-name";
import splitExportDeclaration from "@babel/helper-split-export-declaration"; import splitExportDeclaration from "@babel/helper-split-export-declaration";
@ -92,7 +93,7 @@ export function createClassFeaturePlugin({
}, },
visitor: { visitor: {
Class(path: NodePath<t.Class>, state) { Class(path: NodePath<t.Class>, state: File) {
if (this.file.get(versionKey) !== version) return; if (this.file.get(versionKey) !== version) return;
verifyUsedFeatures(path, this.file); verifyUsedFeatures(path, this.file);
@ -198,6 +199,7 @@ export function createClassFeaturePlugin({
{ {
privateFieldsAsProperties: privateFieldsAsProperties ?? loose, privateFieldsAsProperties: privateFieldsAsProperties ?? loose,
noDocumentAll, noDocumentAll,
innerBinding,
}, },
state, state,
); );

View File

@ -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)

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
{
"plugins": ["proposal-class-properties"]
}

View File

@ -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
};

View File

@ -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)

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
{
"plugins": ["proposal-class-properties"]
}

View File

@ -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
};

View File

@ -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)

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
{
"plugins": ["proposal-class-properties"]
}

View File

@ -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
};

View File

@ -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)

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
{
"plugins": ["proposal-class-properties"]
}

View File

@ -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
};

View File

@ -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)

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
{
"plugins": ["proposal-class-properties"]
}

View File

@ -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
};