Skip to main content

max / alloy

Catch every spelling of a renamed process::Command The rename check required the line to open `use std::process`, which a pub use re-export, an alias inside a braced use group, and a plain type alias all walk past, taking the chokepoint test with them. Key it on the path and the rename instead, and assert each of the three spellings.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-23 20:51 UTC
Signed with PGP, not checked
Commit: 03df368f0da02b3920a21ead5c1a7163b87f6d26
Parent: 2664bdf
1 file changed, +59 insertions, -10 deletions
@@ -141,20 +141,29 @@
141 141 .collect()
142 142 }
143 143
144 - /// The lines that rename something out of `std::process` as they import it.
144 + /// The lines that give something out of `std::process` a second name.
145 145 ///
146 - /// The alias is the hole in [`spawns`], and it is the whole hole: `Command` has
147 - /// to arrive under some name, and every other way of writing that name still
148 - /// contains the literal. `use std::process::Command as Proc;` does not, and
149 - /// `Proc::new` walks past a matcher keyed on `Command::new(`. Forbidding the
150 - /// rename is narrower than forbidding the import, which cannot be forbidden:
151 - /// `shell.rs` names `Command` as a type in `Flow::Suspend(Command)` without
152 - /// spawning anything.
146 + /// The rename is the hole in [`spawns`]: `Command` has to arrive under some
147 + /// name, and every other way of writing that name still contains the literal.
148 + /// `use std::process::Command as Proc;` does not, and `Proc::new` walks past a
149 + /// matcher keyed on `Command::new(`. Forbidding the rename is narrower than
150 + /// forbidding the import, which cannot be forbidden: `shell.rs` names `Command`
151 + /// as a type in `Flow::Suspend(Command)` without spawning anything.
152 + ///
153 + /// Keyed on the path and the rename rather than on how the line opens. An
154 + /// earlier version required the line to start `use std::process`, which three
155 + /// ordinary spellings walk past: a `pub use` re-export, the alias sitting inside
156 + /// a braced `use std::{...}` group, and a `type` alias that imports nothing at
157 + /// all. All three are exercised in
158 + /// [`every_spelling_of_the_rename_is_flagged`].
153 159 fn process_aliases(text: &str) -> Vec<&str> {
154 160 text.lines()
155 - .filter(|line| line.trim_start().starts_with("use std::process"))
156 161 .filter(|line| !is_comment(line))
157 - .filter(|line| line.contains(" as "))
162 + .filter(|line| line.contains("std::process") || line.contains("process::Command"))
163 + .filter(|line| {
164 + let code = line.trim_start();
165 + line.contains(" as ") || code.starts_with("type ") || code.starts_with("pub type ")
166 + })
158 167 .collect()
159 168 }
160 169
@@ -320,6 +329,46 @@
320 329 assert!(spawns(" /// Command::new(\"x\")").is_empty());
321 330 }
322 331
332 + /// The rename has more than one spelling, and each of them is a spawn.
333 + ///
334 + /// Every line here reaches `Proc::new` with `std::process::Command` behind it,
335 + /// and none of them contains the literal [`spawns`] is keyed on, so a matcher
336 + /// that misses one leaves the chokepoint open to a routine refactor.
337 + #[test]
338 + fn every_spelling_of_the_rename_is_flagged() {
339 + for line in [
340 + "pub use std::process::Command as Proc;",
341 + "use std::{process::Command as Proc};",
342 + "type Proc = std::process::Command;",
343 + ] {
344 + let source = format!("{line}\n\n Proc::new(\"wipefs\")\n");
345 + assert!(
346 + spawns(&source).is_empty(),
347 + "the literal check is not what catches this: {line}"
348 + );
349 + assert_eq!(
350 + process_aliases(&source),
351 + [line],
352 + "the rename check misses {line}"
353 + );
354 + }
355 +
356 + // The alias inside a group spans lines when rustfmt breaks it up, and the
357 + // line carrying the rename is the one that has to be seen.
358 + let broken = "use std::{\n process::Command as Proc,\n path::PathBuf,\n};\n";
359 + assert_eq!(process_aliases(broken), [" process::Command as Proc,"]);
360 +
361 + // And the ordinary imports stay unflagged, whatever shape they take.
362 + for plain in [
363 + "use std::process::Command;\n",
364 + "use std::process::{Child, Stdio};\n",
365 + "pub use std::process::Command;\n",
366 + " std::process::exit(1);\n",
367 + ] {
368 + assert!(process_aliases(plain).is_empty(), "{plain}");
369 + }
370 + }
371 +
323 372 /// `modules` reads the directory the crate is actually built from, all of it.
324 373 ///
325 374 /// A path that resolved to nothing would make every check above pass by having