From c5584a3c953f14031fd4061009e08bca8a7d3365 Mon Sep 17 00:00:00 2001 From: tangminghuan Date: Mon, 25 Aug 2025 11:34:30 +0800 Subject: [PATCH] Refactor the method for configuring rules. Issues: https://gitee.com/openharmony/arkcompiler_ets_frontend/issues/ICUWHV Signed-off-by: HuSenlin --- ets2panda/linter/config/rule-bundles.json | 63 ++++++++ .../linter/{ => config}/rule-config.json | 0 ets2panda/linter/config/user-rules.json | 43 ++++++ ets2panda/linter/package.json | 2 +- ets2panda/linter/src/cli/LinterCLI.ts | 24 +++ ets2panda/linter/src/lib/HomeCheck.ts | 145 ++++++++++++++++-- .../utils/functions/ConfiguredRulesProcess.ts | 2 +- 7 files changed, 264 insertions(+), 15 deletions(-) create mode 100644 ets2panda/linter/config/rule-bundles.json rename ets2panda/linter/{ => config}/rule-config.json (100%) create mode 100644 ets2panda/linter/config/user-rules.json diff --git a/ets2panda/linter/config/rule-bundles.json b/ets2panda/linter/config/rule-bundles.json new file mode 100644 index 0000000000..294eea0579 --- /dev/null +++ b/ets2panda/linter/config/rule-bundles.json @@ -0,0 +1,63 @@ +{ + "schemaVersion": 1, + "bundles": [ + { + "id": "interop-backward-dfa", + "driver": "@migration/interop-backward-dfa", + "description": "跨 ArkTS/TS/JS 的 Object/Reflect 等内置方法在 1.1/1.2 场景互操作的 DFA 规则集合", + "children": [ + "@migration/arkts-interop-d2s-dynamic-object-on-static-instance", + "@migration/arkts-interop-d2s-dynamic-reflect-on-static-instance", + "@migration/arkts-interop-d2s-static-object-on-dynamic-instance", + "@migration/arkts-interop-d2s-static-reflect-on-dynamic-instance", + "@migration/arkts-interop-s2d-dynamic-object-on-static-instance", + "@migration/arkts-interop-s2d-dynamic-reflect-on-static-instance", + "@migration/arkts-interop-s2d-static-object-on-dynamic-instance", + "@migration/arkts-interop-s2d-static-reflect-on-dynamic-instance", + "@migration/arkts-interop-ts2s-ts-object-on-static-instance", + "@migration/arkts-interop-ts2s-ts-reflect-on-static-instance", + "@migration/arkts-interop-js2s-js-object-on-static-instance", + "@migration/arkts-interop-js2s-js-reflect-on-static-instance" + ] + }, + { + "id": "interop-dynamic-object-literals", + "driver": "@migration/interop-dynamic-object-literals", + "description": "跨语言对象字面量互操作", + "children": [ + "@migration/arkts-interop-d2s-object-literal", + "@migration/arkts-interop-ts2s-object-literal" + ] + }, + { + "id": "arkts-instance-method-bind-this", + "driver": "@migration/arkts-instance-method-bind-this", + "description": "", + "children": [ + "@migration/arkts-instance-method-bind-this", + "@migration/arkui-buildparam-passing" + ] + }, + { + "id": "interop-boxed-type-check", + "driver": "@migration/interop-boxed-type-check", + "description": "跨语言装箱类型互操作检查", + "children": [ + "@migration/arkts-interop-s2d-boxed-type", + "@migration/arkts-interop-d2s-boxed-type", + "@migration/arkts-interop-ts2s-boxed-type", + "@migration/arkts-interop-js2s-boxed-type" + ] + }, + { + "id": "numeric-semantics", + "driver": "@migration/arkts-numeric-semantic", + "description": "数值/索引语义相关(通常无单独 driver)", + "children": [ + "@migration/arkts-numeric-semantic", + "@migration/sdk-api-num2int", + "@migration/arkts-array-index-expr-type" + ] + } + ] +} diff --git a/ets2panda/linter/rule-config.json b/ets2panda/linter/config/rule-config.json similarity index 100% rename from ets2panda/linter/rule-config.json rename to ets2panda/linter/config/rule-config.json diff --git a/ets2panda/linter/config/user-rules.json b/ets2panda/linter/config/user-rules.json new file mode 100644 index 0000000000..0027b7bb4c --- /dev/null +++ b/ets2panda/linter/config/user-rules.json @@ -0,0 +1,43 @@ +{ + "rules": { + "@migration/arkts-obj-literal-generate-class-instance": 1, + "@migration/arkts-no-ts-like-as": 1, + "@migration/arkui-data-observation": 1, + "@migration/arkui-stateful-appstorage": 1, + "@migration/arkui-no-update-in-build": 1, + "@migration/arkui-custombuilder-passing": 1, + "@migration/no-method-overriding-field-check": 1, + "@migration/interop-assign": 1, + "@migration/interop-js-modify-property": 1, + "@migration/arkts-interop-s2d-object-literal": 1, + "@migration/arkts-interop-s2d-dynamic-call-builtin-api-not-in-static": 1, + + "@migration/arkts-instance-method-bind-this": 1, + "@migration/arkui-buildparam-passing": 1, + + "@migration/arkts-interop-d2s-dynamic-object-on-static-instance": 1, + "@migration/arkts-interop-d2s-dynamic-reflect-on-static-instance": 1, + "@migration/arkts-interop-d2s-static-object-on-dynamic-instance": 1, + "@migration/arkts-interop-d2s-static-reflect-on-dynamic-instance": 1, + "@migration/arkts-interop-s2d-dynamic-object-on-static-instance": 1, + "@migration/arkts-interop-s2d-dynamic-reflect-on-static-instance": 1, + "@migration/arkts-interop-s2d-static-object-on-dynamic-instance": 1, + "@migration/arkts-interop-s2d-static-reflect-on-dynamic-instance": 1, + "@migration/arkts-interop-ts2s-ts-object-on-static-instance": 1, + "@migration/arkts-interop-ts2s-ts-reflect-on-static-instance": 1, + "@migration/arkts-interop-js2s-js-object-on-static-instance": 1, + "@migration/arkts-interop-js2s-js-reflect-on-static-instance": 1, + + "@migration/arkts-interop-d2s-object-literal": 1, + "@migration/arkts-interop-ts2s-object-literal": 1, + + "@migration/arkts-interop-s2d-boxed-type": 1, + "@migration/arkts-interop-d2s-boxed-type": 1, + "@migration/arkts-interop-ts2s-boxed-type": 1, + "@migration/arkts-interop-js2s-boxed-type": 1, + + "@migration/arkts-numeric-semantic": 1, + "@migration/sdk-api-num2int": 1, + "@migration/arkts-array-index-expr-type": 1 + } +} diff --git a/ets2panda/linter/package.json b/ets2panda/linter/package.json index 8005b79486..8bfc9b02df 100644 --- a/ets2panda/linter/package.json +++ b/ets2panda/linter/package.json @@ -4,7 +4,7 @@ "main": "dist/tslinter.js", "bin": "bin/tslinter.js", "files": [ - "dist/*","rule-config.json","docs/rules-cn/*" + "dist/*","rule-config.json","docs/rules-cn/*","config/*" ], "private": true, "license": "Apache-2.0", diff --git a/ets2panda/linter/src/cli/LinterCLI.ts b/ets2panda/linter/src/cli/LinterCLI.ts index be7e4cdaaa..a0e1279a28 100644 --- a/ets2panda/linter/src/cli/LinterCLI.ts +++ b/ets2panda/linter/src/cli/LinterCLI.ts @@ -129,6 +129,30 @@ async function executeHomeCheckTask(scanTaskRelatedInfo: ScanTaskRelatedInfo): P const result = await migrationTool.start(); migrationTool = null; scanTaskRelatedInfo.homeCheckResult = transferIssues2ProblemInfo(result); + const enabledLeavesArr: string[] = (cmdOptions as any).__enabledHomecheckLeaves || []; + const enabledShort = new Set( + enabledLeavesArr + .map((id) => (typeof id === 'string' ? id.split('/').pop() : undefined)) + .filter(Boolean) + .map((s) => (s as string).toLowerCase()) + ); + const pickShortFromDesc = (desc?: string): string | null => { + if (!desc) return null; + const m = desc.match(/\(([a-z0-9-]+)\)/i); + return m ? m[1].toLowerCase() : null; + }; + + if (enabledShort.size > 0) { + const filteredMap = new Map(); + for (const [filePath, problems] of scanTaskRelatedInfo.homeCheckResult) { + const keep = problems.filter((p) => { + const short = pickShortFromDesc(p.rule); + return short ? enabledShort.has(short) : true; + }); + if (keep.length) filteredMap.set(filePath, keep); + } + scanTaskRelatedInfo.homeCheckResult = filteredMap; + } for (const [filePath, problems] of scanTaskRelatedInfo.homeCheckResult) { if ( !scanTaskRelatedInfo.cmdOptions.scanWholeProjectInHomecheck && diff --git a/ets2panda/linter/src/lib/HomeCheck.ts b/ets2panda/linter/src/lib/HomeCheck.ts index 6ad5e01ad9..69d5c7c8dc 100644 --- a/ets2panda/linter/src/lib/HomeCheck.ts +++ b/ets2panda/linter/src/lib/HomeCheck.ts @@ -12,16 +12,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import * as fs from 'node:fs'; 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 { Logger } from './Logger'; import { shouldProcessFile } from './LinterRunner'; interface RuleConfigInfo { - ruleSet: string[]; + ruleSet?: string[]; + rules?: Record; + files: string[]; } interface ProjectConfigInfo { @@ -34,29 +37,145 @@ interface ProjectConfigInfo { reportDir: string; languageTags: Map; fileOrFolderToCheck: string[]; + logLevel?: 'DEBUG' | 'INFO' | 'ERROR'; + arkAnalyzerLogLevel?: 'DEBUG' | 'ERROR'; +} + +const RULE_ID_RE = /^(@migration\/[a-z0-9-]+|[a-z0-9-]+)$/i; + +function stripJsonComments(input: string): string { + return input.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, ''); +} + +function readJsoncFile(filePath: string): any { + const raw = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(stripJsonComments(raw)); +} + +function loadResources(): { userRules: Record; bundles: Record } { + const userRulesPath = path.join(process.cwd(), 'config/user-rules.json'); + if (!fs.existsSync(userRulesPath)) { + throw new Error(`Critical Error: 'user-rules.json' not found at: ${userRulesPath}`); + } + const bundlesPath = path.join(process.cwd(), 'config/rule-bundles.json'); + if (!fs.existsSync(bundlesPath)) { + throw new Error(`Critical Error: 'rule-bundles.json' not found at: ${bundlesPath}`); + } + + const userRules = readJsoncFile(userRulesPath); + if (!userRules || typeof userRules !== 'object') { + throw new Error(`Error: 'user-rules.json' is malformed.`); + } + + type BundlesShape = { [mother: string]: string[] } | { bundles: Array<{ driver: string; children: string[] }> }; + const obj = readJsoncFile(bundlesPath) as BundlesShape; + let bundles: Record; + + if (Array.isArray((obj as any)?.bundles)) { + const map: Record = {}; + for (const b of (obj as any).bundles) { + if (typeof b?.driver === 'string' && Array.isArray(b?.children)) { + map[b.driver] = b.children.filter((x: any) => typeof x === 'string'); + } + } + bundles = map; + } else { + bundles = obj as Record; + } + + return { userRules, bundles }; +} + +function collectDisabledLeaves(cfg: Record): Set { + 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; +} + +function collectEnabledLeaves(cfg: Record): Set { + const enabled = new Set(); + const block = (cfg['plugin:@migration/all'] ?? cfg['rules'] ?? cfg) as Record; + for (const [rule, v] of Object.entries(block)) { + const on = (v === 1) || (v === true); + if (on) enabled.add(rule); + } + return enabled; +} + +function buildRulesForHomeCheck( + enabledLeaves: Set, + bundles: Record +): Record { + const rules: Record = {}; + + const allBundleChildren = new Set(); + for (const children of Object.values(bundles)) { + for (const c of children) allBundleChildren.add(c); + } + + const allMothers = new Set(Object.keys(bundles)); + + for (const leaf of enabledLeaves) { + if (!allBundleChildren.has(leaf) && !allMothers.has(leaf)) { + rules[leaf] = 1; + } + } + + for (const [mother, children] of Object.entries(bundles)) { + if (children.some((c) => enabledLeaves.has(c)) || enabledLeaves.has(mother)) { + rules[mother] = 1; + } + } + + return rules; } export function getHomeCheckConfigInfo(cmdOptions: CommandLineOptions): { ruleConfigInfo: RuleConfigInfo; projectConfigInfo: ProjectConfigInfo; } { - let inputFiles = cmdOptions.inputFiles; + const inputFiles = cmdOptions.inputFiles.filter((input) => shouldProcessFile(cmdOptions, input)); 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 = { - ruleSet: ['plugin:@migration/all'], + inputFiles.forEach((file) => languageTags.set(path.normalize(file), 2)); + + const { userRules, bundles } = loadResources(); + const enabledLeaves = collectEnabledLeaves(userRules); + const disabledLeaves = collectDisabledLeaves(userRules); + const rules = buildRulesForHomeCheck(enabledLeaves, bundles); + + const sanitizedRules: Record = {}; + for (const [k, v] of Object.entries(rules)) { + if ((v === 1) && /^@migration\//.test(k)) { + sanitizedRules[k] = 1; + } + } + const ruleConfigInfo: RuleConfigInfo = { + rules: sanitizedRules, files: ['**/*.ets', '**/*.ts', '**/*.js'] }; - const projectConfigInfo = { + (cmdOptions as any).__disabledHomecheckLeaves = Array.from(disabledLeaves); + (cmdOptions as any).__enabledHomecheckLeaves = Array.from(enabledLeaves); + + try { + const outDir = cmdOptions.outputFilePath ? cmdOptions.outputFilePath : process.cwd(); + fs.mkdirSync(outDir, { recursive: true }); + + const dumpPath = path.join(outDir, 'effective-homecheck-config.json'); + fs.writeFileSync(dumpPath, JSON.stringify(ruleConfigInfo, null, 2), 'utf8'); + + } catch (e: any) { + Logger.error(`[homecheck.ruleConfigInfo] failed to write dump file: ${e?.message ?? e}`); + } + + const projectConfigInfo: ProjectConfigInfo = { projectName: cmdOptions.arktsWholeProjectPath, projectPath: cmdOptions.arktsWholeProjectPath, logPath: cmdOptions.outputFilePath ? path.join(cmdOptions.outputFilePath, 'HomeCheck.log') : './HomeCheck.log', @@ -108,4 +227,4 @@ export function transferIssues2ProblemInfo(fileIssuesArray: FileIssues[]): Map