//! 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 { 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 = 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"), ); }