docs(app-iced): M101 r3 —— 补「页外值 / 首帧取数」共享点,守门收窄,文档口径统一
r2 复审 P1:报告与代码文档的「仍然共享」清单只写了 3 项,漏了「读页外真值 / 首帧取数」这半边。
本轮按复审给的第一条路(文档/契约层面诚实交代,不动结构):
- `settings/mod.rs` 的「仍然共享」扩到 6 条,新增:
4. 读页外真值 —— `States` 只有 appearance + groups,General 要显示 `Model.lang` 就得改
`settings/state.rs` + `pages/mod.rs`(2 个共享文件)或在 `app/state.rs` 用真值初始化
`Groups.language_appearance`(1 个);给出建议形状:一次性给 `States` 加 `model: &'a Model`。
5. 首帧取数 / 异步发起 —— `Body` 返回不了 `Task`,组自己发不了首帧探测;两条路(`Boot` 分支派发
1 个共享文件;或把 `Body` 返回类型加一条 `Task` 旁路,1 个文件一次性)与「不改的代价」都写明。
6. 隐性代价:`Groups: Sync + Default`(`demo_groups` 的 `static LazyLock` 撑 harness),
非 `Sync` 字段会让编译红在共享的 `settings/state.rs`;给了改法。
- `general.rs` 文档改成正确指引(消息加在本线的 `language_appearance.rs`,落点
`app/update/settings/language_appearance.rs`,**不要**改 `pages/mod.rs` / `pages/message.rs` /
`app/update/page_messages.rs`),并指向上面第 4、5 条。
- `tests/mod.rs` 的「接真控件改 3 个文件」订正为 2 个,并删掉对**已被删除**的
`only_the_appearance_group_carries_a_real_control_body` 的点名;`appearance_section.rs` 的
模块文档与断言消息同步订正(`body = Some(appearance::body)`)。
- 守门 `the_group_dispatch_has_no_catch_all_arm` 判据**结构化收窄**:用大括号深度计数只看
`match message {` 本层(深度 1)的臂,嵌套 match 整段跳过、`|` 折行 or-pattern 续行放行;
只有「本层里含 `=>` 且不以该组变体路径开头」才算兜底。新增两条守门自测:
`the_guard_ignores_nested_matches_and_folded_or_patterns`(复审 P2-3 的假红探针)与
`the_guard_still_catches_both_catch_all_shapes`(`_ =>` 与 `other =>` 仍要红);
误伤类别如实写进测试 doc 的「抓不住什么」。
- P3:`java::Message::Ping` 的文档改成指向真实存在的绊线(`tests/java_section.rs` 的
`the_java_group_is_still_demo_rows`);`demo_groups()` 文档写明 `Sync + Default` 的隐性要求。
This commit is contained in:
1 parent
029ece3f17
commit
c166944c8e
7 files changed
+206
-55
No files matched your search
@@ -4,10 +4,39 @@
|
||||
//! 兜底会把「加了变体忘了接线」从**编译错误**降级成「点了没反应」。两级 dispatch 各守一条:
|
||||
//! 那一层守 `pages::Message`,本文件守 `GroupMessage` 与四个 handler。
|
||||
//!
|
||||
//! r1 复审的 P3-2 指出上一版有洞:只拦 `_ =>` 的话,`other => { … }` 这种 **binding 兜底**
|
||||
//! 照样取消穷尽性、照样静默。本版判据反过来写——**除已知臂外,任何含 `=>` 的行都算兜底**:
|
||||
//! `settings.rs` 里每条臂都必须以 `pages::settings::GroupMessage::` 开头且恰好 4 条;handler 里
|
||||
//! 每条臂都必须以该组自己的变体路径开头;三个还是空枚举的 handler 一行 `=>` 都不许有。
|
||||
//! ## 判据(r2 复审 P2-3 之后收窄过一次)
|
||||
//!
|
||||
//! 上一版的判据是「除已知前缀外,任何含 `=>` 的行都算兜底」——**过宽**:往 handler 里加一个
|
||||
//! **没有兜底臂**的嵌套 match(`match true { true => …, false => … }`),或者 rustfmt 把长
|
||||
//! or-pattern 折行成 `| X =>`,都会让它假红。四条填页线一写真实逻辑就撞,守门反而推高成本。
|
||||
//!
|
||||
//! 现在的判据是**结构化的**:只找 `match message {` 这一层(用大括号深度计数把嵌套 match 排除
|
||||
//! 在外),**只看深度 1 的臂**——
|
||||
//!
|
||||
//! - `app/update/settings.rs` 的 `on_settings`:恰好 **4 条**臂,每条以 `pages::settings::GroupMessage::`
|
||||
//! (或短写 `GroupMessage::`)开头;
|
||||
//! - 四个 handler:每条臂以该组自己的 `pages::settings::<线>::Message::`(或短写 `Message::`)开头,
|
||||
//! 还没有臂的(空枚举 `match message {}`)一条臂都没有;
|
||||
//! - 以 `|` 开头的折行 or-pattern 续行放行(它不可能是兜底)。
|
||||
//!
|
||||
//! ## 抓得住什么 / 抓不住什么
|
||||
//!
|
||||
//! 抓得住:
|
||||
//! 1. `on_settings` 里加 `_ =>` 或 binding 臂 `other =>`(`GroupMessage` 不再穷尽);
|
||||
//! 2. handler 把 `match message {}` 改成 `_ => Task::none()` / `other => { … }`;
|
||||
//! 3. `on_settings` 的臂数不是 4(删了一条、或把两条合并);
|
||||
//! 4. handler 的臂写成别的组的变体路径(前缀不对)。
|
||||
//!
|
||||
//! 抓不住(老实列出来):
|
||||
//! 1. **嵌套 match 里的兜底**——深度 ≥2 的臂不看。这是刻意的:嵌套 match 的穷尽性由它自己的类型
|
||||
//! 保证,跟 `GroupMessage` 漏接线无关;代价是没人守「嵌套 match 自己写了个 `_ =>`」;
|
||||
//! 2. **跨行 pattern**(除 `|` 续行)——把 struct 字段拆成多行、`=>` 落在第 3 行那种写法会被
|
||||
//! 当成「前缀不对」而误判兜底。当前代码风格不出现;真出现时放宽规则,别把守门删掉;
|
||||
//! 3. **字符串/字符字面量里的 `{` `}`** 会扰乱深度计数(本文件与四个 handler 都没有这种写法);
|
||||
//! 4. handler 忽略消息(`let _ = message; Task::none()`)、两个 handler 身体写反或写成一样
|
||||
//! ——类型对得上就编得过,只有人读能看出来;
|
||||
//! 5. `match message` 被改名或换变量名时找不到匹配,臂数为 0 —— 这时
|
||||
//! `the_group_dispatch_has_exactly_four_arms` 会红(不是静默通过)。
|
||||
//!
|
||||
//! 站位:本文件是 `app::update::settings::tests`,`include_str!` 的路径相对本文件所在目录。
|
||||
|
||||
@@ -21,6 +50,58 @@ use crate::cli::Options;
|
||||
use crate::pages;
|
||||
use crate::theme::Families;
|
||||
|
||||
/// `on_settings` 的臂允许的前缀。
|
||||
const GROUP_ARMS: &[&str] = &["pages::settings::GroupMessage::", "GroupMessage::"];
|
||||
|
||||
/// 一行的净大括号增量(`{` 加、`}` 减)。
|
||||
fn brace_delta(line: &str) -> i32 {
|
||||
let open = line.matches('{').count() as i32;
|
||||
let close = line.matches('}').count() as i32;
|
||||
open - close
|
||||
}
|
||||
|
||||
/// 扫一条 `match message { … }`,只看**本层**(深度 1)的臂。
|
||||
///
|
||||
/// 返回 `(本层的合法臂数, 违规行)`:违规 = 本层里含 `=>`、又不是「已知前缀开头」的行
|
||||
/// (`|` 开头的折行 or-pattern 续行放行)。嵌套 match 的臂在深度 ≥2,整段跳过。
|
||||
fn dispatch_arms(path: &str, source: &str, allowed: &[&str]) -> (usize, Vec<String>) {
|
||||
let mut arms = 0usize;
|
||||
let mut offending = Vec::new();
|
||||
let mut depth = 0i32;
|
||||
let mut inside = false;
|
||||
|
||||
for (index, line) in source.lines().enumerate() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !inside {
|
||||
if trimmed.contains("match message {") {
|
||||
inside = true;
|
||||
depth = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if depth == 1 && trimmed.contains("=>") {
|
||||
let pattern = trimmed.trim_start_matches('|').trim_start();
|
||||
if allowed.iter().any(|prefix| pattern.starts_with(prefix)) {
|
||||
arms += 1;
|
||||
} else if !trimmed.starts_with('|') {
|
||||
offending.push(format!("{path}:{}: {trimmed}", index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
depth += brace_delta(trimmed);
|
||||
if depth <= 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(arms, offending)
|
||||
}
|
||||
|
||||
/// 不碰磁盘的 App(mock 后端 + 内存设置桥),与 `app/tests.rs` 的 `headless` 同一副骨架。
|
||||
fn headless() -> App {
|
||||
const FAMILIES: Families = Families {
|
||||
@@ -38,69 +119,51 @@ fn headless() -> App {
|
||||
)
|
||||
}
|
||||
|
||||
/// 收集「不该存在的臂」:非注释行里出现 `=>`,但不以该文件允许的前缀开头。
|
||||
fn offending_arms(path: &str, source: &str, allowed: Option<&str>) -> Vec<String> {
|
||||
source
|
||||
.lines()
|
||||
.enumerate()
|
||||
.filter_map(|(index, line)| {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("//") || !trimmed.contains("=>") {
|
||||
return None;
|
||||
}
|
||||
match allowed {
|
||||
Some(prefix) if trimmed.starts_with(prefix) => None,
|
||||
_ => Some(format!("{path}:{}: {trimmed}", index + 1)),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
// --------------------------------------------------------------- 守门本体
|
||||
|
||||
/// `on_settings` 与四个 handler 里都**不许**出现任何形式的兜底臂(`_ =>` 或 `other =>`)。
|
||||
///
|
||||
/// 抓得住:① 给 `on_settings` 加 `_ =>` 或 `other =>`(`GroupMessage` 不再穷尽);
|
||||
/// ② 四个 handler 把 `match message {}` 改成 `_ => Task::none()` / `other => { … }`(同样取消穷尽性);
|
||||
/// ③ 给 settings.rs 多接一条不认识的臂(前缀不对)。
|
||||
/// 抓不住:handler 忽略消息(`let _ = message; Task::none()`)、两个 handler 身体写反或写成一样
|
||||
/// —— 类型对得上就编得过;也抓不住「新变体接了个空壳」。那两条只有人读能看得出来。
|
||||
/// `on_settings` 与四个 handler 的**本层**臂里不许出现任何形式的兜底(`_ =>` 或 `other =>`)。
|
||||
#[test]
|
||||
fn the_group_dispatch_has_no_catch_all_arm() {
|
||||
let sources: [(&str, &str, Option<&str>); 5] = [
|
||||
let sources: [(&str, &str, &[&str]); 5] = [
|
||||
(
|
||||
"app/update/settings.rs",
|
||||
include_str!("../settings.rs"),
|
||||
Some("pages::settings::GroupMessage::"),
|
||||
GROUP_ARMS,
|
||||
),
|
||||
(
|
||||
"app/update/settings/java.rs",
|
||||
include_str!("java.rs"),
|
||||
Some("pages::settings::java::Message::"),
|
||||
&["pages::settings::java::Message::", "Message::"],
|
||||
),
|
||||
(
|
||||
"app/update/settings/download.rs",
|
||||
include_str!("download.rs"),
|
||||
None,
|
||||
&["pages::settings::download::Message::", "Message::"],
|
||||
),
|
||||
(
|
||||
"app/update/settings/language_appearance.rs",
|
||||
include_str!("language_appearance.rs"),
|
||||
None,
|
||||
&[
|
||||
"pages::settings::language_appearance::Message::",
|
||||
"Message::",
|
||||
],
|
||||
),
|
||||
(
|
||||
"app/update/settings/ai_about.rs",
|
||||
include_str!("ai_about.rs"),
|
||||
None,
|
||||
&["pages::settings::ai_about::Message::", "Message::"],
|
||||
),
|
||||
];
|
||||
|
||||
let mut offending = Vec::new();
|
||||
for (path, source, allowed) in sources {
|
||||
offending.extend(offending_arms(path, source, allowed));
|
||||
let (_, bad) = dispatch_arms(path, source, allowed);
|
||||
offending.extend(bad);
|
||||
}
|
||||
|
||||
assert!(
|
||||
offending.is_empty(),
|
||||
"设置页分组消息的 dispatch 里出现了兜底臂({offending:?}):\
|
||||
"设置页分组消息的 dispatch 本层出现了兜底臂({offending:?}):\
|
||||
任何 catch-all / binding 臂都会取消 `GroupMessage` 的穷尽性,\
|
||||
新变体漏接线时编译器不再报 E0004"
|
||||
);
|
||||
@@ -108,16 +171,15 @@ fn the_group_dispatch_has_no_catch_all_arm() {
|
||||
|
||||
/// `on_settings` 必须**恰好四条**臂,一条一条显式写全(不是「至少」)。
|
||||
///
|
||||
/// 抓得住:有人删掉一条臂、或者把两条合到一起(`A(..) | B(..) =>` 这种也过不了上面那条前缀判据,
|
||||
/// 这里是第二条独立判据)。
|
||||
/// 抓不住:臂的顺序(顺序不影响语义)。
|
||||
/// 抓得住:删掉一条臂、把两条合并、或者把 `match message` 改名(改名后扫不到臂 ⇒ 0 ≠ 4)。
|
||||
/// 抓不住:臂的顺序(顺序不影响语义)、每条臂接到了哪个 handler(类型对得上就编得过)。
|
||||
#[test]
|
||||
fn the_group_dispatch_has_exactly_four_arms() {
|
||||
let source = include_str!("../settings.rs");
|
||||
let arms = source
|
||||
.lines()
|
||||
.filter(|line| line.trim().starts_with("pages::settings::GroupMessage::"))
|
||||
.count();
|
||||
let (arms, _) = dispatch_arms(
|
||||
"app/update/settings.rs",
|
||||
include_str!("../settings.rs"),
|
||||
GROUP_ARMS,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
arms, 4,
|
||||
@@ -125,6 +187,60 @@ fn the_group_dispatch_has_exactly_four_arms() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **守门自身不误伤**:嵌套 match 与折行的 or-pattern 都不算兜底(r2 复审 P2-3 的探针)。
|
||||
///
|
||||
/// 这条是上一版判据假红的回归测试:复审往 handler 里加 `match true { true => …, false => … }`
|
||||
/// 就让它 FAILED;现在这类写法必须放行。
|
||||
#[test]
|
||||
fn the_guard_ignores_nested_matches_and_folded_or_patterns() {
|
||||
let synthetic = "\
|
||||
impl App {
|
||||
fn on_settings_java(&mut self, message: pages::settings::java::Message) -> Task<Message> {
|
||||
match message {
|
||||
pages::settings::java::Message::Probe => {
|
||||
let nested = match true { true => 10u8, false => 11u8 };
|
||||
let _ = nested;
|
||||
Task::none()
|
||||
}
|
||||
| pages::settings::java::Message::Retry => Task::none(),
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
let (arms, offending) = dispatch_arms(
|
||||
"synthetic",
|
||||
synthetic,
|
||||
&["pages::settings::java::Message::"],
|
||||
);
|
||||
assert!(
|
||||
offending.is_empty(),
|
||||
"正常代码被误判成兜底(这正是 r2 复审 P2-3 的假红):{offending:?}"
|
||||
);
|
||||
assert_eq!(arms, 2, "嵌套 match 里的两条臂不算本层臂");
|
||||
}
|
||||
|
||||
/// **守门仍然有牙**:`_ =>` 与 binding 臂 `other =>` 都要被本层判成兜底。
|
||||
#[test]
|
||||
fn the_guard_still_catches_both_catch_all_shapes() {
|
||||
for arm in [
|
||||
"_ => Task::none(),",
|
||||
"other => { let _ = other; Task::none() }",
|
||||
] {
|
||||
let synthetic = format!(
|
||||
"impl App {{\n fn on_settings_java(&mut self, message: M) -> Task<Message> {{\n match message {{\n {arm}\n }}\n }}\n}}\n"
|
||||
);
|
||||
let (_, offending) = dispatch_arms("synthetic", &synthetic, GROUP_ARMS);
|
||||
assert_eq!(
|
||||
offending.len(),
|
||||
1,
|
||||
"`{arm}` 是兜底,必须被判红(实际 {offending:?})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 活的端到端证据
|
||||
|
||||
/// **活的端到端证据**:一条组消息真的从外壳那条既有通道走到该组的 handler(r1 复审要求)。
|
||||
///
|
||||
/// 路径:`app::Message::Page` → `on_page` → `pages::Message::Settings` → `on_settings` →
|
||||
|
||||
Reference in new issue
Block a user