flowrs-engine/crates/flowrs-core/tests/rename_leaks.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

105 lines
4.1 KiB
Rust

//! A widget's string literals must survive codegen unchanged.
//!
//! Every widget output is renamed per node so two nodes of the same type do not collide:
//! `model` becomes `model_0`. The rename is textual, and it used to reach places that are
//! not bindings.
//!
//! Two escapes had already shipped when this was written:
//!
//! - **Into a type path.** Four generative widgets emit `let image: image::DynamicImage`.
//! Renaming the binding rewrote the type to `image_4::DynamicImage`, and since the
//! `// Cargo.toml needs:` hint is inferred by scanning for `crate::`, the program
//! declared a dependency on a crate named `image_4`.
//! - **Into data.** `neural.load_model` emits `model_for_path("model.onnx")`. The rename
//! changed the PATH to `"model_0.onnx"`, so the generated program opened a file that
//! does not exist.
//!
//! The first class fails a compile. **The second does not** — `"model_0.onnx"` is a
//! perfectly good string, and the program builds and then cannot find its model at run
//! time. No compile harness can catch it, which is why this test reads the emitted text
//! instead: it is the only guard that can see a rename that produced valid, wrong Rust.
use flowrs_core::Graph;
/// Split a line into its string literals, honouring `\"` escapes.
fn literals(line: &str) -> Vec<String> {
let mut out = Vec::new();
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c != '"' {
continue;
}
let mut lit = String::new();
while let Some(c) = chars.next() {
match c {
'\\' => {
lit.push(c);
if let Some(n) = chars.next() {
lit.push(n);
}
}
'"' => break,
_ => lit.push(c),
}
}
out.push(lit);
}
out
}
/// Every widget, generated alone, with its emitted string literals inspected.
///
/// Node ids start at 0, so a leaked rename appears as a `_0` suffix. That is a suffix no
/// widget author writes by hand — `n_fft`, `image_2x` and the like do not end in `_0` —
/// so it is a reliable signature rather than a heuristic that needs a growing exception
/// list. An inline format argument is the one legitimate place a renamed binding may
/// appear inside a literal, so `{...}` spans are excluded.
#[test]
fn no_widget_leaks_a_renamed_binding_into_a_string_literal() {
let registry = flowrs_core::graph::widget::builtin_registry();
let mut leaks: Vec<String> = Vec::new();
for widget in registry.all() {
let id = widget.id();
let mut g = Graph::new();
g.add_node(&id, 0.0, 0.0);
let Ok(src) = g.to_rust() else { continue };
for line in src.lines() {
for lit in literals(line) {
// Drop `{...}` spans: an inline format argument legitimately names a
// binding, and the renamer is supposed to rewrite it there.
let mut outside = String::new();
let mut depth = 0usize;
let mut it = lit.chars().peekable();
while let Some(c) = it.next() {
match c {
'{' if it.peek() == Some(&'{') => {
it.next();
outside.push_str("{{");
}
'{' => depth += 1,
'}' => depth = depth.saturating_sub(1),
_ if depth == 0 => outside.push(c),
_ => {}
}
}
for tok in outside.split(|c: char| !(c.is_alphanumeric() || c == '_')) {
if tok.len() > 2 && tok.ends_with("_0") {
leaks.push(format!("{id}: `{tok}` in \"{lit}\""));
}
}
}
}
}
leaks.sort();
leaks.dedup();
assert!(
leaks.is_empty(),
"a per-node rename reached inside {} string literal(s) — the generated program \
still compiles, and is wrong:\n{}",
leaks.len(),
leaks.join("\n"),
);
}