const { readFileSync, existsSync } = require('fs') const { resolve } = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const { parse } = require('@vue/compiler-sfc') /** * AST分析模块 * 负责Babel和Vue模板的AST分析 */ class AstAnalyzer { constructor() { this.processedButtons = new Set() } /** * 使用Babel AST分析查找调用指定服务方法的方法 * @param {string} content - 组件内容 * @param {string} serviceName - 服务名称 * @param {string} apiMethodName - API方法名称 * @returns {Array} 调用该API的方法名数组 */ findMethodsCallingServiceWithBabel(content, serviceName, apiMethodName) { const callingMethods = [] try { // 解析script部分 const scriptMatch = content.match(/]*>([\s\S]*?)<\/script>/) if (!scriptMatch) { return callingMethods } const scriptContent = scriptMatch[1] // 使用Babel解析JavaScript const ast = parser.parse(scriptContent, { sourceType: 'module', plugins: ['jsx', 'typescript'] }) // 遍历AST查找方法定义 traverse(ast, { // 查找const方法定义 VariableDeclarator(path) { if (path.node.id && path.node.id.type === 'Identifier' && path.node.init && path.node.init.type === 'ArrowFunctionExpression') { const componentMethodName = path.node.id.name const methodPath = path.get('init') // 检查该方法是否调用了指定的API if (this.checkMethodCallsServiceWithBabel(methodPath, serviceName, apiMethodName)) { callingMethods.push(componentMethodName) } } }, // 查找ObjectMethod定义 ObjectMethod(path) { if (path.node.key && path.node.key.type === 'Identifier') { const componentMethodName = path.node.key.name const methodPath = path // 检查该方法是否调用了指定的API if (this.checkMethodCallsServiceWithBabel(methodPath, serviceName, apiMethodName)) { callingMethods.push(componentMethodName) } } } }) } catch (error) { // 静默处理错误 } return callingMethods } /** * 使用Babel检查方法是否调用了指定的service方法 * @param {Object} methodPath - 方法路径 * @param {string} serviceName - 服务名称 * @param {string} apiMethodName - API方法名称 * @returns {boolean} 是否调用了指定的API */ checkMethodCallsServiceWithBabel(methodPath, serviceName, apiMethodName) { let hasServiceCall = false traverse(methodPath.node, { CallExpression(path) { const node = path.node if (node.callee && node.callee.type === 'MemberExpression') { const object = node.callee.object const property = node.callee.property if (object && property && object.type === 'Identifier' && property.type === 'Identifier') { if (object.name === serviceName && property.name === apiMethodName) { hasServiceCall = true } } } } }, methodPath.scope, methodPath) return hasServiceCall } /** * 使用AST分析模板中的事件绑定 * @param {string} templateContent - 模板内容 * @param {string} componentName - 组件名称 * @param {Array} apiMethodNames - API方法名数组 * @param {Array} triggerSources - 触发器源数组 * @param {string} filePath - 文件路径 */ findTriggersInTemplateWithAST(templateContent, componentName, apiMethodNames, triggerSources, filePath) { try { // 使用Vue模板编译器解析模板 const ast = parse(templateContent, { sourceMap: false, filename: 'template.vue' }) // 遍历AST找到事件绑定 this.traverseTemplateAST(ast, (node) => { if (node.type === 1 && node.tag === 'button') { // 元素节点且是button标签 this.processButtonNode(node, componentName, apiMethodNames, triggerSources, this.processedButtons, filePath) } }) } catch (error) { // 静默处理错误 } } /** * 遍历Vue模板AST * @param {Object} ast - AST节点 * @param {Function} callback - 回调函数 */ traverseTemplateAST(ast, callback) { if (!ast || !ast.children) return ast.children.forEach(child => { if (child.type === 1) { // 元素节点 callback(child) this.traverseTemplateAST(child, callback) } }) } /** * 处理按钮节点 * @param {Object} node - 按钮节点 * @param {string} componentName - 组件名称 * @param {Array} apiMethodNames - API方法名数组 * @param {Array} triggerSources - 触发器源数组 * @param {Set} processedButtons - 已处理的按钮集合 * @param {string} filePath - 文件路径 */ processButtonNode(node, componentName, apiMethodNames, triggerSources, processedButtons, filePath) { if (!node.props) return let buttonName = null let clickHandler = null // 提取按钮属性 node.props.forEach(prop => { if (prop.name === 'name' && prop.value && prop.value.content) { buttonName = prop.value.content } else if (prop.name === 'onClick' && prop.value && prop.value.type === 4) { // 处理@click事件 if (prop.value.exp && prop.value.exp.children) { const exp = prop.value.exp.children[0] if (exp && exp.type === 4) { // 简单标识符 clickHandler = exp.content } } } }) // 检查是否匹配API方法 if (clickHandler && apiMethodNames.includes(clickHandler)) { if (buttonName && !processedButtons.has(buttonName)) { triggerSources.push({ component: componentName, triggerName: buttonName, triggerType: 'button' }) processedButtons.add(buttonName) } else if (!buttonName && !processedButtons.has(clickHandler)) { const generatedName = this.generateUniqueButtonName(componentName, clickHandler) triggerSources.push({ component: componentName, triggerName: generatedName, triggerType: 'button' }) processedButtons.add(clickHandler) // 为按钮添加name属性 if (filePath) { this.addNameAttributeToButton(filePath, node, generatedName) } } } } /** * 生成唯一的按钮名称 * @param {string} componentName - 组件名称 * @param {string} clickHandler - 点击处理器 * @returns {string} 唯一的按钮名称 */ generateUniqueButtonName(componentName, clickHandler) { const baseName = componentName.toLowerCase() const methodSuffix = clickHandler.toLowerCase() return `${baseName}-${methodSuffix}` } /** * 为按钮添加name属性 * @param {string} filePath - 文件路径 * @param {Object} buttonNode - 按钮节点 * @param {string} generatedName - 生成的名称 */ addNameAttributeToButton(filePath, buttonNode, generatedName) { try { const content = readFileSync(filePath, 'utf-8') // 检查按钮是否已经有name属性 const hasName = buttonNode.props && buttonNode.props.some(prop => prop.name === 'name') if (hasName) { return } // 在button标签中添加name属性 const buttonHtml = this.nodeToHtml(buttonNode) const updatedButtonHtml = buttonHtml.replace(/]*)>/, ``) // 替换文件中的按钮HTML const updatedContent = content.replace(buttonHtml, updatedButtonHtml) // 写回文件 require('fs').writeFileSync(filePath, updatedContent, 'utf-8') } catch (error) { // 静默处理错误 } } /** * 将AST节点转换为HTML字符串 * @param {Object} node - AST节点 * @returns {string} HTML字符串 */ nodeToHtml(node) { if (!node) return '' let html = `<${node.tag}` if (node.props) { node.props.forEach(prop => { if (prop.name === 'onClick' && prop.value && prop.value.type === 4) { html += ` @click="${prop.value.exp.children[0].content}"` } else if (prop.name !== 'onClick') { html += ` ${prop.name}` if (prop.value && prop.value.content) { html += `="${prop.value.content}"` } } }) } html += '>' if (node.children && node.children.length > 0) { node.children.forEach(child => { if (child.type === 2) { // 文本节点 html += child.content } else if (child.type === 1) { // 元素节点 html += this.nodeToHtml(child) } }) } html += `` return html } } module.exports = AstAnalyzer