跨端: ArkTS 编译期硬规则进判据 —— 我插的常量表把 import 挤到了后面,assembleHap 报 arkts-no-misplaced-imports

`hvigorw assembleHap` 在 `f31bc02`/`b806a05` 上都红了一条:

    ERROR: ArkTS:ERROR …/MainPage.ets
    "import" statements after other statements are not allowed  (arkts-no-misplaced-imports)

**是我造成的**:P4c 那笔我在 `MainPage.ets` 里插了 `NAV_MATERIAL_OF` 那张(带注释的)
常量表,位置在**既有 import 之前**。ArkTS 要求所有 import 在任何语句之前,
常量表算语句 —— 编译器直接报错。

## 为什么我那一笔的判据一条都没抓到它

因为**这个仓库里没有任何判据会跑 ArkTS 的编译规则**。我的判据判的是"表达式对不对、
接没接上、颜色写没写死…",它们全绿 —— **文本层面确实没问题**,问题只有编译器知道。
pi 是构建时撞上的。

⇒ 教训不是"下次小心",是**把编译器能抓、而判据不抓的那一类固化下来**。
这一类里有一批**纯文本就能判、不需要设备**,所以它们不该待在"等设备才能验"的欠账里。

## 做了什么

- 新增 `test/harmony-arkts.test.mjs`(3 条,不需要设备 ⇒ **不进** STATIC_ONLY):
  · **所有 26 个 `.ets` 的 import 必须在任何其它语句之前**(就是这次报的那条);
  · 全仓 `.ets` 的词汇层硬坑(对象解构 / `any` / `unknown` / 函数表达式)
    —— 这几条此前**只在"我自己新写的那个页面"里判**,而我恰恰是在**改既有文件**时犯的下一个错,
    编译期硬规则不该按"谁写的"分覆盖;
  · **一条自检**:造已知坏样本(常量插在两组 import 之间)确认检查会红、合法样本不误报
    —— 否则"全绿"可能只是扫描逻辑失效(我第一版就把多行 import 的成员行误判成了语句)。
- 接进 `SUITE`(判据文件数 22 → 23)。
- 文件头明确写了它**不能**替代 `hvigorw`:覆盖的只是"能静态判出来的那几类",
  类型推断/重载解析/Sendable 那些仍然只有 build 能验 ——
  不许把这个文件的存在读成"编译已经验过了"。
- 变异测试 5 条**全部被抓**,含**精确复现我那个错的形状**(把 import 搬到常量表之后)。
  基线 `7/7✓`(逐字节还原)。
- 现口径:`mutants=52 ran=52 skipped=0 on_new_criteria=36 baseline=7/7✓`。

## 工作树里**不是我做的**两处改动(已核实为正确,我没有提交也没有回退)

工作树是共享的:`AdminUsersPage.ets` 与 `MainPage.ets` 在我提交**之后**被别的进程改过,
两处都是**修构建错误**,都核实过是对的:

1. `MainPage.ets`:把两组 import(`CalendarPage`、`NavItems`)从 `NAV_MATERIAL_OF` 表**之后**
   挪到文件最前 —— 就是上面那条 `arkts-no-misplaced-imports`;
2. `AdminUsersPage.ets`:`Chip(text, bg, fg: string)` → `ResourceColor`
   (`Theme.surfaceMuted`/`textSubtle` 是 `Resource`、`chipNeutralBg` 是 `string`,
   第 368 行那个三目因此是 `Resource | string` ⇒ 旧签名编译不过)。

我不提交别人的活、更不回退它;但它们**让基线必须重算**,而**重算基线是有意动作**
(随手重算会把"某次变异没还原"永久掩盖掉),所以 `baseline.sha` 顶部记了原因与哈希来源。

## 未验

- 这三条判据**只覆盖静态可判的那几类**。`assembleHap` 仍然只有真正构建才能验 ——
  这次就是构建先于我所有判据发现的问题,下次还可能是。
- Go 侧 `debt_registry_test.go` 仍未跑(沙箱无 Go 模块缓存),只有 `gofmt`。
This commit is contained in:
2026-09-15 11:27:35 +08:00
parent bcd4f97e37
commit 7f4fa2629e
5 changed files with 184 additions and 2 deletions

View File

@ -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<string, number> = {',
" '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<string, number> = {',
" '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 ')}`);
});

View File

@ -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 出现在我一个没新写的文件里"
}
]

View File

@ -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<string, BlurStyle> = \\{\n 'COMPONENT_THIN': BlurStyle\\.COMPONENT_THIN,",
"repl":"import { Theme as T2 } from '../common/Theme';\nconst NAV_MATERIAL_OF: Record<string, BlurStyle> = {\n 'COMPONENT_THIN': BlurStyle.COMPONENT_THIN,",
"test":"arkts","why":"★ 把 import 搬到常量表**之后**(= 精确复现我犯的那个错)"}
]

View File

@ -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"
}

View File

@ -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:清单里的文件必须真的存在(写错名字 = 那条判据永远不跑)