max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
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 |