test(app-iced): 接线守卫改判「代码行」,注释掉不再算接线(M252 r1 P1-1)
复审实测:三处接线守卫都是纯文本 `contains`,把真接线注释掉(行注释,或 `/*` 独占一行的 块注释)之后 943 条用例一条不红;`field.rs` 那处连 rustc 的 unused-variable 兜底也能用 `let _submit` 一起绕掉 —— 功能整条静默失效时,测试与 lint 两条线同时不响。 - `pages/layout/source/code.rs`(新):`code_lines()` 剥 `//` 与跨行/可嵌套 `/* … */`, 字符串与字符字面量整段保留(`"http://x"` 里的 `//` 不剥);`code_line()` / `has_code_line()` 给「某行代码的行首就是那个调用」这条谓词。边界写在模块文档:原始字符串里出现 `"` 是唯一 可能朝松方向错的形态(本仓 src 现无此写法),其余偏差都是假红。 - 三处守卫改用它:`text_field_forwards_the_submit_callback`(行首是 `on_submit_maybe(` 且实参不是 `None`、消息来自 `submit_message`)、`the_rename_field_wires_enter`(回车与 「改名」按钮的行都含 `commit_rename()`,且都不含第二种页面意图 —— 一并关掉 r1 P2-1 「Debug 自证撑不住判据」)、`the_accent_field_wires_enter_to_commit`(含产地那条代码行)。 - `submit_is_wired_only_at_the_two_call_sites` 的 `scan()` 换成同一谓词并跳过测试文件 (守卫自己的 fixture 里也有 `.on_submit(` 样本)。 - T29 顺手收敛 scope 内的同族旧断言:`pages/instances/tests.rs` 的行距/散文接线判据与 `pages/instances/tests/i18n.rs` 的五条取词判据改走 `code_text()`(一行改动、方向不变)。
This commit is contained in:
1 parent
92a3c1bbc8
commit
a17f6fb29f
7 files changed
+464
-45
No files matched your search
@@ -210,22 +210,34 @@ fn submit_is_opt_in() {
|
|||||||
/// 回归(T27 反例 ①):`text_field` 必须把 `Field` 上的提交回调**转发**给 `text_input`。
|
/// 回归(T27 反例 ①):`text_field` 必须把 `Field` 上的提交回调**转发**给 `text_input`。
|
||||||
///
|
///
|
||||||
/// iced 的 `Element` 没有内省接口,所以这条守的是**源码结构**(本仓既有同一条纪律,见
|
/// iced 的 `Element` 没有内省接口,所以这条守的是**源码结构**(本仓既有同一条纪律,见
|
||||||
/// `pages::layout::source` 的模块文档)。两条断言合起来才等于「转发」:消息只从
|
/// `pages::layout::source` 的模块文档)。三条断言合起来才等于「转发」:有一**行代码**在调
|
||||||
/// `Field::submit_message()` 取(唯一读取口),且它被交给 `on_submit_maybe`。
|
/// `on_submit_maybe`、它的实参不是写死的 `None`、消息确实来自 `Field::submit_message`(唯一读取口)。
|
||||||
///
|
///
|
||||||
/// 抓得住:删掉 `.on_submit_maybe(..)` 那一行 / 改成 `on_submit_maybe(None)` / 把
|
/// **判据是「代码行」,不是「文本里还有那个字符串」**(M252 r1 的 P1-1):断言走
|
||||||
/// `Field::on_submit` 存着不读(变异实测见 M252 报告)——本条立刻红。
|
/// `source::code_line`,注释先被剥掉。所以下面这些改法都会红:
|
||||||
/// 抓不住:`text_input` 内部的键处理(那是 iced 的实现,不在这里)。
|
/// - 删掉 `.on_submit_maybe(..)` 那一行;
|
||||||
|
/// - 把它**注释掉**(`//` 行注释,或 `/*` 独占一行的块注释);
|
||||||
|
/// - 改成 `.on_submit_maybe(None)` ——**即使同时**把 `let submit` 写成 `let _submit` 把 rustc 的
|
||||||
|
/// unused-variable 兜底也绕掉(复审实测过:那样测试与 lint 两条线会同时静默)。
|
||||||
|
///
|
||||||
|
/// 抓不住:`text_input` 内部的键处理(iced 的实现);等价重构(把值先赋给别的名字再传)——
|
||||||
|
/// 那要同时改 needle,见 `pages::layout::source::code` 的边界一节。
|
||||||
#[test]
|
#[test]
|
||||||
fn text_field_forwards_the_submit_callback() {
|
fn text_field_forwards_the_submit_callback() {
|
||||||
let body = crate::pages::layout::source::function_body(FIELD_SOURCE, "pub fn text_field");
|
let body = crate::pages::layout::source::function_body(FIELD_SOURCE, "pub fn text_field");
|
||||||
|
let call =
|
||||||
|
crate::pages::layout::source::code_line(body, ".on_submit_maybe(").unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"`text_field` 里没有一行**代码**在调 `on_submit_maybe`(注释掉也算没有):\n{body}"
|
||||||
|
)
|
||||||
|
});
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("submit_message("),
|
!call.contains("None"),
|
||||||
"提交消息必须从 `Field::submit_message()` 取(唯一读取口),实际函数体:\n{body}"
|
"提交消息被写死成 `None`(回车什么都不发,且功能静默失效):{call}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
body.contains(".on_submit_maybe("),
|
crate::pages::layout::source::code_text(body).contains("submit_message("),
|
||||||
"取到之后必须交给 `text_input` 的 `on_submit_maybe`,实际函数体:\n{body}"
|
"提交消息必须从 `Field::submit_message()` 取(唯一读取口;注释里写不算),实际函数体:\n{body}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +248,8 @@ fn text_field_forwards_the_submit_callback() {
|
|||||||
/// 这条不靠「我看了一遍所有调用点」:白名单对不上就红;新增一处接线必须来这里登记,
|
/// 这条不靠「我看了一遍所有调用点」:白名单对不上就红;新增一处接线必须来这里登记,
|
||||||
/// 这份摩擦是故意的。
|
/// 这份摩擦是故意的。
|
||||||
///
|
///
|
||||||
/// 抓得住:任何一处**新增/删除**接在构建链上的 `.on_submit(..)`(本仓的链一律一行一条)。
|
/// 抓得住:任何一处**新增/删除/注释掉**接在构建链上的 `.on_submit(..)`(本仓的链一律一行一条;
|
||||||
|
/// 注释先被 [`scan`] 剥掉,所以 `/*` 独占一行的块注释也算删除)。
|
||||||
/// 抓不住:写在别处的同名文本(注释/字符串里的例子)、写成同一行的接线,也不管那两处接的
|
/// 抓不住:写在别处的同名文本(注释/字符串里的例子)、写成同一行的接线,也不管那两处接的
|
||||||
/// 消息对不对(由 `pages` 侧各自的用例钉)。
|
/// 消息对不对(由 `pages` 侧各自的用例钉)。
|
||||||
#[test]
|
#[test]
|
||||||
@@ -255,10 +268,13 @@ fn submit_is_wired_only_at_the_two_call_sites() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 递归找 `dir` 下**有一行以 `.on_submit(` 开头**的 `.rs`,记下相对 `root` 的路径(`/` 分隔)。
|
/// 递归找 `dir` 下**有一行代码以 `.on_submit(` 开头**的 `.rs`,记下相对 `root` 的路径(`/` 分隔)。
|
||||||
///
|
///
|
||||||
/// 为什么按「行首」而不是整篇 `contains`:注释与字符串里出现的 `.on_submit(` 例子(包括本文件
|
/// 谓词与三处接线守卫同一个(`source::has_code_line`):注释与字符串里出现的 `.on_submit(`
|
||||||
/// 自己)会假红。代价是**写成同一行的接线扫不到**——本仓的构建链一律一行一条,这条取舍记在上面。
|
/// 例子不算接线。代价是**写成同一行的接线扫不到**——本仓的构建链一律一行一条。
|
||||||
|
///
|
||||||
|
/// **测试文件跳过**([`is_test_path`]):守卫自己的 fixture 里有 `.on_submit(` 的样本(本文件、
|
||||||
|
/// `pages/layout/source/code/tests.rs`),那不是调用点;测试也不会去接生产 UI。
|
||||||
fn scan(root: &std::path::Path, dir: &std::path::Path, found: &mut Vec<String>) {
|
fn scan(root: &std::path::Path, dir: &std::path::Path, found: &mut Vec<String>) {
|
||||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||||
return;
|
return;
|
||||||
@@ -267,17 +283,26 @@ fn scan(root: &std::path::Path, dir: &std::path::Path, found: &mut Vec<String>)
|
|||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
scan(root, &path, found);
|
scan(root, &path, found);
|
||||||
} else if path.extension().is_some_and(|ext| ext == "rs") {
|
} else if path.extension().is_some_and(|ext| ext == "rs") && !is_test_path(&path) {
|
||||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if text
|
if crate::pages::layout::source::has_code_line(&text, ".on_submit(") {
|
||||||
.lines()
|
|
||||||
.any(|line| line.trim_start().starts_with(".on_submit("))
|
|
||||||
{
|
|
||||||
let rel = path.strip_prefix(root).unwrap_or(&path);
|
let rel = path.strip_prefix(root).unwrap_or(&path);
|
||||||
found.push(rel.to_string_lossy().replace('\\', "/"));
|
found.push(rel.to_string_lossy().replace('\\', "/"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 这个路径是不是**测试模块**的文件:任一组件叫 `tests` / `tests.rs`,或文件名以 `_tests.rs` 收尾。
|
||||||
|
///
|
||||||
|
/// 本仓的测试文件就这几种形状(`*/tests.rs`、`*/tests/*.rs`、`*_tests.rs`),全部由
|
||||||
|
/// `#[cfg(test)]` 或 `tests/` 目标挂载;生产文件的 inline `mod tests` 不在这里判(那些文件本身
|
||||||
|
/// 也可能是真调用点,例如 `section/accent.rs`)。
|
||||||
|
fn is_test_path(path: &std::path::Path) -> bool {
|
||||||
|
path.components().any(|component| {
|
||||||
|
let name = component.as_os_str().to_string_lossy();
|
||||||
|
name == "tests" || name == "tests.rs" || name.ends_with("_tests.rs")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -98,27 +98,32 @@ fn list_column_spaces_rows_only_from_the_shared_constant() {
|
|||||||
let rows_chain = source::statement(file, "let mut rows = column(");
|
let rows_chain = source::statement(file, "let mut rows = column(");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("ROW_SPACING"),
|
source::code_text(&body).contains("ROW_SPACING"),
|
||||||
"列表行距必须从 ROW_SPACING 取,不许另写:\n{body}"
|
"列表行距必须从 ROW_SPACING 取(注释里写不算),不许另写:\n{body}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("LIST_BOTTOM_INSET"),
|
source::code_text(&body).contains("LIST_BOTTOM_INSET"),
|
||||||
"底部内缩必须从 LIST_BOTTOM_INSET 取:\n{body}"
|
"底部内缩必须从 LIST_BOTTOM_INSET 取(注释里写不算):\n{body}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// ① 行容器的构建链上不许叠间距(`column![head, scroller]` 上的合法间距不在这条链里)
|
// ① 行容器的构建链上不许叠间距(`column![head, scroller]` 上的合法间距不在这条链里)。
|
||||||
|
// 判据走「代码行」(`source::code_text` 剥注释):注释掉的 `.spacing(..)` 等于没有间距。
|
||||||
assert!(
|
assert!(
|
||||||
!rows_chain.contains(".spacing("),
|
!source::code_text(rows_chain).contains(".spacing("),
|
||||||
"行容器的构建链上又出现了间距(M102 ② 的页级附加量):\n{rows_chain}"
|
"行容器的构建链上又出现了间距(M102 ② 的页级附加量):\n{rows_chain}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// ② 语句序列:每一处 `rows.spacing(…)` 的实参都必须**顺绑定追回 `ROW_SPACING`**
|
// ② 语句序列:每一处 `rows.spacing(…)` 的实参都必须**顺绑定追回 `ROW_SPACING`**
|
||||||
// (直接用它、或 `let spacing = ROW_SPACING` 再中转都算;先 `let extra = Space::X1;`
|
// (直接用它、或 `let spacing = ROW_SPACING` 再中转都算;先 `let extra = Space::X1;`
|
||||||
// 再 `rows.spacing(ui.space(extra))` 则红——r4 复审的变异 M4)
|
// 再 `rows.spacing(ui.space(extra))` 则红——r4 复审的变异 M4)
|
||||||
|
//
|
||||||
|
// 判据走**代码文本**(`source::code_text` 剥注释):注释掉的 `rows.spacing(..)` 不算「应用了间距」,
|
||||||
|
// 否则把那一行注释掉整条用例会绿着通过(T29 的同族退化)。
|
||||||
|
let body = source::code_text(&body);
|
||||||
let applied = source::call_arguments(&body, "rows.spacing(");
|
let applied = source::call_arguments(&body, "rows.spacing(");
|
||||||
assert!(
|
assert!(
|
||||||
!applied.is_empty(),
|
!applied.is_empty(),
|
||||||
"`list_column()` 里没有把 ROW_SPACING 应用到行容器的调用:\n{body}"
|
"`list_column()` 里没有把 ROW_SPACING 应用到行容器的调用(注释掉也算没有):\n{body}"
|
||||||
);
|
);
|
||||||
for argument in &applied {
|
for argument in &applied {
|
||||||
let ident = argument
|
let ident = argument
|
||||||
@@ -155,11 +160,11 @@ fn the_detail_note_goes_through_the_prose_box() {
|
|||||||
let body = source::function_body(include_str!("../instances/detail.rs"), "fn push_text");
|
let body = source::function_body(include_str!("../instances/detail.rs"), "fn push_text");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("frame::prose("),
|
source::code_text(body).contains("frame::prose("),
|
||||||
"正文段没有走 frame::prose:\n{body}"
|
"正文段没有走 frame::prose(注释里写不算):\n{body}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!body.contains("frame::body("),
|
!source::code_text(body).contains("frame::body("),
|
||||||
"正文段被塞进了整页块(会失去 72 字符行宽承诺):\n{body}"
|
"正文段被塞进了整页块(会失去 72 字符行宽承诺):\n{body}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
use unlucky_i18n::catalog::MISSING;
|
use unlucky_i18n::catalog::MISSING;
|
||||||
use unlucky_i18n::{Catalog, Key, Lang};
|
use unlucky_i18n::{Catalog, Key, Lang};
|
||||||
|
|
||||||
|
use crate::pages::layout::source;
|
||||||
|
|
||||||
use super::super::Texts;
|
use super::super::Texts;
|
||||||
|
|
||||||
/// 中文界面里允许出现的 ASCII 词:**品牌名与技术标识**。
|
/// 中文界面里允许出现的 ASCII 词:**品牌名与技术标识**。
|
||||||
@@ -341,24 +343,24 @@ fn every_english_literal_in_the_page_sources_is_whitelisted() {
|
|||||||
.unwrap_or_else(|| panic!("PAGE_SOURCES 里没有 {name}:拆文件之后忘了登记?"))
|
.unwrap_or_else(|| panic!("PAGE_SOURCES 里没有 {name}:拆文件之后忘了登记?"))
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
source("instances.rs").contains("Texts::of("),
|
source::code_text(source("instances.rs")).contains("Texts::of("),
|
||||||
"实例页没有从 Texts 取词"
|
"实例页没有从 Texts 取词(注释里写不算)"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
source("instances.rs").contains("Header::new(texts.title())"),
|
source::code_text(source("instances.rs")).contains("Header::new(texts.title())"),
|
||||||
"页头标题必须是词表里的那条(`Header::new` 里写死字符串 = 中文界面上顶着英文标题)"
|
"页头标题必须是词表里的那条(`Header::new` 里写死字符串 = 中文界面上顶着英文标题)"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
source("instances/text.rs").contains("Key::Instances"),
|
source::code_text(source("instances/text.rs")).contains("Key::Instances"),
|
||||||
"text.rs 没有引用本族的 key"
|
"text.rs 没有引用本族的 key(注释里写不算)"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
source("app/view/instances.rs").contains("Key::InstancesStage"),
|
source::code_text(source("app/view/instances.rs")).contains("Key::InstancesStage"),
|
||||||
"阶段标签没有走目录"
|
"阶段标签没有走目录(注释里写不算)"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
source("instances/launch_args.rs").contains("texts.launch_title()"),
|
source::code_text(source("instances/launch_args.rs")).contains("texts.launch_title()"),
|
||||||
"启动参数区没有走目录"
|
"启动参数区没有走目录(注释里写不算)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ use super::super::state::Message as PageMessage;
|
|||||||
///
|
///
|
||||||
/// `PageMessage` 只有 `Debug + Clone`(没有 `PartialEq`,那些枚举不在本 mission 的 scope),
|
/// `PageMessage` 只有 `Debug + Clone`(没有 `PartialEq`,那些枚举不在本 mission 的 scope),
|
||||||
/// 所以这里比 `Debug` 文本:枚举的 `Debug` 打印变体名与字段,变体不同必然文本不同。
|
/// 所以这里比 `Debug` 文本:枚举的 `Debug` 打印变体名与字段,变体不同必然文本不同。
|
||||||
|
///
|
||||||
|
/// 这条只钉「产地生产什么」;**「两条路都用了这个产地」由 [`the_rename_field_wires_enter`] 钉**
|
||||||
|
/// (r1 复审 P2-1:上一版把两件事写进这里,而实际覆盖只在文本型断言里)。
|
||||||
#[test]
|
#[test]
|
||||||
fn enter_takes_the_same_message_as_the_rename_button() {
|
fn enter_takes_the_same_message_as_the_rename_button() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -33,20 +36,49 @@ fn enter_takes_the_same_message_as_the_rename_button() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `rename_row` 里那枚输入框真的接了 `on_submit`,且接的是 `commit_rename`(与按钮同一个)。
|
/// 回车与「改名」按钮**在源码里**都接 `commit_rename()`,且都没接第二种页面意图。
|
||||||
///
|
///
|
||||||
/// 抓得住(T27 反例):把 `.on_submit(..)` 那一行删掉 → 第一条断言红;把它接成
|
/// 判据是**代码行**(`source::code_line` 先剥注释),所以下面这些改法都会红:
|
||||||
/// `CancelRenameWorld` → 第二条断言红。
|
/// - 把 `.on_submit(..)` 那一行删掉,或注释掉(`//` / `/*` 独占一行的块注释);
|
||||||
/// 抓不住:回车键真的到达控件(iced 的键路径,见模块文档)。
|
/// - 把它接成别的页面意图(取消 / 重开 / 逐键输入)——按钮那一行同样不许;
|
||||||
|
/// - 让两条路走**不同**的产地(例如按钮仍写 `PageMessage::CommitRenameWorld` 字面量):
|
||||||
|
/// 那时按钮那行不含 `commit_rename()`,第二条断言红。
|
||||||
|
///
|
||||||
|
/// 抓不住:回车键真的到达控件(那是 iced 的 `Widget::update`,见模块文档);同一行里写完的
|
||||||
|
/// 等价接线(本仓构建链一律一行一条)。
|
||||||
#[test]
|
#[test]
|
||||||
fn the_rename_field_wires_enter() {
|
fn the_rename_field_wires_enter() {
|
||||||
let body = source::function_body(include_str!("../saves/rows.rs"), "fn rename_row");
|
let body = source::function_body(include_str!("../saves/rows.rs"), "fn rename_row");
|
||||||
|
let enter = source::code_line(body, ".on_submit(").unwrap_or_else(|| {
|
||||||
|
panic!("`rename_row` 里没有一行**代码**接 `on_submit`(注释掉也算没有):\n{body}")
|
||||||
|
});
|
||||||
assert!(
|
assert!(
|
||||||
body.contains(".on_submit("),
|
enter.contains("commit_rename()"),
|
||||||
"输入框没接回车(去掉接线这条必须红),实际函数体:\n{body}"
|
"回车必须接 `commit_rename()`(与「改名」按钮同一个产地):{enter}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let button = source::code_lines(body)
|
||||||
|
.into_iter()
|
||||||
|
.find(|line| line.contains("saves_rename_confirm()"))
|
||||||
|
.unwrap_or_else(|| panic!("找不到「改名」按钮那一行:\n{body}"));
|
||||||
assert!(
|
assert!(
|
||||||
body.matches("commit_rename()").count() >= 2,
|
button.contains("commit_rename()"),
|
||||||
"回车与「改名」按钮必须共用 `commit_rename()`(两条路同一个 Message),实际函数体:\n{body}"
|
"「改名」按钮必须用同一个产地(RenameWorld 只有一处写法):{button}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// P2-1:两条路都只许这一个页面意图——接成别的(取消/重开/逐键输入)就是行为变了。
|
||||||
|
for line in [&enter, &button] {
|
||||||
|
for other in [
|
||||||
|
"CancelRenameWorld",
|
||||||
|
"StartRenameWorld",
|
||||||
|
"RenameWorldChanged",
|
||||||
|
"BackupWorld",
|
||||||
|
"RequestDeleteWorld",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!line.contains(other),
|
||||||
|
"回车/按钮接成了别的页面意图({other}):{line}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,16 @@
|
|||||||
//! 所以它只配一条**明确的契约**用:`frame::body` / `frame::prose` / `list_column` 这几处的
|
//! 所以它只配一条**明确的契约**用:`frame::body` / `frame::prose` / `list_column` 这几处的
|
||||||
//! 写法本身就是契约的一部分(它们各自只有 5~10 行),不是为了抠实现细节。
|
//! 写法本身就是契约的一部分(它们各自只有 5~10 行),不是为了抠实现细节。
|
||||||
//! 真要守像素,还是得靠软件渲染金图(`shot::diff`,绑 Windows + 字体栈,属另一个决定)。
|
//! 真要守像素,还是得靠软件渲染金图(`shot::diff`,绑 Windows + 字体栈,属另一个决定)。
|
||||||
|
//!
|
||||||
|
//! ## 「接线在不在」必须走 [`code_line`](T29,M252 r1 的 P1)
|
||||||
|
//!
|
||||||
|
//! 文本判据有个致命退化:把真接线**注释掉**、字符串留在注释里,`contains(..)` 照样绿。
|
||||||
|
//! 所以断言「某处接了 `X`」时不许直接 `contains`,统一走 [`code_line`] / [`has_code_line`]
|
||||||
|
//! (注释剥掉、按行首判,边界见 [`code`] 的模块文档)。
|
||||||
|
|
||||||
|
mod code;
|
||||||
|
|
||||||
|
pub(crate) use code::{code_line, code_lines, code_text, has_code_line};
|
||||||
|
|
||||||
/// 把 `source` 里第一个 `signature` 起始、到该函数**顶层**右花括号为止的文本切出来。
|
/// 把 `source` 里第一个 `signature` 起始、到该函数**顶层**右花括号为止的文本切出来。
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
//! **剥注释**的行扫描:接线守卫的判据只能看「代码行」,不能看「文本里还有没有那个字符串」。
|
||||||
|
//!
|
||||||
|
//! ## 为什么要有这个文件(T29 的第三次复发)
|
||||||
|
//!
|
||||||
|
//! 接线守卫是拿源码文本当判据的(`Element` 没有内省接口,见 [`super`] 的模块文档)。用
|
||||||
|
//! `text.contains(".on_submit(")` 写时,把真接线**注释掉**就整条骗过:字符串还在,功能没了。
|
||||||
|
//! M252 复审实测了四种注释型变异(行注释 / `/*` 独占一行的块注释 / `.on_submit_maybe(None)`
|
||||||
|
//! 再加 `let _submit` 绕掉 rustc 的 unused 兜底),**943 条用例一条不红**;同一批「真删除」
|
||||||
|
//! 变异则每条都红。⇒ 守卫不是没用,是**只认一种改法**。
|
||||||
|
//!
|
||||||
|
//! 所以:接线类断言一律先过 [`code_lines`](注释剥掉、只留代码),再按**行首**判 [`code_line`]。
|
||||||
|
//!
|
||||||
|
//! ## 剥什么(三条都要记)
|
||||||
|
//!
|
||||||
|
//! | 形态 | 处理 |
|
||||||
|
//! |---|---|
|
||||||
|
//! | `// …` / `/// …` / `//! …` | 剥到行尾 |
|
||||||
|
//! | `/* … */`,**跨行、可嵌套**(Rust 允许嵌套块注释) | 整段剥掉;行结构保留(只被注释占满的行变空串) |
|
||||||
|
//! | 字符串字面量里的 `//` / `/*`(`"http://x"`、`"a /* b"`) | **不剥**:字面量整段原样搬过去 |
|
||||||
|
//! | 字符字面量 `'"'` / `'\''` | 整段搬过去,不会误开字符串 |
|
||||||
|
//!
|
||||||
|
//! ## 剥不了什么(边界,改守卫前先读这一节)
|
||||||
|
//!
|
||||||
|
//! 1. **原始字符串**没有被特判,只是**偶然正确**:`r"…"` 与 `r#"…"#` 里的那个 `"` 会被当普通
|
||||||
|
//! 字符串起点,于是内容(含 `//`)原样保留。真正会出错的是**原始字符串里出现 `"` 或 `\"`**
|
||||||
|
//! (`r#"a " b // c"#`):扫描器会提前收尾,后面那段按代码处理 ⇒ 若那段正好是注释就会**漏剥**
|
||||||
|
//! (方向变成「假绿」)。这是本模块唯一能朝松方向错的形态,登记在此。本仓 `src/**` 现在没有
|
||||||
|
//! 这种写法(8 处 `r#"` 全是 JSON 字面量,无内嵌引号/反斜杠)。
|
||||||
|
//! 2. **跨行普通字符串**(行尾 `\` 续行)**不识别**:字符串状态每行复位,续行的下一行会当代码。
|
||||||
|
//! 后果是**假红**(把字符串里的一段当代码看见,反而更啰嗦)。
|
||||||
|
//! 3. 它**不判语义**:注释剥掉之后,「`.on_submit(..)` 在不在」仍然只是文本事实。等价重构
|
||||||
|
//! (先赋值再调用)可能绕过——所以接线守卫的 needle 与写法要一起维护(见各守卫的 docstring)。
|
||||||
|
//!
|
||||||
|
//! 除第 1 条那个狭窄形态外,其余偏差都是**假红**:宁可假红,也不要「注释掉仍绿」。
|
||||||
|
|
||||||
|
/// 逐行的代码文本(注释已剥)。**行数与输入一一对应**:只被注释占满的行变成空串。
|
||||||
|
///
|
||||||
|
/// 保留行结构是为了让失败信息能直接对回源码行,也让「行首」这条谓词有意义。
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn code_lines(source: &str) -> Vec<String> {
|
||||||
|
let mut lines = Vec::with_capacity(source.lines().count());
|
||||||
|
// 块注释的嵌套深度(0 = 不在注释里)。跨行靠它带着走。
|
||||||
|
let mut depth = 0_usize;
|
||||||
|
|
||||||
|
for line in source.lines() {
|
||||||
|
let chars: Vec<char> = line.chars().collect();
|
||||||
|
let mut code = String::with_capacity(chars.len());
|
||||||
|
let mut index = 0;
|
||||||
|
|
||||||
|
while index < chars.len() {
|
||||||
|
let ch = chars[index];
|
||||||
|
|
||||||
|
if depth > 0 {
|
||||||
|
if ch == '/' && chars.get(index + 1) == Some(&'*') {
|
||||||
|
depth += 1;
|
||||||
|
index += 2;
|
||||||
|
} else if ch == '*' && chars.get(index + 1) == Some(&'/') {
|
||||||
|
depth -= 1;
|
||||||
|
index += 2;
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '/' && chars.get(index + 1) == Some(&'/') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ch == '/' && chars.get(index + 1) == Some(&'*') {
|
||||||
|
depth = 1;
|
||||||
|
index += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '"' {
|
||||||
|
index = copy_string_literal(&chars, index, &mut code);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ch == '\'' {
|
||||||
|
index = copy_char_literal(&chars, index, &mut code);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.push(ch);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(code.trim_end().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 把从 `start`(一个 `"`)起的字符串字面量原样搬进 `code`,返回下一个未处理的下标。
|
||||||
|
///
|
||||||
|
/// 「原样」= 里面的 `//`、`/*` 都不当注释(这条正是本模块存在的理由之一)。
|
||||||
|
/// 行内没有闭合引号时搬到最后(跨行字符串见模块文档第 2 条边界)。
|
||||||
|
fn copy_string_literal(chars: &[char], start: usize, code: &mut String) -> usize {
|
||||||
|
code.push(chars[start]);
|
||||||
|
let mut index = start + 1;
|
||||||
|
|
||||||
|
while index < chars.len() {
|
||||||
|
let ch = chars[index];
|
||||||
|
code.push(ch);
|
||||||
|
index += 1;
|
||||||
|
|
||||||
|
match ch {
|
||||||
|
// 转义:把被转义的字符一起搬走(`\"` 不会提前收尾)
|
||||||
|
'\\' => {
|
||||||
|
if index < chars.len() {
|
||||||
|
code.push(chars[index]);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'"' => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 把从 `start`(一个 `'`)起的**字符字面量**原样搬过去;生命周期(`'a`)只搬那个引号。
|
||||||
|
///
|
||||||
|
/// 为什么值得单独判:`'"'` 会让「见到 `"` 就当字符串开头」的扫描器错位,于是整行的注释剥错。
|
||||||
|
fn copy_char_literal(chars: &[char], start: usize, code: &mut String) -> usize {
|
||||||
|
code.push(chars[start]);
|
||||||
|
let index = start + 1;
|
||||||
|
|
||||||
|
// `'\n'` / `'\''`:反斜杠 + 一个字符 + 收尾引号
|
||||||
|
if chars.get(index) == Some(&'\\') && chars.get(index + 2) == Some(&'\'') {
|
||||||
|
for offset in index..index + 3 {
|
||||||
|
if let Some(ch) = chars.get(offset) {
|
||||||
|
code.push(*ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index + 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `'x'`:一个字符 + 收尾引号
|
||||||
|
if chars.get(index + 1) == Some(&'\'') {
|
||||||
|
if let Some(ch) = chars.get(index) {
|
||||||
|
code.push(*ch);
|
||||||
|
}
|
||||||
|
code.push('\'');
|
||||||
|
return index + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期 / 未闭合:只搬引号,余下的照常扫
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `source` 里第一条**去注释后行首是 `prefix`** 的代码行(`None` = 没有这样一行)。
|
||||||
|
///
|
||||||
|
/// 接线守卫统一用这个谓词:`code_line(rows_rs, ".on_submit(")` —— 那行被注释掉就查不到。
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn code_line(source: &str, prefix: &str) -> Option<String> {
|
||||||
|
code_lines(source)
|
||||||
|
.into_iter()
|
||||||
|
.find(|line| line.trim_start().starts_with(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`code_line`] 的布尔形态(全文件扫描那种只关心「有没有」的场合)。
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn has_code_line(source: &str, prefix: &str) -> bool {
|
||||||
|
code_line(source, prefix).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 整份源码剥注释后的文本(行结构保留)。
|
||||||
|
///
|
||||||
|
/// **T29 的统一收敛口**:仓库里还有一批 `source.contains("…")` 形式的接线断言(见 M252 r1
|
||||||
|
/// 报告的清单)。它们在问的是同一件事——「这段接线在不在」——只是写法更早。把
|
||||||
|
/// `source.contains(..)` 换成 `code_text(&source).contains(..)` 是一行改动:判据方向不变,
|
||||||
|
/// 但「注释掉仍绿」这条退化被堵上。新写的断言优先用 [`code_line`](能钉「行首是哪个调用」)。
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn code_text(source: &str) -> String {
|
||||||
|
code_lines(source).join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
//! [`code_lines`] 自己的断言(门禁的门禁,T28:守卫守不到自己就等于没有)。
|
||||||
|
//!
|
||||||
|
//! 这一族用例的形状统一:**喂一段源码文本 → 断言剥完剩下什么**。它们不读真文件,所以不会因为
|
||||||
|
//! 别人改页面而假红;真文件上的效果由三处接线守卫自己证。
|
||||||
|
|
||||||
|
use super::{code_line, code_lines, has_code_line};
|
||||||
|
|
||||||
|
/// 行结构必须保留:只被注释占满的行是空串,代码行原样(去掉行尾空白)。
|
||||||
|
#[test]
|
||||||
|
fn it_strips_line_comments_and_keeps_the_line_count() {
|
||||||
|
let source = "\
|
||||||
|
let a = 1; // 尾注释
|
||||||
|
// 整行注释
|
||||||
|
/// 文档注释
|
||||||
|
let b = 2;
|
||||||
|
/* 单行块注释 */
|
||||||
|
let c = 3;
|
||||||
|
";
|
||||||
|
let lines = code_lines(source);
|
||||||
|
assert_eq!(lines.len(), 6, "行数必须一一对应:{lines:?}");
|
||||||
|
assert_eq!(lines[0], "let a = 1;");
|
||||||
|
assert_eq!(lines[1], "");
|
||||||
|
assert_eq!(lines[2], "");
|
||||||
|
assert_eq!(lines[3], "let b = 2;");
|
||||||
|
assert_eq!(lines[4], "");
|
||||||
|
assert_eq!(lines[5], "let c = 3;");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 跨行 / 嵌套块注释:整段剥掉,注释结束处的代码要接着留下来。
|
||||||
|
#[test]
|
||||||
|
fn it_strips_block_comments_across_lines_and_nested() {
|
||||||
|
let source = "\
|
||||||
|
let a = 1;
|
||||||
|
/* 开头 /* 嵌套 */ 还在注释里
|
||||||
|
.on_submit(comment_only()) <- 这一行整个在注释里
|
||||||
|
*/ let b = 2;
|
||||||
|
let c = /* 行内 */ 3;
|
||||||
|
";
|
||||||
|
let lines = code_lines(source);
|
||||||
|
assert_eq!(lines[0], "let a = 1;");
|
||||||
|
assert_eq!(lines[1], "");
|
||||||
|
assert_eq!(lines[2], "", "块注释里的「接线」不许当成代码");
|
||||||
|
assert_eq!(lines[3], " let b = 2;", "注释结束后同一行的代码要留下");
|
||||||
|
assert_eq!(lines[4], "let c = 3;");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 字符串字面量里的 `//` 与 `/*` **不剥**(否则会把后半行代码剥掉 ⇒ 假红方向)。
|
||||||
|
#[test]
|
||||||
|
fn it_keeps_slashes_inside_string_literals() {
|
||||||
|
let source = r##"
|
||||||
|
let url = "http://example.com/a//b"; let after = 1;
|
||||||
|
let odd = "a /* not a comment */ b"; let tail = 2;
|
||||||
|
let raw = r#"a // b"#; let tail2 = 3;
|
||||||
|
"##;
|
||||||
|
let lines = code_lines(source);
|
||||||
|
assert!(
|
||||||
|
lines[1].contains("http://example.com/a//b"),
|
||||||
|
"{:?}",
|
||||||
|
lines[1]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
lines[1].contains("let after = 1;"),
|
||||||
|
"字符串里的 `//` 不该吃掉后半行:{:?}",
|
||||||
|
lines[1]
|
||||||
|
);
|
||||||
|
assert!(lines[2].contains("let tail = 2;"), "{:?}", lines[2]);
|
||||||
|
// 原始字符串**没有特判**:靠其中那个 `"` 偶然按普通字符串处理(模块文档边界第 1 条)。
|
||||||
|
assert!(lines[3].contains("a // b"), "{:?}", lines[3]);
|
||||||
|
assert!(lines[3].contains("let tail2 = 3;"), "{:?}", lines[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 已知缺口(模块文档边界第 1 条):原始字符串里出现 `"` 时扫描器提前收尾,后面那段按代码看。
|
||||||
|
///
|
||||||
|
/// 这条用例把那个**朝松方向**的形态钉住:不是「希望这样」,是「哪天修了要知道」。
|
||||||
|
#[test]
|
||||||
|
fn raw_strings_with_inner_quotes_are_a_known_gap() {
|
||||||
|
let source = "let s = r#\"a \" b // c\"#; let after = 1;\n";
|
||||||
|
let lines = code_lines(source);
|
||||||
|
assert!(
|
||||||
|
!lines[0].contains("let after = 1;"),
|
||||||
|
"已知缺口:内嵌引号的原始字符串会让后半行被当注释剥掉(这里只钉现状):{:?}",
|
||||||
|
lines[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 字符字面量里的引号不会让扫描器错位(`'"'` 之后还有注释与代码)。
|
||||||
|
#[test]
|
||||||
|
fn char_literals_do_not_open_a_string() {
|
||||||
|
let source = "\
|
||||||
|
let quote = '\"'; let after = 1; // 尾注释
|
||||||
|
let nl = '\\n'; let more = 2;
|
||||||
|
let lifetime: &'a str = \"x\";
|
||||||
|
";
|
||||||
|
let lines = code_lines(source);
|
||||||
|
assert!(lines[0].contains("let after = 1;"), "{:?}", lines[0]);
|
||||||
|
assert!(!lines[0].contains("尾注释"), "{:?}", lines[0]);
|
||||||
|
assert!(lines[1].contains("let more = 2;"), "{:?}", lines[1]);
|
||||||
|
assert!(lines[2].contains("&'a str = \"x\""), "{:?}", lines[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 行首谓词:注释里的「接线」查不到,代码里的查得到(本模块的全部意义)。
|
||||||
|
#[test]
|
||||||
|
fn the_line_predicate_only_sees_code() {
|
||||||
|
let commented = "\
|
||||||
|
let field = text_field(
|
||||||
|
ui,
|
||||||
|
Field::new(text)
|
||||||
|
// .on_submit(|| Message::Instances(commit_rename())),
|
||||||
|
,
|
||||||
|
on_input,
|
||||||
|
);
|
||||||
|
";
|
||||||
|
assert!(
|
||||||
|
!has_code_line(commented, ".on_submit("),
|
||||||
|
"注释掉的那一行不许被当成交接"
|
||||||
|
);
|
||||||
|
|
||||||
|
let blocked = "\
|
||||||
|
let field = text_field(
|
||||||
|
ui,
|
||||||
|
Field::new(text)
|
||||||
|
/*
|
||||||
|
.on_submit(|| Message::Instances(commit_rename())),
|
||||||
|
*/
|
||||||
|
,
|
||||||
|
on_input,
|
||||||
|
);
|
||||||
|
";
|
||||||
|
assert!(
|
||||||
|
!has_code_line(blocked, ".on_submit("),
|
||||||
|
"块注释掉的那一行不许被当成交接"
|
||||||
|
);
|
||||||
|
|
||||||
|
let wired = "\
|
||||||
|
let field = text_field(
|
||||||
|
ui,
|
||||||
|
Field::new(text)
|
||||||
|
.on_submit(|| Message::Instances(commit_rename())),
|
||||||
|
on_input,
|
||||||
|
);
|
||||||
|
";
|
||||||
|
let line = code_line(wired, ".on_submit(").expect("接在代码里就该查得到");
|
||||||
|
assert!(line.contains("commit_rename()"), "{line}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// needle 带实参时才分得清「转发」与「写死 None」——这正是 M252 复审的第二格。
|
||||||
|
#[test]
|
||||||
|
fn the_predicate_tells_a_wired_argument_from_a_hardcoded_none() {
|
||||||
|
let wired = " .on_submit_maybe(submit)\n";
|
||||||
|
let none = " .on_submit_maybe(None)\n";
|
||||||
|
assert!(has_code_line(wired, ".on_submit_maybe("));
|
||||||
|
assert!(
|
||||||
|
has_code_line(none, ".on_submit_maybe("),
|
||||||
|
"两条都「有这一行」"
|
||||||
|
);
|
||||||
|
|
||||||
|
let picked = |source: &str| {
|
||||||
|
code_line(source, ".on_submit_maybe(")
|
||||||
|
.expect("有那一行")
|
||||||
|
.contains("None")
|
||||||
|
};
|
||||||
|
assert!(!picked(wired), "传的是值,不是 None");
|
||||||
|
assert!(picked(none), "写死 None 必须被认出来");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user