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.
407 lines
13 KiB
407 lines
13 KiB
const { readFileSync, existsSync, readdirSync } = require('fs') |
|
const { resolve } = require('path') |
|
const AstAnalyzer = require('./ast-analyzer') |
|
|
|
/** |
|
* 触发器分析模块 |
|
* 负责分析组件中的按钮和事件触发器 |
|
*/ |
|
class TriggerAnalyzer { |
|
constructor() { |
|
this.astAnalyzer = new AstAnalyzer() |
|
} |
|
|
|
/** |
|
* 查找API方法的触发器源 |
|
* @param {Array} apiMappings - API映射数组 |
|
* @returns {Array} 增强的API映射数组 |
|
*/ |
|
findTriggerSourcesForApiMappings(apiMappings) { |
|
const enhancedApiMappings = [] |
|
|
|
apiMappings.forEach(moduleMapping => { |
|
const enhancedModuleMapping = { |
|
...moduleMapping, |
|
apiMappings: [] |
|
} |
|
|
|
moduleMapping.apiMappings.forEach(apiMapping => { |
|
const triggerSources = this.findTriggerSourcesForApiMethod( |
|
apiMapping.serviceName, |
|
apiMapping.apiMethodName, |
|
moduleMapping.module |
|
) |
|
|
|
if (triggerSources.length > 0) { |
|
enhancedModuleMapping.apiMappings.push({ |
|
...apiMapping, |
|
triggerSources: triggerSources |
|
}) |
|
} |
|
}) |
|
|
|
if (enhancedModuleMapping.apiMappings.length > 0) { |
|
enhancedApiMappings.push(enhancedModuleMapping) |
|
} |
|
}) |
|
|
|
return enhancedApiMappings |
|
} |
|
|
|
/** |
|
* 查找API方法的触发器源 |
|
* @param {string} serviceName - 服务名称 |
|
* @param {string} apiMethodName - API方法名称 |
|
* @param {string} moduleName - 模块名称 |
|
* @returns {Array} 触发器源数组 |
|
*/ |
|
findTriggerSourcesForApiMethod(serviceName, apiMethodName, moduleName) { |
|
const triggerSources = [] |
|
|
|
// 1. 检查路由组件 |
|
const routeComponents = this.getRouteComponents(moduleName) |
|
routeComponents.forEach(route => { |
|
const triggerAnalysis = this.analyzeComponentWithAST( |
|
route.component, |
|
serviceName, |
|
apiMethodName |
|
) |
|
|
|
if (triggerAnalysis.triggerSources.length > 0) { |
|
triggerSources.push(...triggerAnalysis.triggerSources) |
|
} |
|
}) |
|
|
|
// 2. 检查模块中的所有组件 |
|
const componentsInModule = this.getModuleComponents(moduleName) |
|
componentsInModule.forEach(componentName => { |
|
const triggerAnalysis = this.analyzeComponentWithAST( |
|
componentName, |
|
serviceName, |
|
apiMethodName |
|
) |
|
|
|
if (triggerAnalysis.triggerSources.length > 0) { |
|
triggerSources.push(...triggerAnalysis.triggerSources) |
|
} |
|
}) |
|
|
|
// 去重 |
|
const uniqueTriggerSources = this.deduplicateTriggerSources(triggerSources) |
|
|
|
return uniqueTriggerSources |
|
} |
|
|
|
/** |
|
* 获取路由组件 |
|
* @param {string} moduleName - 模块名称 |
|
* @returns {Array} 路由组件数组 |
|
*/ |
|
getRouteComponents(moduleName) { |
|
const routeComponents = [] |
|
|
|
try { |
|
const routeConfigPath = resolve(__dirname, '../../src/renderer/router/index.js') |
|
if (existsSync(routeConfigPath)) { |
|
const routeContent = readFileSync(routeConfigPath, 'utf-8') |
|
|
|
// 简单的路由解析,查找模块相关的路由 |
|
const routeMatches = routeContent.match(new RegExp(`component:\\s*['"]${moduleName}[^'"]*['"]`, 'g')) |
|
if (routeMatches) { |
|
routeMatches.forEach(match => { |
|
const componentMatch = match.match(/component:\s*['"]([^'"]+)['"]/) |
|
if (componentMatch) { |
|
routeComponents.push({ |
|
component: componentMatch[1] |
|
}) |
|
} |
|
}) |
|
} |
|
} |
|
} catch (error) { |
|
// 静默处理错误 |
|
} |
|
|
|
return routeComponents |
|
} |
|
|
|
/** |
|
* 获取模块中的所有组件 |
|
* @param {string} moduleName - 模块名称 |
|
* @returns {Array} 组件名称数组 |
|
*/ |
|
getModuleComponents(moduleName) { |
|
const components = [] |
|
|
|
try { |
|
const modulePath = resolve(__dirname, '../../src/renderer/modules', moduleName) |
|
if (existsSync(modulePath)) { |
|
// 查找views目录 |
|
const viewsPath = resolve(modulePath, 'views') |
|
if (existsSync(viewsPath)) { |
|
const viewFiles = readdirSync(viewsPath).filter(file => file.endsWith('.vue')) |
|
viewFiles.forEach(file => { |
|
components.push(file.replace('.vue', '')) |
|
}) |
|
} |
|
|
|
// 查找components目录 |
|
const componentsPath = resolve(modulePath, 'components') |
|
if (existsSync(componentsPath)) { |
|
const componentFiles = readdirSync(componentsPath).filter(file => file.endsWith('.vue')) |
|
componentFiles.forEach(file => { |
|
components.push(file.replace('.vue', '')) |
|
}) |
|
} |
|
} |
|
} catch (error) { |
|
// 静默处理错误 |
|
} |
|
|
|
return components |
|
} |
|
|
|
/** |
|
* 使用AST分析组件 |
|
* @param {string} componentName - 组件名称 |
|
* @param {string} serviceName - 服务名称 |
|
* @param {string} apiMethodName - API方法名称 |
|
* @returns {Object} 触发器分析结果 |
|
*/ |
|
analyzeComponentWithAST(componentName, serviceName, apiMethodName) { |
|
const triggerSources = [] |
|
|
|
try { |
|
const filePath = this.findComponentFile(componentName) |
|
if (!filePath) { |
|
return { triggerSources: [] } |
|
} |
|
|
|
const content = readFileSync(filePath, 'utf-8') |
|
|
|
// 1. 检查组件的authType属性,如果是public则跳过 |
|
const authTypeMatch = content.match(/authType\s*[=:]\s*["']([^"']+)["']/) |
|
if (authTypeMatch && authTypeMatch[1] === 'public') { |
|
return { triggerSources: [] } |
|
} |
|
|
|
// 2. 检查组件是否包含目标API调用 |
|
if (!content.includes(`${serviceName}.${apiMethodName}`)) { |
|
return { triggerSources: [] } |
|
} |
|
|
|
// 3. 使用Babel AST分析找到调用该API的方法 |
|
const callingMethods = this.astAnalyzer.findMethodsCallingServiceWithBabel(content, serviceName, apiMethodName) |
|
|
|
if (callingMethods.length > 0) { |
|
// 4. 分析每个调用方法的触发源 |
|
const methodTriggers = this.analyzeMethodTriggerSources(content, componentName, callingMethods, filePath) |
|
triggerSources.push(...methodTriggers) |
|
} else { |
|
// 如果没有找到按钮触发器,记录页面级触发器 |
|
triggerSources.push({ |
|
component: componentName, |
|
triggerName: null, |
|
triggerType: 'page' |
|
}) |
|
} |
|
|
|
} catch (error) { |
|
// 静默处理错误 |
|
} |
|
|
|
return { triggerSources } |
|
} |
|
|
|
/** |
|
* 查找组件文件 |
|
* @param {string} componentName - 组件名称 |
|
* @returns {string|null} 组件文件路径 |
|
*/ |
|
findComponentFile(componentName) { |
|
const possiblePaths = [ |
|
resolve(__dirname, '../../src/renderer/modules', componentName, 'views', `${componentName}.vue`), |
|
resolve(__dirname, '../../src/renderer/modules', componentName, 'components', `${componentName}.vue`), |
|
resolve(__dirname, '../../src/renderer/components', `${componentName}.vue`), |
|
resolve(__dirname, '../../src/renderer/views', `${componentName}.vue`) |
|
] |
|
|
|
for (const path of possiblePaths) { |
|
if (existsSync(path)) { |
|
return path |
|
} |
|
} |
|
|
|
return null |
|
} |
|
|
|
/** |
|
* 分析方法的触发源 |
|
* @param {string} content - 组件内容 |
|
* @param {string} componentName - 组件名称 |
|
* @param {Array} methodNames - 方法名数组 |
|
* @param {string} filePath - 文件路径 |
|
* @returns {Array} 触发器源数组 |
|
*/ |
|
analyzeMethodTriggerSources(content, componentName, methodNames, filePath = null) { |
|
const triggerSources = [] |
|
|
|
// 提取模板部分 |
|
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/) |
|
if (!templateMatch) { |
|
return triggerSources |
|
} |
|
|
|
const templateContent = templateMatch[1] |
|
|
|
// 使用正则表达式查找调用这些方法的按钮 |
|
methodNames.forEach(methodName => { |
|
// 1. 查找@click="methodName"或@click="methodName(" |
|
const clickPattern = new RegExp(`@click\\s*=\\s*["']${methodName}\\s*\\([^)]*\\)["']`, 'g') |
|
const clickMatches = templateContent.match(clickPattern) |
|
|
|
if (clickMatches) { |
|
clickMatches.forEach((match, index) => { |
|
// 查找包含这个@click的button标签 |
|
const buttonMatch = this.findButtonContainingClick(templateContent, match) |
|
if (buttonMatch) { |
|
const buttonName = this.extractButtonName(buttonMatch) |
|
const triggerType = buttonName ? 'button' : 'button' |
|
|
|
triggerSources.push({ |
|
component: componentName, |
|
triggerName: buttonName || this.astAnalyzer.generateUniqueButtonName(componentName, methodName), |
|
triggerType: triggerType |
|
}) |
|
} |
|
}) |
|
} |
|
|
|
// 2. 查找@submit.prevent="methodName"的表单提交按钮 |
|
const submitPattern = new RegExp(`@submit\\.prevent\\s*=\\s*["']${methodName}["']`, 'g') |
|
const submitMatches = templateContent.match(submitPattern) |
|
|
|
if (submitMatches) { |
|
submitMatches.forEach((match, index) => { |
|
// 查找包含这个@submit.prevent的form标签 |
|
const formMatch = this.findFormContainingSubmit(templateContent, match) |
|
if (formMatch) { |
|
// 查找表单中的type="submit"按钮 |
|
const submitButtonMatch = this.findSubmitButtonInForm(formMatch) |
|
if (submitButtonMatch) { |
|
const buttonName = this.extractButtonName(submitButtonMatch) |
|
const triggerType = buttonName ? 'button' : 'button' |
|
|
|
triggerSources.push({ |
|
component: componentName, |
|
triggerName: buttonName || this.astAnalyzer.generateUniqueButtonName(componentName, methodName), |
|
triggerType: triggerType |
|
}) |
|
} else { |
|
// 如果没有找到具体的提交按钮,记录为form类型 |
|
triggerSources.push({ |
|
component: componentName, |
|
triggerName: null, |
|
triggerType: 'form' |
|
}) |
|
} |
|
} |
|
}) |
|
} |
|
}) |
|
|
|
return triggerSources |
|
} |
|
|
|
/** |
|
* 查找包含指定@click的button标签 |
|
* @param {string} templateContent - 模板内容 |
|
* @param {string} clickMatch - @click匹配 |
|
* @returns {string|null} 按钮HTML |
|
*/ |
|
findButtonContainingClick(templateContent, clickMatch) { |
|
const clickIndex = templateContent.indexOf(clickMatch) |
|
|
|
if (clickIndex === -1) return null |
|
|
|
// 从@click位置往前查找最近的<button标签 |
|
const beforeClick = templateContent.substring(0, clickIndex) |
|
const lastButtonIndex = beforeClick.lastIndexOf('<button') |
|
|
|
if (lastButtonIndex === -1) return null |
|
|
|
// 从<button开始往后查找对应的</button> |
|
const afterButton = templateContent.substring(lastButtonIndex) |
|
const buttonEndIndex = afterButton.indexOf('</button>') |
|
|
|
if (buttonEndIndex === -1) return null |
|
|
|
return afterButton.substring(0, buttonEndIndex + 9) // 包含</button> |
|
} |
|
|
|
/** |
|
* 提取按钮名称 |
|
* @param {string} buttonHtml - 按钮HTML |
|
* @returns {string|null} 按钮名称 |
|
*/ |
|
extractButtonName(buttonHtml) { |
|
const nameMatch = buttonHtml.match(/name\s*=\s*["']([^"']+)["']/) |
|
return nameMatch ? nameMatch[1] : null |
|
} |
|
|
|
/** |
|
* 查找包含指定@submit.prevent的form标签 |
|
* @param {string} templateContent - 模板内容 |
|
* @param {string} submitMatch - @submit.prevent匹配 |
|
* @returns {string|null} 表单HTML |
|
*/ |
|
findFormContainingSubmit(templateContent, submitMatch) { |
|
const submitIndex = templateContent.indexOf(submitMatch) |
|
|
|
if (submitIndex === -1) return null |
|
|
|
// 从@submit.prevent位置往前查找最近的<form标签 |
|
const beforeSubmit = templateContent.substring(0, submitIndex) |
|
const lastFormIndex = beforeSubmit.lastIndexOf('<form') |
|
|
|
if (lastFormIndex === -1) return null |
|
|
|
// 从<form开始往后查找对应的</form> |
|
const afterForm = templateContent.substring(lastFormIndex) |
|
const formEndIndex = afterForm.indexOf('</form>') |
|
|
|
if (formEndIndex === -1) return null |
|
|
|
return afterForm.substring(0, formEndIndex + 7) // 包含</form> |
|
} |
|
|
|
/** |
|
* 查找表单中的type="submit"按钮 |
|
* @param {string} formHtml - 表单HTML |
|
* @returns {string|null} 提交按钮HTML |
|
*/ |
|
findSubmitButtonInForm(formHtml) { |
|
const submitButtonPattern = /<button[^>]*type\s*=\s*["']submit["'][^>]*>[\s\S]*?<\/button>/g |
|
const match = submitButtonPattern.exec(formHtml) |
|
return match ? match[0] : null |
|
} |
|
|
|
/** |
|
* 去重触发器源 |
|
* @param {Array} triggerSources - 触发器源数组 |
|
* @returns {Array} 去重后的触发器源数组 |
|
*/ |
|
deduplicateTriggerSources(triggerSources) { |
|
const seen = new Set() |
|
return triggerSources.filter(trigger => { |
|
const key = `${trigger.component}-${trigger.triggerName}-${trigger.triggerType}` |
|
if (seen.has(key)) { |
|
return false |
|
} |
|
seen.add(key) |
|
return true |
|
}) |
|
} |
|
} |
|
|
|
module.exports = TriggerAnalyzer
|
|
|