diff --git a/packages/transpiler/class-transformer/src/plugin.ts b/packages/transpiler/class-transformer/src/plugin.ts index 3c035355..43f5447b 100644 --- a/packages/transpiler/class-transformer/src/plugin.ts +++ b/packages/transpiler/class-transformer/src/plugin.ts @@ -17,15 +17,18 @@ import { PluginObj } from '@babel/core'; import { Option } from './types'; import * as babel from '@babel/core'; import { PluginProvider } from './pluginProvider'; +import { ThisPatcher } from './thisPatcher'; export default function (api: typeof babel, options: Option): PluginObj { const pluginProvider = new PluginProvider(api, options); - + const thisPatcher = new ThisPatcher(api); return { name: 'zouyu-2', visitor: { FunctionDeclaration(path) { pluginProvider.functionDeclarationVisitor(path); + + thisPatcher.patch(path); }, }, }; diff --git a/packages/transpiler/class-transformer/src/pluginProvider.ts b/packages/transpiler/class-transformer/src/pluginProvider.ts index a0b54c70..31dda409 100644 --- a/packages/transpiler/class-transformer/src/pluginProvider.ts +++ b/packages/transpiler/class-transformer/src/pluginProvider.ts @@ -89,25 +89,8 @@ class ClassComponentTransformer { private readonly t: typeof t; private readonly functionScope: Scope; - valueWrapper(node) { - return this.t.file(this.t.program([this.t.isStatement(node) ? node : this.t.expressionStatement(node)])); - } - addProperty(prop: t.ClassProperty | t.ClassMethod, name?: string) { this.properties.push(prop); - if (name) { - // replace the variable in scope to process the variable in class scope - // e.g. replace () => count++ to () => this.count++ - // TODO: search for better solution - this.functionScope.rename(name, `this.${name}`); - this.functionScope.path.traverse({ - Identifier: path => { - if (path.node.name === `this.${name}`) { - path.replaceWith(this.t.memberExpression(this.t.thisExpression(), this.t.identifier(name))); - } - }, - }); - } } constructor(babelApi: typeof babel, fnNode: NodePath) { diff --git a/packages/transpiler/class-transformer/src/test/fn2Class.test.ts b/packages/transpiler/class-transformer/src/test/fn2Class.test.ts index 55ea9d87..72536df3 100644 --- a/packages/transpiler/class-transformer/src/test/fn2Class.test.ts +++ b/packages/transpiler/class-transformer/src/test/fn2Class.test.ts @@ -204,15 +204,9 @@ describe('fn2Class', () => { transform(` export default function CountComp() { let count = 0; - let countDown; - let color for (let i = 0; i < count; i++) { console.log(\`The count change to: \${i}\`); } - for (let i = 0; i < dbCount; i++) { - console.log('color changed:', getColor()); - } - function () return <>
{count}
diff --git a/packages/transpiler/class-transformer/src/thisPatcher.ts b/packages/transpiler/class-transformer/src/thisPatcher.ts new file mode 100644 index 00000000..aa88c121 --- /dev/null +++ b/packages/transpiler/class-transformer/src/thisPatcher.ts @@ -0,0 +1,115 @@ +import { type types as t, NodePath } from '@babel/core'; +import * as babel from '@babel/core'; + +export class ThisPatcher { + private readonly babelApi: typeof babel; + private readonly t: typeof t; + + private programNode: t.Program | undefined; + + constructor(babelApi: typeof babel) { + this.babelApi = babelApi; + this.t = babelApi.types; + } + + patch = (classPath: NodePath) => { + const classBodyNode = classPath.node.body; + const availPropNames = classBodyNode.body + .filter( + (def): def is Exclude => + !this.t.isTSIndexSignature(def) && !this.t.isStaticBlock(def) + ) + .map(def => ('name' in def.key ? def.key.name : null)); + + for (const memberOrMethod of classBodyNode.body) { + classPath.scope.traverse(memberOrMethod, { + Identifier: (path: NodePath) => { + const idNode = path.node; + if ('key' in memberOrMethod && idNode === memberOrMethod.key) return; + const idName = idNode.name; + if ( + availPropNames.includes(idName) && + !this.isMemberExpression(path) && + !this.isVariableDeclarator(path) && + !this.isAttrFromFunction(path, idName, memberOrMethod) && + !this.isObjectKey(path) + ) { + path.replaceWith(this.t.memberExpression(this.t.thisExpression(), this.t.identifier(idName))); + path.skip(); + } + }, + }); + } + }; + + /** + * check if the identifier is from a function param, e.g: + * class MyClass { + * ok = 1 + * myFunc1 = () => ok // change to myFunc1 = () => this.ok + * myFunc2 = ok => ok // don't change !!!! + * } + */ + isAttrFromFunction(path: NodePath, idName: string, stopNode: t.ClassBody['body'][number]) { + let reversePath = path.parentPath; + + const checkParam = (param: t.Node): boolean => { + // ---- 3 general types: + // * represent allow nesting + // ---0 Identifier: (a) + // ---1 RestElement: (...a) * + // ---1 Pattern: 3 sub Pattern + // -----0 AssignmentPattern: (a=1) * + // -----1 ArrayPattern: ([a, b]) * + // -----2 ObjectPattern: ({a, b}) + if (this.t.isIdentifier(param)) return param.name === idName; + if (this.t.isAssignmentPattern(param)) return checkParam(param.left); + if (this.t.isArrayPattern(param)) { + return param.elements.map(el => checkParam(el)).includes(true); + } + if (this.t.isObjectPattern(param)) { + return param.properties.map((prop: any) => prop.key.name).includes(idName); + } + if (this.t.isRestElement(param)) return checkParam(param.argument); + + return false; + }; + + while (reversePath && reversePath.node !== stopNode) { + const node = reversePath.node; + if (this.t.isArrowFunctionExpression(node) || this.t.isFunctionDeclaration(node)) { + for (const param of node.params) { + if (checkParam(param)) return true; + } + } + reversePath = reversePath.parentPath; + } + if (this.t.isClassMethod(stopNode)) { + for (const param of stopNode.params) { + if (checkParam(param)) return true; + } + } + return false; + } + + /** + * check if the identifier is already like `this.a` / `xx.a` but not like `a.xx` / xx[a] + */ + isMemberExpression(path: NodePath) { + const parentNode = path.parentPath.node; + return this.t.isMemberExpression(parentNode) && parentNode.property === path.node && !parentNode.computed; + } + + /** + * check if the identifier is a variable declarator like `let a = 1` `for (let a in array)` + */ + isVariableDeclarator(path: NodePath) { + const parentNode = path.parentPath.node; + return this.t.isVariableDeclarator(parentNode) && parentNode.id === path.node; + } + + isObjectKey(path: NodePath) { + const parentNode = path.parentPath.node; + return this.t.isObjectProperty(parentNode) && parentNode.key === path.node; + } +}