From c40f4eda3c570248a54f482d89e8d57a6403fb1d Mon Sep 17 00:00:00 2001 From: tangminghuan Date: Tue, 19 Aug 2025 10:08:26 +0800 Subject: [PATCH] feat(migration): export merged ruleMap to debug file (observability only) Closes #ICTKHI Signed-off-by: tangminghuan --- ets2panda/linter/src/cli/CommandLineParser.ts | 17 +- ets2panda/linter/src/cli/LinterCLI.ts | 125 ++++++++- .../linter/src/lib/CommandLineOptions.ts | 2 + ets2panda/linter/src/lib/HomeCheck.ts | 256 +++++++++++++++++- 4 files changed, 385 insertions(+), 15 deletions(-) diff --git a/ets2panda/linter/src/cli/CommandLineParser.ts b/ets2panda/linter/src/cli/CommandLineParser.ts index 5da3f81929..8c82727bb5 100644 --- a/ets2panda/linter/src/cli/CommandLineParser.ts +++ b/ets2panda/linter/src/cli/CommandLineParser.ts @@ -45,7 +45,16 @@ interface ParsedCommand { opts: OptionValues; args: ProcessedArguments; } - +//test +function formHomecheckRulesOptions(cmdOptions: CommandLineOptions, commanderOpts: OptionValues): void { + if (commanderOpts.homecheckRuleConfig) { + cmdOptions.homecheckRuleConfigPath = path.normalize(commanderOpts.homecheckRuleConfig); + } + if (commanderOpts.homecheckRuleBundles) { + cmdOptions.homecheckRuleBundlesPath = path.normalize(commanderOpts.homecheckRuleBundles); + } +} +//test const getFiles = (dir: string): string[] => { const resultFiles: string[] = []; if (dir.includes(ARKTS_IGNORE_DIRS_OH_MODULES)) { @@ -202,6 +211,8 @@ function formCommandLineOptions(parsedCmd: ParsedCommand): CommandLineOptions { formSdkOptions(opts, options); formMigrateOptions(opts, options); formArkts2Options(opts, options); + formHomecheckRulesOptions(opts, options); // ← 新增这一行 + return opts; } @@ -286,6 +297,10 @@ function createCommand(): Command { option('--migrate', 'run as ArkTS migrator'). option('--skip-linter', 'skip linter rule validation and autofix'). option('--homecheck', 'added homecheck rule validation'). + //test + option('--homecheck-rule-config ', 'Path to HomeCheck rule config (JSON/JSONC)'). + option('--homecheck-rule-bundles ', 'Path to rulebundles mapping JSON'). + //test option('--no-migration-backup-file', 'Disable the backup files in migration mode'). option('--migration-max-pass ', 'Maximum number of migration passes'). option('--migration-report', 'Generate migration report'). diff --git a/ets2panda/linter/src/cli/LinterCLI.ts b/ets2panda/linter/src/cli/LinterCLI.ts index b0851047c3..ba45f53218 100644 --- a/ets2panda/linter/src/cli/LinterCLI.ts +++ b/ets2panda/linter/src/cli/LinterCLI.ts @@ -61,35 +61,148 @@ export function run(): void { } } +// async function runIdeInteractiveMode(cmdOptions: CommandLineOptions): Promise { +// cmdOptions.followSdkSettings = true; +// cmdOptions.disableStrictDiagnostics = true; +// const timeRecorder = new TimeRecorder(); +// const scanTaskRelatedInfo = {} as ScanTaskRelatedInfo; +// const compileOptions = compileLintOptions(cmdOptions); +// scanTaskRelatedInfo.cmdOptions = cmdOptions; +// scanTaskRelatedInfo.timeRecorder = timeRecorder; +// scanTaskRelatedInfo.compileOptions = compileOptions; +// await executeScanTask(scanTaskRelatedInfo); + +// const statisticsReportInPutInfo = scanTaskRelatedInfo.statisticsReportInPutInfo; +// statisticsReportInPutInfo.statisticsReportName = 'scan-problems-statistics.json'; +// statisticsReportInPutInfo.totalProblemNumbers = getTotalProblemNumbers(scanTaskRelatedInfo.mergedProblems); +// statisticsReportInPutInfo.cmdOptions = cmdOptions; +// statisticsReportInPutInfo.timeRecorder = timeRecorder; + +// if (!cmdOptions.linterOptions.migratorMode && statisticsReportInPutInfo.cmdOptions.linterOptions.projectFolderList) { +// await statistic.generateScanProbelemStatisticsReport(statisticsReportInPutInfo); +// } + + +// // 从 HomeCheck.ts 中已经塞进来的禁用子规则列表(full 形态:@migration/xxx-yyy) +// const disabledLeavesArr: string[] = (cmdOptions as any).__disabledHomecheckLeaves || []; +// const disabledFull = new Set(disabledLeavesArr); // 例:@migration/arkts-xxx-yyy +// const disabledShort = new Set(disabledLeavesArr.map(id => id.split('/').pop()!)); // 例:arkts-xxx-yyy + +// // 提取单条问题的规则标识(尽量拿到 fullId;拿不到则回退到短码) +// function shouldKeepProblem(p: any /* ProblemInfo */): boolean { +// const desc: string = p.rule ?? ''; + +// // 短码:描述中常见 "(arkts-xxx-yyy)" +// const mShort = desc.match(/\(([a-z0-9-]+)\)/i); +// const short = mShort?.[1]; + +// // fullId 优先:autofixTitle(有些命中会带 ruleId),否则尝试从描述里直接截取 +// let full: string | undefined = typeof p.autofixTitle === 'string' ? p.autofixTitle : undefined; +// if (!full) { +// const mFull = desc.match(/@migration\/[a-z0-9-]+/i); +// full = mFull?.[0]; +// } + +// // 命中禁用子规则则剔除 +// if (short && disabledShort.has(short)) return false; +// if (full && disabledFull.has(full)) return false; + +// // 兜底:描述里如果包含任何禁用 fullId,也剔除 +// for (const f of disabledFull) { +// if (desc.includes(f)) return false; +// } +// return true; +// } + +// const mergedProblems = scanTaskRelatedInfo.mergedProblems; +// const filtered = new Map(); + +// for (const [file, probs] of mergedProblems) { +// const keep = probs.filter(shouldKeepProblem); +// if (keep.length) filtered.set(file, keep); +// } + +// // 后续输出用 filtered +// const reportData = Object.fromEntries(filtered); +// const reportName: string = 'scan-report.json'; +// await statistic.generateReportFile(reportName, reportData, cmdOptions.outputFilePath); + +// for (const [filePath, problems] of filtered) { +// const reportLine = JSON.stringify({ filePath, problems }) + '\n'; +// await processSyncOut(reportLine); +// } + + +// await processSyncErr('{"content":"report finish","messageType":1,"indictor":1}\n'); +// process.exit(0); +// } async function runIdeInteractiveMode(cmdOptions: CommandLineOptions): Promise { cmdOptions.followSdkSettings = true; cmdOptions.disableStrictDiagnostics = true; + const timeRecorder = new TimeRecorder(); const scanTaskRelatedInfo = {} as ScanTaskRelatedInfo; const compileOptions = compileLintOptions(cmdOptions); + scanTaskRelatedInfo.cmdOptions = cmdOptions; scanTaskRelatedInfo.timeRecorder = timeRecorder; scanTaskRelatedInfo.compileOptions = compileOptions; + await executeScanTask(scanTaskRelatedInfo); + // ============== 最终过滤(仅依据描述里的短码) ============== + const mergedProblems = scanTaskRelatedInfo.mergedProblems; + + // 从 HomeCheck 阶段塞进来的“被禁用子规则(值为0)” + const disabledLeavesArr: string[] = (cmdOptions as any).__disabledHomecheckLeaves || []; + + // 统一为短码集合:@migration/xxx-yyy -> xxx-yyy(小写) + const disabledShort = new Set( + disabledLeavesArr + .map(id => id.split('/').pop()) + .filter(Boolean) + .map(s => (s as string).toLowerCase()) + ); + + // 从描述里提取 (xxx-yyy) 的短码 + const pickShortFromDesc = (desc?: string): string | null => { + if (!desc) return null; + const m = desc.match(/\(([a-z0-9-]+)\)/i); + return m ? m[1].toLowerCase() : null; + }; + + // 依据短码过滤;提取不到短码则保留,避免误删 + const filtered = new Map(); + for (const [file, probs] of mergedProblems) { + const keep = probs.filter((p: ProblemInfo) => { + const short = pickShortFromDesc(p.rule); + if (!short) return true; + return !disabledShort.has(short); + }); + if (keep.length) filtered.set(file, keep); + } + + // ============== 统计与输出都使用过滤后的结果 ============== const statisticsReportInPutInfo = scanTaskRelatedInfo.statisticsReportInPutInfo; statisticsReportInPutInfo.statisticsReportName = 'scan-problems-statistics.json'; - statisticsReportInPutInfo.totalProblemNumbers = getTotalProblemNumbers(scanTaskRelatedInfo.mergedProblems); + statisticsReportInPutInfo.totalProblemNumbers = getTotalProblemNumbers(filtered); statisticsReportInPutInfo.cmdOptions = cmdOptions; statisticsReportInPutInfo.timeRecorder = timeRecorder; - if (!cmdOptions.linterOptions.migratorMode && statisticsReportInPutInfo.cmdOptions.linterOptions.projectFolderList) { + if (!cmdOptions.linterOptions.migratorMode && + statisticsReportInPutInfo.cmdOptions.linterOptions.projectFolderList) { await statistic.generateScanProbelemStatisticsReport(statisticsReportInPutInfo); } - const mergedProblems = scanTaskRelatedInfo.mergedProblems; - const reportData = Object.fromEntries(mergedProblems); - const reportName: string = 'scan-report.json'; + const reportData = Object.fromEntries(filtered); + const reportName = 'scan-report.json'; await statistic.generateReportFile(reportName, reportData, cmdOptions.outputFilePath); - for (const [filePath, problems] of mergedProblems) { + + for (const [filePath, problems] of filtered) { const reportLine = JSON.stringify({ filePath, problems }) + '\n'; await processSyncOut(reportLine); } + await processSyncErr('{"content":"report finish","messageType":1,"indictor":1}\n'); process.exit(0); } diff --git a/ets2panda/linter/src/lib/CommandLineOptions.ts b/ets2panda/linter/src/lib/CommandLineOptions.ts index 21ab3de973..de31c5db50 100644 --- a/ets2panda/linter/src/lib/CommandLineOptions.ts +++ b/ets2panda/linter/src/lib/CommandLineOptions.ts @@ -35,4 +35,6 @@ export interface CommandLineOptions { scanWholeProjectInHomecheck?: boolean; ruleConfig?: string; autofixCheck?: boolean; + homecheckRuleConfigPath?: string; // 用户传入的启用规则清单(JSON/JSONC) + homecheckRuleBundlesPath?: string; // 一对多映射(rulebundles.json),可选 } diff --git a/ets2panda/linter/src/lib/HomeCheck.ts b/ets2panda/linter/src/lib/HomeCheck.ts index 6ad5e01ad9..e59a6e5060 100644 --- a/ets2panda/linter/src/lib/HomeCheck.ts +++ b/ets2panda/linter/src/lib/HomeCheck.ts @@ -13,6 +13,69 @@ * limitations under the License. */ +// import * as path from 'node:path'; +// import type { FileIssues, RuleFix } from 'homecheck'; +// import type { CommandLineOptions } from './CommandLineOptions'; +// import type { ProblemInfo } from './ProblemInfo'; +// import { FaultID } from './Problems'; +// import { shouldProcessFile } from './LinterRunner'; + +// interface RuleConfigInfo { +// ruleSet: string[]; +// } + +// interface ProjectConfigInfo { +// projectName: string | undefined; +// projectPath: string | undefined; +// logPath: string; +// arkCheckPath: string; +// ohosSdkPath: string; +// hmsSdkPath: string; +// reportDir: string; +// languageTags: Map; +// fileOrFolderToCheck: string[]; +// } + +// export function getHomeCheckConfigInfo(cmdOptions: CommandLineOptions): { +// ruleConfigInfo: RuleConfigInfo; +// projectConfigInfo: ProjectConfigInfo; +// } { +// let inputFiles = cmdOptions.inputFiles; +// let fliesTocheck: string[] = inputFiles; +// if (cmdOptions.scanWholeProjectInHomecheck === true) { +// fliesTocheck = []; +// } +// inputFiles = inputFiles.filter((input) => { +// return shouldProcessFile(cmdOptions, input); +// }); +// const languageTags = new Map(); +// inputFiles.forEach((file) => { +// languageTags.set(path.normalize(file), 2); +// }); +// const ruleConfigInfo = { +// rules:{"@migration/arkts-obj-literal-generate-class-instance":1,"@migration/arkts-no-ts-like-as":1,"@migration/arkui-data-observation":1}, +// //ruleSet: ['plugin:@migration/all'], +// ruleSet: [''], +// files: ['**/*.ets', '**/*.ts', '**/*.js'] +// }; +// const projectConfigInfo = { +// projectName: cmdOptions.arktsWholeProjectPath, +// projectPath: cmdOptions.arktsWholeProjectPath, +// logPath: cmdOptions.outputFilePath ? path.join(cmdOptions.outputFilePath, 'HomeCheck.log') : './HomeCheck.log', +// arkCheckPath: './node_modules/homecheck', +// ohosSdkPath: cmdOptions.sdkDefaultApiPath ? cmdOptions.sdkDefaultApiPath : '', +// hmsSdkPath: cmdOptions.sdkExternalApiPath ? cmdOptions.sdkExternalApiPath[0] : '', +// reportDir: './', +// languageTags: languageTags, +// fileOrFolderToCheck: fliesTocheck, +// logLevel: cmdOptions.verbose ? 'DEBUG' : 'INFO', +// arkAnalyzerLogLevel: cmdOptions.verbose ? 'DEBUG' : 'ERROR' +// }; +// return { ruleConfigInfo, projectConfigInfo }; +// } + +import { Logger } from './Logger'; +import * as fs from 'node:fs'; import * as path from 'node:path'; import type { FileIssues, RuleFix } from 'homecheck'; import type { CommandLineOptions } from './CommandLineOptions'; @@ -20,10 +83,124 @@ import type { ProblemInfo } from './ProblemInfo'; import { FaultID } from './Problems'; import { shouldProcessFile } from './LinterRunner'; +// ========== 新增:类型补全 ========== interface RuleConfigInfo { - ruleSet: string[]; + ruleSet?: string[]; + rules?: Record; + files: string[]; +} + +// ========== 新增:读 JSON/JSONC(去注释) ========== +function stripJsonComments(input: string): string { + // 简单处理 // 与 /* */ 注释,够用即可(如需更强可换 json5/jsonc-parser) + return input + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|\s)\/\/.*$/gm, ''); +} + +function readJsonc(filePath: string): any { + const raw = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(stripJsonComments(raw)); +} + +// ========== 新增:加载 rulebundles(母规则 -> 叶子规则列表) ========== +function loadRuleBundles(bundlesPath?: string): Record { + if (!bundlesPath) return {}; + const abs = path.isAbsolute(bundlesPath) ? bundlesPath : path.resolve(process.cwd(), bundlesPath); + if (!fs.existsSync(abs)) { + throw new Error(`rulebundles file not found: ${abs}`); + } + const obj = readJsonc(abs); + + // 兼容两种形态: + // A) 你现在这份:{ schemaVersion, bundles: [{driver, children[]}, ...] } + // B) 扁平映射:{ "": ["", ""], ... } + if (Array.isArray(obj?.bundles)) { + const map: Record = {}; + for (const b of obj.bundles) { + if (typeof b?.driver === 'string' && Array.isArray(b?.children)) { + map[b.driver] = b.children.filter((x: any) => typeof x === 'string'); + } + } + return map; + } + + // B 形态:直接返回 + return obj as Record; +} + +function collectDisabledLeaves(userCfgPath: string): Set { + const abs = path.isAbsolute(userCfgPath) ? userCfgPath : path.resolve(process.cwd(), userCfgPath); + const cfg = readJsonc(abs); + const block: Record = + (cfg['plugin:@migration/all'] as any) ?? (cfg['rules'] as any) ?? cfg; + + const disabled = new Set(); + for (const [rule, val] of Object.entries(block)) { + if (Number(val) === 0) disabled.add(rule); + } + return disabled; } +// ========== 新增:从用户 JSON 里抽取“开启的叶子规则” ========== +function collectEnabledLeaves(userCfgPath: string): Set { + const abs = path.isAbsolute(userCfgPath) ? userCfgPath : path.resolve(process.cwd(), userCfgPath); + if (!fs.existsSync(abs)) { + throw new Error(`homecheck-rule-config not found: ${abs}`); + } + const cfg = readJsonc(abs); + + // 兼容两种形态: + // 1) { "plugin:@migration/all": { "": 0/1, ... } } + // 2) { "rules": { "": 0/1, ... } } // 你前面尝试过的形态 + const block: Record = + cfg['plugin:@migration/all'] ?? + cfg['rules'] ?? + cfg; + + const enabled = new Set(); + for (const [rule, v] of Object.entries(block as Record)) { + const on = (v === 1) || (v === true); // 显式只接受 1/true + if (on) enabled.add(rule); +} + + return enabled; +} + +// ========== 新增:根据“开启的叶子规则”+ bundles 计算最终要喂给 HomeCheck 的 rules ========== +function buildRulesForHomeCheck( + enabledLeaves: Set, + bundles: Record +): Record { + const rules: Record = {}; + // 1) 计算所有 bundle 子规则的全集,用来区分“一对一” vs “一对多(bundle 的叶子)” + const allBundleChildren = new Set(); + for (const children of Object.values(bundles)) { + for (const c of children) allBundleChildren.add(c); + } + + // 2) 允许用户直接显式打开“母规则”(少见但兼容) + const allMothers = new Set(Object.keys(bundles)); + + // 3) 先把“一对一(非 bundle 子规则)”直接写入 rules + for (const leaf of enabledLeaves) { + if (!allBundleChildren.has(leaf) && !allMothers.has(leaf)) { + // 既不是某个 bundle 的子规则,也不是母规则名 => 一对一,直接开 + rules[leaf] = 1; + } + } + + // 4) 对于每个 bundle:如果它的任意子规则被用户打开,打开“母规则” + for (const [mother, children] of Object.entries(bundles)) { + if (children.some((c) => enabledLeaves.has(c)) || enabledLeaves.has(mother)) { + rules[mother] = 1; + } + } + + return rules; +} + +// ========== 你已有:ProjectConfigInfo ========== interface ProjectConfigInfo { projectName: string | undefined; projectPath: string | undefined; @@ -34,6 +211,8 @@ interface ProjectConfigInfo { reportDir: string; languageTags: Map; fileOrFolderToCheck: string[]; + logLevel?: 'DEBUG' | 'INFO' | 'ERROR'; + arkAnalyzerLogLevel?: 'DEBUG' | 'ERROR'; } export function getHomeCheckConfigInfo(cmdOptions: CommandLineOptions): { @@ -48,15 +227,75 @@ export function getHomeCheckConfigInfo(cmdOptions: CommandLineOptions): { inputFiles = inputFiles.filter((input) => { return shouldProcessFile(cmdOptions, input); }); + const languageTags = new Map(); inputFiles.forEach((file) => { languageTags.set(path.normalize(file), 2); }); - const ruleConfigInfo = { - ruleSet: ['plugin:@migration/all'], - files: ['**/*.ets', '**/*.ts', '**/*.js'] - }; - const projectConfigInfo = { + + // === 关键:根据用户是否传入 homecheck-rule-config 切换两种路径 === + let ruleConfigInfo: RuleConfigInfo; + + if (cmdOptions.homecheckRuleConfigPath) { + // 1) 计算启用的叶子规则 + 一对多母规则 + const enabledLeaves = collectEnabledLeaves(cmdOptions.homecheckRuleConfigPath); + const disabledLeaves = collectDisabledLeaves(cmdOptions.homecheckRuleConfigPath); + const bundles = cmdOptions.homecheckRuleBundlesPath ? loadRuleBundles(cmdOptions.homecheckRuleBundlesPath) : {}; + const rules = buildRulesForHomeCheck(enabledLeaves, bundles); + + // 2) 只保留合法的 @migration/*,并把值统一成 1 + const sanitizedRules: Record = {}; + for (const [k, v] of Object.entries(rules)) { + if ((v === 1 ) && /^@migration\//.test(k)) { + sanitizedRules[k] = 1; + } + } + + // 3) 最终交给 HomeCheck 的配置(注意:不传 ruleSet) + ruleConfigInfo = { + rules: sanitizedRules, + files: ['**/*.ets', '**/*.ts', '**/*.js'] + }; + + // // 4) 输出到文件,便于观测 + // const dumpDir = cmdOptions.outputFilePath ? path.resolve(cmdOptions.outputFilePath) : process.cwd(); + // const dumpPath = path.join(dumpDir, 'effective-homecheck-config.json'); + // const debugPath = path.join(dumpDir, 'effective-homecheck-debug.json'); + + // try { + // fs.mkdirSync(dumpDir, { recursive: true }); + + // // 仅最终喂给 HomeCheck 的内容 + // fs.writeFileSync(dumpPath, JSON.stringify(ruleConfigInfo, null, 2), 'utf8'); + + // // 调试辅助:把启用的叶子&触发的母规则也一起记录 + // const triggeredMothers = Object.entries(bundles) + // .filter(([m, leaves]) => leaves.some(l => enabledLeaves.has(l))) + // .map(([m]) => m); + // const debugDump = { + // enabledLeaves: Array.from(enabledLeaves).sort(), + // triggeredMotherRules: triggeredMothers.sort(), + // rules: sanitizedRules + // }; + // fs.writeFileSync(debugPath, JSON.stringify(debugDump, null, 2), 'utf8'); + + // Logger.info(`[homecheck.ruleConfigInfo] dumped to: ${dumpPath}`); + // Logger.info(`[homecheck.ruleConfigInfo] debug info dumped to: ${debugPath}`); + // } catch (e: any) { + // Logger.error(`[homecheck.ruleConfigInfo] failed to write dump files: ${e?.message ?? e}`); + // } + (cmdOptions as any).__disabledHomecheckLeaves = Array.from(disabledLeaves); + (cmdOptions as any).__enabledHomecheckLeaves = Array.from(enabledLeaves); + } else { + // 没有传入自定义配置:用全量规则集 + ruleConfigInfo = { + ruleSet: ['plugin:@migration/all'], + files: ['**/*.ets', '**/*.ts', '**/*.js'] + }; + } + + + const projectConfigInfo: ProjectConfigInfo = { projectName: cmdOptions.arktsWholeProjectPath, projectPath: cmdOptions.arktsWholeProjectPath, logPath: cmdOptions.outputFilePath ? path.join(cmdOptions.outputFilePath, 'HomeCheck.log') : './HomeCheck.log', @@ -64,11 +303,12 @@ export function getHomeCheckConfigInfo(cmdOptions: CommandLineOptions): { ohosSdkPath: cmdOptions.sdkDefaultApiPath ? cmdOptions.sdkDefaultApiPath : '', hmsSdkPath: cmdOptions.sdkExternalApiPath ? cmdOptions.sdkExternalApiPath[0] : '', reportDir: './', - languageTags: languageTags, + languageTags, fileOrFolderToCheck: fliesTocheck, logLevel: cmdOptions.verbose ? 'DEBUG' : 'INFO', arkAnalyzerLogLevel: cmdOptions.verbose ? 'DEBUG' : 'ERROR' }; + return { ruleConfigInfo, projectConfigInfo }; } @@ -108,4 +348,4 @@ export function transferIssues2ProblemInfo(fileIssuesArray: FileIssues[]): Map