1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
//! Mechanical guard against `.rs` files under `src/` that hold `#[test]` functions but are never
//! wired into the crate's module tree.
//!
//! `mod generators;` was missing from `src/codegen/config_gen/tests.rs`, so
//! `src/codegen/config_gen/tests/generators.rs` and its 18 `#[test]` functions were never part of
//! the crate — `cargo test --lib` never listed them, never ran them, and never warned, because an
//! undeclared module file is simply not source input to `rustc`. This file makes that shape
//! unrepresentable going forward: it re-derives the real module graph from `src/lib.rs` (and
//! `src/main.rs`) by resolving every `mod` item the way `rustc` does, and fails when a source file
//! containing `#[test]` sits outside that graph.
//!
//! ## Why this is not a regex over parent files
//!
//! A naive scan for `mod <stem>;` in the obvious parent file produces dozens of false positives in
//! this tree, because the real module shapes are richer than "sibling file, sibling declaration":
//!
//! - `foo/bar.rs` is declared as `mod bar;` in `foo.rs` **or** in `foo/mod.rs` — both are valid
//! parents and a scan that only checks one produces a false positive on the other.
//! - Submodules declared inside an inline `#[cfg(test)] mod tests { mod x; }` block resolve to
//! `foo/tests/x.rs`, not `foo/x.rs` — the inline module name becomes a path segment even though
//! it has no file of its own.
//! - `#[path = "..."]` attributes redirect resolution entirely, and — per `rustc`'s own rule — are
//! always resolved relative to the directory of the file that carries the attribute, not to the
//! implied submodule directory of the module doing the declaring. `types.rs` declaring
//! `#[path = "types/tests.rs"] mod tests;` and `all_commands.rs` declaring
//! `#[path = "all_commands_tests.rs"] mod tests;` both exist in this tree and resolve to
//! different shapes for the same reason.
//!
//! [`reachable_src_files`] performs the same resolution `rustc` performs: it walks `Item::Mod`
//! nodes with `syn`, follows inline module bodies by descending a virtual directory one segment
//! per inline `mod name { .. }`, and treats `#[path]` as relative to the *containing file's*
//! directory rather than the declaring module's submodule directory.
//!
//! Where it runs: nowhere new. An ordinary integration test picked up by `cargo test --workspace`.
#![allow(clippy::print_stdout, clippy::print_stderr, clippy::dbg_macro)]
use std::collections::BTreeSet;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use syn::visit::Visit;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
/// Repo-relative, `/`-separated paths of every Rust source file under `src/`.
///
/// `git ls-files` is authoritative because the rule governs committed content.
fn all_src_sources() -> Vec<String> {
let output = Command::new("git")
.arg("-C")
.arg(repo_root())
.args(["ls-files", "-z", "--", "src/*.rs"])
.output()
.unwrap_or_else(|error| panic!("git ls-files: {error}"));
assert!(
output.status.success(),
"git ls-files failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let mut paths: Vec<String> = String::from_utf8(output.stdout)
.expect("git ls-files output must be UTF-8")
.split('\0')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
paths.sort();
assert!(
paths.len() > 500,
"expected src/ to hold hundreds of source files; found {} — the enumeration is broken, \
not the tree",
paths.len()
);
paths
}
fn parse(repo_root: &Path, relative: &Path) -> syn::File {
let absolute = repo_root.join(relative);
let source =
std::fs::read_to_string(&absolute).unwrap_or_else(|error| panic!("read {}: {error}", relative.display()));
syn::parse_file(&source).unwrap_or_else(|error| panic!("parse {}: {error}", relative.display()))
}
fn is_test_fn(function: &syn::ItemFn) -> bool {
function.attrs.iter().any(|attribute| attribute.path().is_ident("test"))
}
/// True if `file`, parsed on its own (including any inline `mod { .. }` bodies it contains),
/// holds at least one `#[test]` function.
struct HasTestFn(bool);
impl<'ast> Visit<'ast> for HasTestFn {
fn visit_item_fn(&mut self, function: &'ast syn::ItemFn) {
if is_test_fn(function) {
self.0 = true;
}
syn::visit::visit_item_fn(self, function);
}
}
fn file_contains_test_fn(file: &syn::File) -> bool {
let mut finder = HasTestFn(false);
finder.visit_file(file);
finder.0
}
/// The string value of a `#[path = "..."]` attribute, if present.
fn path_attr_value(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().find_map(|attribute| {
if !attribute.path().is_ident("path") {
return None;
}
let syn::Meta::NameValue(name_value) = &attribute.meta else {
return None;
};
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(literal),
..
}) = &name_value.value
else {
return None;
};
Some(literal.value())
})
}
/// Collapse `..` and `.` components the way a filesystem path resolver would, without touching
/// the filesystem. `#[path]` values in this tree are all simple relative subpaths, but this keeps
/// the resolver correct if one ever climbs a directory.
fn normalize(path: PathBuf) -> PathBuf {
let mut out: Vec<Component> = Vec::new();
for component in path.components() {
match component {
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
other => out.push(other),
}
}
out.into_iter().collect()
}
/// Resolve a `mod name;` (no inline body) declaration to the real file it names, `rustc`-style.
///
/// `file_dir` is the directory of the file that *contains* the `mod` item — the anchor for
/// `#[path]`. `children_dir` is that file's own submodule directory — `foo/` for both `foo.rs`
/// and `foo/mod.rs`, and `foo/tests/` for a `mod x;` written inside an inline
/// `mod tests { .. }` block in `foo.rs`. Ordinary (non-`#[path]`) resolution uses `children_dir`.
///
/// `path_anchor` is where a `#[path]` value is resolved from, and it is NOT simply the containing
/// file's directory. Per `rustc`, in a non-`mod-rs` file the attribute anchors to that file's
/// directory only at the file's top level; inside an inline `mod name { .. }` the inline segments
/// count as directories, so `#[path = "x.rs"]` written in `mod tests` inside `foo.rs` resolves to
/// `foo/tests/x.rs`. Anchoring unconditionally to the file's directory made this gate report
/// three genuinely-compiled test files as dead. ~keep
fn resolve_external_mod(
repo_root: &Path,
path_anchor: &Path,
children_dir: &Path,
ident: &str,
explicit_path: Option<String>,
) -> Option<PathBuf> {
if let Some(explicit) = explicit_path {
let candidate = normalize(path_anchor.join(explicit));
return repo_root.join(&candidate).is_file().then_some(candidate);
}
let as_file = normalize(children_dir.join(format!("{ident}.rs")));
if repo_root.join(&as_file).is_file() {
return Some(as_file);
}
let as_mod_rs = normalize(children_dir.join(ident).join("mod.rs"));
if repo_root.join(&as_mod_rs).is_file() {
return Some(as_mod_rs);
}
None
}
/// The repo-relative path an `include!("...")` item-position macro invocation names, resolved
/// relative to the directory of the file containing the invocation (the same anchor `#[path]`
/// uses). Any macro shape other than a single string-literal argument is not `include!` as used
/// for module splicing and is ignored.
fn include_target(mac: &syn::Macro, file_dir: &Path) -> Option<PathBuf> {
let literal: syn::LitStr = mac.parse_body().ok()?;
Some(normalize(file_dir.join(literal.value())))
}
/// Walk one parsed file's items, recording every file `mod` declaration reaches (directly or
/// through further nested modules) into `reachable`.
///
/// `file_dir` is fixed for the whole file (used only to anchor `include!`). `path_anchor` starts
/// as `file_dir` and switches to the inline module's directory once inside one. `children_dir` starts
/// as the file's own submodule directory and descends one segment per inline `mod name { .. }`
/// nesting level, mirroring where `rustc` would look for `mod x;` written inside that block.
fn walk_items(
repo_root: &Path,
items: &[syn::Item],
file_dir: &Path,
path_anchor: &Path,
children_dir: &Path,
reachable: &mut BTreeSet<PathBuf>,
) {
for item in items {
if let syn::Item::Macro(item_macro) = item {
// `include!("path.rs")` splices the target file's tokens in place — it is a distinct
// reachability mechanism from `mod`, resolved relative to the *current file's own*
// directory (like `#[path]`), and it does not open a new module: any `mod` items the
// spliced file goes on to declare are resolved as if written at this exact point, so
// recursion keeps the same `file_dir`/`children_dir`. `alef` uses this to split large
// generator files (e.g. `backends/jni/gen_shims.rs`) into pieces that stay in one
// module. ~keep
if item_macro.mac.path.is_ident("include")
&& let Some(included) = include_target(&item_macro.mac, file_dir)
&& repo_root.join(&included).is_file()
&& reachable.insert(included.clone())
{
let file = parse(repo_root, &included);
walk_items(repo_root, &file.items, file_dir, path_anchor, children_dir, reachable);
}
continue;
}
let syn::Item::Mod(module) = item else { continue };
let explicit_path = path_attr_value(&module.attrs);
let ident = module.ident.to_string();
match &module.content {
Some((_, inline_items)) => {
// An inline body has no file of its own; its submodules resolve one directory
// segment deeper than the enclosing file's own submodule directory.
let inline_children_dir = children_dir.join(&ident);
walk_items(
repo_root,
inline_items,
file_dir,
&inline_children_dir,
&inline_children_dir,
reachable,
);
}
None => {
let used_explicit_path = explicit_path.is_some();
let Some(resolved) = resolve_external_mod(repo_root, path_anchor, children_dir, &ident, explicit_path)
else {
// Unresolvable mod declaration is a different bug (a broken build), not this
// gate's concern; rustc itself will refuse to compile it.
continue;
};
if reachable.insert(resolved.clone()) {
let file = parse(repo_root, &resolved);
let new_file_dir = resolved
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(""));
// rustc rule, confirmed empirically against two nesting depths (E0583's
// suggested paths in both cases): a module resolved via an explicit `#[path]`
// treats the resolved file exactly like a `mod.rs` — its own further plain
// `mod z;` children live in the SAME directory as the resolved file, not in a
// subdirectory named after the resolved file's stem or the module's own name.
// Without `#[path]`, the ordinary `mod.rs` vs `foo.rs` stem rule applies.
let new_children_dir =
if used_explicit_path || resolved.file_name().and_then(|n| n.to_str()) == Some("mod.rs") {
new_file_dir.clone()
} else {
resolved.with_extension("")
};
walk_items(
repo_root,
&file.items,
&new_file_dir,
&new_file_dir,
&new_children_dir,
reachable,
);
}
}
}
}
}
/// Every file reachable from a crate root (`src/lib.rs`, `src/main.rs`), by real `mod`
/// resolution.
fn reachable_src_files(repo_root: &Path) -> BTreeSet<PathBuf> {
let mut reachable = BTreeSet::new();
for root in ["src/lib.rs", "src/main.rs"] {
let root_path = repo_root.join(root);
if !root_path.is_file() {
continue;
}
let relative = PathBuf::from(root);
reachable.insert(relative.clone());
let file = parse(repo_root, &relative);
// A crate root's own submodule directory is `src/`, exactly like a `mod.rs`.
walk_items(
repo_root,
&file.items,
Path::new("src"),
Path::new("src"),
Path::new("src"),
&mut reachable,
);
}
reachable
}
/// A `.rs` file under `src/` that holds a `#[test]` function but is not reachable via `mod` from
/// any crate root never compiles, so `cargo test` silently never lists or runs those tests.
#[test]
fn every_src_test_file_is_reachable_via_mod_declaration() {
let repo_root = repo_root();
let reachable = reachable_src_files(&repo_root);
let mut offenders = Vec::new();
for path in all_src_sources() {
let relative = PathBuf::from(&path);
let file = parse(&repo_root, &relative);
if file_contains_test_fn(&file) && !reachable.contains(&relative) {
offenders.push(path);
}
}
assert!(
offenders.is_empty(),
"{} `.rs` file(s) under src/ hold a `#[test]` function but are not reachable via any \
`mod` declaration from src/lib.rs or src/main.rs, so cargo never compiles them and the \
tests inside never run:\n{}\n\n\
Add the missing `mod <name>;` declaration in the parent module, or delete the file if \
it is genuinely dead.",
offenders.len(),
offenders
.iter()
.map(|p| format!(" {p}"))
.collect::<Vec<_>>()
.join("\n")
);
}