diff --git a/client/electron/test/harmony-arkts.test.mjs b/client/electron/test/harmony-arkts.test.mjs new file mode 100644 index 0000000..52d74ee --- /dev/null +++ b/client/electron/test/harmony-arkts.test.mjs @@ -0,0 +1,145 @@ +/** + * ArkTS **编译期**硬规则的判据(本机可跑,不需要设备)。 + * + * ── 这一整个文件的来历 ── + * + * `hvigorw assembleHap` 在 `f31bc02` / `7647c24` 上都红了一条: + * + * ERROR: ArkTS:ERROR File: …/MainPage.ets + * "import" statements after other statements are not allowed (arkts-no-misplaced-imports) + * + * 原因是**我**在 `MainPage.ets` 里把 `NAV_MATERIAL_OF` 那张(带注释的)常量表 + * **插在了既有 import 之前** —— 而这个仓库里**没有一条判据会跑 ArkTS 的编译规则**: + * 我那一笔的判据判的是"表达式对不对/接没接上",它们全绿,因为**文本层面没问题**, + * 问题只有编译器知道。pi 是构建时撞上的。 + * + * ⇒ 教训不是"下次小心",是**把编译器能抓、而判据不抓的那一类固化下来**。 + * 一组 import 位置、解构、`any`、函数表达式这些**都不需要设备**、纯文本就能判, + * 所以它们**不该**待在"等设备才能验"的欠账里。 + * + * ⚠️ 这个文件**不能**替代 `hvigorw`:它覆盖的是"能静态判出来的那几类"。 + * ArkTS 还有大量只有编译器知道的事(类型推断、重载解析、Sendable…)—— + * 那部分仍然只有 build 能验,不许把这个文件的存在读成"编译已经验过了"。 + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { code } from './lib/read.mjs'; + +// 与其它鸿蒙判据同口径(见 `harmony-admin.test.mjs` 的文件头) +const ROOT = '/home/program/agentmail'; +const ETS_ROOT = join(ROOT, 'client/harmony/entry/src/main/ets'); + +function allEts(dir = ETS_ROOT) { + const out = []; + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) { out.push(...allEts(p)); continue; } + if (e.name.endsWith('.ets')) out.push(p); + } + return out; +} + +const rel = (p) => p.slice(ETS_ROOT.length + 1); + +/** + * 逐行扫"最后一个 import"与"第一个非 import 语句"的位置。 + * + * 要注意 import 可能是**多行**的(`import {\n a,\n b\n} from '…';`), + * 所以不能只看以 `import` 开头的行 —— 多行块里的成员名会被误判成"语句"。 + * (我第一版就这么误判过:把 `NAV_BAR_BOTTOM,` 当成了一条语句。) + */ +function importOrder(src) { + const lines = src.split('\n'); + let lastImport = 0; + let firstOther = null; + let inBlock = false; + for (let i = 0; i < lines.length; i++) { + const ls = lines[i].trim(); + if (!ls || ls.startsWith('//') || ls.startsWith('*') || ls.startsWith('/*') || ls.startsWith('*/')) continue; + if (ls.startsWith('import ')) { + lastImport = i + 1; + inBlock = !ls.replace(/\s+$/, '').endsWith(';'); + continue; + } + if (inBlock) { + // import 块内:`} from '…';` 或成员行 + if (ls.startsWith('}') || ls.endsWith(';')) { lastImport = i + 1; inBlock = false; } + continue; + } + if (firstOther === null) firstOther = { line: i + 1, text: ls.slice(0, 60) }; + } + return { lastImport, firstOther }; +} + +test('★ ArkTS:所有 import 必须在任何其它语句之前(arkts-no-misplaced-imports)', () => { + /* + * 这条是**构建时撞出来的**(见文件头)。它判的是"文件级语句顺序", + * 与"import 写全没写全""路径对不对"是不同的事 —— 那些别处判。 + */ + const bad = []; + for (const f of allEts()) { + const { lastImport, firstOther } = importOrder(code(f)); + if (firstOther && firstOther.line < lastImport) { + bad.push(`${rel(f)}:最后一个 import 在第 ${lastImport} 行,` + + `但第 ${firstOther.line} 行已是语句「${firstOther.text}」`); + } + } + assert.deepEqual(bad, [], + `★ 这些文件把 import 写在了其它语句**之后** —— ArkTS 编译器会直接报错,构建不过:\n ${bad.join('\n ')}\n` + + ' 修法:把 import 全部挪到文件最前(常量表、类、函数都要在它们之后)。'); +}); + +test('★ 判据自检:import 顺序检查必须能抓到"常量插在 import 之前"', () => { + /* + * 这条自检是**必须的**:上面那条今天全绿,而它绿的原因可能是"真的没问题", + * 也可能是"我的扫描逻辑失效了"(把整段当注释跳过、多行 import 判错…)。 + * 造一个**已知坏样本**,确认它会被判红 —— 这正是这次事故的形状。 + */ + const badSample = [ + "import { a } from './a';", + '', + 'const TABLE: Record = {', + " 'x': 1", + '};', + '', + "import { b } from './b';", + '', + 'export function use(): number { return TABLE.x + b; }' + ].join('\n'); + const r = importOrder(badSample); + assert.ok(r.firstOther && r.firstOther.line < r.lastImport, + '自检失败:检查逻辑抓不到"常量插在两组 import 之间"——那正是本次构建报错的形状'); + // 反向:合法样本不许误报 + const goodSample = [ + "import { a } from './a';", + "import { b } from './b';", + '', + 'const TABLE: Record = {', + " 'x': 1", + '};' + ].join('\n'); + const g = importOrder(goodSample); + assert.ok(g.firstOther && g.firstOther.line > g.lastImport, '自检失败:合法样本被误报'); +}); + +test('ArkTS 词汇层硬坑:全仓 .ets 不许出现解构 / any / unknown / 函数表达式', () => { + /* + * 这几条此前**只在我新写的那个页面里**判(`harmony-admin.test.mjs` ⑨), + * 也就是说"我自己新写的文件"有判据、"别的文件"没有 —— 而我恰恰是在 + * **改既有文件**(`MainPage.ets`)时犯的下一个错。 + * ⇒ 铺到全部 `.ets`:编译期硬规则不该按"谁写的"分覆盖。 + */ + const bad = []; + for (const f of allEts()) { + const src = code(f); + const hits = []; + if (/const\s*\{[^}]*\}\s*=/.test(src) || /let\s*\{[^}]*\}\s*=/.test(src)) hits.push('对象解构'); + if (/\bany\b/.test(src)) hits.push('any'); + if (/\bunknown\b/.test(src)) hits.push('unknown'); + if (/\bfunction\s*\(/.test(src)) hits.push('函数表达式'); + if (hits.length) bad.push(`${rel(f)}:${hits.join('、')}`); + } + assert.deepEqual(bad, [], `★ ArkTS 硬坑(编译不过):\n ${bad.join('\n ')}`); +}); diff --git a/client/electron/test/mutants/jobs/jobs-arkts.json b/client/electron/test/mutants/jobs/jobs-arkts.json new file mode 100644 index 0000000..2800ebf --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-arkts.json @@ -0,0 +1,23 @@ +[ + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "import \\{ MailDetailParams, ComposeParams \\} from '../model/RouteParams';\n", + "repl": "", + "test": "arkts", + "why": "把 import 顺序检查的靶子挪走:删掉一条 import(若检查只看行号会误判)" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/SettingsPage.ets", + "pat": "import \\{ Theme \\} from '\\.\\./common/Theme';", + "repl": "const INSERTED: number = 1;\nimport { Theme } from '../common/Theme';", + "test": "arkts", + "why": "★把常量插在既有 import 之前(本次构建报错的形状)" + }, + { + "file": "client/harmony/entry/src/main/ets/common/Theme.ets", + "pat": " static readonly fontTiny: number = 11;", + "repl": " static readonly fontTiny: number = 11;\n static readonly tmpAny: any = null;", + "test": "arkts", + "why": "全仓词汇检查:any 出现在我一个没新写的文件里" + } +] diff --git a/client/electron/test/mutants/jobs/jobs-arkts2.json b/client/electron/test/mutants/jobs/jobs-arkts2.json new file mode 100644 index 0000000..00853e7 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-arkts2.json @@ -0,0 +1,10 @@ +[ + {"file":"client/harmony/entry/src/main/ets/pages/SettingsPage.ets", + "pat":"import \\{ Theme \\} from '\\.\\./common/Theme';", + "repl":"const INSERTED: number = 1;\nimport { Theme } from '../common/Theme';", + "test":"arkts","why":"★ 把常量插在既有 import 之前(本次构建报错的形状)"}, + {"file":"client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat":"const NAV_MATERIAL_OF: Record = \\{\n 'COMPONENT_THIN': BlurStyle\\.COMPONENT_THIN,", + "repl":"import { Theme as T2 } from '../common/Theme';\nconst NAV_MATERIAL_OF: Record = {\n 'COMPONENT_THIN': BlurStyle.COMPONENT_THIN,", + "test":"arkts","why":"★ 把 import 搬到常量表**之后**(= 精确复现我犯的那个错)"} +] diff --git a/client/electron/test/mutants/test-keys.json b/client/electron/test/mutants/test-keys.json index 78b8596..d8027b8 100644 --- a/client/electron/test/mutants/test-keys.json +++ b/client/electron/test/mutants/test-keys.json @@ -10,5 +10,6 @@ "appearance": "test/harmony-appearance.test.mjs", "nav": "test/harmony-nav.test.mjs", "cross": "test/cross-client-theme.test.mjs", - "wallpaper": "test/harmony-presets.test.mjs" + "wallpaper": "test/harmony-presets.test.mjs", + "arkts": "test/harmony-arkts.test.mjs" } diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 3db5361..51cce58 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -89,7 +89,10 @@ const SUITE = [ ['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 22], // P4c 图片上传:阈值与两档策略 / 失败必带原因 / 退档判定只有一处 / // release 都 await / 解码按目标尺寸 / multipart 字段名 / 上传后重新同步 - ['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 29] + ['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 29], + // ArkTS **编译期**硬规则(纯文本可判、不需要设备)。这一条是构建撞出来的: + // 我把常量表插在了既有 import 之前 ⇒ arkts-no-misplaced-imports,而当时没有任何判据会跑它。 + ['test/harmony-arkts.test.mjs', [], 3] ]; // 自检 1:清单里的文件必须真的存在(写错名字 = 那条判据永远不跑)