test(app-iced): M102 r5 行距护栏追绑定(M4 假绿做死)+ 等价重构不再假红(M3/M6)
r4 复审的结论是「产品代码没问题,两条 p1 都在护栏的『抓得住』与实测不符」:
- **M4(真正的假绿,做死)**:`rows = rows.spacing(ui.space(Space::X1));` 只要另起一条语句
就能绕过「只锁构建链」的旧护栏。现在 `list_column_spaces_rows_only_from_the_shared_constant`
用 `function_region` 取整段函数,再用 `source::call_arguments` 拿每一处 `rows.spacing(…)`
的实参,并用新增的 `source::derived_from` 顺 `let` / `Some(x) =` 绑定往回追(最多三跳):
实参必须追回 `ROW_SPACING`。于是
· 直接 `ui.space(spacing)`(来自 `if let Some(spacing) = ROW_SPACING`)→ 绿;
· `let spacing = ROW_SPACING;` 再中转(H3)→ 绿;
· 先把令牌挪进 `let extra = Space::X1;` 再 `rows.spacing(ui.space(extra))`(M4)→ 红。
- **M3(假红)**:日志接线护栏改用 `function_region`,于是 `wrap::breakable` 外面套一层同义
包装(`fn soft(…) { mono_owned(ui, wrap::breakable(…)) }`)不再假红;整条链路都没碰
`wrap::breakable` 照样红。
- **M6(假红)**:新增 `source::region_across`(多文件转调跟踪,`frame.rs` + `layout/block.rs`),
把盒子原样搬进 `block::body_box` 再转调不再假红;`align_x` 改判**实参**(`Alignment::Start`
与 `iced::Alignment::Start` 同等放行),于是搬进别的模块后路径写全也不假红。
- **M1/M2 按塔的裁定不加断言**:与 `frame::body` 逐像素等价的另写就是等价重构;docstring 写明
「抓得住:语义变化;抓不住:逐像素等价的另写」。
最终变异矩阵(本轮自跑、每条跑完 `git checkout --` 回滚、白名单已核):
基线 429 passed / 0 failed;M4 红(只红 list_column_spaces_rows_only_from_the_shared_constant);
M3 绿;M6 绿;G 红(只红 single_column_pages_call_the_body_box_not_the_centred_one);
I 红(只红 the_page_wires_the_soft_breaks_into_both_rendering_paths);
旧 p1 复跑 A 红、D 红,等价重构 J 绿。
This commit is contained in:
1 parent
f499961e5b
commit
4b26a0a9dc
5 files changed
+299
-39
No files matched your search
@@ -54,19 +54,47 @@ pub(crate) fn statement<'a>(source: &'a str, start: &str) -> &'a str {
|
||||
&rest[..end]
|
||||
}
|
||||
|
||||
/// `signature` 的函数体 **+ 它内部转调的同文件函数体**(最多两层)。
|
||||
/// `signature` 的函数体 **+ 它内部转调的函数体**(同文件,最多两层)。
|
||||
///
|
||||
/// 为什么不只切一个函数:**等价重构不该假红**。把 `frame::body` 拆成
|
||||
/// `body` → `body_impl` 两层再转调,盒子一个像素都没变,`function_body` 却会拿到三行转调桩、
|
||||
/// needle 全缺(r2 复审的变异 H1 就是这样假红的)。这里把「本文件内被它调用的函数」也算进契约,
|
||||
/// needle 全缺(r2 复审的变异 H1 就是这样假红的)。这里把「被它调用的函数」也算进契约,
|
||||
/// 于是「拆一层再转调」放行,「换成居中盒子」照样红。
|
||||
///
|
||||
/// 边界:只跟**同一文件内**的转调(跨文件转调、先赋局部变量再建盒不在覆盖内,见调用点 docstring)。
|
||||
/// 跨文件转调用 [`region_across`]。两者都不跟「先赋局部变量再建盒」的写法。
|
||||
pub(crate) fn function_region(source: &str, signature: &str) -> String {
|
||||
let mut region = function_body(source, signature).to_owned();
|
||||
region_across(&[source], signature)
|
||||
}
|
||||
|
||||
/// 多文件版的 [`function_region`]:[`expand`] 会在给定的**所有**文件里按名字找转调目标。
|
||||
///
|
||||
/// 存在的理由(r4 复审的变异 M6):把盒子原样搬进 `layout/block.rs::body_box` 再由
|
||||
/// `frame::body` 转调,是**等价重构**,单文件版会把 needle 全丢掉并假红。
|
||||
/// 现在把 `frame.rs` 与 `layout/block.rs` 一起交进来,搬运不再被惩罚。
|
||||
///
|
||||
/// # Panics
|
||||
/// `signature` 在任何一个文件里都找不到时 panic(护栏前置条件坏了要立刻红)。
|
||||
pub(crate) fn region_across(sources: &[&str], signature: &str) -> String {
|
||||
let mut region = sources
|
||||
.iter()
|
||||
.find(|source| source.contains(signature))
|
||||
.map(|source| function_body(source, signature).to_owned())
|
||||
.unwrap_or_else(|| panic!("这几个文件里都找不到签名 {signature:?}"));
|
||||
|
||||
for _ in 0..2 {
|
||||
let mut grew = false;
|
||||
if !expand(&mut region, sources) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
region
|
||||
}
|
||||
|
||||
/// 把 `region` 里出现、且定义在 `sources` 某处的函数体补进来。返回是否有增长。
|
||||
fn expand(region: &mut String, sources: &[&str]) -> bool {
|
||||
let mut grew = false;
|
||||
|
||||
for source in sources {
|
||||
let mut cursor = 0;
|
||||
|
||||
while let Some(found) = source[cursor..].find("fn ") {
|
||||
@@ -81,23 +109,126 @@ pub(crate) fn function_region(source: &str, signature: &str) -> String {
|
||||
continue;
|
||||
}
|
||||
|
||||
let called = format!("{name}(");
|
||||
// `defined` 不带 `(`:签名可能带泛型(`fn body_impl<'a>(`),
|
||||
// 带括号的 needle 会找不到定义、直接把护栏自己搞 panic。
|
||||
let defined = format!("fn {name}");
|
||||
if region.contains(&called) && !region.contains(&defined) && source.contains(&defined) {
|
||||
if region.contains(&format!("{name}("))
|
||||
&& !region.contains(&defined)
|
||||
&& source.contains(&defined)
|
||||
{
|
||||
region.push('\n');
|
||||
// `defined` 不带 `(`:签名可能带泛型(`fn body_impl<'a>(`),
|
||||
// 带括号的 needle 会找不到定义、直接把护栏自己搞 panic。
|
||||
region.push_str(function_body(source, &defined));
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !grew {
|
||||
break;
|
||||
grew
|
||||
}
|
||||
|
||||
/// `haystack` 里每一处 `callee(…)` 调用的**实参文本**(不含括号本身)。
|
||||
///
|
||||
/// 括号按深度配对,所以 `ui.space(Space::X1)` 这种嵌套实参会完整取到 `ui.space(Space::X1)`;
|
||||
/// 找不到闭合括号时取到文本末尾(宁可让断言看见多余文本,也不静默返回空)。
|
||||
pub(crate) fn call_arguments<'a>(haystack: &'a str, callee: &str) -> Vec<&'a str> {
|
||||
let mut found = Vec::new();
|
||||
let mut from = 0;
|
||||
|
||||
while let Some(index) = haystack[from..].find(callee) {
|
||||
let start = from + index + callee.len();
|
||||
let mut depth = 1usize;
|
||||
let mut end = haystack.len();
|
||||
|
||||
for (offset, ch) in haystack[start..].char_indices() {
|
||||
match ch {
|
||||
'(' => depth += 1,
|
||||
')' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end = start + offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
found.push(&haystack[start..end]);
|
||||
from = end.max(start);
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
|
||||
/// 顺绑定链往回追:`ident` 是否(直接或经至多 3 跳中转)来自 `root`。
|
||||
///
|
||||
/// 认两种绑定形状(本仓就这两种):
|
||||
///
|
||||
/// - `let <ident> = <expr>;`
|
||||
/// - `if let Some(<ident>) = <expr> {`(`while let` 同理,形状一样)
|
||||
///
|
||||
/// 为什么需要它(r4 复审的变异 M4):把写死的令牌先挪到一条 `let extra = Space::X1;`,
|
||||
/// 再 `rows = rows.spacing(ui.space(extra));` —— 只看实参文本「有没有 `Space::`」会漏掉它。
|
||||
/// 顺着绑定追回源头就能区分「真的来自 `ROW_SPACING`(含 `let spacing = ROW_SPACING` 再中转,
|
||||
/// 变异 H3)」与「中转了一层写死令牌」。
|
||||
pub(crate) fn derived_from(body: &str, ident: &str, root: &str) -> bool {
|
||||
let mut frontier = vec![ident.to_owned()];
|
||||
|
||||
for _ in 0..3 {
|
||||
let mut next = Vec::new();
|
||||
|
||||
for name in &frontier {
|
||||
for rhs in binding_rhs(body, name) {
|
||||
if rhs.contains(root) {
|
||||
return true;
|
||||
}
|
||||
|
||||
next.extend(
|
||||
rhs.split(|ch: char| !(ch.is_alphanumeric() || ch == '_'))
|
||||
.filter(|token| {
|
||||
token
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|first| first.is_alphabetic())
|
||||
})
|
||||
.map(str::to_owned),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if next.is_empty() {
|
||||
return false;
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// `ident` 在 `body` 里的绑定表达式(`let` 与 `Some(ident) =` 两种形状)。
|
||||
fn binding_rhs<'a>(body: &'a str, ident: &str) -> Vec<&'a str> {
|
||||
let mut rhs = Vec::new();
|
||||
|
||||
for pattern in [format!("let {ident} = "), format!("let mut {ident} = ")] {
|
||||
let mut from = 0;
|
||||
while let Some(index) = body[from..].find(&pattern) {
|
||||
let start = from + index + pattern.len();
|
||||
let end = body[start..].find(';').map_or(body.len(), |at| start + at);
|
||||
rhs.push(&body[start..end]);
|
||||
from = end;
|
||||
}
|
||||
}
|
||||
|
||||
region
|
||||
let pattern = format!("Some({ident}) = ");
|
||||
let mut from = 0;
|
||||
while let Some(index) = body[from..].find(&pattern) {
|
||||
let start = from + index + pattern.len();
|
||||
let end = body[start..].find('{').map_or(body.len(), |at| start + at);
|
||||
rhs.push(&body[start..end]);
|
||||
from = end;
|
||||
}
|
||||
|
||||
rhs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -103,6 +103,74 @@ fn a_missing_signature_fails_loudly() {
|
||||
let _ = function_body(SAMPLE, "pub fn nope");
|
||||
}
|
||||
|
||||
/// 跨文件转调也要跟进去(`frame::body` → `layout::block::body_box` 就是这种形状)。
|
||||
///
|
||||
/// 变异 M6(把盒子原样搬进 `layout/block.rs`)在单文件版下假红;多文件版应当放行。
|
||||
#[test]
|
||||
fn the_region_follows_delegation_across_files() {
|
||||
let frame = "\
|
||||
pub fn body(a: u8) -> u8 {
|
||||
block::body_box(a) + 1
|
||||
}
|
||||
";
|
||||
let block = "\
|
||||
pub fn body_box(a: u8) -> u8 {
|
||||
a * 7
|
||||
}
|
||||
";
|
||||
let region = region_across(&[frame, block], "pub fn body(");
|
||||
assert!(region.contains("body_box(a) + 1"), "{region}");
|
||||
assert!(region.contains("a * 7"), "跨文件转调没跟进去:{region}");
|
||||
}
|
||||
|
||||
/// `call_arguments` 取到的是**实参原文**(嵌套括号完整),这是 M4 那条护栏的判据基础。
|
||||
#[test]
|
||||
fn call_arguments_returns_the_nested_argument_text() {
|
||||
let source = "\
|
||||
rows = rows.spacing(ui.space(Space::X1));
|
||||
rows = rows.spacing(ui.space(step));
|
||||
column![head, scroller].spacing(ui.space(Space::X2));
|
||||
";
|
||||
let args = call_arguments(source, "rows.spacing(");
|
||||
assert_eq!(args, vec!["ui.space(Space::X1)", "ui.space(step)"]);
|
||||
assert!(
|
||||
args[0].contains("Space::") && !args[1].contains("Space::"),
|
||||
"实参文本要能区分「写死令牌」与「经变量中转」:{args:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `derived_from` 顺绑定链追源头:真源放行、中转一层写死令牌判否(变异 M4 的判据)。
|
||||
#[test]
|
||||
fn binding_tracing_separates_the_truth_source_from_a_smuggled_token() {
|
||||
let honest = "\
|
||||
if let Some(spacing) = ROW_SPACING {
|
||||
rows = rows.spacing(ui.space(spacing));
|
||||
}
|
||||
";
|
||||
assert!(derived_from(honest, "spacing", "ROW_SPACING"));
|
||||
|
||||
// 经局部变量中转一次(变异 H3 的等价重构形状):仍要追到 ROW_SPACING
|
||||
let via_local = "\
|
||||
let spacing = ROW_SPACING;
|
||||
if let Some(step) = spacing {
|
||||
rows = rows.spacing(ui.space(step));
|
||||
}
|
||||
";
|
||||
assert!(derived_from(via_local, "step", "ROW_SPACING"));
|
||||
|
||||
// 把写死令牌挪到另一条 let 再传(变异 M4):追不到 ROW_SPACING
|
||||
let smuggled = "\
|
||||
if let Some(spacing) = ROW_SPACING {
|
||||
rows = rows.spacing(ui.space(spacing));
|
||||
}
|
||||
|
||||
let extra = Space::X1;
|
||||
rows = rows.spacing(ui.space(extra));
|
||||
";
|
||||
assert!(!derived_from(smuggled, "extra", "ROW_SPACING"));
|
||||
assert!(derived_from(smuggled, "spacing", "ROW_SPACING"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "找不到语句起点")]
|
||||
fn a_missing_statement_start_fails_loudly() {
|
||||
|
||||
Reference in new issue
Block a user