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

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