105 lines
4.1 KiB
Rust
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"),
|
|
);
|
|
}
|