Skip to main content

stern4rust/
registry_parser.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use syn::Item;
6use syn::spanned::Spanned;
7
8use crate::registry_item::RegistryItem;
9use crate::source_file::SourceFile;
10
11// Finds what does not belong in a registry file, and names it.
12//
13// A registry -- `tests/all_tests.rs` or any `mod.rs` below it -- holds the
14// header and `pub mod` declarations, nothing else. Everything else is a stray,
15// and this is where each one is turned into a line and a name so the rule can
16// say which thing to remove rather than only that something is wrong.
17pub struct RegistryParser;
18
19impl RegistryParser {
20    // None means the file does not parse. That is rustc's to report, far more
21    // clearly than this could.
22    pub fn strays(file: &SourceFile) -> Option<Vec<RegistryItem>> {
23        let syntax = syn::parse_file(&file.contents()).ok()?;
24        Some(
25            syntax
26                .items
27                .iter()
28                .filter(|item| !Self::is_declaration(item))
29                .map(|item| Self::stray(file, item))
30                .collect(),
31        )
32    }
33
34    // A declaration points at a file, whether or not it is `pub` -- a private
35    // `mod name;` compiles that file just as well, and being compiled is the
36    // whole point. A module with a body declares nothing: it is code hiding in
37    // the one file a reader scans expecting a list.
38    fn is_declaration(item: &Item) -> bool {
39        matches!(item, Item::Mod(module) if module.content.is_none())
40    }
41
42    fn stray(file: &SourceFile, item: &Item) -> RegistryItem {
43        let line = item.span().start().line;
44        RegistryItem::new(line, &Self::label(file, item, line))
45    }
46
47    // Named by identifier wherever there is one, because "remove the constant
48    // LIMIT" is a whole instruction and "remove the item on line 5" is half of
49    // one. The two kinds without an identifier fall back to the line as
50    // written, which is what a reader would search the file for anyway.
51    fn label(file: &SourceFile, item: &Item, line: usize) -> String {
52        match item {
53            Item::Const(inner) => format!("the constant `{}`", inner.ident),
54            Item::Enum(inner) => format!("the enum `{}`", inner.ident),
55            Item::Fn(inner) => format!("the function `{}`", inner.sig.ident),
56            Item::Impl(_) => format!("the impl block `{}`", Self::source_line(file, line)),
57            Item::Mod(inner) => format!("the inline module `{}`", inner.ident),
58            Item::Static(inner) => format!("the static `{}`", inner.ident),
59            Item::Struct(inner) => format!("the struct `{}`", inner.ident),
60            Item::Trait(inner) => format!("the trait `{}`", inner.ident),
61            Item::Type(inner) => format!("the type alias `{}`", inner.ident),
62            Item::Use(_) => format!("the import `{}`", Self::source_line(file, line)),
63            _ => format!("`{}`", Self::source_line(file, line)),
64        }
65    }
66
67    fn source_line(file: &SourceFile, line: usize) -> String {
68        file.lines()
69            .get(line.saturating_sub(1))
70            .map_or_else(String::new, |text| text.trim().to_string())
71    }
72}