From f1c367eb5a0d21983b15de30568caa6f7fc4d117 Mon Sep 17 00:00:00 2001 From: tangminghuan Date: Fri, 22 Aug 2025 10:49:36 +0800 Subject: [PATCH] feat(linter): add copy-assets script and update resources & HomeCheck Signed-off-by: tangminghuan --- ets2panda/linter/package.json | 7 +- ets2panda/linter/resources/rulebundles.json | 63 +++++++ ets2panda/linter/resources/user-rules.json | 43 +++++ ets2panda/linter/scripts/copy-assets.js | 8 + ets2panda/linter/src/lib/HomeCheck.ts | 191 ++++++++++++++++++-- 5 files changed, 298 insertions(+), 14 deletions(-) create mode 100644 ets2panda/linter/resources/rulebundles.json create mode 100644 ets2panda/linter/resources/user-rules.json create mode 100644 ets2panda/linter/scripts/copy-assets.js diff --git a/ets2panda/linter/package.json b/ets2panda/linter/package.json index 8005b79486..92a96bee5f 100644 --- a/ets2panda/linter/package.json +++ b/ets2panda/linter/package.json @@ -4,10 +4,12 @@ "main": "dist/tslinter.js", "bin": "bin/tslinter.js", "files": [ - "dist/*","rule-config.json","docs/rules-cn/*" + "dist/**","rule-config.json","docs/rules-cn/**" ], "private": true, "license": "Apache-2.0", + + "scripts": { "tsc": "tsc", "webpack": "webpack -t node --config webpack.config.js", @@ -15,7 +17,8 @@ "clean": "rimraf build dist bundle", "compile": "npm run tsc", "postcompile": "node scripts/testRunner/post-compile.mjs", - "build": "npm run clean && npm run compile && npm run webpack && npm run pack:linter", + "copy-assets": "node scripts/copy-assets.js", + "build": "npm run clean && npm run compile && npm run webpack && npm run copy-assets && npm run pack:linter", "install-ohos-typescript": "node scripts/install-ohos-typescript-and-homecheck.mjs", "pack:linter": "rimraf bundle && mkdir bundle && npm pack && mv panda-tslinter-*.tgz bundle", "pretest": " npm run fix", diff --git a/ets2panda/linter/resources/rulebundles.json b/ets2panda/linter/resources/rulebundles.json new file mode 100644 index 0000000000..294eea0579 --- /dev/null +++ b/ets2panda/linter/resources/rulebundles.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/resources/user-rules.json b/ets2panda/linter/resources/user-rules.json new file mode 100644 index 0000000000..0027b7bb4c --- /dev/null +++ b/ets2panda/linter/resources/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/scripts/copy-assets.js b/ets2panda/linter/scripts/copy-assets.js new file mode 100644 index 0000000000..19e6b7d0d1 --- /dev/null +++ b/ets2panda/linter/scripts/copy-assets.js @@ -0,0 +1,8 @@ +const fs = require('fs-extra'); +const path = require('path'); + +const FROM = path.join(__dirname, '..', 'resources'); +const TO = path.join(__dirname, '..', 'dist', 'resources'); + +fs.copySync(FROM, TO, { overwrite: true }); +console.log('[build] copied resources → dist/resources'); \ No newline at end of file diff --git a/ets2panda/linter/src/lib/HomeCheck.ts b/ets2panda/linter/src/lib/HomeCheck.ts index 6ad5e01ad9..d83be798b8 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[]; + rules?: Record; + files: string[]; } interface ProjectConfigInfo { @@ -34,29 +37,190 @@ interface ProjectConfigInfo { reportDir: string; languageTags: Map; fileOrFolderToCheck: string[]; + logLevel?: 'DEBUG' | 'INFO' | 'ERROR'; + arkAnalyzerLogLevel?: 'DEBUG' | 'ERROR'; +} + +/* ====================== 文件读取与解析 ====================== */ + +/** 规则 ID 过滤:允许 @migration/xxx-yyy 或短码 xxx-yyy */ +const RULE_ID_RE = /^(@migration\/[a-z0-9-]+|[a-z0-9-]+)$/i; + +/** + * 读取 JSON 文件,并移除其中的单行和多行注释。 + * 这样可以处理 .jsonc 格式的文件。 + */ +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)); +} + +/** + * 从固定的 dist/resources 目录读取配置。 + */ +function loadResources(): { userRules: Record; bundles: Record } { + // 修正路径以包含 'dist' 目录,确保能找到正确位置 + const resourcesDir = path.join(__dirname, '../../package/dist/resources'); + + if (!fs.existsSync(resourcesDir)) { + throw new Error(`Critical Error: 'resources' directory not found at expected path: ${resourcesDir}. Please check your project structure.`); + } + + const userRulesPath = path.join(resourcesDir, 'user-rules.json'); + if (!fs.existsSync(userRulesPath)) { + throw new Error(`Critical Error: 'user-rules.json' not found at: ${userRulesPath}`); + } + const bundlesPath = path.join(resourcesDir, 'rulebundles.json'); + if (!fs.existsSync(bundlesPath)) { + throw new Error(`Critical Error: 'rulebundles.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(); + // 兼容多种配置形态,并只接受 1 或 true + 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; } +/** + * 根据“开启的叶子规则”+ bundles 计算最终要喂给 HomeCheck 的 rules。 + */ +function buildRulesForHomeCheck( + enabledLeaves: Set, + bundles: Record +): Record { + const rules: Record = {}; + + // 1) 计算所有 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)) { + 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; +} + +/* ====================== 主函数:配置拼装与输出 ====================== */ + 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); + + // 只保留合法的 @migration/*,并把值统一成 1 + const sanitizedRules: Record = {}; + for (const [k, v] of Object.entries(rules)) { + if ((v === 1) && /^@migration\//.test(k)) { + sanitizedRules[k] = 1; + } + } + + // 最终交给 HomeCheck 的配置 + const ruleConfigInfo: RuleConfigInfo = { + rules: sanitizedRules, files: ['**/*.ets', '**/*.ts', '**/*.js'] }; - const projectConfigInfo = { + + // 将启用的和禁用的叶子规则附加到 cmdOptions 以供后续使用 + (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'); + + Logger.info(`[homecheck.ruleConfigInfo] dumped to: ${dumpPath}`); + } 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', @@ -64,14 +228,17 @@ 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 }; } +/* ====================== Issues → ProblemInfo 转换 ====================== */ + export function transferIssues2ProblemInfo(fileIssuesArray: FileIssues[]): Map { const result = new Map(); fileIssuesArray.forEach((fileIssues) => { -- Gitee