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」的文件跑词法器(标识符不可能被空白拆开,