diff --git a/game-runtime/tools/amodel-gen/tests/check-banned.test.mjs b/game-runtime/tools/amodel-gen/tests/check-banned.test.mjs index 230c1ea7..a1380fa7 100644 --- a/game-runtime/tools/amodel-gen/tests/check-banned.test.mjs +++ b/game-runtime/tools/amodel-gen/tests/check-banned.test.mjs @@ -102,3 +102,35 @@ test('④ 合规 ctx 写法零报', () => { const errs = _bannedLineErrors(src, 'game-logic.js'); assert.equal(errs.length, 0, JSON.stringify(errs)); }); + +// ───────── ⑤ 字符串内的 // 不 desync stripCode(2026-07-08 trpg-cyber 假阴性根治回归) ───────── + +test('⑤ 字符串内含 // 不吞后续真代码(赛博主题回归)', () => { + // 赛博主题实测:'> JACK IN // FLOOR 01' 字符串里的 //,旧 stripCode 行注释先剥、吞掉闭合引号 → + // 引号失衡 → 后续字符串正则跨行匹配 → 级联误抹真代码(实测抹 169 行,连 bundle.tick/handleTap 一起抹) + // → check 误报红线缺失 → 模型明明写对却被反复拒 → thrash 到步数顶熔断失败。回归:字符串后的真 + // Math.random 仍须命中且报对行号(证明字符串内 // 未 desync 后续码)。 + const src = [ + 'export function createGame(){', // 1 + " const banner = '> JACK IN // FLOOR 01';", // 2 // 在字符串内 + ' const r = Math.random();', // 3 真违规,须命中 + ' return { _forensicsView(){}, handleTap(){} };', // 4 + '}', // 5 + ].join('\n'); + const errs = _bannedLineErrors(src, 'game-logic.js'); + assert.ok(hasBanned(errs, 'Math\\.random'), `字符串内 // 不应吞掉后续真代码:${JSON.stringify(errs)}`); + assert.ok(errs.some((e) => e.includes('第 3 行')), `Math.random 应报第 3 行:${JSON.stringify(errs)}`); +}); + +test('⑤ 字符串内的 // 后接注释形态的文本零误杀', () => { + // 双引号串含 //,且串内出现禁用 API 名(Date.now);既不该吞后续码、串内 API 名也不该误报。 + const src = [ + 'export function createGame(){', // 1 + ' const url = "sys://net/Date.now() 只是文案 // 提示";', // 2 串内 // + API 名 + ' const t = ctx.time.nowMs();', // 3 合规 + ' return { _forensicsView(){}, handleTap(){} };', // 4 + '}', // 5 + ].join('\n'); + const errs = _bannedLineErrors(src, 'game-logic.js'); + assert.equal(errs.length, 0, `串内 // 与 API 名都不该报,后续合规码也不该被误伤:${JSON.stringify(errs)}`); +}); diff --git a/game-runtime/tools/amodel-gen/tools.mjs b/game-runtime/tools/amodel-gen/tools.mjs index c26f0544..e60f88ab 100644 --- a/game-runtime/tools/amodel-gen/tools.mjs +++ b/game-runtime/tools/amodel-gen/tools.mjs @@ -135,15 +135,46 @@ const BANNED = [ /** 去注释 + 字符串字面量,避免红线/结构正则误伤文档注释里的示例(如注释里写「禁用 Math.random()」)。 * 抹除时把内容换成【等量空格、保留换行】——结果串与原文行号一一对应(红线报错据此定位到行), - * 且长度不变、不误匹配(空格非 \w、非 `.`,既有的 matchAll/结构正则语义不变)。 */ + * 且长度不变、不误匹配(空格非 \w、非 `.`,既有的 matchAll/结构正则语义不变)。 + * + * 【为什么必须单遍扫描,不能用多条独立正则】(2026-07-08 n=5 收敛环 trpg-cyber 坐实的 check 假阴性根治): + * 旧版 5 条独立 .replace 先剥行注释、再剥字符串——但注释与字符串会互相嵌套:字符串里的 `//` + * (赛博主题 '> JACK IN // FLOOR 01' 这类)会被行注释正则误当注释、吞到行尾(含闭合引号)→ 引号失衡 + * → 后续字符串正则从游离引号起跨行匹配 → 级联误抹真代码(实测抹掉 169 行,连 bundle.tick/_forensicsView/ + * handleTap 一起抹)→ check 误报红线方法"缺失"→ 模型明明写对了却被反复拒 → thrash 到步数顶熔断、烧钱失败。 + * 单遍状态机逐字符扫,同一时刻只处于「码/行注释/块注释/单引/双引/模板」一种态,嵌套的 `//`、引号各归其态、 + * 绝不越界——这是「注释与字符串互嵌」这类词法问题唯一正确的解法。逐字节保长、换行保留(行号对齐不变)。 + */ function stripCode(s) { - const blank = (m) => m.replace(/[^\n]/g, ' '); // 非换行→空格、换行留 - return s - .replace(/\/\*[\s\S]*?\*\//g, blank) // 块注释(可跨行) - .replace(/\/\/[^\n]*/g, blank) // 行注释 - .replace(/'(?:[^'\\]|\\.)*'/g, blank) // 单引号串 - .replace(/"(?:[^"\\]|\\.)*"/g, blank) // 双引号串 - .replace(/`(?:[^`\\]|\\.)*`/g, blank); // 模板串(可跨行) + const out = []; + const n = s.length; + let state = 'code'; // code | line | block | sq(单引) | dq(双引) | tpl(模板) + for (let i = 0; i < n; i++) { + const c = s[i]; + const c2 = i + 1 < n ? s[i + 1] : ''; + if (state === 'code') { + if (c === '/' && c2 === '/') { state = 'line'; out.push(' '); i += 1; continue; } + if (c === '/' && c2 === '*') { state = 'block'; out.push(' '); i += 1; continue; } + if (c === "'") { state = 'sq'; out.push(' '); continue; } + if (c === '"') { state = 'dq'; out.push(' '); continue; } + if (c === '`') { state = 'tpl'; out.push(' '); continue; } + out.push(c); continue; // 正常码逐字节保留(正则字面量按码处理,与旧版同,不新增误伤面) + } + if (state === 'line') { // 行注释:遇换行收尾 + if (c === '\n') { state = 'code'; out.push('\n'); continue; } + out.push(' '); continue; + } + if (state === 'block') { // 块注释:遇 */ 收尾,换行保留 + if (c === '*' && c2 === '/') { state = 'code'; out.push(' '); i += 1; continue; } + out.push(c === '\n' ? '\n' : ' '); continue; + } + // 字符串三态(sq/dq/tpl):转义跳一对、遇匹配闭合退回码、换行保留(模板可跨行;单双引跨行容错) + const quote = state === 'sq' ? "'" : state === 'dq' ? '"' : '`'; + if (c === '\\') { out.push(' '); out.push(c2 === '\n' ? '\n' : (c2 ? ' ' : '')); i += 1; continue; } + if (c === quote) { state = 'code'; out.push(' '); continue; } + out.push(c === '\n' ? '\n' : ' '); continue; + } + return out.join(''); } /** 插件键 → api.d.ts 目录(host-config 注入的 canonical 键名;特殊:juice/save/palettePost/physics 名≠dir)。 */