Skip to main content

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