From 0e3812a8b27c67a21f9734809166a796b578da32 Mon Sep 17 00:00:00 2001 From: chenweiyi <737649321@qq.com> Date: Thu, 14 Sep 2023 19:51:40 +0800 Subject: [PATCH] feat: support .jsx return object data --- src/analyze/tsx.ts | 434 ++++++++++++++++++++++++++----- src/analyze/utils.ts | 8 +- src/utils/traverse.ts | 29 ++- test/tsx/TestComponent.graph2.ts | 35 +++ test/tsx/TestComponent.nodes2.ts | 1 + test/tsx/TestComponent2.vue | 43 +++ test/tsx/index.test.ts | 16 +- 7 files changed, 488 insertions(+), 78 deletions(-) create mode 100644 test/tsx/TestComponent.graph2.ts create mode 100644 test/tsx/TestComponent.nodes2.ts create mode 100644 test/tsx/TestComponent2.vue diff --git a/src/analyze/tsx.ts b/src/analyze/tsx.ts index 7701403..8648c86 100644 --- a/src/analyze/tsx.ts +++ b/src/analyze/tsx.ts @@ -12,64 +12,263 @@ import { parseNodeObjectPattern, parseNodeArrayPattern, parseNodeFunctionPattern, - parseEdgeIdentifierPattern, - parseEdgeObjectPattern, - parseEdgeArrayPattern, + parseEdgeLeftIdentifierPattern, + parseEdgeLeftObjectPattern, + parseEdgeLeftArrayPattern, parseEdgeFunctionPattern, parseReturnJsxPattern, IUsedNode, - parseReturnObjectPattern, } from '../utils/traverse'; +interface IProcessMain { + node: t.Node, + lineOffset?: number, + addInfo?: boolean, + parentScope?: Scope, + parentNode?: t.Node, + parentPath?: NodePath + spread?: string[] +} -export function processTsx(ast: t.Node, lineOffset = 0, addInfo = true, - parentScope?: Scope, parentPath?: t.Node, _spread?: string[]) { - - const nodeCollection = new NodeCollection(lineOffset, addInfo); - - const nodesUsedInTemplate = new Set(); +interface IProcessBranch { + node: t.ObjectExpression, + lineOffset?: number, + addInfo?: boolean, + parentScope?: Scope, + parentNode?: t.ExportDefaultDeclaration, + parentPath: NodePath + spread?: string[] +} - const graph = { - nodes: new Set(), - edges: new Map>(), +interface IReturnData { + graph: { + nodes: Set; + edges: Map>; + spread?: Map>; }; + nodeCollection: NodeCollection; + nodesUsedInTemplate: Set; +} + +export function processTsx(params : IProcessMain) { + + let result: IReturnData | undefined; + + + function processByReturnNotJSX(params: IProcessBranch) { + const {node, parentPath, lineOffset, addInfo} = params; + const spread: string[] = []; + + const nodeCollection = new NodeCollection(lineOffset, addInfo); + + const graph = { + nodes: new Set(), + edges: new Map>(), + }; + + // 解析return, 收集spread + traverse(node, { + ObjectMethod(path1) { + if ( + ( + parentPath.node.declaration.type === 'ObjectExpression' + && path1.parent === parentPath.node.declaration + ) || ( + parentPath.node.declaration.type === 'CallExpression' + && path1.parent === parentPath.node.declaration.arguments[0] + ) + ) { + if(path1.node.key.type === 'Identifier' && path1.node.key.name === 'setup') { + // setup return + path1.traverse({ + ReturnStatement(path2) { + // get setup return obj spread + if(path2.node.argument?.type === 'ObjectExpression') { + const returnNode = path2.node.argument; + traverse(returnNode, { + SpreadElement(path3) { + // ...toRefs(xxx) + if( + path3.node.argument.type === 'CallExpression' + && path3.node.argument.callee.type === 'Identifier' + && path3.node.argument.callee.name === 'toRefs' + && path3.node.argument.arguments[0].type === 'Identifier' + ) { + spread.push(path3.node.argument.arguments[0].name); + } + // ...xxx + else if( + path3.node.argument.type === 'Identifier' + ) { + spread.push(path3.node.argument.name); + } + }, + }, path2.scope, path2); + } + }, + }); + } + } + }, + }, parentPath.scope, parentPath); + + const { + graph : { + nodes: tempNodes, + edges: tempEdges, + spread: tempSpread, + }, + nodeCollection: tempNodeCollection, + nodesUsedInTemplate, + } = processByReturnJSX({ node, parentPath, spread, lineOffset, addInfo }); + + // 处理合并节点 + traverse(node, { + ObjectMethod(path1) { + if ( + ( + parentPath.node.declaration.type === 'ObjectExpression' + && path1.parent === parentPath.node.declaration + ) || ( + parentPath.node.declaration.type === 'CallExpression' + && path1.parent === parentPath.node.declaration.arguments[0] + ) + ) { + if(path1.node.key.type === 'Identifier' && path1.node.key.name === 'setup') { + // setup return + path1.traverse({ + ReturnStatement(path2) { + // get setup return obj spread + if(path2.node.argument?.type === 'ObjectExpression') { + const returnNode = path2.node.argument; + traverse(returnNode, { + ObjectProperty(path3) { + // not spread node + if(path3.parent === returnNode) { + if(path3.node.key.type === 'Identifier' && path3.node.value.type === 'Identifier') { + const valName = path3.node.value.name; + if(!graph.nodes.has(valName)) { + graph.nodes.add(valName); + nodeCollection.addTypedNode( + valName, + tempNodeCollection.nodes.get(valName)! + ); + } + if(!graph.edges.has(valName)) { + graph.edges.set(valName, new Set([...Array.from( + tempEdges.get(valName) || new Set() + )])); + } + + const name = path3.node.key.name; + if(name !== valName) { + graph.nodes.add(name); + nodeCollection.addNode(name, path3.node.key); + graph.edges.set(name, new Set([valName])); + } + } + } + }, + SpreadElement(path3) { + // ...toRefs(xxx) + if( + path3.node.argument.type === 'CallExpression' + && path3.node.argument.callee.type === 'Identifier' + && path3.node.argument.callee.name === 'toRefs' + && path3.node.argument.arguments[0].type === 'Identifier' + && tempSpread.get(path3.node.argument.arguments[0].name) + ) { + tempSpread.get(path3.node.argument.arguments[0].name)?.forEach((name) => { + graph.nodes.add(name); + nodeCollection.addTypedNode(name, tempNodeCollection.nodes.get(name)!); + if(!graph.edges.get(name)) { + graph.edges.set(name, new Set()); + tempEdges.get(name)?.forEach((edge) => { + graph.edges.get(name)?.add(edge); + }); + } + }); + } + // ...xxx + else if( + path3.node.argument.type === 'Identifier' + && tempSpread.get(path3.node.argument.name) + ) { + tempSpread.get(path3.node.argument.name)?.forEach((name) => { + graph.nodes.add(name); + nodeCollection.addTypedNode(name, tempNodeCollection.nodes.get(name)!); + if(!graph.edges.get(name)) { + graph.edges.set(name, new Set()); + tempEdges.get(name)?.forEach((edge) => { + graph.edges.get(name)?.add(edge); + }); + } + }); + } + }, + }, path2.scope, path2); + } + }, + }); + } + } + }, + }, parentPath.scope, parentPath); + + return { + graph, + nodeCollection, + nodesUsedInTemplate, + }; + } + + function processByReturnJSX(params: IProcessBranch) { + const {node, parentPath, spread = [], lineOffset, addInfo } = params; + + const nodeCollection = new NodeCollection(lineOffset, addInfo); + const nodesUsedInTemplate = new Set(); - function addNode ({ name, node, path, scope}: IAddNode) { - const binding = path.scope.getBinding(name); - if (scope === binding?.scope) { - graph.nodes.add(name); - nodeCollection.addNode(name, node); - if(!graph.edges.get(name)) { - graph.edges.set(name, new Set()); + const graph = { + nodes: new Set(), + edges: new Map>(), + spread: new Map>(), + }; + + function addNode ({ name, node, path, scope}: IAddNode) { + const binding = path.scope.getBinding(name); + if (scope === binding?.scope) { + graph.nodes.add(name); + nodeCollection.addNode(name, node); + if(!graph.edges.get(name)) { + graph.edges.set(name, new Set()); + } } } - } - function addEdge ({ fromName, toName, path, scope, collectionNodes }: IAddEdge) { - const binding = path.scope.getBinding(toName); - if (scope === binding?.scope && collectionNodes.has(toName)) { - graph.edges.get(fromName)?.add(toName); + function addEdge ({ fromName, toName, path, scope, toScope, collectionNodes }: IAddEdge) { + const bindingScope = toScope || path.scope.getBinding(toName)?.scope; + if (scope === bindingScope && collectionNodes.has(toName)) { + graph.edges.get(fromName)?.add(toName); + } } - } - function addUsed ({name, path, parentPath} : IUsedNode) { - const binding = path.scope.getBinding(name); - if (binding?.scope === parentPath.scope) { - nodesUsedInTemplate.add(name); + function addUsed ({name, path, parentPath} : IUsedNode) { + const binding = path.scope.getBinding(name); + if (binding?.scope === parentPath.scope) { + nodesUsedInTemplate.add(name); + } } - } - function process(node: t.ObjectExpression, path: NodePath) { - // 收集节点 + // 收集节点, 并收集spread依赖 traverse(node, { ObjectMethod(path1) { if ( ( - path.node.declaration.type === 'ObjectExpression' - && path1.parent === path.node.declaration + parentPath.node.declaration.type === 'ObjectExpression' + && path1.parent === parentPath.node.declaration ) || ( - path.node.declaration.type === 'CallExpression' - && path1.parent === path.node.declaration.arguments[0] + parentPath.node.declaration.type === 'CallExpression' + && path1.parent === parentPath.node.declaration.arguments[0] ) ) { if(path1.node.key.type === 'Identifier' && path1.node.key.name === 'setup') { @@ -80,7 +279,64 @@ export function processTsx(ast: t.Node, lineOffset = 0, addInfo = true, parseNodeIdentifierPattern({ path: path1, rootScope: setupScope, - cb: addNode, + cb: (params) => { + if (!spread.includes(params.name)) { + addNode(params); + } else { + if(path1.node.init?.type === 'ObjectExpression') { + path1.node.init?.properties.forEach(prop => { + if( + (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') + && prop.key.type === 'Identifier' + ) { + const keyName = prop.key.name; + graph.nodes.add(keyName); + nodeCollection.addNode(keyName, prop); + if(!graph.edges.get(keyName)) { + graph.edges.set(keyName, new Set()); + } + if(graph.spread.has(params.name)) { + graph.spread.get(params.name)?.add(keyName); + } else { + graph.spread.set(params.name, new Set([keyName])); + } + } else if(prop.type === 'SpreadElement') { + console.warn('not support spread in spread'); + } + }); + } + + if( + path1.node.init?.type === 'CallExpression' + && path1.node.init?.callee.type === 'Identifier' + && path1.node.init?.callee.name === 'reactive' + ) { + const arg = path1.node.init?.arguments[0]; + if(arg.type === 'ObjectExpression') { + arg.properties.forEach(prop => { + if( + (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') + && prop.key.type === 'Identifier' + ) { + const keyName = prop.key.name; + graph.nodes.add(keyName); + nodeCollection.addNode(keyName, prop); + if(!graph.edges.get(keyName)) { + graph.edges.set(keyName, new Set()); + } + if(graph.spread.has(params.name)) { + graph.spread.get(params.name)?.add(keyName); + } else { + graph.spread.set(params.name, new Set([keyName])); + } + } else if(prop.type === 'SpreadElement') { + console.warn('not support spread in spread'); + } + }); + } + } + } + }, }); parseNodeObjectPattern({ @@ -113,48 +369,43 @@ export function processTsx(ast: t.Node, lineOffset = 0, addInfo = true, parentPath: path1, cb: addUsed, }); - // setup return Object - parseReturnObjectPattern({ - path: path2, - parentPath: path1, - // TODO - cb: addUsed, - }); }, }); } } }, - }, path.scope, path); + }, parentPath.scope, parentPath); // 收集边 traverse(node, { ObjectMethod(path1) { if ( - (path.node.declaration.type === 'ObjectExpression' && path1.parent === path.node.declaration) + (parentPath.node.declaration.type === 'ObjectExpression' && path1.parent === parentPath.node.declaration) || - (path.node.declaration.type === 'CallExpression' && path1.parent === path.node.declaration.arguments[0]) + (parentPath.node.declaration.type === 'CallExpression' + && path1.parent === parentPath.node.declaration.arguments[0]) ) { if(path1.node.key.type === 'Identifier' && path1.node.key.name === 'setup') { // setup const setupScope = path1.scope; traverse(path1.node, { VariableDeclarator(path1) { - parseEdgeIdentifierPattern({ + parseEdgeLeftIdentifierPattern({ path: path1, rootScope: setupScope, collectionNodes: graph.nodes, cb: addEdge, + spread, }); - parseEdgeObjectPattern({ + parseEdgeLeftObjectPattern({ path: path1, rootScope: setupScope, collectionNodes: graph.nodes, cb: addEdge, }); - parseEdgeArrayPattern({ + parseEdgeLeftArrayPattern({ path: path1, rootScope: setupScope, collectionNodes: graph.nodes, @@ -173,33 +424,82 @@ export function processTsx(ast: t.Node, lineOffset = 0, addInfo = true, } } }, - }, path.scope, path); + }, parentPath.scope, parentPath); + + return { + graph, + nodeCollection, + nodesUsedInTemplate, + }; } + + function process(params: IProcessBranch) { + const { node, parentPath } = params; + // 解析return, 收集spread + traverse(node, { + ObjectMethod(path1) { + if ( + ( + parentPath.node.declaration.type === 'ObjectExpression' + && path1.parent === parentPath.node.declaration + ) || ( + parentPath.node.declaration.type === 'CallExpression' + && path1.parent === parentPath.node.declaration.arguments[0] + ) + ) { + if(path1.node.key.type === 'Identifier' && path1.node.key.name === 'setup') { + // setup return + path1.traverse({ + ReturnStatement(path2) { + if (path2.node.argument + && (path2.node.argument.type === 'ArrowFunctionExpression' + || path2.node.argument.type === 'FunctionExpression') + && (path2.node.argument.body.type === 'JSXElement' + || path2.node.argument.body.type === 'JSXFragment') + ) { + result = processByReturnJSX(params); + } else { + result = processByReturnNotJSX(params); + } + }, + }); + } + } + }, + }, parentPath.scope, parentPath); + + } - traverse(ast, { + traverse(params.node, { ExportDefaultDeclaration(path) { - // export default {} if(path.node.declaration.type === 'ObjectExpression') { - process(path.node.declaration, path); - } - // export default defineComponent({}) - else if(path.node.declaration.type === 'CallExpression' + // export default {} + process({ + ...params, + node: path.node.declaration, + parentNode: path.node, + parentPath: path, + }); + } else if( + path.node.declaration.type === 'CallExpression' && path.node.declaration.callee.type === 'Identifier' && path.node.declaration.callee.name === 'defineComponent' && path.node.declaration.arguments[0].type === 'ObjectExpression' ) { - process(path.node.declaration.arguments[0], path); + // export default defineComponent({}) + process({ + ...params, + node: path.node.declaration.arguments[0], + parentNode: path.node, + parentPath: path, + }); } }, }); - return { - graph, - nodeCollection, - nodesUsedInTemplate, - }; + return result!; } @@ -227,7 +527,11 @@ export async function analyze( }); if (res && res.ast) { - const { graph, nodeCollection, nodesUsedInTemplate } = processTsx(res.ast, lineOffset, addInfo); + const { graph, nodeCollection, nodesUsedInTemplate } = processTsx({ + node: res.ast, + lineOffset, + addInfo, + }); // --- return { graph: nodeCollection.map(graph), diff --git a/src/analyze/utils.ts b/src/analyze/utils.ts index 47f8701..2d4936c 100644 --- a/src/analyze/utils.ts +++ b/src/analyze/utils.ts @@ -79,9 +79,11 @@ export class NodeCollection { this.nodes.set(label, { label, type: node.type, - info: { - ...(node.info || {}), - }, + ...(this.addInfo ? { + info: { + ...(node.info || {}), + }, + } : {}), }); } diff --git a/src/utils/traverse.ts b/src/utils/traverse.ts index 4bdceed..057b57c 100644 --- a/src/utils/traverse.ts +++ b/src/utils/traverse.ts @@ -22,8 +22,9 @@ export interface IUsedNode { export interface IAddEdge { fromName: string, toName: string, - path: NodePath, + path: NodePath, scope: Scope, + toScope?: Scope, collectionNodes: Set, } @@ -39,6 +40,7 @@ export interface IParseNodeBase extends IParseVariable{ export interface IParseEdgeBase extends IParseVariable{ cb?: (params: IAddEdge) => void collectionNodes: Set + spread?: string[] } @@ -296,7 +298,7 @@ export function parseNodeFunctionPattern({path, rootScope, cb}: IParseNodeFuncti } } -export function parseEdgeIdentifierPattern({path, rootScope, cb, collectionNodes}: IParseEdgeBase) { +export function parseEdgeLeftIdentifierPattern({path, rootScope, cb, collectionNodes, spread}: IParseEdgeBase) { if (!path.node.id || path.node.id.type !== 'Identifier') return; if (path.node.init?.type && ['ArrowFunctionExpression', 'FunctionExpression', 'CallExpression'].includes(path.node.init.type) @@ -315,12 +317,26 @@ export function parseEdgeIdentifierPattern({path, rootScope, cb, collectionNodes collectionNodes, }); }, + MemberExpression(path1) { + if (spread?.length && path1.node.object.type === 'Identifier' + && spread.includes(path1.node.object.name) + && path1.node.property.type === 'Identifier') { + cb?.({ + fromName: name, + toName: path1.node.property.name, + toScope: path1.scope.getBinding(path1.node.object.name)?.scope, + path: path1, + scope: rootScope, + collectionNodes, + }); + } + }, }, path.scope, path); } } } -export function parseEdgeObjectPattern({path, rootScope, cb, collectionNodes} : IParseEdgeBase) { +export function parseEdgeLeftObjectPattern({path, rootScope, cb, collectionNodes} : IParseEdgeBase) { if (!path.node.id || path.node.id.type !== 'ObjectPattern') return; if (path.node.init?.type && ['ArrowFunctionExpression', 'FunctionExpression', 'CallExpression'].includes(path.node.init.type) @@ -353,7 +369,7 @@ export function parseEdgeObjectPattern({path, rootScope, cb, collectionNodes} : } } -export function parseEdgeArrayPattern({path, rootScope, cb, collectionNodes} : IParseEdgeBase) { +export function parseEdgeLeftArrayPattern({path, rootScope, cb, collectionNodes} : IParseEdgeBase) { if (!path.node.id || path.node.id.type !== 'ArrayPattern') return; if (path.node.init?.type && ['ArrowFunctionExpression', 'FunctionExpression', 'CallExpression'].includes(path.node.init.type) @@ -424,8 +440,3 @@ export function parseReturnJsxPattern({path, parentPath, cb}: IParseReturnJSX) { }); } } - -export function parseReturnObjectPattern({path, parentPath, cb}: IParseReturnJSX) { - // TODO: - -} diff --git a/test/tsx/TestComponent.graph2.ts b/test/tsx/TestComponent.graph2.ts new file mode 100644 index 0000000..7677a19 --- /dev/null +++ b/test/tsx/TestComponent.graph2.ts @@ -0,0 +1,35 @@ +import { NodeType } from '@/analyze/utils'; + +const edges = new Map<{ + label: string, type: NodeType}, + Set<{label: string, type: NodeType}> +>(); + +const focus = {label: 'focus', type: NodeType.fun}; +const open = {label: 'open', type: NodeType.fun}; +const AA = {label: 'AA', type: NodeType.var}; +const aa = {label: 'aa', type: NodeType.var}; +const x = {label: 'x', type: NodeType.var}; +const y = {label: 'y', type: NodeType.var}; + + + +edges.set(focus, new Set([aa, x])); +edges.set(aa, new Set([])); +edges.set(AA, new Set([aa])); +edges.set(x, new Set([])); +edges.set(y, new Set([])); +edges.set(open, new Set([])); + + +export const graph = { + nodes: new Set([ + focus, + aa, + AA, + x, + y, + open, + ]), + edges, +}; \ No newline at end of file diff --git a/test/tsx/TestComponent.nodes2.ts b/test/tsx/TestComponent.nodes2.ts new file mode 100644 index 0000000..b2a9c41 --- /dev/null +++ b/test/tsx/TestComponent.nodes2.ts @@ -0,0 +1 @@ +export const nodes = new Set(['open', 'aa']); \ No newline at end of file diff --git a/test/tsx/TestComponent2.vue b/test/tsx/TestComponent2.vue new file mode 100644 index 0000000..1567fe7 --- /dev/null +++ b/test/tsx/TestComponent2.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/test/tsx/index.test.ts b/test/tsx/index.test.ts index bed0093..e3a426c 100644 --- a/test/tsx/index.test.ts +++ b/test/tsx/index.test.ts @@ -3,11 +3,15 @@ import path from 'node:path'; import { parse, analyzeTemplate, analyzeTsx } from '@/index'; import {graph as graphRes} from './TestComponent.graph'; +import {graph as graphRes2} from './TestComponent.graph2'; import {nodes as nodesRes} from './TestComponent.nodes'; +import {nodes as nodesRes2} from './TestComponent.nodes2'; describe('test analyze', () => { const source = fs.readFileSync(path.resolve(__dirname, './TestComponent.vue'), 'utf-8'); + const source2 = fs.readFileSync(path.resolve(__dirname, './TestComponent2.vue'), 'utf-8'); const sfc = parse(source); + const sfc2 = parse(source2); it('test analyze tsx setup', async () => { const { graph, nodesUsedInTemplate } = await analyzeTsx( sfc.descriptor.script?.content!, @@ -16,6 +20,16 @@ describe('test analyze', () => { ); expect(graph).toEqual(graphRes); - expect(nodesUsedInTemplate).toEqual(nodesRes); + expect(nodesUsedInTemplate).toEqual(nodesRes); + }); + + it('test analyze tsx setup not jsx', async () => { + const { graph } = await analyzeTsx( + sfc2.descriptor.script?.content!, + (sfc2.descriptor.script?.loc.start.line || 1) - 1, + false + ); + + expect(graph).toEqual(graphRes2); }); }); \ No newline at end of file