/** * 判据目录里**唯一**允许读文本文件的两个入口 —— 名字自己解释该选哪个。 * * # 为什么要有这个模块(pi 2026-09-14 §4:同一处坑我踩了两次) * * 规范里写着"判代码读剥离版、判理由读原文",我 P5 写过一次、当天又踩了一次 * (`'rejected'` 那段**注释**里正好写着 `allowed-once`,被当成"这里会放行"误报)。 * **第二次犯规说明问题不在记性,在形态**:靠人记得执行的规范一定会有下一次。 * 所以把"用哪个读取器"从**记忆**变成**代码里的一个词**,并且可以被判据检查。 * * - `code(path)`:**剥掉注释**。判"代码里有没有这个调用/这个值"时必须用它 —— * 否则解释性注释("这里写 'rejected' 而不是 'denied',因为只认 allowed-once") * 会被当代码读,产生假红/假绿。 * - `prose(path)`:**原文**。判"理由写清了没/文档里有没有这句话"时用它。 * * 选错的典型症状:断言里的标识符恰好在同文件的注释里出现过(这类误报几乎都集中在 * "解释性注释与它解释的标识符同名"的地方)。 */ import { readFileSync } from 'node:fs'; /** * 剥掉注释:只用于"代码里有什么"。 * * ★ **行号必须保持不变** —— 这一条是硬要求,不是风格问题。 * 原来块注释是用 `''` 直接抹掉的,而块注释**自带换行**,抹掉它就把后面所有行的行号 * 整体前移。后果实测(我自己的 `harmony-arkts` 判据报违规时): * 报出"最后一个 import 在第 64 行、第 47 行已是语句",而**真实文件里是第 80 / 63 行** —— * 全仓的判据都在用 `文件:行号` 定位(`grep -n`、编辑器跳转),**报出来的行号必须能直接用**, * 否则读者第一步就得先猜"这是剥过的还是没剥的"。 * 修法:块注释里的每个换行都**换成等价数量的空行**(而不是整块删掉)。 */ export function stripComments(src) { return src .replace(/\/\*[\s\S]*?\*\//g, (m) => '\n'.repeat((m.match(/\n/g) || []).length)) .replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // 行注释(避开 https:// 这类;它不含换行,行号天然不变) } /** 读文件并**剥掉注释** —— 判"代码里有什么"用这个 */ export function code(path) { return stripComments(readFileSync(path, 'utf8')); } /** * 把**字符串/模板串的"体"**抹成空白(保留引号与换行)。 * * ★ 为什么需要它:`stripComments` 只去注释,**不去字符串**。于是"在源码里找某个写法" * 的那类判据会**咬到字符串里的文字** —— 这个族本仓已经踩过三次 * (`@ohos`、`toISOString`、以及"自检的报错文案把自己数进去")。 * 实测(pi 2026-09-15 用它自己的正则复现,我也复现): * probs.push(`oops; totalTests += 1`); ← 被 MATCHED(假阳性) * 而代码是对的 ⇒ **判据开始消费散文**,下一个人会去改**文案**来哄判据。 * * ★ **行号必须不变**(与 `stripComments` 同一条硬要求):字符串里的每个换行 * 换成等价数量的空行。而且**保留引号本身**,这样"这里原本有个字符串"仍然看得见。 * * ⚠️ 已知限制,**方向是"假绿",不是"误伤"** —— 这一点我第一版写错了,pi 2026-09-15 测出真方向: * 逐字符扫描,**不区分正则字面量**。单个**不成对**的引号就够把后面一大段当字符串吞掉: * * 输入: const RE = /["']/; totalTests += 1; * 抹除后: const RE = /[" ← 从那个 " 起,一路吞到**下一个 "** * 命中: 0 处 * * 两种后果,**要防的是第二种**: * ① 常见的是**假红**(唯一那处写被吞 ⇒ 报"出现 0 处",而文案还指不到真因); * ② **假绿**:被吞的区间里正好藏着**第二处写**,而第一处写在区间之外 ⇒ 计数仍是 1 * ⇒ **"重复"就在那里,判据却绿**。 * 实测:`const RE = /["x]; totalTests += 1; OK"]/ ; totalTests += 1;` * ⇒ 真身 **2** 处,抹除后只数出 **1** 处。 * * ★ 而"第二处写"正是这条判据**唯一存在的理由** ⇒ ② 是**绕过**。 * 所以这条判据的绿只能保证:"**在它能看见的文本里**没有第二处写" —— 不是"没有第二处写"。 * **把绿当成证明,就会在这里栽。** * * (修它需要词法状态机 —— 不划算,因为下一版要改成"记录投影", * 那时"计两次"根本没有对应的语句可写,这一族连扫描对象都不存在。) */ export function stripStrings(src) { let out = ''; let i = 0; while (i < src.length) { const ch = src[i]; if (ch !== '"' && ch !== "'" && ch !== '`') { out += ch; i += 1; continue; } const quote = ch; out += ch; i += 1; while (i < src.length) { const d = src[i]; if (d === '\\') { out += ' '; i += 2; continue; } // 转义:两个字符都抹掉(等长) if (d === '\n') { out += '\n'; i += 1; continue; } // 换行**保留**(行号不变) if (d === quote) { out += d; i += 1; break; } out += ' '; i += 1; } } return out; } /** 读文件**原文** —— 判"注释/文档里写了什么"用这个 */ export function prose(path) { return readFileSync(path, 'utf8'); } /** 读**二进制**(安装包、图片等)—— 需要 Buffer 时用它,别在判据里裸用 readFileSync */ export function bytes(path) { return readFileSync(path); }