From 5d58301e2e7f87d971e55c8836571d9e9356bd75 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 2 Nov 2026 15:33:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20M91=20r4=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=86=85=E5=A4=8D=E7=94=A8=E4=B8=BB=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9C=BA=EF=BC=88=E7=BB=88=E7=BB=93=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E5=86=85=20raw=20string=20/=20=E5=AD=97=E7=AC=A6=E5=AD=97?= =?UTF-8?q?=E9=9D=A2=E9=87=8F=E7=9B=B2=E5=8C=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:属性外的状态机(普通/行注释/块注释/字符串/raw/字符)只覆盖属性外;属性内另写了一套 「遇 " 跳到下一个 "」,于是属性内的 raw string 与字符字面量完全不认识: - 假绿:`#[doc = r#"a "b"#]` 之后跟一个真 `#[allow(unsafe_code)]` → exit 0(r2 能抓到,是新盲区) - 误红:`#[doc = r#"x" allow(unsafe_code) is banned "y"#]`(纯文档)→ 被报「多出来的 unsafe 落点」 修法(结构性,不是补分支):把逐字符分派抽成 step(),主循环与属性解析**共用同一个函数**; 属性解析只多保留一层「[ ] 深度决定何时离开属性」。属性内的字符串/raw/字符字面量因此与 属性外同一套识别,两处永不发叉。 - g_plain 标记「该位置是否处于普通态代码」:注释与字符串内容既不参与属性起点判定, 也不参与属性内的 [ ] 计数与文本匹配(所以纯文档里的 allow 文本不会误报) - 回归用例写进 yml 注释:四个形态(含 CRLF)+ 反方向纯文档例,改词法器前先跑 - 已验:gawk 5.4.0 / gawk --posix / mawk 1.3.4(debian:bookworm-slim)三种实现对四个 回归用例与全仓 601 个文件给出完全一致的结果(恰好一个落点) --- .github/workflows/guards.yml | 165 ++++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 71 deletions(-) diff --git a/.github/workflows/guards.yml b/.github/workflows/guards.yml index af6f476..d75a289 100644 --- a/.github/workflows/guards.yml +++ b/.github/workflows/guards.yml @@ -188,90 +188,113 @@ jobs: # - 注释(行注释/块注释)与字符串(普通/raw)里的属性文本**刻意不算**:它们不构成放行; # 代价是「真把 allow 写进字符串」这种写法不会被当作落点(它也确实不放行任何代码); # - manifest 侧 `[lints.rust] unsafe_code = "allow"`:本门禁只扫 `*.rs`,看不见 Cargo.toml; + # + # r4 回归用例(属性内的 raw string / 字符字面量曾经是盲区;现在两处共用 step(), + # 它们不再是边界,但下面五条必须一直保持红/绿正确 —— 改这段词法器前先跑它们): + # 红 #[doc = r#"a "b"#] 后跟真 allow + # 红 #[cfg_attr(docsrs, doc = r##"a " b "##)] 后跟真 allow + # 红 #[cfg_attr(docsrs, my_macro(39 as char))] 一类含字符字面量/转义的属性后跟真 allow + # 具体形态:#![cfg_attr(docsrs, my_macro('\''))] 后跟真 allow + # 红 以上三者的 CRLF 版本 + # 绿 #[doc = r#"x" allow(unsafe_code) is banned "y"#](纯文档,不得误报) scan() { awk -v path="$1" ' - function lex( s, line, n, i, c, nx, state, depth, hashes, j, ok, k, ch, ch2, adepth, attr, q) { - q = "\047" + # ---- 唯一的状态机:主循环与属性内**共用**同一个 step() ---- + # 为什么必须复用:r2 是「属性外一套状态机、属性内另写一套」,两套各自演进, + # r3 的属性内就只剩「遇 " 跳到下一个 "」,于是 raw string / 字符字面量在属性内 + # 完全不认识 —— `#[doc = r#"a "b"#]` 之后的一切对门禁失明(假绿), + # `#[doc = r#"x" allow(unsafe_code) "y"#]` 这种纯文档被误报(误红)。 + # 现在两处只认这一个 step(),属性解析只多一层「[ ] 深度决定何时离开属性」。 + # + # 状态:0 普通 / 1 行注释 / 2 块注释(可嵌套)/ 3 字符串 / 4 raw 字符串 / 5 字符字面量。 + # 返回值 = 本次消费到的位置(`//`、`/*`、`r#"`、raw 收尾会一次吃掉多个字符); + # g_plain=1 表示该位置处在「普通态代码」里(注释/字符串内容一律 0, + # 所以它们既不会被当成属性、也不会参与属性内的 [ ] 计数与文本匹配)。 + function step(s, n, i, c, nx, j, h, ok) { + c = substr(s, i, 1) + g_plain = 0 + if (g_state == 1) { if (c == "\n") g_state = 0; return i } + if (g_state == 2) { + if (c == "/" && substr(s, i + 1, 1) == "*") { g_depth++; return i + 1 } + if (c == "*" && substr(s, i + 1, 1) == "/") { g_depth--; if (g_depth <= 0) g_state = 0; return i + 1 } + return i + } + if (g_state == 3) { + if (c == "\\") return i + 1 + if (c == "\"" || c == "\n") g_state = 0 + return i + } + if (g_state == 4) { + if (c == "\"") { + ok = 1 + for (j = 1; j <= g_hashes; j++) if (substr(s, i + j, 1) != "#") ok = 0 + if (ok) { g_state = 0; return i + g_hashes } + } + return i + } + if (g_state == 5) { + if (c == "\\") return i + 1 + if (c == g_q || c == "\n") g_state = 0 + return i + } + if (c == "/" && substr(s, i + 1, 1) == "/") { g_state = 1; return i + 1 } + if (c == "/" && substr(s, i + 1, 1) == "*") { g_state = 2; g_depth = 1; return i + 1 } + if (c == "\"") { g_state = 3; return i } + if (c == g_q) { + nx = substr(s, i + 1, 1) + if (nx == "\\" || substr(s, i + 2, 1) == g_q) { g_state = 5; return i } + g_plain = 1 + return i + } + if (c == "r" || c == "b") { + j = i + 1 + if (c == "b") { + if (substr(s, j, 1) == "\"") { g_state = 3; return j } + if (substr(s, j, 1) == g_q) { g_state = 5; return j } + if (substr(s, j, 1) != "r") { g_plain = 1; return i } + j++ + } + h = 0 + while (substr(s, j, 1) == "#") { h++; j++ } + if (substr(s, j, 1) == "\"") { g_hashes = h; g_state = 4; return j } + g_plain = 1 + return i + } + g_plain = 1 + return i + } + function lex( s, line, n, i, j, k, ch, adepth, attr) { s = "" while ((getline line < path) > 0) s = s line "\n" close(path) n = length(s) - state = 0; depth = 0; hashes = 0; found = 0 + g_state = 0; g_depth = 0; g_hashes = 0; g_found = 0 for (i = 1; i <= n; i++) { - c = substr(s, i, 1) - if (state == 1) { if (c == "\n") state = 0; continue } - if (state == 2) { - if (c == "/" && substr(s, i + 1, 1) == "*") { depth++; i++; continue } - if (c == "*" && substr(s, i + 1, 1) == "/") { depth--; i++; if (depth <= 0) state = 0; continue } - continue - } - if (state == 3) { - if (c == "\\") { i++; continue } - if (c == "\"" || c == "\n") state = 0 - continue - } - if (state == 4) { - if (c == "\"") { - ok = 1 - for (j = 1; j <= hashes; j++) if (substr(s, i + j, 1) != "#") ok = 0 - if (ok) { state = 0; i += hashes } - } - continue - } - if (state == 5) { - if (c == "\\") { i++; continue } - if (c == q || c == "\n") state = 0 - continue - } - if (c == "/" && substr(s, i + 1, 1) == "/") { state = 1; i++; continue } - if (c == "/" && substr(s, i + 1, 1) == "*") { state = 2; depth = 1; i++; continue } - if (c == "\"") { state = 3; continue } - if (c == q) { - nx = substr(s, i + 1, 1) - if (nx == "\\" || substr(s, i + 2, 1) == q) state = 5 - continue - } - if (c == "r" || c == "b") { - j = i + 1 - if (c == "b") { - if (substr(s, j, 1) == "\"") { state = 3; i = j; continue } - if (substr(s, j, 1) == q) { state = 5; i = j; continue } - if (substr(s, j, 1) != "r") continue - j++ - } - hashes = 0 - while (substr(s, j, 1) == "#") { hashes++; j++ } - if (substr(s, j, 1) == "\"") { state = 4; i = j; continue } - continue - } - if (c == "#") { - j = i + 1 - if (substr(s, j, 1) == "!") j++ - if (substr(s, j, 1) != "[") continue - attr = ""; adepth = 0; k = j - while (k <= n) { + i = step(s, n, i) + if (!g_plain || substr(s, i, 1) != "#") continue + j = i + 1 + if (substr(s, j, 1) == "!") j++ + if (substr(s, j, 1) != "[") continue + # 属性:**复用 step()**,只多一层「[ ] 深度决定何时离开属性」。 + # 属性里的字符串 / raw 字符串 / 字符字面量因此与属性外同一套识别。 + attr = ""; adepth = 0; k = j + while (k <= n) { + k = step(s, n, k) + if (g_plain) { ch = substr(s, k, 1) - if (ch == "\"") { - k++ - while (k <= n) { - ch2 = substr(s, k, 1) - if (ch2 == "\\") { k += 2; continue } - if (ch2 == "\"") break - k++ - } - } else if (ch == "[") { adepth++ } - else if (ch == "]") { adepth--; if (adepth == 0) break } + if (ch == "[") adepth++ + else if (ch == "]") adepth-- attr = attr ch - k++ + if (adepth <= 0) break } - if (attr ~ /(allow|expect)\([^]]*unsafe_code/) found = 1 - i = k - continue + k++ } + if (attr ~ /(allow|expect)\([^]]*unsafe_code/) g_found = 1 + i = k } - return found + return g_found } - BEGIN { exit(lex() ? 0 : 1) } + BEGIN { g_q = "\047"; g_found = 0; exit(lex() ? 0 : 1) } ' } # 只对「文本里出现过 unsafe_code」的文件跑词法器(标识符不可能被空白拆开,