Fail when nothing was translated, not just when the file is empty
Running rustnim over libcosmic turned up three files that "succeeded": exit 0, a 6,984-byte output file, and zero translated items. Every item in them was gated behind a feature that is off, so they were correctly dropped -- and then the prelude alone was written out as if it were a translation. That is the failure in findings/ wearing a disguise. The existing guard checks the output file is non-empty, which the prelude satisfies on its own. So the count is now of items actually emitted, and zero is an error that names how many were dropped by #[cfg] and how to enable them. tests/cases/905 pins it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0777223 parent: 5fdd5be modified
src/lower.rs +27 -0 | @@ -237,6 +237,10 @@ pub struct Lowerer { | ||
| 237 | 237 | /// Module names supplied as separate input files. A `mod x;` naming one |
| 238 | 238 | /// of these is satisfied by that file having been passed in. |
| 239 | 239 | pub modules: Vec<String>, |
| 240 | + /// How many items were actually translated. If this is zero the input | |
| 241 | + /// produced nothing but the prelude, and reporting success for that is | |
| 242 | + /// the precise failure this project exists to avoid -- see `findings/`. | |
| 243 | + emitted: usize, | |
| 240 | 244 | /// Cargo features that are on, as `--cfg feature=<name>`. `#[cfg]` is |
| 241 | 245 | /// evaluated against these exactly as rustc would, so an item that is |
| 242 | 246 | /// dropped here is genuinely not part of the program being compiled. |
| @@ -278,6 +282,7 @@ impl Lowerer { | ||
| 278 | 282 | forwards: Vec::new(), |
| 279 | 283 | aliases: HashMap::new(), |
| 280 | 284 | modules: Vec::new(), |
| 285 | + emitted: 0, | |
| 281 | 286 | features: Vec::new(), |
| 282 | 287 | dropped_by_cfg: 0, |
| 283 | 288 | ret: None, |
| @@ -385,6 +390,25 @@ impl Lowerer { | ||
| 385 | 390 | } |
| 386 | 391 | } |
| 387 | 392 | |
| 393 | + // An input that translates to nothing is a failure, however plausible | |
| 394 | + // the output file looks. The prelude alone is not a translation. | |
| 395 | + if self.emitted == 0 { | |
| 396 | + return Err(format!( | |
| 397 | + "nothing was translated: the input has no items this lowering \ | |
| 398 | + emits{}. Writing a file containing only the prelude would \ | |
| 399 | + report success for work that was not done", | |
| 400 | + if self.dropped_by_cfg > 0 { | |
| 401 | + format!( | |
| 402 | + " ({} item(s) were dropped by `#[cfg]`; enable them with \ | |
| 403 | + `--cfg feature=<name>`)", | |
| 404 | + self.dropped_by_cfg | |
| 405 | + ) | |
| 406 | + } else { | |
| 407 | + String::new() | |
| 408 | + } | |
| 409 | + )); | |
| 410 | + } | |
| 411 | + | |
| 388 | 412 | if self.fns.contains_key(&(String::new(), "main".to_string())) { |
| 389 | 413 | self.blank(); |
| 390 | 414 | self.line("when isMainModule:"); |
| @@ -934,6 +958,9 @@ impl Lowerer { | ||
| 934 | 958 | } |
| 935 | 959 | |
| 936 | 960 | fn item_inner(&mut self, item: &Item) -> Result<(), String> { |
| 961 | + if !matches!(item, Item::Use(_) | Item::ExternCrate(_) | Item::Mod(_) | Item::Type(_)) { | |
| 962 | + self.emitted += 1; | |
| 963 | + } | |
| 937 | 964 | match item { |
| 938 | 965 | Item::Fn(f) => { |
| 939 | 966 | let nim = self.fn_name(&self.cur_mod, &f.sig.ident.to_string()); |
| @@ -237,6 +237,10 @@ pub struct Lowerer { | |||
| 237 | /// Module names supplied as separate input files. A `mod x;` naming one | 237 | /// Module names supplied as separate input files. A `mod x;` naming one |
| 238 | /// of these is satisfied by that file having been passed in. | 238 | /// of these is satisfied by that file having been passed in. |
| 239 | pub modules: Vec<String>, | 239 | pub modules: Vec<String>, |
| 240 | + /// How many items were actually translated. If this is zero the input | ||
| 241 | + /// produced nothing but the prelude, and reporting success for that is | ||
| 242 | + /// the precise failure this project exists to avoid -- see `findings/`. | ||
| 243 | + emitted: usize, | ||
| 240 | /// Cargo features that are on, as `--cfg feature=<name>`. `#[cfg]` is | 244 | /// Cargo features that are on, as `--cfg feature=<name>`. `#[cfg]` is |
| 241 | /// evaluated against these exactly as rustc would, so an item that is | 245 | /// evaluated against these exactly as rustc would, so an item that is |
| 242 | /// dropped here is genuinely not part of the program being compiled. | 246 | /// dropped here is genuinely not part of the program being compiled. |
| @@ -278,6 +282,7 @@ impl Lowerer { | |||
| 278 | forwards: Vec::new(), | 282 | forwards: Vec::new(), |
| 279 | aliases: HashMap::new(), | 283 | aliases: HashMap::new(), |
| 280 | modules: Vec::new(), | 284 | modules: Vec::new(), |
| 285 | + emitted: 0, | ||
| 281 | features: Vec::new(), | 286 | features: Vec::new(), |
| 282 | dropped_by_cfg: 0, | 287 | dropped_by_cfg: 0, |
| 283 | ret: None, | 288 | ret: None, |
| @@ -385,6 +390,25 @@ impl Lowerer { | |||
| 385 | } | 390 | } |
| 386 | } | 391 | } |
| 387 | 392 | ||
| 393 | + // An input that translates to nothing is a failure, however plausible | ||
| 394 | + // the output file looks. The prelude alone is not a translation. | ||
| 395 | + if self.emitted == 0 { | ||
| 396 | + return Err(format!( | ||
| 397 | + "nothing was translated: the input has no items this lowering \ | ||
| 398 | + emits{}. Writing a file containing only the prelude would \ | ||
| 399 | + report success for work that was not done", | ||
| 400 | + if self.dropped_by_cfg > 0 { | ||
| 401 | + format!( | ||
| 402 | + " ({} item(s) were dropped by `#[cfg]`; enable them with \ | ||
| 403 | + `--cfg feature=<name>`)", | ||
| 404 | + self.dropped_by_cfg | ||
| 405 | + ) | ||
| 406 | + } else { | ||
| 407 | + String::new() | ||
| 408 | + } | ||
| 409 | + )); | ||
| 410 | + } | ||
| 411 | + | ||
| 388 | if self.fns.contains_key(&(String::new(), "main".to_string())) { | 412 | if self.fns.contains_key(&(String::new(), "main".to_string())) { |
| 389 | self.blank(); | 413 | self.blank(); |
| 390 | self.line("when isMainModule:"); | 414 | self.line("when isMainModule:"); |
| @@ -934,6 +958,9 @@ impl Lowerer { | |||
| 934 | } | 958 | } |
| 935 | 959 | ||
| 936 | fn item_inner(&mut self, item: &Item) -> Result<(), String> { | 960 | fn item_inner(&mut self, item: &Item) -> Result<(), String> { |
| 961 | + if !matches!(item, Item::Use(_) | Item::ExternCrate(_) | Item::Mod(_) | Item::Type(_)) { | ||
| 962 | + self.emitted += 1; | ||
| 963 | + } | ||
| 937 | match item { | 964 | match item { |
| 938 | Item::Fn(f) => { | 965 | Item::Fn(f) => { |
| 939 | let nim = self.fn_name(&self.cur_mod, &f.sig.ident.to_string()); | 966 | let nim = self.fn_name(&self.cur_mod, &f.sig.ident.to_string()); |
added
tests/cases/905-reject-nothing-translated.rs +8 -0 | new file mode 100644 | ||
| @@ -0,0 +1,8 @@ | ||
| 1 | +//@ reject: nothing was translated | |
| 2 | +// Exiting 0 with a file containing only the prelude would report success for | |
| 3 | +// work that was not done. That is the failure in `findings/`, and it is not | |
| 4 | +// caught by checking the output file is non-empty. | |
| 5 | +#[cfg(feature = "something")] | |
| 6 | +pub fn only_under_a_feature() -> i32 { | |
| 7 | + 1 | |
| 8 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | +//@ reject: nothing was translated | ||
| 2 | +// Exiting 0 with a file containing only the prelude would report success for | ||
| 3 | +// work that was not done. That is the failure in `findings/`, and it is not | ||
| 4 | +// caught by checking the output file is non-empty. | ||
| 5 | +#[cfg(feature = "something")] | ||
| 6 | +pub fn only_under_a_feature() -> i32 { | ||
| 7 | + 1 | ||
| 8 | +} | ||