From 1960f23c220a04ec63e43123269d6e1c06bbc1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=B8=85=E9=9B=A8?= Date: Tue, 3 Aug 2021 03:22:37 +0800 Subject: [PATCH] Add typings to `create-class-features-plugin` helper (#13570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolò Ribaudo --- .../src/decorators.ts | 52 +- .../src/features.ts | 24 +- .../src/fields.ts | 540 +++++++++++------- .../src/index.ts | 104 ++-- .../src/misc.ts | 40 +- .../src/index.ts | 132 +++-- .../babel-helper-replace-supers/src/index.ts | 16 +- .../private/static-self-method/options.json | 2 +- packages/babel-traverse/src/index.ts | 4 +- packages/babel-traverse/src/scope/index.ts | 2 + 10 files changed, 582 insertions(+), 334 deletions(-) diff --git a/packages/babel-helper-create-class-features-plugin/src/decorators.ts b/packages/babel-helper-create-class-features-plugin/src/decorators.ts index ae1f58f17e..d4175e68a3 100644 --- a/packages/babel-helper-create-class-features-plugin/src/decorators.ts +++ b/packages/babel-helper-create-class-features-plugin/src/decorators.ts @@ -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; + +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; + +export function buildDecoratedClass( + ref: t.Identifier, + path: NodePath, + 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) { path.replaceWith(replacement); - return path.get(classPathDesc); + return path.get(classPathDesc) as NodePath; }, }; } diff --git a/packages/babel-helper-create-class-features-plugin/src/features.ts b/packages/babel-helper-create-class-features-plugin/src/features.ts index 8793f550b2..4af9baed39 100644 --- a/packages/babel-helper-create-class-features-plugin/src/features.ts +++ b/packages/babel-helper-create-class-features-plugin/src/features.ts @@ -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."); } 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 34c99c678c..695979cdd1 100644 --- a/packages/babel-helper-create-class-features-plugin/src/fields.ts +++ b/packages/babel-helper-create-class-features-plugin/src/fields.ts @@ -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; + +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( + visitor: Visitor, +) { + const privateNameVisitor: Visitor = { ...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 +>({ 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, + member: NodePath, + ): t.Expression; +} - const memo = scope.maybeGenerateMemoised(object); - if (!memo) { - return; - } +const privateNameHandlerSpec: Handler & 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 = { 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, + privateNamesMap: PrivateNamesMap, { privateFieldsAsProperties, noDocumentAll }, state, ) { @@ -421,7 +473,7 @@ export function transformPrivateNamesUsage( ? privateNameHandlerLoose : privateNameHandlerSpec; - memberExpressionToFunctions(body, privateNameVisitor, { + memberExpressionToFunctions(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, + 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, + 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, + 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, + 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, + 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, +) { 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, + 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, + 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, + 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; + 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` + // 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) { for (const prop of props) { prop.remove(); } 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 359085b52b..d0f30d693c 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 { 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, 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; + const isDecorated = hasDecorators(path.node); + const props: PropPath[] = []; const elements = []; - const computedPaths = []; - const privateNames = new Set(); + const computedPaths: NodePath[] = []; + const privateNames = new Set(); 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) => 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) { 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) { 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"; } } diff --git a/packages/babel-helper-create-class-features-plugin/src/misc.ts b/packages/babel-helper-create-class-features-plugin/src/misc.ts index 6bdf0b645d..679d9912cd 100644 --- a/packages/babel-helper-create-class-features-plugin/src/misc.ts +++ b/packages/babel-helper-create-class-features-plugin/src/misc.ts @@ -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) { 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) { if (this.scope.hasOwnBinding(path.node.name)) { this.scope.rename(path.node.name); path.skip(); } }, }; -function handleClassTDZ(path, state) { +function handleClassTDZ( + path: NodePath, + 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, + constructor: NodePath | undefined, + nodes: t.Statement[], + renamer?: (visitor: Visitor, 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[]; } 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, + computedPaths: NodePath[], + file: File, +) { + const declarations: t.Statement[] = []; const state = { classBinding: path.node.id && path.scope.getBinding(path.node.id.name), file, diff --git a/packages/babel-helper-member-expression-to-functions/src/index.ts b/packages/babel-helper-member-expression-to-functions/src/index.ts index a6530540f9..60a97adcf1 100644 --- a/packages/babel-helper-member-expression-to-functions/src/index.ts +++ b/packages/babel-helper-member-expression-to-functions/src/index.ts @@ -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; + private _map: WeakMap; 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, + 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; + const handle = { memoise() { // noop. }, - // todo(flow->ts) member:NodePath, 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; // 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 = 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; // 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; 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 { + memoise?( + this: HandlerState & State, + member: Member, + count: number, + ): void; + destructureSet( + this: HandlerState & State, + member: Member, + ): t.Expression; + boundGet(this: HandlerState & State, member: Member): t.Expression; + simpleSet?(this: HandlerState & State, member: Member): t.Expression; + get(this: HandlerState & State, member: Member): t.Expression; + set( + this: HandlerState & State, + member: Member, + value: t.Expression, + ): t.Expression; + call( + this: HandlerState & State, + member: Member, + args: t.CallExpression["arguments"], + ): t.Expression; + optionalCall( + this: HandlerState & State, + member: Member, + args: t.OptionalCallExpression["arguments"], + ): t.Expression; +} + +export interface HandlerState extends Handler { + handle( + this: HandlerState & 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( + path: NodePath, + visitor: Visitor>, + state: Handler & CustomState, +) { path.traverse(visitor, { ...handle, ...state, diff --git a/packages/babel-helper-replace-supers/src/index.ts b/packages/babel-helper-replace-supers/src/index.ts index 617c6c7746..c85d5313de 100644 --- a/packages/babel-helper-replace-supers/src/index.ts +++ b/packages/babel-helper-replace-supers/src/index.ts @@ -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(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, }); } diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-self-method/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-self-method/options.json index c13414457a..044d20fa9d 100644 --- a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-self-method/options.json +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/static-self-method/options.json @@ -1,3 +1,3 @@ { - "plugins": ["proposal-class-properties", "transform-block-scoping"] + "plugins": ["proposal-class-properties", "proposal-private-methods", "transform-block-scoping"] } diff --git a/packages/babel-traverse/src/index.ts b/packages/babel-traverse/src/index.ts index 0d9b5ddf00..c4fcf7817a 100644 --- a/packages/babel-traverse/src/index.ts +++ b/packages/babel-traverse/src/index.ts @@ -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"; diff --git a/packages/babel-traverse/src/scope/index.ts b/packages/babel-traverse/src/scope/index.ts index c77a0b3e8a..85153c2d29 100644 --- a/packages/babel-traverse/src/scope/index.ts +++ b/packages/babel-traverse/src/scope/index.ts @@ -317,6 +317,8 @@ const collectorVisitor: Visitor = { let uid = 0; +export type { Binding }; + export default class Scope { uid;