You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
9.0 KiB
294 lines
9.0 KiB
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(/<script[^>]*>([\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(/<button([^>]*)>/, `<button$1 name="${generatedName}">`) |
|
|
|
// 替换文件中的按钮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 += `</${node.tag}>` |
|
return html |
|
} |
|
} |
|
|
|
module.exports = AstAnalyzer
|
|
|