Add typings to create-class-features-plugin helper (#13570)

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
This commit is contained in:
王清雨 2021-08-03 03:22:37 +08:00 committed by GitHub
parent b3ab4769d0
commit 1960f23c22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 582 additions and 334 deletions

View File

@ -1,21 +1,26 @@
import { types as t, template } from "@babel/core";
import type { File } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import ReplaceSupers from "@babel/helper-replace-supers";
import nameFunction from "@babel/helper-function-name";
export function hasOwnDecorators(node) {
type Decorable = Extract<t.Node, { decorators?: t.Decorator[] | null }>;
export function hasOwnDecorators(node: t.Node) {
// @ts-expect-error(flow->ts) TODO: maybe we could add t.isDecoratable to make ts happy
return !!(node.decorators && node.decorators.length);
}
export function hasDecorators(node) {
export function hasDecorators(node: t.Class) {
return hasOwnDecorators(node) || node.body.body.some(hasOwnDecorators);
}
function prop(key, value) {
function prop(key: string, value?: t.Expression) {
if (!value) return null;
return t.objectProperty(t.identifier(key), value);
}
function method(key, body) {
function method(key: string, body: t.Statement[]) {
return t.objectMethod(
"method",
t.identifier(key),
@ -24,8 +29,8 @@ function method(key, body) {
);
}
function takeDecorators(node) {
let result;
function takeDecorators(node: Decorable) {
let result: t.ArrayExpression | undefined;
if (node.decorators && node.decorators.length > 0) {
result = t.arrayExpression(
node.decorators.map(decorator => decorator.expression),
@ -47,7 +52,12 @@ function getKey(node) {
// NOTE: This function can be easily bound as .bind(file, classRef, superRef)
// to make it easier to use it in a loop.
function extractElementDescriptor(/* this: File, */ classRef, superRef, path) {
function extractElementDescriptor(
this: File,
classRef: t.Identifier,
superRef: t.Identifier,
path: ClassElementPath,
) {
const { node, scope } = path;
const isMethod = path.isClassMethod();
@ -68,17 +78,17 @@ function extractElementDescriptor(/* this: File, */ classRef, superRef, path) {
}).replace();
const properties: t.ObjectExpression["properties"] = [
prop("kind", t.stringLiteral(isMethod ? node.kind : "field")),
prop("decorators", takeDecorators(node)),
prop("kind", t.stringLiteral(t.isClassMethod(node) ? node.kind : "field")),
prop("decorators", takeDecorators(node as Decorable)),
prop("static", node.static && t.booleanLiteral(true)),
prop("key", getKey(node)),
].filter(Boolean);
if (isMethod) {
if (t.isClassMethod(node)) {
const id = node.computed ? null : node.key;
t.toExpression(node);
properties.push(prop("value", nameFunction({ node, id, scope }) || node));
} else if (node.value) {
} else if (t.isClassProperty(node) && node.value) {
properties.push(
method("value", template.statements.ast`return ${node.value}`),
);
@ -91,7 +101,7 @@ function extractElementDescriptor(/* this: File, */ classRef, superRef, path) {
return t.objectExpression(properties);
}
function addDecorateHelper(file) {
function addDecorateHelper(file: File) {
try {
return file.addHelper("decorate");
} catch (err) {
@ -105,7 +115,15 @@ function addDecorateHelper(file) {
}
}
export function buildDecoratedClass(ref, path, elements, file) {
type ClassElement = t.Class["body"]["body"][number];
type ClassElementPath = NodePath<ClassElement>;
export function buildDecoratedClass(
ref: t.Identifier,
path: NodePath<t.Class>,
elements: ClassElementPath[],
file: File,
) {
const { node, scope } = path;
const initializeId = scope.generateUidIdentifier("initialize");
const isDeclaration = node.id && path.isDeclaration();
@ -115,7 +133,7 @@ export function buildDecoratedClass(ref, path, elements, file) {
node.type = "ClassDeclaration";
if (!node.id) node.id = t.cloneNode(ref);
let superId;
let superId: t.Identifier;
if (superClass) {
superId = scope.generateUidIdentifierBasedOnNode(node.superClass, "super");
node.superClass = superId;
@ -124,7 +142,7 @@ export function buildDecoratedClass(ref, path, elements, file) {
const classDecorators = takeDecorators(node);
const definitions = t.arrayExpression(
elements
// Ignore TypeScript's abstract methods (see #10514)
// @ts-expect-error Ignore TypeScript's abstract methods (see #10514)
.filter(element => !element.node.abstract)
.map(extractElementDescriptor.bind(file, node.id, superId)),
);
@ -155,9 +173,9 @@ export function buildDecoratedClass(ref, path, elements, file) {
return {
instanceNodes: [template.statement.ast`${t.cloneNode(initializeId)}(this)`],
wrapClass(path) {
wrapClass(path: NodePath<t.Class>) {
path.replaceWith(replacement);
return path.get(classPathDesc);
return path.get(classPathDesc) as NodePath;
},
};
}

View File

@ -1,3 +1,5 @@
import type { File } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import { hasOwnDecorators } from "./decorators";
export const FEATURES = Object.freeze({
@ -36,7 +38,7 @@ const looseKey = "@babel/plugin-class-features/looseKey";
const looseLowPriorityKey =
"@babel/plugin-class-features/looseLowPriorityKey/#__internal__@babel/preset-env__please-overwrite-loose-instead-of-throwing";
export function enableFeature(file, feature, loose) {
export function enableFeature(file: File, feature: number, loose: boolean) {
// We can't blindly enable the feature because, if it was already set,
// "loose" can't be changed, so that
// @babel/plugin-class-properties { loose: true }
@ -46,12 +48,14 @@ export function enableFeature(file, feature, loose) {
if (!hasFeature(file, feature) || canIgnoreLoose(file, feature)) {
file.set(featuresKey, file.get(featuresKey) | feature);
if (
// @ts-expect-error
loose ===
"#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error"
) {
setLoose(file, feature, true);
file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) | feature);
} else if (
// @ts-expect-error
loose ===
"#__internal__@babel/preset-env__prefer-false-but-true-is-ok-if-it-prevents-an-error"
) {
@ -62,8 +66,8 @@ export function enableFeature(file, feature, loose) {
}
}
let resolvedLoose: void | true | false;
let higherPriorityPluginName: void | string;
let resolvedLoose: boolean | undefined;
let higherPriorityPluginName: string | undefined;
for (const [mask, name] of featuresSameLoose) {
if (!hasFeature(file, mask)) continue;
@ -103,26 +107,26 @@ export function enableFeature(file, feature, loose) {
}
}
function hasFeature(file, feature) {
function hasFeature(file: File, feature: number) {
return !!(file.get(featuresKey) & feature);
}
export function isLoose(file, feature) {
export function isLoose(file: File, feature: number) {
return !!(file.get(looseKey) & feature);
}
function setLoose(file, feature, loose) {
function setLoose(file: File, feature: number, loose: boolean) {
if (loose) file.set(looseKey, file.get(looseKey) | feature);
else file.set(looseKey, file.get(looseKey) & ~feature);
file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) & ~feature);
}
function canIgnoreLoose(file, feature) {
function canIgnoreLoose(file: File, feature: number) {
return !!(file.get(looseLowPriorityKey) & feature);
}
export function verifyUsedFeatures(path, file) {
export function verifyUsedFeatures(path: NodePath, file: File) {
if (hasOwnDecorators(path.node)) {
if (!hasFeature(file, FEATURES.decorators)) {
throw path.buildCodeFrameError(
@ -145,8 +149,8 @@ export function verifyUsedFeatures(path, file) {
}
}
// NOTE: path.isPrivateMethod() it isn't supported in <7.2.0
if (path.isPrivateMethod?.()) {
// NOTE: path.isClassPrivateMethod() it isn't supported in <7.2.0
if (path.isClassPrivateMethod?.()) {
if (!hasFeature(file, FEATURES.privateMethods)) {
throw path.buildCodeFrameError("Class private methods are not enabled.");
}

View File

@ -1,35 +1,53 @@
import { template, traverse, types as t } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import type { File } from "@babel/core";
import type { NodePath, Visitor } from "@babel/traverse";
import ReplaceSupers, {
environmentVisitor,
} from "@babel/helper-replace-supers";
import memberExpressionToFunctions from "@babel/helper-member-expression-to-functions";
import type {
Handler,
HandlerState,
} from "@babel/helper-member-expression-to-functions";
import optimiseCall from "@babel/helper-optimise-call-expression";
import annotateAsPure from "@babel/helper-annotate-as-pure";
import * as ts from "./typescript";
export function buildPrivateNamesMap(props) {
const privateNamesMap = new Map();
interface PrivateNameMetadata {
id: t.Identifier;
static: boolean;
method: boolean;
getId?: t.Identifier;
setId?: t.Identifier;
methodId?: t.Identifier;
initAdded?: boolean;
getterDeclared?: boolean;
setterDeclared?: boolean;
}
type PrivateNamesMap = Map<string, PrivateNameMetadata>;
export function buildPrivateNamesMap(props: PropPath[]) {
const privateNamesMap: PrivateNamesMap = new Map();
for (const prop of props) {
const isPrivate = prop.isPrivate();
const isMethod = !prop.isProperty();
const isInstance = !prop.node.static;
if (isPrivate) {
if (prop.isPrivate()) {
const { name } = prop.node.key.id;
const update = privateNamesMap.has(name)
const update: PrivateNameMetadata = privateNamesMap.has(name)
? privateNamesMap.get(name)
: {
id: prop.scope.generateUidIdentifier(name),
static: !isInstance,
method: isMethod,
static: prop.node.static,
method: !prop.isProperty(),
};
if (prop.node.kind === "get") {
update.getId = prop.scope.generateUidIdentifier(`get_${name}`);
} else if (prop.node.kind === "set") {
update.setId = prop.scope.generateUidIdentifier(`set_${name}`);
} else if (prop.node.kind === "method") {
update.methodId = prop.scope.generateUidIdentifier(name);
if (prop.isClassPrivateMethod()) {
if (prop.node.kind === "get") {
update.getId = prop.scope.generateUidIdentifier(`get_${name}`);
} else if (prop.node.kind === "set") {
update.setId = prop.scope.generateUidIdentifier(`set_${name}`);
} else if (prop.node.kind === "method") {
update.methodId = prop.scope.generateUidIdentifier(name);
}
}
privateNamesMap.set(name, update);
}
@ -38,11 +56,11 @@ export function buildPrivateNamesMap(props) {
}
export function buildPrivateNamesNodes(
privateNamesMap,
privateFieldsAsProperties,
privateNamesMap: PrivateNamesMap,
privateFieldsAsProperties: boolean,
state,
) {
const initNodes = [];
const initNodes: t.Statement[] = [];
for (const [name, value] of privateNamesMap) {
// When the privateFieldsAsProperties assumption is enabled,
@ -56,7 +74,7 @@ export function buildPrivateNamesNodes(
const isAccessor = getId || setId;
const id = t.cloneNode(value.id);
let init;
let init: t.Expression;
if (privateFieldsAsProperties) {
init = t.callExpression(state.addHelper("classPrivateFieldLooseKey"), [
@ -78,11 +96,19 @@ export function buildPrivateNamesNodes(
return initNodes;
}
interface PrivateNameVisitorState {
privateNamesMap: PrivateNamesMap;
privateFieldsAsProperties: boolean;
redeclared?: string[];
}
// Traverses the class scope, handling private name references. If an inner
// class redeclares the same private name, it will hand off traversal to the
// restricted visitor (which doesn't traverse the inner class's inner scope).
function privateNameVisitorFactory(visitor) {
const privateNameVisitor = {
function privateNameVisitorFactory<S>(
visitor: Visitor<PrivateNameVisitorState & S>,
) {
const privateNameVisitor: Visitor<PrivateNameVisitorState & S> = {
...visitor,
Class(path) {
@ -134,7 +160,16 @@ function privateNameVisitorFactory(visitor) {
return privateNameVisitor;
}
const privateNameVisitor = privateNameVisitorFactory({
interface PrivateNameState {
privateNamesMap: PrivateNamesMap;
classRef: t.Identifier;
file: File;
noDocumentAll: boolean;
}
const privateNameVisitor = privateNameVisitorFactory<
HandlerState<PrivateNameState> & PrivateNameState
>({
PrivateName(path, { noDocumentAll }) {
const { privateNamesMap, redeclared } = this;
const { node, parentPath } = path;
@ -153,13 +188,17 @@ const privateNameVisitor = privateNameVisitorFactory({
},
});
const privateInVisitor = privateNameVisitorFactory({
const privateInVisitor = privateNameVisitorFactory<{
classRef: t.Identifier;
file: File;
}>({
BinaryExpression(path) {
const { operator, left, right } = path.node;
if (operator !== "in") return;
if (!path.get("left").isPrivateName()) return;
if (!t.isPrivateName(left)) return;
const { privateFieldsAsProperties, privateNamesMap, redeclared } = this;
const { name } = left.id;
if (!privateNamesMap.has(name)) return;
@ -184,197 +223,205 @@ const privateInVisitor = privateNameVisitorFactory({
},
});
const privateNameHandlerSpec = {
memoise(member, count) {
const { scope } = member;
const { object } = member.node;
interface Receiver {
receiver(
this: HandlerState<PrivateNameState> & PrivateNameState,
member: NodePath<t.MemberExpression | t.OptionalMemberExpression>,
): t.Expression;
}
const memo = scope.maybeGenerateMemoised(object);
if (!memo) {
return;
}
const privateNameHandlerSpec: Handler<PrivateNameState & Receiver> & Receiver =
{
memoise(member, count) {
const { scope } = member;
const { object } = member.node;
this.memoiser.set(object, memo, count);
},
const memo = scope.maybeGenerateMemoised(object);
if (!memo) {
return;
}
receiver(member) {
const { object } = member.node;
this.memoiser.set(object, memo, count);
},
if (this.memoiser.has(object)) {
return t.cloneNode(this.memoiser.get(object));
}
receiver(member) {
const { object } = member.node;
return t.cloneNode(object);
},
if (this.memoiser.has(object)) {
return t.cloneNode(this.memoiser.get(object) as t.Expression);
}
get(member) {
const { classRef, privateNamesMap, file } = this;
const { name } = member.node.property.id;
const {
id,
static: isStatic,
method: isMethod,
methodId,
getId,
setId,
} = privateNamesMap.get(name);
const isAccessor = getId || setId;
return t.cloneNode(object);
},
if (isStatic) {
const helperName =
isMethod && !isAccessor
? "classStaticPrivateMethodGet"
: "classStaticPrivateFieldSpecGet";
get(member) {
const { classRef, privateNamesMap, file } = this;
const { name } = (member.node.property as t.PrivateName).id;
const {
id,
static: isStatic,
method: isMethod,
methodId,
getId,
setId,
} = privateNamesMap.get(name);
const isAccessor = getId || setId;
return t.callExpression(file.addHelper(helperName), [
this.receiver(member),
t.cloneNode(classRef),
t.cloneNode(id),
]);
}
if (isStatic) {
const helperName =
isMethod && !isAccessor
? "classStaticPrivateMethodGet"
: "classStaticPrivateFieldSpecGet";
if (isMethod) {
if (isAccessor) {
if (!getId && setId) {
if (file.availableHelper("writeOnlyError")) {
return t.sequenceExpression([
this.receiver(member),
t.callExpression(file.addHelper("writeOnlyError"), [
t.stringLiteral(`#${name}`),
]),
]);
}
console.warn(
`@babel/helpers is outdated, update it to silence this warning.`,
);
}
return t.callExpression(file.addHelper("classPrivateFieldGet"), [
return t.callExpression(file.addHelper(helperName), [
this.receiver(member),
t.cloneNode(classRef),
t.cloneNode(id),
]);
}
return t.callExpression(file.addHelper("classPrivateMethodGet"), [
this.receiver(member),
t.cloneNode(id),
t.cloneNode(methodId),
]);
}
return t.callExpression(file.addHelper("classPrivateFieldGet"), [
this.receiver(member),
t.cloneNode(id),
]);
},
boundGet(member) {
this.memoise(member, 1);
return t.callExpression(
t.memberExpression(this.get(member), t.identifier("bind")),
[this.receiver(member)],
);
},
set(member, value) {
const { classRef, privateNamesMap, file } = this;
const { name } = member.node.property.id;
const {
id,
static: isStatic,
method: isMethod,
setId,
getId,
} = privateNamesMap.get(name);
const isAccessor = getId || setId;
if (isStatic) {
const helperName =
isMethod && !isAccessor
? "classStaticPrivateMethodSet"
: "classStaticPrivateFieldSpecSet";
return t.callExpression(file.addHelper(helperName), [
this.receiver(member),
t.cloneNode(classRef),
t.cloneNode(id),
value,
]);
}
if (isMethod) {
if (setId) {
return t.callExpression(file.addHelper("classPrivateFieldSet"), [
if (isMethod) {
if (isAccessor) {
if (!getId && setId) {
if (file.availableHelper("writeOnlyError")) {
return t.sequenceExpression([
this.receiver(member),
t.callExpression(file.addHelper("writeOnlyError"), [
t.stringLiteral(`#${name}`),
]),
]);
}
console.warn(
`@babel/helpers is outdated, update it to silence this warning.`,
);
}
return t.callExpression(file.addHelper("classPrivateFieldGet"), [
this.receiver(member),
t.cloneNode(id),
]);
}
return t.callExpression(file.addHelper("classPrivateMethodGet"), [
this.receiver(member),
t.cloneNode(id),
t.cloneNode(methodId),
]);
}
return t.callExpression(file.addHelper("classPrivateFieldGet"), [
this.receiver(member),
t.cloneNode(id),
]);
},
boundGet(member) {
this.memoise(member, 1);
return t.callExpression(
t.memberExpression(this.get(member), t.identifier("bind")),
[this.receiver(member)],
);
},
set(member, value) {
const { classRef, privateNamesMap, file } = this;
const { name } = (member.node.property as t.PrivateName).id;
const {
id,
static: isStatic,
method: isMethod,
setId,
getId,
} = privateNamesMap.get(name);
const isAccessor = getId || setId;
if (isStatic) {
const helperName =
isMethod && !isAccessor
? "classStaticPrivateMethodSet"
: "classStaticPrivateFieldSpecSet";
return t.callExpression(file.addHelper(helperName), [
this.receiver(member),
t.cloneNode(classRef),
t.cloneNode(id),
value,
]);
}
return t.sequenceExpression([
if (isMethod) {
if (setId) {
return t.callExpression(file.addHelper("classPrivateFieldSet"), [
this.receiver(member),
t.cloneNode(id),
value,
]);
}
return t.sequenceExpression([
this.receiver(member),
value,
t.callExpression(file.addHelper("readOnlyError"), [
t.stringLiteral(`#${name}`),
]),
]);
}
return t.callExpression(file.addHelper("classPrivateFieldSet"), [
this.receiver(member),
t.cloneNode(id),
value,
t.callExpression(file.addHelper("readOnlyError"), [
t.stringLiteral(`#${name}`),
]),
]);
}
return t.callExpression(file.addHelper("classPrivateFieldSet"), [
this.receiver(member),
t.cloneNode(id),
value,
]);
},
},
destructureSet(member) {
const { classRef, privateNamesMap, file } = this;
const { name } = member.node.property.id;
const { id, static: isStatic } = privateNamesMap.get(name);
if (isStatic) {
try {
// classStaticPrivateFieldDestructureSet was introduced in 7.13.10
// eslint-disable-next-line no-var
var helper = file.addHelper("classStaticPrivateFieldDestructureSet");
} catch {
throw new Error(
"Babel can not transpile `[C.#p] = [0]` with @babel/helpers < 7.13.10, \n" +
"please update @babel/helpers to the latest version.",
destructureSet(member) {
const { classRef, privateNamesMap, file } = this;
const { name } = (member.node.property as t.PrivateName).id;
const { id, static: isStatic } = privateNamesMap.get(name);
if (isStatic) {
try {
// classStaticPrivateFieldDestructureSet was introduced in 7.13.10
// eslint-disable-next-line no-var
var helper = file.addHelper("classStaticPrivateFieldDestructureSet");
} catch {
throw new Error(
"Babel can not transpile `[C.#p] = [0]` with @babel/helpers < 7.13.10, \n" +
"please update @babel/helpers to the latest version.",
);
}
return t.memberExpression(
t.callExpression(helper, [
this.receiver(member),
t.cloneNode(classRef),
t.cloneNode(id),
]),
t.identifier("value"),
);
}
return t.memberExpression(
t.callExpression(helper, [
t.callExpression(file.addHelper("classPrivateFieldDestructureSet"), [
this.receiver(member),
t.cloneNode(classRef),
t.cloneNode(id),
]),
t.identifier("value"),
);
}
},
return t.memberExpression(
t.callExpression(file.addHelper("classPrivateFieldDestructureSet"), [
this.receiver(member),
t.cloneNode(id),
]),
t.identifier("value"),
);
},
call(member, args: (t.Expression | t.SpreadElement)[]) {
// The first access (the get) should do the memo assignment.
this.memoise(member, 1);
call(member, args) {
// The first access (the get) should do the memo assignment.
this.memoise(member, 1);
return optimiseCall(this.get(member), this.receiver(member), args, false);
},
return optimiseCall(this.get(member), this.receiver(member), args, false);
},
optionalCall(member, args: (t.Expression | t.SpreadElement)[]) {
this.memoise(member, 1);
optionalCall(member, args) {
this.memoise(member, 1);
return optimiseCall(this.get(member), this.receiver(member), args, true);
},
};
return optimiseCall(this.get(member), this.receiver(member), args, true);
},
};
const privateNameHandlerLoose = {
const privateNameHandlerLoose: Handler<PrivateNameState> = {
get(member) {
const { privateNamesMap, file } = this;
const { object } = member.node;
const { name } = member.node.property.id;
const { name } = (member.node.property as t.PrivateName).id;
return template.expression`BASE(REF, PROP)[PROP]`({
BASE: file.addHelper("classPrivateFieldLooseBase"),
@ -383,6 +430,11 @@ const privateNameHandlerLoose = {
});
},
set() {
// noop
throw new Error("private name handler with loose = true don't need set()");
},
boundGet(member) {
return t.callExpression(
t.memberExpression(this.get(member), t.identifier("bind")),
@ -408,9 +460,9 @@ const privateNameHandlerLoose = {
};
export function transformPrivateNamesUsage(
ref,
path,
privateNamesMap,
ref: t.Identifier,
path: NodePath<t.Class>,
privateNamesMap: PrivateNamesMap,
{ privateFieldsAsProperties, noDocumentAll },
state,
) {
@ -421,7 +473,7 @@ export function transformPrivateNamesUsage(
? privateNameHandlerLoose
: privateNameHandlerSpec;
memberExpressionToFunctions(body, privateNameVisitor, {
memberExpressionToFunctions<PrivateNameState>(body, privateNameVisitor, {
privateNamesMap,
classRef: ref,
file: state,
@ -436,7 +488,11 @@ export function transformPrivateNamesUsage(
});
}
function buildPrivateFieldInitLoose(ref, prop, privateNamesMap) {
function buildPrivateFieldInitLoose(
ref: t.Expression,
prop: NodePath<t.ClassPrivateProperty>,
privateNamesMap: PrivateNamesMap,
) {
const { id } = privateNamesMap.get(prop.node.key.id.name);
const value = prop.node.value || prop.scope.buildUndefinedNode();
@ -450,7 +506,11 @@ function buildPrivateFieldInitLoose(ref, prop, privateNamesMap) {
`;
}
function buildPrivateInstanceFieldInitSpec(ref, prop, privateNamesMap) {
function buildPrivateInstanceFieldInitSpec(
ref: t.Expression,
prop: NodePath<t.ClassPrivateProperty>,
privateNamesMap: PrivateNamesMap,
) {
const { id } = privateNamesMap.get(prop.node.key.id.name);
const value = prop.node.value || prop.scope.buildUndefinedNode();
@ -462,7 +522,10 @@ function buildPrivateInstanceFieldInitSpec(ref, prop, privateNamesMap) {
})`;
}
function buildPrivateStaticFieldInitSpec(prop, privateNamesMap) {
function buildPrivateStaticFieldInitSpec(
prop: NodePath<t.ClassPrivateProperty>,
privateNamesMap: PrivateNamesMap,
) {
const privateName = privateNamesMap.get(prop.node.key.id.name);
const { id, getId, setId, initAdded } = privateName;
const isAccessor = getId || setId;
@ -497,7 +560,11 @@ function buildPrivateStaticFieldInitSpec(prop, privateNamesMap) {
`;
}
function buildPrivateMethodInitLoose(ref, prop, privateNamesMap) {
function buildPrivateMethodInitLoose(
ref: t.Expression,
prop: NodePath<t.ClassPrivateMethod>,
privateNamesMap: PrivateNamesMap,
) {
const privateName = privateNamesMap.get(prop.node.key.id.name);
const { methodId, id, getId, setId, initAdded } = privateName;
if (initAdded) return;
@ -531,7 +598,11 @@ function buildPrivateMethodInitLoose(ref, prop, privateNamesMap) {
}
}
function buildPrivateInstanceMethodInitSpec(ref, prop, privateNamesMap) {
function buildPrivateInstanceMethodInitSpec(
ref: t.Expression,
prop: NodePath<t.ClassPrivateMethod>,
privateNamesMap: PrivateNamesMap,
) {
const privateName = privateNamesMap.get(prop.node.key.id.name);
const { id, getId, setId, initAdded } = privateName;
@ -554,7 +625,10 @@ function buildPrivateInstanceMethodInitSpec(ref, prop, privateNamesMap) {
return template.statement.ast`${id}.add(${ref})`;
}
function buildPublicFieldInitLoose(ref, prop) {
function buildPublicFieldInitLoose(
ref: t.Expression,
prop: NodePath<t.ClassProperty>,
) {
const { key, computed } = prop.node;
const value = prop.node.value || prop.scope.buildUndefinedNode();
@ -567,20 +641,31 @@ function buildPublicFieldInitLoose(ref, prop) {
);
}
function buildPublicFieldInitSpec(ref, prop, state) {
function buildPublicFieldInitSpec(
ref: t.Expression,
prop: NodePath<t.ClassProperty>,
state,
) {
const { key, computed } = prop.node;
const value = prop.node.value || prop.scope.buildUndefinedNode();
return t.expressionStatement(
t.callExpression(state.addHelper("defineProperty"), [
ref,
computed || t.isLiteral(key) ? key : t.stringLiteral(key.name),
computed || t.isLiteral(key)
? key
: t.stringLiteral((key as t.Identifier).name),
value,
]),
);
}
function buildPrivateStaticMethodInitLoose(ref, prop, state, privateNamesMap) {
function buildPrivateStaticMethodInitLoose(
ref: t.Expression,
prop: NodePath<t.ClassPrivateMethod>,
state,
privateNamesMap: PrivateNamesMap,
) {
const privateName = privateNamesMap.get(prop.node.key.id.name);
const { id, methodId, getId, setId, initAdded } = privateName;
@ -615,8 +700,8 @@ function buildPrivateStaticMethodInitLoose(ref, prop, state, privateNamesMap) {
}
function buildPrivateMethodDeclaration(
prop,
privateNamesMap,
prop: NodePath<t.ClassPrivateMethod>,
privateNamesMap: PrivateNamesMap,
privateFieldsAsProperties = false,
) {
const privateName = privateNamesMap.get(prop.node.key.id.name);
@ -653,6 +738,7 @@ function buildPrivateMethodDeclaration(
return t.functionDeclaration(
t.cloneNode(declId),
// @ts-expect-error params for ClassMethod has TSParameterProperty
params,
body,
generator,
@ -695,13 +781,13 @@ const innerReferencesVisitor = {
};
function replaceThisContext(
path,
ref,
getSuperRef,
file,
isStaticBlock,
constantSuper,
innerBindingRef,
path: PropPath,
ref: t.Identifier,
getSuperRef: () => t.Identifier,
file: File,
isStaticBlock: boolean,
constantSuper: boolean,
innerBindingRef: t.Identifier,
) {
const state = {
classRef: ref,
@ -734,23 +820,29 @@ function replaceThisContext(
return state.needsClassRef;
}
export type PropNode =
| t.ClassProperty
| t.ClassPrivateMethod
| t.ClassPrivateProperty;
export type PropPath = NodePath<PropNode>;
export function buildFieldsInitNodes(
ref,
superRef,
props,
privateNamesMap,
ref: t.Identifier,
superRef: t.Expression | undefined,
props: PropPath[],
privateNamesMap: PrivateNamesMap,
state,
setPublicClassFields,
privateFieldsAsProperties,
constantSuper,
innerBindingRef,
setPublicClassFields: boolean,
privateFieldsAsProperties: boolean,
constantSuper: boolean,
innerBindingRef: t.Identifier,
) {
let needsClassRef = false;
let injectSuperRef;
const staticNodes = [];
const instanceNodes = [];
let injectSuperRef: t.Identifier;
const staticNodes: t.Statement[] = [];
const instanceNodes: t.Statement[] = [];
// These nodes are pure and can be moved to the closest statement position
const pureStaticNodes = [];
const pureStaticNodes: t.FunctionDeclaration[] = [];
const getSuperRef = t.isIdentifier(superRef)
? () => superRef
@ -761,7 +853,7 @@ export function buildFieldsInitNodes(
};
for (const prop of props) {
ts.assertFieldTransformed(prop);
prop.isClassProperty() && ts.assertFieldTransformed(prop);
const isStatic = prop.node.static;
const isInstance = !isStatic;
@ -784,36 +876,47 @@ export function buildFieldsInitNodes(
needsClassRef = needsClassRef || replaced;
}
// TODO(ts): there are so many `ts-expect-error` inside cases since
// ts can not infer type from pre-computed values (or a case test)
// even change `isStaticBlock` to `t.isStaticBlock(prop)` will not make prop
// a `NodePath<t.StaticBlock>`
// this maybe a bug for ts
switch (true) {
case isStaticBlock:
staticNodes.push(
// @ts-expect-error prop is `StaticBlock` here
template.statement.ast`(() => ${t.blockStatement(prop.node.body)})()`,
);
break;
case isStatic && isPrivate && isField && privateFieldsAsProperties:
needsClassRef = true;
staticNodes.push(
// @ts-expect-error checked in switch
buildPrivateFieldInitLoose(t.cloneNode(ref), prop, privateNamesMap),
);
break;
case isStatic && isPrivate && isField && !privateFieldsAsProperties:
needsClassRef = true;
staticNodes.push(
// @ts-expect-error checked in switch
buildPrivateStaticFieldInitSpec(prop, privateNamesMap),
);
break;
case isStatic && isPublic && isField && setPublicClassFields:
needsClassRef = true;
// @ts-expect-error checked in switch
staticNodes.push(buildPublicFieldInitLoose(t.cloneNode(ref), prop));
break;
case isStatic && isPublic && isField && !setPublicClassFields:
needsClassRef = true;
staticNodes.push(
// @ts-expect-error checked in switch
buildPublicFieldInitSpec(t.cloneNode(ref), prop, state),
);
break;
case isInstance && isPrivate && isField && privateFieldsAsProperties:
instanceNodes.push(
// @ts-expect-error checked in switch
buildPrivateFieldInitLoose(t.thisExpression(), prop, privateNamesMap),
);
break;
@ -821,6 +924,7 @@ export function buildFieldsInitNodes(
instanceNodes.push(
buildPrivateInstanceFieldInitSpec(
t.thisExpression(),
// @ts-expect-error checked in switch
prop,
privateNamesMap,
),
@ -830,12 +934,14 @@ export function buildFieldsInitNodes(
instanceNodes.unshift(
buildPrivateMethodInitLoose(
t.thisExpression(),
// @ts-expect-error checked in switch
prop,
privateNamesMap,
),
);
pureStaticNodes.push(
buildPrivateMethodDeclaration(
// @ts-expect-error checked in switch
prop,
privateNamesMap,
privateFieldsAsProperties,
@ -846,12 +952,14 @@ export function buildFieldsInitNodes(
instanceNodes.unshift(
buildPrivateInstanceMethodInitSpec(
t.thisExpression(),
// @ts-expect-error checked in switch
prop,
privateNamesMap,
),
);
pureStaticNodes.push(
buildPrivateMethodDeclaration(
// @ts-expect-error checked in switch
prop,
privateNamesMap,
privateFieldsAsProperties,
@ -861,10 +969,12 @@ export function buildFieldsInitNodes(
case isStatic && isPrivate && isMethod && !privateFieldsAsProperties:
needsClassRef = true;
staticNodes.unshift(
// @ts-expect-error checked in switch
buildPrivateStaticFieldInitSpec(prop, privateNamesMap),
);
pureStaticNodes.push(
buildPrivateMethodDeclaration(
// @ts-expect-error checked in switch
prop,
privateNamesMap,
privateFieldsAsProperties,
@ -876,6 +986,7 @@ export function buildFieldsInitNodes(
staticNodes.unshift(
buildPrivateStaticMethodInitLoose(
t.cloneNode(ref),
// @ts-expect-error checked in switch
prop,
state,
privateNamesMap,
@ -883,6 +994,7 @@ export function buildFieldsInitNodes(
);
pureStaticNodes.push(
buildPrivateMethodDeclaration(
// @ts-expect-error checked in switch
prop,
privateNamesMap,
privateFieldsAsProperties,
@ -890,10 +1002,12 @@ export function buildFieldsInitNodes(
);
break;
case isInstance && isPublic && isField && setPublicClassFields:
// @ts-expect-error checked in switch
instanceNodes.push(buildPublicFieldInitLoose(t.thisExpression(), prop));
break;
case isInstance && isPublic && isField && !setPublicClassFields:
instanceNodes.push(
// @ts-expect-error checked in switch
buildPublicFieldInitSpec(t.thisExpression(), prop, state),
);
break;
@ -906,7 +1020,7 @@ export function buildFieldsInitNodes(
staticNodes: staticNodes.filter(Boolean),
instanceNodes: instanceNodes.filter(Boolean),
pureStaticNodes: pureStaticNodes.filter(Boolean),
wrapClass(path) {
wrapClass(path: NodePath<t.Class>) {
for (const prop of props) {
prop.remove();
}

View File

@ -1,4 +1,5 @@
import { types as t } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import nameFunction from "@babel/helper-function-name";
import splitExportDeclaration from "@babel/helper-split-export-declaration";
import {
@ -7,11 +8,8 @@ import {
transformPrivateNamesUsage,
buildFieldsInitNodes,
} from "./fields";
import {
hasOwnDecorators,
buildDecoratedClass,
hasDecorators,
} from "./decorators";
import type { PropPath } from "./fields";
import { buildDecoratedClass, hasDecorators } from "./decorators";
import { injectInitialization, extractComputedKeys } from "./misc";
import {
enableFeature,
@ -33,20 +31,24 @@ const version = PACKAGE_JSON.version
.reduce((v, x) => v * 1e5 + +x, 0);
const versionKey = "@babel/plugin-class-features/version";
interface Options {
name: string;
feature: number;
loose?: boolean;
// same as PluginObject.manipulateOptions
manipulateOptions: (options: unknown, parserOpts: unknown) => void;
// TODO(flow->ts): change to babel api
api?: { assumption: (key?: string) => boolean | undefined };
}
export function createClassFeaturePlugin({
name,
feature,
loose,
manipulateOptions,
// TODO(Babel 8): Remove the default falue
api = { assumption: () => {} },
}: {
name;
feature;
loose?;
manipulateOptions;
api?;
}) {
api = { assumption: () => void 0 },
}: Options) {
const setPublicClassFields = api.assumption("setPublicClassFields");
const privateFieldsAsProperties = api.assumption("privateFieldsAsProperties");
const constantSuper = api.assumption("constantSuper");
@ -90,25 +92,29 @@ export function createClassFeaturePlugin({
},
visitor: {
Class(path, state) {
Class(path: NodePath<t.Class>, state) {
if (this.file.get(versionKey) !== version) return;
verifyUsedFeatures(path, this.file);
const loose = isLoose(this.file, feature);
let constructor;
let isDecorated = hasOwnDecorators(path.node);
const props = [];
let constructor: NodePath<t.ClassMethod>;
const isDecorated = hasDecorators(path.node);
const props: PropPath[] = [];
const elements = [];
const computedPaths = [];
const privateNames = new Set();
const computedPaths: NodePath<t.ClassProperty | t.ClassMethod>[] = [];
const privateNames = new Set<string>();
const body = path.get("body");
for (const path of body.get("body")) {
verifyUsedFeatures(path, this.file);
if (path.node.computed) {
if (
// check path.node.computed is enough, but ts will complain
(path.isClassProperty() || path.isClassMethod()) &&
path.node.computed
) {
computedPaths.push(path);
}
@ -117,24 +123,24 @@ export function createClassFeaturePlugin({
const getName = `get ${name}`;
const setName = `set ${name}`;
if (path.node.kind === "get") {
if (
privateNames.has(getName) ||
(privateNames.has(name) && !privateNames.has(setName))
) {
throw path.buildCodeFrameError("Duplicate private field");
if (path.isClassPrivateMethod()) {
if (path.node.kind === "get") {
if (
privateNames.has(getName) ||
(privateNames.has(name) && !privateNames.has(setName))
) {
throw path.buildCodeFrameError("Duplicate private field");
}
privateNames.add(getName).add(name);
} else if (path.node.kind === "set") {
if (
privateNames.has(setName) ||
(privateNames.has(name) && !privateNames.has(getName))
) {
throw path.buildCodeFrameError("Duplicate private field");
}
privateNames.add(setName).add(name);
}
privateNames.add(getName).add(name);
} else if (path.node.kind === "set") {
if (
privateNames.has(setName) ||
(privateNames.has(name) && !privateNames.has(getName))
) {
throw path.buildCodeFrameError("Duplicate private field");
}
privateNames.add(setName).add(name);
} else {
if (
(privateNames.has(name) &&
@ -162,14 +168,12 @@ export function createClassFeaturePlugin({
props.push(path);
}
}
if (!isDecorated) isDecorated = hasOwnDecorators(path.node);
}
if (!props.length && !isDecorated) return;
const innerBinding = path.node.id;
let ref;
let ref: t.Identifier;
if (!innerBinding || path.isClassExpression()) {
nameFunction(path);
ref = path.scope.generateUidIdentifier("class");
@ -198,7 +202,11 @@ export function createClassFeaturePlugin({
state,
);
let keysNodes, staticNodes, pureStaticNodes, instanceNodes, wrapClass;
let keysNodes: t.Statement[],
staticNodes: t.Statement[],
instanceNodes: t.Statement[],
pureStaticNodes: t.FunctionDeclaration[],
wrapClass: (path: NodePath<t.Class>) => NodePath;
if (isDecorated) {
staticNodes = pureStaticNodes = keysNodes = [];
@ -239,19 +247,20 @@ export function createClassFeaturePlugin({
);
}
path = wrapClass(path);
path.insertBefore([...privateNamesNodes, ...keysNodes]);
// rename to make ts happy
const wrappedPath = wrapClass(path);
wrappedPath.insertBefore([...privateNamesNodes, ...keysNodes]);
if (staticNodes.length > 0) {
path.insertAfter(staticNodes);
wrappedPath.insertAfter(staticNodes);
}
if (pureStaticNodes.length > 0) {
path
wrappedPath
.find(parent => parent.isStatement() || parent.isDeclaration())
.insertAfter(pureStaticNodes);
}
},
PrivateName(path) {
PrivateName(path: NodePath<t.PrivateName>) {
if (
this.file.get(versionKey) !== version ||
path.parentPath.isPrivate({ key: path.node })
@ -262,7 +271,7 @@ export function createClassFeaturePlugin({
throw path.buildCodeFrameError(`Unknown PrivateName "${path}"`);
},
ExportDefaultDeclaration(path) {
ExportDefaultDeclaration(path: NodePath<t.ExportDefaultDeclaration>) {
if (this.file.get(versionKey) !== version) return;
const decl = path.get("declaration");
@ -276,6 +285,7 @@ export function createClassFeaturePlugin({
} else {
// Annyms class declarations can be
// transformed as if they were expressions
// @ts-expect-error
decl.node.type = "ClassExpression";
}
}

View File

@ -1,9 +1,11 @@
import { template, traverse, types as t } from "@babel/core";
import type { File } from "@babel/core";
import type { NodePath, Scope, Visitor, Binding } from "@babel/traverse";
import { environmentVisitor } from "@babel/helper-replace-supers";
const findBareSupers = traverse.visitors.merge([
{
Super(path) {
Super(path: NodePath<t.Super>) {
const { node, parentPath } = path;
if (parentPath.isCallExpression({ callee: node })) {
this.push(parentPath);
@ -14,18 +16,24 @@ const findBareSupers = traverse.visitors.merge([
]);
const referenceVisitor = {
"TSTypeAnnotation|TypeAnnotation"(path) {
"TSTypeAnnotation|TypeAnnotation"(path: NodePath) {
path.skip();
},
ReferencedIdentifier(path) {
ReferencedIdentifier(path: NodePath<t.Identifier>) {
if (this.scope.hasOwnBinding(path.node.name)) {
this.scope.rename(path.node.name);
path.skip();
}
},
};
function handleClassTDZ(path, state) {
function handleClassTDZ(
path: NodePath<t.Identifier>,
state: {
classBinding: Binding;
file: File;
},
) {
if (
state.classBinding &&
state.classBinding === path.scope.getBinding(path.node.name)
@ -44,7 +52,16 @@ const classFieldDefinitionEvaluationTDZVisitor = {
ReferencedIdentifier: handleClassTDZ,
};
export function injectInitialization(path, constructor, nodes, renamer?) {
interface RenamerState {
scope: Scope;
}
export function injectInitialization(
path: NodePath<t.Class>,
constructor: NodePath<t.ClassMethod> | undefined,
nodes: t.Statement[],
renamer?: (visitor: Visitor<RenamerState>, state: RenamerState) => void,
) {
if (!nodes.length) return;
const isDerived = !!path.node.superClass;
@ -62,7 +79,9 @@ export function injectInitialization(path, constructor, nodes, renamer?) {
newConstructor.body.body.push(template.statement.ast`super(...args)`);
}
[constructor] = path.get("body").unshiftContainer("body", newConstructor);
[constructor] = path
.get("body")
.unshiftContainer("body", newConstructor) as NodePath<t.ClassMethod>[];
}
if (renamer) {
@ -86,8 +105,13 @@ export function injectInitialization(path, constructor, nodes, renamer?) {
}
}
export function extractComputedKeys(ref, path, computedPaths, file) {
const declarations = [];
export function extractComputedKeys(
ref: t.Identifier,
path: NodePath<t.Class>,
computedPaths: NodePath<t.ClassProperty | t.ClassMethod>[],
file: File,
) {
const declarations: t.Statement[] = [];
const state = {
classBinding: path.node.id && path.scope.getBinding(path.node.id.name),
file,

View File

@ -1,17 +1,18 @@
import type { NodePath, Visitor } from "@babel/traverse";
import * as t from "@babel/types";
import { willPathCastToBoolean } from "./util";
class AssignmentMemoiser {
private _map: WeakMap<object, any>;
private _map: WeakMap<t.Expression, { count: number; value: t.LVal }>;
constructor() {
this._map = new WeakMap();
}
has(key) {
has(key: t.Expression) {
return this._map.has(key);
}
get(key) {
get(key: t.Expression) {
if (!this.has(key)) return;
const record = this._map.get(key);
@ -26,14 +27,17 @@ class AssignmentMemoiser {
return value;
}
set(key, value, count) {
set(key: t.Expression, value: t.LVal, count: number) {
return this._map.set(key, { count, value });
}
}
function toNonOptional(path, base) {
function toNonOptional(
path: NodePath<t.Expression>,
base: t.Expression,
): t.Expression {
const { node } = path;
if (path.isOptionalMemberExpression()) {
if (t.isOptionalMemberExpression(node)) {
return t.memberExpression(base, node.property, node.computed);
}
@ -44,15 +48,15 @@ function toNonOptional(path, base) {
const context = path.scope.maybeGenerateMemoised(object) || object;
callee
.get("object")
.replaceWith(t.assignmentExpression("=", context, object));
.replaceWith(t.assignmentExpression("=", context as t.LVal, object));
return t.callExpression(t.memberExpression(base, t.identifier("call")), [
context,
...node.arguments,
...path.node.arguments,
]);
}
return t.callExpression(base, node.arguments);
return t.callExpression(base, path.node.arguments);
}
return path.node;
@ -62,7 +66,7 @@ function toNonOptional(path, base) {
// we are iterating on a path, and replace an ancestor with a new node. Babel
// doesn't always stop traversing the old node tree, and that can cause
// inconsistencies.
function isInDetachedTree(path) {
function isInDetachedTree(path: NodePath) {
while (path) {
if (path.isProgram()) break;
@ -80,13 +84,14 @@ function isInDetachedTree(path) {
return false;
}
type Member = NodePath<t.OptionalMemberExpression | t.MemberExpression>;
const handle = {
memoise() {
// noop.
},
// todo(flow->ts) member:NodePath<t.Expression>, refactor function body to avoid too many typecasts
handle(member: any, noDocumentAll: boolean) {
handle(this: HandlerState, member: Member, noDocumentAll: boolean) {
const { node, parent, parentPath, scope } = member;
if (member.isOptionalMemberExpression()) {
@ -102,14 +107,14 @@ const handle = {
// Everything up to it could be skipped if it `FOO` were nullish. But
// actually, we can consider the `baz` access to be the end. So we're
// looking for the nearest optional chain that is `optional: true`.
const endPath = member.find(({ node, parent, parentPath }) => {
if (parentPath.isOptionalMemberExpression()) {
const endPath = member.find(({ node, parent }) => {
if (t.isOptionalMemberExpression(parent)) {
// We need to check `parent.object` since we could be inside the
// computed expression of a `bad?.[FOO?.BAR]`. In this case, the
// endPath is the `FOO?.BAR` member itself.
return parent.optional || parent.object !== node;
}
if (parentPath.isOptionalCallExpression()) {
if (t.isOptionalCallExpression(parent)) {
// Checking `parent.callee` since we could be in the arguments, eg
// `bad?.(FOO?.BAR)`.
// Also skip `FOO?.BAR` in `FOO?.BAR?.()` since we need to transform the optional call to ensure proper this
@ -119,7 +124,7 @@ const handle = {
);
}
return true;
});
}) as NodePath<t.OptionalMemberExpression>;
// Replace `function (a, x = a.b?.#c) {}` to `function (a, x = (() => a.b?.#c)() ){}`
// so the temporary variable can be injected in correct scope
@ -164,7 +169,7 @@ const handle = {
// But actually, we can consider the `bar` access to be the start. So
// we're looking for the nearest optional chain that is `optional: true`,
// which is guaranteed to be somewhere in the object/callee tree.
let startingOptional = member;
let startingOptional: NodePath<t.Expression> = member;
for (;;) {
if (startingOptional.isOptionalMemberExpression()) {
if (startingOptional.node.optional) break;
@ -193,10 +198,16 @@ const handle = {
const parentIsOptionalCall = parentPath.isOptionalCallExpression({
callee: node,
});
// here we use a function to wrap `parentIsOptionalCall` to get type
// for parent, do not use it anywhere else
// See https://github.com/microsoft/TypeScript/issues/10421
const isOptionalCall = (
parent: t.Node,
): parent is t.OptionalCallExpression => parentIsOptionalCall;
// if parentIsCall is true, it implies that node.extra.parenthesized is always true
const parentIsCall = parentPath.isCallExpression({ callee: node });
startingOptional.replaceWith(toNonOptional(startingOptional, baseRef));
if (parentIsOptionalCall) {
if (isOptionalCall(parent)) {
if (parent.optional) {
parentPath.replaceWith(this.optionalCall(member, parent.arguments));
} else {
@ -209,11 +220,15 @@ const handle = {
member.replaceWith(this.get(member));
}
let regular = member.node;
for (let current = member; current !== endPath; ) {
const { parentPath } = current;
let regular: t.Expression = member.node;
for (let current: NodePath = member; current !== endPath; ) {
const parentPath = current.parentPath as NodePath<t.Expression>;
// skip transforming `Foo.#BAR?.call(FOO)`
if (parentPath === endPath && parentIsOptionalCall && parent.optional) {
if (
parentPath === endPath &&
isOptionalCall(parent) &&
parent.optional
) {
regular = parentPath.node;
break;
}
@ -221,8 +236,8 @@ const handle = {
current = parentPath;
}
let context;
const endParentPath = endPath.parentPath;
let context: t.Identifier;
const endParentPath = endPath.parentPath as NodePath<t.Expression>;
if (
t.isMemberExpression(regular) &&
endParentPath.isOptionalCallExpression({
@ -237,7 +252,7 @@ const handle = {
}
}
let replacementPath = endPath;
let replacementPath: NodePath = endPath;
if (isDeleteOperation) {
replacementPath = endParentPath;
regular = endParentPath.node;
@ -306,7 +321,7 @@ const handle = {
// context and isDeleteOperation can not be both truthy
if (context) {
const endParent = endParentPath.node;
const endParent = endParentPath.node as t.OptionalCallExpression;
endParentPath.replaceWith(
t.optionalCallExpression(
t.optionalMemberExpression(
@ -326,7 +341,7 @@ const handle = {
// MEMBER++ -> _set(MEMBER, (_ref = (+_get(MEMBER))) + 1), _ref
// ++MEMBER -> _set(MEMBER, (+_get(MEMBER)) + 1)
if (parentPath.isUpdateExpression({ argument: node })) {
if (t.isUpdateExpression(parent, { argument: node })) {
if (this.simpleSet) {
member.replaceWith(this.simpleSet(member));
return;
@ -340,7 +355,7 @@ const handle = {
this.memoise(member, 2);
const value = t.binaryExpression(
operator[0],
operator[0] as "+" | "-",
t.unaryExpression("+", this.get(member)),
t.numericLiteral(1),
);
@ -375,7 +390,7 @@ const handle = {
return;
}
const { operator, right: value } = parent;
const { operator, right: value } = parentPath.node;
if (operator === "=") {
parentPath.replaceWith(this.set(member, value));
@ -388,7 +403,7 @@ const handle = {
this.memoise(member, 1);
parentPath.replaceWith(
t.logicalExpression(
operatorTrunc,
operatorTrunc as t.LogicalExpression["operator"],
this.get(member),
this.set(member, value),
),
@ -399,7 +414,11 @@ const handle = {
parentPath.replaceWith(
this.set(
member,
t.binaryExpression(operatorTrunc, this.get(member), value),
t.binaryExpression(
operatorTrunc as t.BinaryExpression["operator"],
this.get(member),
value,
),
),
);
}
@ -409,7 +428,7 @@ const handle = {
// MEMBER(ARGS) -> _call(MEMBER, ARGS)
if (parentPath.isCallExpression({ callee: node })) {
parentPath.replaceWith(this.call(member, parent.arguments));
parentPath.replaceWith(this.call(member, parentPath.node.arguments));
return;
}
@ -425,7 +444,9 @@ const handle = {
);
return;
}
parentPath.replaceWith(this.optionalCall(member, parent.arguments));
parentPath.replaceWith(
this.optionalCall(member, parentPath.node.arguments),
);
return;
}
@ -472,6 +493,45 @@ const handle = {
},
};
export interface Handler<State> {
memoise?(
this: HandlerState<State> & State,
member: Member,
count: number,
): void;
destructureSet(
this: HandlerState<State> & State,
member: Member,
): t.Expression;
boundGet(this: HandlerState<State> & State, member: Member): t.Expression;
simpleSet?(this: HandlerState<State> & State, member: Member): t.Expression;
get(this: HandlerState<State> & State, member: Member): t.Expression;
set(
this: HandlerState<State> & State,
member: Member,
value: t.Expression,
): t.Expression;
call(
this: HandlerState<State> & State,
member: Member,
args: t.CallExpression["arguments"],
): t.Expression;
optionalCall(
this: HandlerState<State> & State,
member: Member,
args: t.OptionalCallExpression["arguments"],
): t.Expression;
}
export interface HandlerState<State = {}> extends Handler<State> {
handle(
this: HandlerState<State> & State,
member: Member,
noDocumentAll: boolean,
): void;
memoiser: AssignmentMemoiser;
}
// We do not provide a default traversal visitor
// Instead, caller passes one, and must call `state.handle` on the members
// it wishes to be transformed.
@ -479,7 +539,11 @@ const handle = {
// get, set, and call methods.
// Optionally, a memoise method may be defined on the state, which will be
// called when the member is a self-referential update.
export default function memberExpressionToFunctions(path, visitor, state) {
export default function memberExpressionToFunctions<CustomState = {}>(
path: NodePath,
visitor: Visitor<HandlerState<CustomState>>,
state: Handler<CustomState> & CustomState,
) {
path.traverse(visitor, {
...handle,
...state,

View File

@ -1,4 +1,4 @@
import type { HubInterface, NodePath } from "@babel/traverse";
import type { HubInterface, NodePath, Scope } from "@babel/traverse";
import traverse from "@babel/traverse";
import memberExpressionToFunctions from "@babel/helper-member-expression-to-functions";
import optimiseCall from "@babel/helper-optimise-call-expression";
@ -271,6 +271,16 @@ type ReplaceSupersOptions = ReplaceSupersOptionsBase &
| { superRef: t.Node; getSuperRef?: undefined }
);
interface ReplaceState {
file: unknown;
scope: Scope;
isDerivedConstructor: boolean;
isStatic: boolean;
isPrivateMethod: boolean;
getObjectRef: ReplaceSupers["getObjectRef"];
getSuperRef: ReplaceSupers["getSuperRef"];
}
export default class ReplaceSupers {
constructor(opts: ReplaceSupersOptions) {
const path = opts.methodPath;
@ -317,7 +327,7 @@ export default class ReplaceSupers {
const handler = this.constantSuper ? looseHandlers : specHandlers;
memberExpressionToFunctions(this.methodPath, visitor, {
memberExpressionToFunctions<ReplaceState>(this.methodPath, visitor, {
file: this.file,
scope: this.methodPath.scope,
isDerivedConstructor: this.isDerivedConstructor,
@ -325,6 +335,8 @@ export default class ReplaceSupers {
isPrivateMethod: this.isPrivateMethod,
getObjectRef: this.getObjectRef.bind(this),
getSuperRef: this.getSuperRef.bind(this),
// we dont need boundGet here, but memberExpressionToFunctions handler needs it.
boundGet: handler.get,
...handler,
});
}

View File

@ -1,3 +1,3 @@
{
"plugins": ["proposal-class-properties", "transform-block-scoping"]
"plugins": ["proposal-class-properties", "proposal-private-methods", "transform-block-scoping"]
}

View File

@ -3,10 +3,10 @@ import * as visitors from "./visitors";
import * as t from "@babel/types";
import * as cache from "./cache";
import type NodePath from "./path";
import type Scope from "./scope";
import type { default as Scope, Binding } from "./scope";
import type { Visitor } from "./types";
export type { Visitor };
export type { Visitor, Binding };
export { default as NodePath } from "./path";
export { default as Scope } from "./scope";
export { default as Hub } from "./hub";

View File

@ -317,6 +317,8 @@ const collectorVisitor: Visitor<CollectVisitorState> = {
let uid = 0;
export type { Binding };
export default class Scope {
uid;