跨端: B 的交付物(跨端纯逻辑一致性判据)+ 照 skill 回扫修掉两处隐形债
用户:「你为什么不加载鸿蒙开发相关skill?」—— 说得对。那份
`arkts-grammar-standards` 写着 "REQUIRED before writing the first .ets file of a
session",而我这轮一直在写 `.ets`。补加载后照它的规则表**逐条回扫**,
当场抓出两处此前没人管的违规。
══ ① 用户要做的 B:`cross-client-logic.test.mjs`(新,6 条判据)
背景:两套纯逻辑各写一份且已分叉(replyTarget 214/170 行、mailGroups 178/459、
appearance 187/324、calendar 208/446)。当天已**踩到**两处分叉
(`participantAddress` 的 `||`、`ThreadPage` 字段全错)。
做法:**同一张用例表喂给两边,逐条比结果**(`--experimental-strip-types`
直接在 node 里跑两侧源码 —— 两边的 model 层都是纯逻辑、无 SDK 依赖)。
不选"生成一份共享源码":harmony 不能 import 工程外文件,
且两边类型系统不同(ArkTS 禁解构/any/对象字面量要具名类型),
生成器要维护"两边都能过"的子集,是另一个大工程。
★ **首轮运行就报出两处真分叉,都不是我踩到才发现**:
① `formatAddress('dsh', undefined, undefined)`:electron 返回 `"dsh"`,
harmony **抛** `Cannot read properties of undefined`。
—— 又是 `omitempty` 那个坑(**第三次**),这次是判据先报的。
② `monthGrid`:electron **固定 6 行**(`grid-rows-6`),harmony **4~6 行**
⇒ 翻月时网格高度跳动。WebUI 的注释明写要避免这个("行数变化会让整个
网格高度跳动,翻月时页面内容上下弹")。
③ 顺着 ② 又发现:WebUI 邻月格子**填真实日期并置灰、可点**
(`CalendarView.tsx:545-556`),harmony 留**空白格**。
★ 判据自身的两次错,都留了档(判据的 bug 与代码的 bug 一样危险):
· 第一版把 `args[0]` 当单个参数传,字符串被当可迭代对象展开 ⇒
`formatAddress('d','s','h')` —— **判据自己造出假分叉**。
· 第一版 `weekStart` 传 0(周日),而两端实际都是 1(周一)⇒ 又一处假分叉。
差一点就去"修"一个不存在的问题。
· `monthGrid` 的投影第一版按 `inMonth ? [y,m,day] : null`,
把"邻月填不填真日期"这个**真分叉**抹平了 —— 投影只该换表示,不该替我看不看。
★ 三类"不同"要分清(写进文件头):**命名不同**(投影归一,不是分叉)、
**签名不同**(ArkTS 没 Date 重载习惯;语义必须一样)、**行为不同**(是分叉,以 electron 为准)。
══ ② skill 回扫抓出的两处隐形债(编译器只告警、判据也不管)
· **正则字面量**(`arkts-no-regexp-literals`):`MailDetailPage.ets:615` 的
`/^\d+$/`(从 2026-09-19 活到今天)。
· **废弃的全局 `router`**:`api/Logout.ets:76` 的 `router.replaceUrl(...)`。
它是个独立函数(没有 `this`)⇒ 拿不到 `UIContext`,改成由调用方传
(两个调用点都持有 `getUIContext()`,零成本)。
★ 这两条为什么能活这么久:**编译器对它们只告警、不挡构建**,
全仓也**没有判据**管 ⇒ 规则事实上不存在。已补两条判据,都做了变异验证。
══ ③ 顺带修正一条**恒真的同义反复**断言
`harmony-calendar` 里 "today 不在本月:不许标在别的月" 那条:
它是在"邻月格子是空 `DayCell`(`iso` 为空串)"时写的 ⇒ `c.iso === today`
**永远不可能**匹配 ⇒ `count === 0` 恒真,**看起来守着一条规则,其实什么都没守**。
改成真不变量:**"被标为今天的那一格,iso 必须就是 today;至多一格"**,
并反向核对"2026-10-01 确实出现在 9 月网格里"(否则那段是空转)。
实测 WebUI `CalendarView.tsx:547` 是逐格 `isSameDay` ⇒ **它会标**,
所以原来那条"不许标"本身就窄了一半。
══ ④ 登记两处盘点发现(**没有**顺手改,因为需要人决定)
· `harmony-dead-pages`:`InboxPage.ets`(238 行) 不可达(不在页面表、无人导航),
`SessionsPage.ets`(170 行) 唯一引用来自 InboxPage ⇒ 一起不可达。
没删是因为 `HARMONY-ALIGN-PLAN.md:214` 把它当变异测试靶子用过 ——
删掉会永久丢代码,是否只是"早期留存"我判断不了。
· `harmony-permission-history`:WebUI 授权栏显示**待决 + 已决策历史**两段
(拿 inbox 自己分组,`PermissionList.tsx:27/174/182`);鸿蒙调专用端点
`/permission/pending`(SQL `WHERE pr.result IS NULL`)⇒ **只拿得到待决的**。
已在 `cross-client-logic` 的 gaps 里如实登记,判据会盯着"不要再少"。
══ 判据状态
`files=33 ran=33 checks=530 pass=530 fail=0 skip=0 red=0 broken=0 unreported=0`;
`baseline=7/7✓`(底本第 9 次重算,已按规矩先 `git diff --quiet HEAD` 取证 + 记录理由)。
`verdict=red` 残余仍是 5 条静态判据的**设备到期提示**(既有机制)。
══ 环境
模拟器昨天起卡死(hdc 能连、shell 超时、CPU 150%、跑了 34 小时),
导致设备判据各跑 836 秒后失败 —— 看起来像"套件卡死"。用户批准后杀掉重启
(`Emulator -start HATriple -noWindow`,`devecocli` 那套因 x11 起不来),
现在**75~150 秒**跑完整套。
This commit is contained in:
@ -153,9 +153,41 @@ test('ArkTS 词汇层硬坑:全仓 .ets 不许出现解构 / any / unknown /
|
||||
if (/\bany\b/.test(src)) hits.push('any');
|
||||
if (/\bunknown\b/.test(src)) hits.push('unknown');
|
||||
if (/\bfunction\s*\(/.test(src)) hits.push('函数表达式');
|
||||
/*
|
||||
* ★★ 2026-09-20 补:**正则字面量**(`arkts-no-regexp-literals`)。
|
||||
*
|
||||
* 这条是照 `arkts-grammar-standards` skill 的规则表逐条回扫时发现的缺口:
|
||||
* · 编译器对它是**告警级**(不挡构建),所以 build 一直是绿的;
|
||||
* · 全仓也没有判据管它 ⇒ `MailDetailPage.budgetDraftValid()` 里那个
|
||||
* `/^\d+$/` 从 2026-09-19 一直活到今天。
|
||||
* 「编译器只告警 + 判据不管」= 这条规则在本仓**事实上不存在**。
|
||||
*
|
||||
* 判法要**避开**三类合法的 `/`:
|
||||
* ① 注释(先剥掉)
|
||||
* ② 除法 `a / b`(前面是标识符/数字/`)`,且后面紧跟空格或标识符)
|
||||
* ③ 字符串里的路径(`'../model/X'`)
|
||||
* 所以只认这个形状:`/` 前面是**行首/`=`/`(`/`,`/`return`/`:`**,
|
||||
* 且到下一个 `/` 之间不含空白与引号 —— 那是正则字面量的典型写法。
|
||||
*/
|
||||
const noComment = src;
|
||||
const reLike = /(?:^|[=(,:]|return)\s*\/(?![/*=\s])(?:[^/\n'"\\]|\\.)+\/[gimsuy]*/gm;
|
||||
if (reLike.test(noComment)) hits.push('正则字面量(用 new RegExp)');
|
||||
if (hits.length) bad.push(`${rel(f)}:${hits.join('、')}`);
|
||||
}
|
||||
assert.deepEqual(bad, [], `★ ArkTS 硬坑(编译不过):\n ${bad.join('\n ')}`);
|
||||
|
||||
/*
|
||||
* 自检:正则字面量那一段探测器要**认得错、不误伤对**。
|
||||
* 不写自检的话,"没报"与"探测器坏了"结果一样 —— 那正是这条规则
|
||||
* 躲过一轮的原因(它此前根本没人探)。
|
||||
*/
|
||||
const probeRe = (t) => /(?:^|[=(,:]|return)\s*\/(?![/*=\s])(?:[^/\n'"\\]|\\.)+\/[gimsuy]*/gm.test(t);
|
||||
assert.equal(probeRe('return /^\\d+$/.test(t);'), true, '要抓得住 return 后的正则字面量');
|
||||
assert.equal(probeRe('const ok = /ab/.test(s);'), true, '要抓得住赋值后的正则字面量');
|
||||
assert.equal(probeRe('return new RegExp(\'^[0-9]+$\').test(t);'), false, '不许误伤 new RegExp');
|
||||
assert.equal(probeRe('const half = total / 2;'), false, '不许误伤除法');
|
||||
assert.equal(probeRe("import { X } from '../model/Y';"), false, '不许误伤字符串里的路径');
|
||||
assert.equal(probeRe('const p = apiBase + path; // a / b'), false, '剥注释后的行内斜杠不误伤');
|
||||
});
|
||||
|
||||
/* ─────────────────── 图标:不许用 Unicode 符号,且尺寸不许直接加在 AmIcon 上 ─────────────────── */
|
||||
@ -496,3 +528,56 @@ test('★ 服务端「只以 omitempty 形式出现」的字段:客户端读
|
||||
assert.equal(probe('const n = mail.attachments.length;', 'attachments'), 1, '要抓得住裸读');
|
||||
assert.equal(probe('const n = (mail.attachments ?? []).length;', 'attachments'), 0, '不许误伤兜底写法');
|
||||
});
|
||||
|
||||
test('★ 不许用已废弃的全局 API(`router` / `promptAction` / `animateTo` / `vp2px`)', () => {
|
||||
/*
|
||||
* ★★ 2026-09-20 加。这条是照 `arkts-grammar-standards` skill 的规则表
|
||||
* **逐条回扫**时发现的:
|
||||
*
|
||||
* · 编译器对它们只**告警**、不挡构建 ⇒ `hvigorw assembleHap` 一直全绿;
|
||||
* · 本仓也**没有判据**管它们;
|
||||
* ⇒ 这些规则在本仓**事实上不存在**,靠"记得别写"维持。
|
||||
*
|
||||
* 实际抓到的:`api/Logout.ets:76` 的 `router.replaceUrl(...)` 全局调用
|
||||
* (从 2026-09-19 一直活到今天)。已改成把 `UIContext` 传进 `performLogout`
|
||||
* 再走 `ui.getRouter()`。
|
||||
*
|
||||
* 为什么值得管:这些是**废弃** API,SDK 升级时是最先被移除的一批;
|
||||
* 而且它们绕开了 `UIContext`,在多实例/多窗口场景下语义是错的。
|
||||
*
|
||||
* 判法:找**裸用**(前面不是 `.` 也不是标识符字符),且排除注释与
|
||||
* `this.getUIContext()` / `ui.getRouter()` 这类**正确**写法 ——
|
||||
* 正确写法里这些词出现在 `getXxx()` 的方法名位置,前面有 `.`。
|
||||
*/
|
||||
const DEPRECATED = [
|
||||
// [正则, 说明]
|
||||
[/(^|[^.\w$])router\.(pushUrl|replaceUrl|back|getParams|clear)\s*\(/m, '全局 router.*(用 this.getUIContext().getRouter() 或 ui.getRouter())'],
|
||||
[/(^|[^.\w$])promptAction\.\w+\s*\(/m, '全局 promptAction(用 this.getUIContext().getPromptAction())'],
|
||||
[/(^|[^.\w$])animateTo\s*\(/m, '全局 animateTo(用 this.getUIContext().animateTo())'],
|
||||
[/(^|[^.\w$])(vp2px|px2vp)\s*\(/m, '全局 vp2px/px2vp(用 this.getUIContext().vp2px(),或 display 密度换算)']
|
||||
];
|
||||
const bad = [];
|
||||
const walkDir = (dir) => {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, e.name);
|
||||
if (e.isDirectory()) { walkDir(full); continue; }
|
||||
if (!/\.ets$/.test(e.name)) continue;
|
||||
const src = code(full); // 先剥注释,免得注释里的说明被当成违规
|
||||
for (const [re, msg] of DEPRECATED) {
|
||||
if (re.test(src)) bad.push(`${e.name}:${msg}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
walkDir(ETS_ROOT);
|
||||
assert.deepEqual(bad, [],
|
||||
`这些地方用了已废弃的全局 API(编译器只告警、不挡构建):\n ${bad.join('\n ')}`);
|
||||
|
||||
// 自检:探测器要认得裸用,也要放过正确写法
|
||||
const hit = (t, i) => DEPRECATED[i][0].test(t);
|
||||
assert.equal(hit('router.replaceUrl({ url: x });', 0), true, '要抓得住全局 router 裸用');
|
||||
assert.equal(hit('ui.getRouter().replaceUrl({ url: x });', 0), false, '不许误伤 ui.getRouter()');
|
||||
assert.equal(hit('this.getUIContext().getRouter().back();', 0), false, '不许误伤 this.getUIContext()');
|
||||
assert.equal(hit('await this.getUIContext().animateTo({}, () => {});', 2), false, '不许误伤 ui.animateTo');
|
||||
assert.equal(hit('animateTo({}, () => {});', 2), true, '要抓得住全局 animateTo');
|
||||
assert.equal(hit('const x = this.getUIContext().vp2px(24);', 3), false, '不许误伤 ui.vp2px');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user