const { readFileSync, writeFileSync, existsSync } = require('fs') const { resolve } = require('path') const { parse } = require('@vue/compiler-sfc') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default /** * 调用链追溯器 * 专门用于追溯method类型的触发源到最终的可视控件 */ class CallChainTracer { constructor() { this.processedFiles = new Set() // 跟踪已处理的文件,避免重复处理 } /** * 清理已处理文件的记录 * 在每次新的处理周期开始时调用 */ clearProcessedFiles() { this.processedFiles.clear() } /** * 追溯所有method类型的触发源到可视控件 * @param {Array} apiMappings - API映射数组 * @returns {Array} 增强的API映射数组 */ traceMethodTriggersToVisualComponents(apiMappings) { const enhancedApiMappings = apiMappings.map(api => { if (!api.triggerSources || api.triggerSources.length === 0) { return api } // 只处理triggerType为method的触发源 const enhancedTriggerSources = api.triggerSources.map(trigger => { if (trigger.triggerType === 'method') { const result = this.traceMethodToVisualComponent( trigger, api.module, api.serviceName, api.apiMethodName ) return result } return trigger }) return { ...api, triggerSources: enhancedTriggerSources } }) return enhancedApiMappings } /** * 追溯单个method到可视控件 * @param {Object} trigger - 触发源对象 * @param {string} moduleName - 模块名称 * @param {string} serviceName - 服务名称 * @param {string} apiMethodName - API方法名称 * @returns {Object} 增强的触发源对象 */ traceMethodToVisualComponent(trigger, moduleName, serviceName, apiMethodName) { try { // 查找组件文件 const componentPath = this.findComponentFile(trigger.component, moduleName) if (!componentPath) { return trigger } const content = readFileSync(componentPath, 'utf-8') // 解析Vue组件 const ast = this.parseVueComponent(content) if (!ast) { return trigger } // 建立函数调用关系映射 const functionCallMap = this.buildFunctionCallMap(ast) // 追溯method到可视控件 const visualContext = this.traceToVisualComponent( trigger.triggerName, functionCallMap, componentPath, trigger.component ) if (visualContext) { const result = { ...trigger, triggerName: visualContext.triggerName, triggerType: visualContext.triggerType } return result } return trigger } catch (error) { return trigger } } /** * 查找组件文件 * @param {string} componentName - 组件名称 * @param {string} moduleName - 模块名称 * @returns {string|null} 组件文件路径 */ findComponentFile(componentName, moduleName) { // 优先在指定模块中查找 if (moduleName) { // 检查views目录 const viewPath = resolve(__dirname, `../../src/renderer/modules/${moduleName}/views/${componentName}.vue`) if (existsSync(viewPath)) { return viewPath } // 检查components目录 const componentPath = resolve(__dirname, `../../src/renderer/modules/${moduleName}/components/${componentName}.vue`) if (existsSync(componentPath)) { return componentPath } } return null } /** * 解析Vue组件内容 * @param {string} content - 组件内容 * @returns {Object|null} AST对象 */ parseVueComponent(content) { try { // 提取script部分 const scriptMatch = content.match(/]*>([\s\S]*?)<\/script>/) if (!scriptMatch) { return null } const scriptContent = scriptMatch[1] // 使用Babel解析JavaScript const ast = parser.parse(scriptContent, { sourceType: 'module', plugins: ['jsx', 'typescript'] }) return ast } catch (error) { return null } } /** * 建立函数调用关系映射 * @param {Object} ast - AST对象 * @returns {Map} 函数调用关系映射 */ buildFunctionCallMap(ast) { const functionCallMap = new Map() const self = this traverse(ast, { CallExpression(path) { const { node } = path if (node.callee && node.callee.type === 'Identifier') { const functionName = node.callee.name const parentFunction = self.findParentFunction(path) if (parentFunction) { if (!functionCallMap.has(functionName)) { functionCallMap.set(functionName, { callers: [] }) } const funcInfo = functionCallMap.get(functionName) if (!funcInfo.callers) funcInfo.callers = [] funcInfo.callers.push(parentFunction) } } } }) return functionCallMap } /** * 查找父级函数 * @param {Object} path - Babel路径对象 * @returns {string|null} 父级函数名 */ findParentFunction(path) { let currentPath = path.parentPath while (currentPath) { const { node } = currentPath if (node.type === 'FunctionDeclaration' && node.id) { return node.id.name } if (node.type === 'VariableDeclarator' && node.id && node.id.type === 'Identifier' && node.init && node.init.type === 'ArrowFunctionExpression') { return node.id.name } if (node.type === 'ObjectMethod' && node.key) { return node.key.name } currentPath = currentPath.parentPath } return null } /** * 追溯method到可视控件 * @param {string} methodName - 方法名称 * @param {Map} functionCallMap - 函数调用关系映射 * @param {string} filePath - 组件文件路径 * @param {string} componentName - 组件名称 * @returns {Object|null} 可视控件上下文 */ traceToVisualComponent(methodName, functionCallMap, filePath, componentName) { try { // 读取组件文件内容 const content = readFileSync(filePath, 'utf-8') // 分析模板中的事件绑定 const visualContext = this.analyzeTemplateForMethod(content, methodName, componentName, filePath) if (visualContext) { return visualContext } // 如果模板中没有找到,检查是否有其他方法调用了这个方法 const callerContext = this.findMethodCaller(methodName, functionCallMap) if (callerContext) { // 检查调用者是否是生命周期钩子 if (this.isLifecycleHook(callerContext)) { return { triggerName: '', triggerType: 'page' } } // 继续递归追溯 return this.traceToVisualComponent(callerContext, functionCallMap, filePath, componentName) } } catch (error) { // 静默处理错误 } return null } /** * 分析模板中的方法调用 * @param {string} content - 组件内容 * @param {string} methodName - 方法名称 * @param {string} componentName - 组件名称 * @param {string} filePath - 组件文件路径 * @returns {Object|null} 可视控件上下文 */ analyzeTemplateForMethod(content, methodName, componentName, filePath) { try { // 使用Vue编译器解析单文件组件 const { descriptor } = parse(content) if (!descriptor.template) { return null } // 解析模板AST const templateAst = descriptor.template.ast if (!templateAst) { return null } // 遍历模板AST,查找事件绑定 const result = this.findEventBindingInTemplate(templateAst, methodName, componentName, filePath) return result } catch (error) { return null } } /** * 在模板AST中查找事件绑定 * @param {Object} node - AST节点 * @param {string} methodName - 方法名称 * @param {string} componentName - 组件名称 * @param {string} filePath - 组件文件路径 * @returns {Object|null} 可视组件上下文 */ findEventBindingInTemplate(node, methodName, componentName, filePath) { if (!node) { return null } // 检查当前节点是否有事件绑定 if (node.props) { for (const prop of node.props) { if (prop.type === 7) { // DIRECTIVE类型 const eventName = prop.name if (eventName === 'on' && prop.arg) { const eventType = prop.arg.content if (['click', 'submit', 'change', 'input'].includes(eventType)) { // 检查事件处理函数 if (prop.exp && this.isMethodCall(prop.exp, methodName)) { const context = this.createVisualContext(node, eventType, componentName, methodName, filePath) // 如果找到的是form,需要查找其中的submit按钮 if (context.triggerType === 'form') { const submitButton = this.findSubmitButtonInForm(node, componentName, methodName, filePath) if (submitButton) { return submitButton } } return context } } } } } } // 递归检查子节点 if (node.children) { for (const child of node.children) { if (child.type === 1) { // ELEMENT类型 const result = this.findEventBindingInTemplate(child, methodName, componentName, filePath) if (result) { return result } } } } return null } /** * 在form中查找submit按钮 * @param {Object} formNode - form AST节点 * @param {string} componentName - 组件名称 * @param {string} methodName - 方法名称 * @param {string} filePath - 组件文件路径 * @returns {Object|null} submit按钮节点和上下文 */ findSubmitButtonInForm(formNode, componentName, methodName, filePath) { if (!formNode || !formNode.children) return null // 递归查找submit按钮 for (const child of formNode.children) { if (child.type === 1) { // ELEMENT类型 if (child.tag === 'button') { // 检查是否是submit按钮 const props = child.props || [] const attributes = {} props.forEach(prop => { if (prop.type === 6) { // ATTRIBUTE attributes[prop.name] = prop.value ? prop.value.content : '' } }) if (attributes.type === 'submit') { let triggerName = attributes.name if (!triggerName) { // 生成唯一的name属性:组件名+唯一尾缀 triggerName = `${componentName.toLowerCase()}-submit-${this.generateUniqueSuffix()}` } return { node: child, triggerName: triggerName, triggerType: 'button' } } } // 递归查找子元素 const result = this.findSubmitButtonInForm(child, componentName, methodName, filePath) if (result) return result } } return null } /** * 检查表达式是否是方法调用 * @param {Object} exp - 表达式节点 * @param {string} methodName - 方法名称 * @returns {boolean} 是否是方法调用 */ isMethodCall(exp, methodName) { if (!exp) { return false } // 简单的方法调用:methodName if (exp.type === 4 && exp.content === methodName) { // SIMPLE_EXPRESSION return true } // 带参数的方法调用:methodName(...) - 检查content是否以methodName开头 if (exp.type === 4 && exp.content && exp.content.startsWith(methodName + '(')) { return true } // 带参数的方法调用:methodName(...) if (exp.type === 8) { // COMPOUND_EXPRESSION const children = exp.children if (children && children.length >= 1) { const firstChild = children[0] if (firstChild.type === 4 && firstChild.content === methodName) { return true } } } // 检查AST中的方法调用 if (exp.ast) { const result = this.checkAstMethodCall(exp.ast, methodName) return result } return false } /** * 检查AST中的方法调用 * @param {Object} ast - AST节点 * @param {string} methodName - 方法名称 * @returns {boolean} 是否是方法调用 */ checkAstMethodCall(ast, methodName) { if (!ast) return false // 简单标识符:methodName if (ast.type === 'Identifier' && ast.name === methodName) { return true } // 方法调用:methodName(...) if (ast.type === 'CallExpression') { if (ast.callee && ast.callee.type === 'Identifier' && ast.callee.name === methodName) { return true } } return false } /** * 创建可视组件上下文 * @param {Object} node - AST节点 * @param {string} eventType - 事件类型 * @param {string} componentName - 组件名称 * @param {string} methodName - 方法名称 * @param {string} filePath - 组件文件路径 * @returns {Object} 可视组件上下文 */ createVisualContext(node, eventType, componentName, methodName, filePath) { const tag = node.tag const props = node.props || [] // 提取元素属性 const attributes = {} props.forEach(prop => { if (prop.type === 6) { // ATTRIBUTE attributes[prop.name] = prop.value ? prop.value.content : '' } }) let triggerName = attributes.name || attributes.id || '' let triggerType = 'button' // 默认类型 // 根据标签类型确定触发源类型 if (tag === 'form') { triggerType = 'form' } else if (tag === 'button') { triggerType = 'button' } else if (tag === 'input') { triggerType = 'input' } else if (tag === 'select') { triggerType = 'select' } else if (tag === 'textarea') { triggerType = 'textarea' } else { // 其他元素类型 triggerType = 'element' } // 如果没有名称,生成一个 if (!triggerName) { triggerName = `${componentName.toLowerCase()}-${tag}-${this.generateUniqueSuffix()}` } // 先处理form追溯到button的逻辑 if (triggerType === 'form') { const submitButton = this.findSubmitButtonInForm(node, componentName, methodName, filePath) if (submitButton) { // 如果找到submit按钮,为真正的button节点添加属性 this.modifyVueElementWithCompiler(filePath, submitButton.node, submitButton.triggerName, submitButton.triggerType) return { triggerName: submitButton.triggerName, triggerType: submitButton.triggerType } } } // 为最终确定的控件添加name属性和权限控制 if (triggerType === 'button') { this.modifyVueElementWithCompiler(filePath, node, triggerName, triggerType) } return { triggerName: triggerName, triggerType: triggerType } } /** * 查找方法的调用者 * @param {string} methodName - 方法名称 * @param {Map} functionCallMap - 函数调用关系映射 * @returns {string|null} 调用者方法名 */ findMethodCaller(methodName, functionCallMap) { const funcInfo = functionCallMap.get(methodName) if (funcInfo && funcInfo.callers && funcInfo.callers.length > 0) { // 返回第一个调用者 return funcInfo.callers[0] } return null } /** * 检查方法名是否是生命周期钩子 * @param {string} methodName - 方法名称 * @returns {boolean} 是否是生命周期钩子 */ isLifecycleHook(methodName) { const lifecycleHooks = [ 'onMounted', 'onCreated', 'onBeforeMount', 'onBeforeCreate', 'onUpdated', 'onBeforeUpdate', 'onUnmounted', 'onBeforeUnmount', 'mounted', 'created', 'beforeMount', 'beforeCreate', 'updated', 'beforeUpdate', 'unmounted', 'beforeUnmount', 'setup' ] return lifecycleHooks.includes(methodName) } /** * 生成唯一后缀 * @returns {string} 唯一后缀 */ generateUniqueSuffix() { return Math.random().toString(36).substr(2, 6) } /** * 使用精确位置修改Vue文件中的元素 * @param {string} filePath - Vue文件路径 * @param {Object} node - AST节点 * @param {string} nameValue - name属性值 * @param {string} triggerType - 触发类型 */ modifyVueElementWithCompiler(filePath, node, nameValue, triggerType) { try { // 检查文件是否已经被处理过,避免重复处理 const fileKey = `${filePath}-${nameValue}` if (this.processedFiles.has(fileKey)) { return } const content = readFileSync(filePath, 'utf-8') // 获取节点的位置信息 const loc = node.loc if (!loc) { return } const startLine = loc.start.line const startColumn = loc.start.column const endLine = loc.end.line const endColumn = loc.end.column const lines = content.split('\n') // 检查是否已经存在name属性或:visible属性,避免重复修改 const targetLine = lines[startLine - 1] if (targetLine.includes(`name="`) || targetLine.includes(`name='`) || targetLine.includes(`:visible="false"`) || targetLine.includes(`:visible='false'`)) { return } // 处理单行元素 if (startLine === endLine) { this.modifySingleLineElement(lines, startLine - 1, startColumn, endColumn, nameValue, triggerType) } else { // 处理多行元素 this.modifyMultiLineElement(lines, startLine - 1, startColumn, endLine - 1, endColumn, nameValue, triggerType) } // 写回文件 const newContent = lines.join('\n') writeFileSync(filePath, newContent, 'utf-8') // 标记文件为已处理 this.processedFiles.add(fileKey) } catch (error) { // 静默处理错误 } } /** * 修改单行元素 * @param {Array} lines - 文件行数组 * @param {number} lineIndex - 行索引 * @param {number} startCol - 开始列 * @param {number} endCol - 结束列 * @param {string} nameValue - name属性值 * @param {string} triggerType - 触发类型 */ modifySingleLineElement(lines, lineIndex, startCol, endCol, nameValue, triggerType) { const line = lines[lineIndex] // 检查是否已经存在name属性或:visible属性,避免重复修改 if (line.includes(`name="`) || line.includes(`name='`) || line.includes(`:visible="false"`) || line.includes(`:visible='false'`)) { return } // 找到开始标签的结束位置 // AST列号从1开始,需要转换为从0开始的索引 const tagStart = line.indexOf('<', startCol - 1) const tagEnd = line.indexOf('>', tagStart) if (tagStart !== -1 && tagEnd !== -1) { const beforeTag = line.substring(0, tagEnd) const afterTag = line.substring(tagEnd) // 构建新的属性 let newAttributes = ` name="${nameValue}"` if (triggerType === 'button') { newAttributes += ` :visible="false"` } else { newAttributes += ` :disabled="true"` } lines[lineIndex] = `${beforeTag}${newAttributes}${afterTag}` } } /** * 修改多行元素 * @param {Array} lines - 文件行数组 * @param {number} startLineIndex - 开始行索引 * @param {number} startCol - 开始列 * @param {number} endLineIndex - 结束行索引 * @param {number} endCol - 结束列 * @param {string} nameValue - name属性值 * @param {string} triggerType - 触发类型 */ modifyMultiLineElement(lines, startLineIndex, startCol, endLineIndex, endCol, nameValue, triggerType) { const startLine = lines[startLineIndex] // 检查整个多行元素是否已经存在name属性或:visible属性,避免重复修改 let hasExistingAttributes = false for (let i = startLineIndex; i <= endLineIndex && i < lines.length; i++) { const line = lines[i] if (line.includes(`name="`) || line.includes(`name='`) || line.includes(`:visible="false"`) || line.includes(`:visible='false'`)) { hasExistingAttributes = true break } } if (hasExistingAttributes) { return } // 找到开始标签的结束位置 // AST列号从1开始,需要转换为从0开始的索引 const tagStart = startLine.indexOf('<', startCol - 1) let tagEnd = startLine.indexOf('>', tagStart) let tagEndLineIndex = startLineIndex // 如果当前行没有找到结束标签,查找后续行 if (tagStart !== -1 && tagEnd === -1) { for (let i = startLineIndex; i <= endLineIndex && i < lines.length; i++) { const line = lines[i] const endPos = line.indexOf('>') if (endPos !== -1) { tagEnd = endPos tagEndLineIndex = i break } } } if (tagStart !== -1 && tagEnd !== -1) { // 构建新的属性 let newAttributes = ` name="${nameValue}"` if (triggerType === 'button') { newAttributes += ` :visible="false"` } else { newAttributes += ` :disabled="true"` } // 在结束标签前添加属性 const endLine = lines[tagEndLineIndex] const beforeEndTag = endLine.substring(0, tagEnd) const afterEndTag = endLine.substring(tagEnd) lines[tagEndLineIndex] = `${beforeEndTag}${newAttributes}${afterEndTag}` } else { // 如果找不到结束标签,尝试在开始标签后直接添加 if (tagStart !== -1) { const beforeTag = startLine.substring(0, tagStart + 1) // 包含 < 字符 const afterTag = startLine.substring(tagStart + 1) let newAttributes = ` name="${nameValue}"` if (triggerType === 'button') { newAttributes += ` :visible="false"` } else { newAttributes += ` :disabled="true"` } lines[startLineIndex] = `${beforeTag}${newAttributes} ${afterTag}` } } } } module.exports = CallChainTracer