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