|
|
|
@ -1,7 +1,8 @@
@@ -1,7 +1,8 @@
|
|
|
|
|
const { readFileSync, existsSync, readdirSync } = require('fs') |
|
|
|
|
const { readFileSync, writeFileSync, existsSync, readdirSync } = require('fs') |
|
|
|
|
const { resolve } = require('path') |
|
|
|
|
const parser = require('@babel/parser') |
|
|
|
|
const traverse = require('@babel/traverse').default |
|
|
|
|
const { parse } = require('@vue/compiler-sfc') |
|
|
|
|
const AstAnalyzer = require('./ast-analyzer') |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -16,11 +17,15 @@ class TriggerAnalyzer {
@@ -16,11 +17,15 @@ class TriggerAnalyzer {
|
|
|
|
|
/** |
|
|
|
|
* 查找API方法的触发器源 - 第一步:第一层触发源分析 |
|
|
|
|
* @param {Array} apiMappings - API映射数组 |
|
|
|
|
* @param {Array} routes - 路由配置数组 |
|
|
|
|
* @returns {Array} 增强的API映射数组 |
|
|
|
|
*/ |
|
|
|
|
findTriggerSourcesForApiMappings(apiMappings) { |
|
|
|
|
findTriggerSourcesForApiMappings(apiMappings, routes) { |
|
|
|
|
const enhancedApiMappings = [] |
|
|
|
|
|
|
|
|
|
// 创建组件到authType的映射
|
|
|
|
|
const componentAuthMap = this.createComponentAuthMap(routes) |
|
|
|
|
|
|
|
|
|
apiMappings.forEach(moduleMapping => { |
|
|
|
|
const enhancedModuleMapping = { |
|
|
|
|
...moduleMapping, |
|
|
|
@ -35,15 +40,25 @@ class TriggerAnalyzer {
@@ -35,15 +40,25 @@ class TriggerAnalyzer {
|
|
|
|
|
moduleMapping.module |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// 为所有API映射添加triggerSources字段(即使为空)
|
|
|
|
|
enhancedModuleMapping.apiMappings.push({ |
|
|
|
|
...apiMapping, |
|
|
|
|
triggerSources: triggerSources || [] |
|
|
|
|
}) |
|
|
|
|
// 过滤掉authType为public的组件的触发源
|
|
|
|
|
const filteredTriggerSources = this.filterPublicComponentTriggers( |
|
|
|
|
triggerSources,
|
|
|
|
|
componentAuthMap |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// 只有当有触发源时才添加API映射
|
|
|
|
|
if (filteredTriggerSources.length > 0) { |
|
|
|
|
enhancedModuleMapping.apiMappings.push({ |
|
|
|
|
...apiMapping, |
|
|
|
|
triggerSources: filteredTriggerSources |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// 返回所有模块映射(包括没有触发源的)
|
|
|
|
|
enhancedApiMappings.push(enhancedModuleMapping) |
|
|
|
|
// 只有当模块有API映射时才添加模块映射
|
|
|
|
|
if (enhancedModuleMapping.apiMappings.length > 0) { |
|
|
|
|
enhancedApiMappings.push(enhancedModuleMapping) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
return enhancedApiMappings |
|
|
|
@ -453,6 +468,115 @@ class TriggerAnalyzer {
@@ -453,6 +468,115 @@ class TriggerAnalyzer {
|
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 创建组件到authType的映射 |
|
|
|
|
* @param {Array} routes - 路由配置数组 |
|
|
|
|
* @returns {Map} 组件到authType的映射 |
|
|
|
|
*/ |
|
|
|
|
createComponentAuthMap(routes) { |
|
|
|
|
const componentAuthMap = new Map() |
|
|
|
|
|
|
|
|
|
// 从Vue组件文件中读取authType属性
|
|
|
|
|
routes.forEach(route => { |
|
|
|
|
if (route.component) { |
|
|
|
|
const authType = this.getComponentAuthType(route.component) |
|
|
|
|
if (authType) { |
|
|
|
|
componentAuthMap.set(route.component, authType) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// 扫描所有模块目录中的组件文件,查找authType属性
|
|
|
|
|
const moduleDirs = ['core', 'user-management', 'role-management', 'system-settings'] |
|
|
|
|
moduleDirs.forEach(moduleDir => { |
|
|
|
|
this.scanComponentsForAuthType(moduleDir, componentAuthMap) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
return componentAuthMap |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 扫描指定模块目录中的组件文件,查找authType属性 |
|
|
|
|
* @param {string} moduleDir - 模块目录名 |
|
|
|
|
* @param {Map} componentAuthMap - 组件到authType的映射 |
|
|
|
|
*/ |
|
|
|
|
scanComponentsForAuthType(moduleDir, componentAuthMap) { |
|
|
|
|
try { |
|
|
|
|
const modulePath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}`) |
|
|
|
|
if (existsSync(modulePath)) { |
|
|
|
|
const files = readdirSync(modulePath, { recursive: true }) |
|
|
|
|
|
|
|
|
|
files.forEach(file => { |
|
|
|
|
if (typeof file === 'string' && file.endsWith('.vue')) { |
|
|
|
|
const filePath = resolve(modulePath, file) |
|
|
|
|
const componentName = this.extractComponentNameFromPath(file) |
|
|
|
|
|
|
|
|
|
// 如果还没有这个组件的authType信息,则尝试获取
|
|
|
|
|
if (!componentAuthMap.has(componentName)) { |
|
|
|
|
const authType = this.getComponentAuthType(componentName) |
|
|
|
|
if (authType) { |
|
|
|
|
componentAuthMap.set(componentName, authType) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn(`扫描模块 ${moduleDir} 的组件时出错:`, error.message) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 从Vue组件文件中获取authType属性 |
|
|
|
|
* @param {string} componentName - 组件名称 |
|
|
|
|
* @returns {string|null} authType值 |
|
|
|
|
*/ |
|
|
|
|
getComponentAuthType(componentName) { |
|
|
|
|
try { |
|
|
|
|
// 在modules目录中查找组件文件
|
|
|
|
|
const moduleDirs = ['core', 'user-management', 'role-management', 'system-settings'] |
|
|
|
|
|
|
|
|
|
for (const moduleDir of moduleDirs) { |
|
|
|
|
const componentPath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}/components/${componentName}.vue`) |
|
|
|
|
if (existsSync(componentPath)) { |
|
|
|
|
const content = readFileSync(componentPath, 'utf-8') |
|
|
|
|
const authTypeMatch = content.match(/<template\s+authType\s*=\s*["']([^"']+)["']/) |
|
|
|
|
if (authTypeMatch) { |
|
|
|
|
return authTypeMatch[1] |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 也检查views目录
|
|
|
|
|
const viewPath = resolve(__dirname, `../../src/renderer/modules/${moduleDir}/views/${componentName}.vue`) |
|
|
|
|
if (existsSync(viewPath)) { |
|
|
|
|
const content = readFileSync(viewPath, 'utf-8') |
|
|
|
|
const authTypeMatch = content.match(/<template\s+authType\s*=\s*["']([^"']+)["']/) |
|
|
|
|
if (authTypeMatch) { |
|
|
|
|
return authTypeMatch[1] |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn(`获取组件 ${componentName} 的authType时出错:`, error.message) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 过滤掉authType为public的组件的触发源 |
|
|
|
|
* @param {Array} triggerSources - 触发源数组 |
|
|
|
|
* @param {Map} componentAuthMap - 组件到authType的映射 |
|
|
|
|
* @returns {Array} 过滤后的触发源数组 |
|
|
|
|
*/ |
|
|
|
|
filterPublicComponentTriggers(triggerSources, componentAuthMap) { |
|
|
|
|
return triggerSources.filter(trigger => { |
|
|
|
|
const authType = componentAuthMap.get(trigger.component) |
|
|
|
|
// 如果组件没有定义authType,或者authType不是public,则保留
|
|
|
|
|
return !authType || authType !== 'public' |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 从文件路径提取组件名称 |
|
|
|
|
* @param {string} filePath - 文件路径 |
|
|
|
@ -481,8 +605,8 @@ class TriggerAnalyzer {
@@ -481,8 +605,8 @@ class TriggerAnalyzer {
|
|
|
|
|
const ast = this.parseVueComponent(content) |
|
|
|
|
if (!ast) return triggerSources |
|
|
|
|
|
|
|
|
|
// 查找API调用和函数调用关系
|
|
|
|
|
const apiCalls = this.findApiCallsWithContext(ast, serviceName, apiMethodName) |
|
|
|
|
// 查找API调用和函数调用关系,传递文件路径用于追溯
|
|
|
|
|
const apiCalls = this.findApiCallsWithContext(ast, serviceName, apiMethodName, filePath, componentName) |
|
|
|
|
|
|
|
|
|
// 为每个API调用创建触发源
|
|
|
|
|
apiCalls.forEach(call => { |
|
|
|
@ -531,9 +655,11 @@ class TriggerAnalyzer {
@@ -531,9 +655,11 @@ class TriggerAnalyzer {
|
|
|
|
|
* @param {Object} ast - AST对象 |
|
|
|
|
* @param {string} serviceName - 服务名称 |
|
|
|
|
* @param {string} apiMethodName - API方法名称 |
|
|
|
|
* @param {string} filePath - 组件文件路径 |
|
|
|
|
* @param {string} componentName - 组件名称 |
|
|
|
|
* @returns {Array} API调用信息数组 |
|
|
|
|
*/ |
|
|
|
|
findApiCallsWithContext(ast, serviceName, apiMethodName) { |
|
|
|
|
findApiCallsWithContext(ast, serviceName, apiMethodName, filePath, componentName) { |
|
|
|
|
const apiCalls = [] |
|
|
|
|
const self = this |
|
|
|
|
const functionCallMap = new Map() // 存储函数调用关系
|
|
|
|
@ -592,7 +718,7 @@ class TriggerAnalyzer {
@@ -592,7 +718,7 @@ class TriggerAnalyzer {
|
|
|
|
|
node.callee.property.name === apiMethodName) { |
|
|
|
|
|
|
|
|
|
// 查找调用上下文
|
|
|
|
|
const context = self.findCallContextWithChain(path, functionCallMap) |
|
|
|
|
const context = self.findCallContextWithChain(path, functionCallMap, filePath, componentName) |
|
|
|
|
|
|
|
|
|
apiCalls.push({ |
|
|
|
|
triggerName: context.triggerName, |
|
|
|
@ -638,9 +764,11 @@ class TriggerAnalyzer {
@@ -638,9 +764,11 @@ class TriggerAnalyzer {
|
|
|
|
|
* 查找API调用的上下文(包含调用链分析) |
|
|
|
|
* @param {Object} path - Babel路径对象 |
|
|
|
|
* @param {Map} functionCallMap - 函数调用关系映射 |
|
|
|
|
* @param {string} filePath - 组件文件路径 |
|
|
|
|
* @param {string} componentName - 组件名称 |
|
|
|
|
* @returns {Object} 上下文信息 |
|
|
|
|
*/ |
|
|
|
|
findCallContextWithChain(path, functionCallMap) { |
|
|
|
|
findCallContextWithChain(path, functionCallMap, filePath, componentName) { |
|
|
|
|
let currentPath = path |
|
|
|
|
let triggerName = '' |
|
|
|
|
let triggerType = 'function' |
|
|
|
@ -721,9 +849,482 @@ class TriggerAnalyzer {
@@ -721,9 +849,482 @@ class TriggerAnalyzer {
|
|
|
|
|
currentPath = currentPath.parentPath |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 如果找到的是method类型,继续向上追溯找到最终的可视组件
|
|
|
|
|
if (triggerType === 'method') { |
|
|
|
|
const finalContext = this.traceToVisualComponent(path, triggerName, functionCallMap, filePath, componentName) |
|
|
|
|
if (finalContext) { |
|
|
|
|
return finalContext |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return { triggerName, triggerType } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 追溯method类型的触发源到最终的可视组件 |
|
|
|
|
* @param {Object} path - Babel路径对象 |
|
|
|
|
* @param {string} methodName - 方法名称 |
|
|
|
|
* @param {Map} functionCallMap - 函数调用关系映射 |
|
|
|
|
* @param {string} filePath - 组件文件路径 |
|
|
|
|
* @returns {Object|null} 最终的可视组件上下文 |
|
|
|
|
*/ |
|
|
|
|
traceToVisualComponent(path, methodName, functionCallMap, filePath, componentName) { |
|
|
|
|
try { |
|
|
|
|
// 使用传入的文件路径
|
|
|
|
|
const componentPath = filePath |
|
|
|
|
if (!componentPath) return null |
|
|
|
|
|
|
|
|
|
// 读取组件文件内容
|
|
|
|
|
const content = readFileSync(componentPath, 'utf-8') |
|
|
|
|
|
|
|
|
|
// 分析模板中的事件绑定
|
|
|
|
|
const visualContext = this.analyzeTemplateForMethod(content, methodName, componentName, filePath) |
|
|
|
|
if (visualContext) { |
|
|
|
|
return visualContext |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 如果模板中没有找到,检查是否有其他方法调用了这个方法
|
|
|
|
|
const callerContext = this.findMethodCaller(methodName, functionCallMap) |
|
|
|
|
if (callerContext) { |
|
|
|
|
// 检查调用者是否是生命周期钩子
|
|
|
|
|
const callerInfo = functionCallMap.get(callerContext) |
|
|
|
|
if (callerInfo) { |
|
|
|
|
// 检查调用者是否是生命周期钩子
|
|
|
|
|
if (this.isLifecycleHook(callerContext)) { |
|
|
|
|
return { |
|
|
|
|
triggerName: '', |
|
|
|
|
triggerType: 'page' |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 继续递归追溯
|
|
|
|
|
return this.traceToVisualComponent(path, callerContext, functionCallMap, filePath, componentName) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn(`追溯可视组件时出错:`, error.message) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 检查方法名是否是生命周期钩子 |
|
|
|
|
* @param {string} methodName - 方法名称 |
|
|
|
|
* @returns {boolean} 是否是生命周期钩子 |
|
|
|
|
*/ |
|
|
|
|
isLifecycleHook(methodName) { |
|
|
|
|
const lifecycleHooks = [ |
|
|
|
|
'setup', |
|
|
|
|
'onMounted', |
|
|
|
|
'onCreated', |
|
|
|
|
'onBeforeMount', |
|
|
|
|
'onBeforeCreate', |
|
|
|
|
'onUpdated', |
|
|
|
|
'onBeforeUpdate', |
|
|
|
|
'onUnmounted', |
|
|
|
|
'onBeforeUnmount', |
|
|
|
|
'mounted', |
|
|
|
|
'created', |
|
|
|
|
'beforeMount', |
|
|
|
|
'beforeCreate', |
|
|
|
|
'updated', |
|
|
|
|
'beforeUpdate', |
|
|
|
|
'unmounted', |
|
|
|
|
'beforeUnmount', |
|
|
|
|
'watch' |
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
return lifecycleHooks.includes(methodName) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 分析模板中与指定方法相关的事件绑定 |
|
|
|
|
* @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,查找事件绑定
|
|
|
|
|
return this.findEventBindingInTemplate(templateAst, methodName, componentName, filePath) |
|
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn(`解析Vue模板时出错:`, error.message) |
|
|
|
|
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) |
|
|
|
|
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 - 方法名称 |
|
|
|
|
* @returns {Object|null} submit按钮上下文 |
|
|
|
|
*/ |
|
|
|
|
findSubmitButtonInForm(formNode, componentName, methodName) { |
|
|
|
|
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()}-${this.generateUniqueSuffix()}` |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
triggerName: triggerName, |
|
|
|
|
triggerType: 'button' |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 递归查找子元素
|
|
|
|
|
const result = this.findSubmitButtonInForm(child, componentName, methodName) |
|
|
|
|
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) { |
|
|
|
|
return this.checkAstMethodCall(exp.ast, methodName) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 : '' |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// 根据标签类型确定组件类型和名称
|
|
|
|
|
if (tag === 'button') { |
|
|
|
|
let triggerName = attributes.name |
|
|
|
|
if (!triggerName) { |
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}` |
|
|
|
|
// 自动为Vue文件添加name属性
|
|
|
|
|
this.addNameAttributeToVueFile(filePath, node, triggerName) |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
triggerName: triggerName, |
|
|
|
|
triggerType: 'button' |
|
|
|
|
} |
|
|
|
|
} else if (tag === 'form') { |
|
|
|
|
let triggerName = attributes.name |
|
|
|
|
if (!triggerName) { |
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}` |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
triggerName: triggerName, |
|
|
|
|
triggerType: 'form' |
|
|
|
|
} |
|
|
|
|
} else if (tag === 'input') { |
|
|
|
|
let triggerName = attributes.name |
|
|
|
|
if (!triggerName) { |
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}` |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
triggerName: triggerName, |
|
|
|
|
triggerType: 'input' |
|
|
|
|
} |
|
|
|
|
} else if (tag === 'a') { |
|
|
|
|
let triggerName = attributes.name |
|
|
|
|
if (!triggerName) { |
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}` |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
triggerName: triggerName, |
|
|
|
|
triggerType: 'link' |
|
|
|
|
} |
|
|
|
|
} else if (tag === 'i') { |
|
|
|
|
let triggerName = attributes.name |
|
|
|
|
if (!triggerName) { |
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}` |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
triggerName: triggerName, |
|
|
|
|
triggerType: 'icon' |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
let triggerName = attributes.name |
|
|
|
|
if (!triggerName) { |
|
|
|
|
// 生成唯一的name属性:组件名+唯一尾缀
|
|
|
|
|
triggerName = `${componentName.toLowerCase()}-${this.generateUniqueSuffix()}` |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
triggerName: triggerName, |
|
|
|
|
triggerType: 'element' |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 生成唯一的尾缀 |
|
|
|
|
* @returns {string} 唯一尾缀 |
|
|
|
|
*/ |
|
|
|
|
generateUniqueSuffix() { |
|
|
|
|
// 生成6位随机字符串,类似 "2m2snp"
|
|
|
|
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789' |
|
|
|
|
let result = '' |
|
|
|
|
for (let i = 0; i < 6; i++) { |
|
|
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length)) |
|
|
|
|
} |
|
|
|
|
return result |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 为Vue文件中的button添加name属性 |
|
|
|
|
* @param {string} filePath - Vue文件路径 |
|
|
|
|
* @param {Object} node - AST节点 |
|
|
|
|
* @param {string} nameValue - name属性值 |
|
|
|
|
*/ |
|
|
|
|
addNameAttributeToVueFile(filePath, node, nameValue) { |
|
|
|
|
try { |
|
|
|
|
// 读取Vue文件内容
|
|
|
|
|
const content = readFileSync(filePath, 'utf-8') |
|
|
|
|
|
|
|
|
|
// 获取节点的位置信息
|
|
|
|
|
const loc = node.loc |
|
|
|
|
if (!loc) { |
|
|
|
|
console.warn(`节点没有位置信息,无法添加name属性`) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 找到button标签的开始位置
|
|
|
|
|
const startLine = loc.start.line |
|
|
|
|
const startColumn = loc.start.column |
|
|
|
|
|
|
|
|
|
console.log(`尝试为第${startLine}行第${startColumn}列的button添加name属性: ${nameValue}`) |
|
|
|
|
|
|
|
|
|
// 将内容按行分割
|
|
|
|
|
const lines = content.split('\n') |
|
|
|
|
|
|
|
|
|
// 找到对应的行
|
|
|
|
|
if (startLine > 0 && startLine <= lines.length) { |
|
|
|
|
const lineIndex = startLine - 1 |
|
|
|
|
let line = lines[lineIndex] |
|
|
|
|
|
|
|
|
|
console.log(`当前行内容: ${line}`) |
|
|
|
|
|
|
|
|
|
// 在button标签中添加name属性
|
|
|
|
|
// 查找button标签的开始位置
|
|
|
|
|
const buttonStartMatch = line.match(/<button[^>]*>/) |
|
|
|
|
if (buttonStartMatch) { |
|
|
|
|
// 如果button标签在同一行结束
|
|
|
|
|
const buttonEndIndex = buttonStartMatch.index + buttonStartMatch[0].length |
|
|
|
|
const beforeButton = line.substring(0, buttonEndIndex - 1) // 去掉最后的>
|
|
|
|
|
const afterButton = line.substring(buttonEndIndex - 1) |
|
|
|
|
|
|
|
|
|
// 添加name属性
|
|
|
|
|
const newLine = `${beforeButton} name="${nameValue}">${afterButton}` |
|
|
|
|
lines[lineIndex] = newLine |
|
|
|
|
|
|
|
|
|
// 写回文件
|
|
|
|
|
const newContent = lines.join('\n') |
|
|
|
|
writeFileSync(filePath, newContent, 'utf-8') |
|
|
|
|
|
|
|
|
|
console.log(`已为 ${filePath} 中的button添加name属性: ${nameValue}`) |
|
|
|
|
} else { |
|
|
|
|
// 如果button标签跨行,查找button开始标签
|
|
|
|
|
const buttonStartMatch = line.match(/<button[^>]*$/) |
|
|
|
|
if (buttonStartMatch) { |
|
|
|
|
// 在button开始标签后添加name属性
|
|
|
|
|
const beforeButton = line.substring(0, buttonStartMatch.index + buttonStartMatch[0].length) |
|
|
|
|
const afterButton = line.substring(buttonStartMatch.index + buttonStartMatch[0].length) |
|
|
|
|
|
|
|
|
|
// 添加name属性
|
|
|
|
|
const newLine = `${beforeButton} name="${nameValue}"${afterButton}` |
|
|
|
|
lines[lineIndex] = newLine |
|
|
|
|
|
|
|
|
|
// 写回文件
|
|
|
|
|
const newContent = lines.join('\n') |
|
|
|
|
writeFileSync(filePath, newContent, 'utf-8') |
|
|
|
|
|
|
|
|
|
console.log(`已为 ${filePath} 中的跨行button添加name属性: ${nameValue}`) |
|
|
|
|
} else { |
|
|
|
|
console.warn(`未找到button标签`) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
console.warn(`行号超出范围: ${startLine}`) |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn(`为Vue文件添加name属性时出错:`, error.message) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 查找方法的调用者 |
|
|
|
|
*/ |
|
|
|
|
findMethodCaller(methodName, functionCallMap) { |
|
|
|
|
const funcInfo = functionCallMap.get(methodName) |
|
|
|
|
if (funcInfo && funcInfo.callers && funcInfo.callers.length > 0) { |
|
|
|
|
return funcInfo.callers[0] // 返回第一个调用者
|
|
|
|
|
} |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 查找API调用的上下文 |
|
|
|
|
* @param {Object} path - Babel路径对象 |
|
|
|
|