fix(ci): M91 r4 —— 属性内复用主状态机(终结属性内 raw string / 字符字面量盲区)

根因:属性外的状态机(普通/行注释/块注释/字符串/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 个文件给出完全一致的结果(恰好一个落点)
This commit is contained in:
AskaEth committed 2026-11-02 15:33:48 +08:00
1 parent 03ce813c32
commit 5d58301e2e
1 file changed
+94 -71
+94 -71
View File
@@ -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」的文件跑词法器(标识符不可能被空白拆开,