Skip to main content

sofie_model/
usage.rs

1//! Names are matched without resolving paths: the dead code check can miss dead code but never calls live code dead.
2
3use std::collections::BTreeMap;
4
5use proc_macro2::{TokenStream, TokenTree};
6use syn::ext::IdentExt;
7use syn::visit::{self, Visit};
8use syn::{Attribute, ImplItem, Item, ItemImpl, ItemUse, LitStr, Macro, MetaList, Type};
9
10use crate::syntax::{is_public, line_of};
11
12/// String literals count, since a generator, a format string or `serde(with = "…")` can name an item there.
13#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct Mentions {
15    counts: BTreeMap<String, usize>,
16}
17
18impl Mentions {
19    #[must_use]
20    pub fn of(syntax: &syn::File) -> Self {
21        let mut mentions = Self::default();
22        mentions.visit_file(syntax);
23        mentions
24    }
25
26    pub fn add(&mut self, other: &Self) {
27        for (name, count) in &other.counts {
28            *self.counts.entry(name.clone()).or_default() += count;
29        }
30    }
31
32    #[must_use]
33    pub fn count(&self, name: &str) -> usize {
34        self.counts.get(name).copied().unwrap_or_default()
35    }
36
37    fn mention(&mut self, name: String) {
38        *self.counts.entry(name).or_default() += 1;
39    }
40
41    fn mention_tokens(&mut self, tokens: TokenStream) {
42        let mut after_pound = false;
43        for tree in tokens {
44            match tree {
45                TokenTree::Ident(ident) => self.mention(ident.unraw().to_string()),
46                TokenTree::Literal(literal) => self.mention_words(&literal.to_string()),
47                TokenTree::Group(group) if !(after_pound && is_doc_tokens(&group.stream())) => {
48                    self.mention_tokens(group.stream());
49                }
50                TokenTree::Group(_) => {}
51                TokenTree::Punct(ref punct) => {
52                    after_pound = punct.as_char() == '#' || (after_pound && punct.as_char() == '!');
53                    continue;
54                }
55            }
56            after_pound = false;
57        }
58    }
59
60    fn mention_words(&mut self, literal: &str) {
61        if !literal.contains('"') {
62            return;
63        }
64        let words = literal
65            .split(|character: char| !(character.is_alphanumeric() || character == '_'))
66            .filter(|word| word.starts_with(|first: char| first.is_alphabetic() || first == '_'));
67        for word in words {
68            self.mention(word.to_owned());
69        }
70    }
71}
72
73impl<'ast> Visit<'ast> for Mentions {
74    fn visit_ident(&mut self, ident: &'ast proc_macro2::Ident) {
75        self.mention(ident.unraw().to_string());
76    }
77
78    fn visit_lit_str(&mut self, literal: &'ast LitStr) {
79        self.mention_words(&literal.token().to_string());
80    }
81
82    fn visit_attribute(&mut self, attribute: &'ast Attribute) {
83        if !attribute.path().is_ident("doc") {
84            visit::visit_attribute(self, attribute);
85        }
86    }
87
88    fn visit_meta_list(&mut self, list: &'ast MetaList) {
89        visit::visit_meta_list(self, list);
90        self.mention_tokens(list.tokens.clone());
91    }
92
93    fn visit_macro(&mut self, macro_call: &'ast Macro) {
94        visit::visit_macro(self, macro_call);
95        self.mention_tokens(macro_call.tokens.clone());
96    }
97
98    fn visit_item_use(&mut self, item_use: &'ast ItemUse) {
99        if !is_public(&item_use.vis) {
100            visit::visit_item_use(self, item_use);
101        }
102    }
103
104    /// `impl Order { … }` and `impl Display for Order` do not use `Order`;
105    /// generic arguments of the type and the trait still count.
106    fn visit_item_impl(&mut self, implementation: &'ast ItemImpl) {
107        for attribute in &implementation.attrs {
108            self.visit_attribute(attribute);
109        }
110        self.visit_generics(&implementation.generics);
111        if let Some((trait_path, _)) = &implementation.trait_ {
112            self.visit_path(trait_path);
113        }
114        match implementation.self_ty.as_ref() {
115            Type::Path(self_type) => {
116                for segment in &self_type.path.segments {
117                    self.visit_path_arguments(&segment.arguments);
118                }
119            }
120            other => self.visit_type(other),
121        }
122        for item in &implementation.items {
123            self.visit_impl_item(item);
124        }
125    }
126}
127
128fn is_doc_tokens(attribute: &TokenStream) -> bool {
129    matches!(attribute.clone().into_iter().next(), Some(TokenTree::Ident(ident)) if ident == "doc")
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct PublicItem {
134    pub name: String,
135    pub line: usize,
136}
137
138#[must_use]
139pub fn public_items(syntax: &syn::File) -> Vec<PublicItem> {
140    let mut found = Vec::new();
141    collect_items(&syntax.items, &mut found);
142    found
143}
144
145fn collect_items(items: &[Item], found: &mut Vec<PublicItem>) {
146    for item in items {
147        match item {
148            Item::Mod(module) if !is_test_only(&module.attrs) => {
149                if let Some((_, inner)) = &module.content {
150                    collect_items(inner, found);
151                }
152            }
153            Item::Impl(implementation) if implementation.trait_.is_none() => {
154                collect_impl_items(&implementation.items, found);
155            }
156            Item::Fn(function)
157                if function.sig.ident == "main" || is_procedural_macro(&function.attrs) => {}
158            _ => found.extend(public_item(item)),
159        }
160    }
161}
162
163fn collect_impl_items(items: &[ImplItem], found: &mut Vec<PublicItem>) {
164    for item in items {
165        let (visibility, ident, attributes) = match item {
166            ImplItem::Fn(function) => (&function.vis, &function.sig.ident, &function.attrs),
167            ImplItem::Const(constant) => (&constant.vis, &constant.ident, &constant.attrs),
168            _ => continue,
169        };
170        if is_public(visibility) && !is_test_only(attributes) {
171            found.push(named(ident));
172        }
173    }
174}
175
176fn public_item(item: &Item) -> Option<PublicItem> {
177    let (visibility, ident, attributes) = match item {
178        Item::Const(item) => (&item.vis, &item.ident, &item.attrs),
179        Item::Enum(item) => (&item.vis, &item.ident, &item.attrs),
180        Item::Fn(item) => (&item.vis, &item.sig.ident, &item.attrs),
181        Item::Static(item) => (&item.vis, &item.ident, &item.attrs),
182        Item::Struct(item) => (&item.vis, &item.ident, &item.attrs),
183        Item::Trait(item) => (&item.vis, &item.ident, &item.attrs),
184        Item::TraitAlias(item) => (&item.vis, &item.ident, &item.attrs),
185        Item::Type(item) => (&item.vis, &item.ident, &item.attrs),
186        Item::Union(item) => (&item.vis, &item.ident, &item.attrs),
187        _ => return None,
188    };
189    (is_public(visibility) && !is_test_only(attributes)).then(|| named(ident))
190}
191
192fn named(ident: &proc_macro2::Ident) -> PublicItem {
193    PublicItem {
194        name: ident.unraw().to_string(),
195        line: line_of(ident),
196    }
197}
198
199fn is_test_only(attributes: &[Attribute]) -> bool {
200    attributes.iter().any(|attribute| {
201        attribute.path().is_ident("test")
202            || (attribute.path().is_ident("cfg")
203                && attribute
204                    .meta
205                    .require_list()
206                    .is_ok_and(|list| list.tokens.to_string() == "test"))
207    })
208}
209
210fn is_procedural_macro(attributes: &[Attribute]) -> bool {
211    attributes.iter().any(|attribute| {
212        ["proc_macro", "proc_macro_derive", "proc_macro_attribute"]
213            .iter()
214            .any(|name| attribute.path().is_ident(name))
215    })
216}
217
218#[cfg(test)]
219mod tests {
220    use super::{Mentions, public_items};
221
222    fn parse(source: &str) -> syn::File {
223        syn::parse_str(source).unwrap()
224    }
225
226    #[test]
227    fn counts_names_outside_docs_re_exports_and_impl_headers() {
228        let file = parse(
229            "pub use a::Reexported;\nuse b::Imported;\n\
230             impl Display for Order<Line> { fn f() { g!(Macro, \"{Formatted} x\"); } }\n\
231             /// Uses `Documented`.\n#[serde(with = \"Attributed\")]\nstruct S;\n",
232        );
233
234        let mentions = Mentions::of(&file);
235
236        for name in [
237            "Imported",
238            "Display",
239            "Line",
240            "Macro",
241            "Formatted",
242            "Attributed",
243        ] {
244            assert_eq!(mentions.count(name), 1, "{name}");
245        }
246        for name in ["Documented", "Reexported", "Order"] {
247            assert_eq!(mentions.count(name), 0, "{name}");
248        }
249    }
250
251    #[test]
252    fn lists_public_items_but_trait_impls_main_macros_and_tests() {
253        let file = parse(
254            "pub struct Kept;\nstruct Private;\nimpl Kept { pub fn method() {} fn private() {} }\n\
255             impl Clone for Kept { fn clone(&self) -> Self { Kept } }\npub fn main() {}\n\
256             #[proc_macro_derive(X)]\npub fn derive(item: T) -> T { item }\n\
257             #[cfg(test)]\nmod tests { pub fn helper() {} }\nmod inner { pub const LIMIT: u8 = 1; }\n",
258        );
259
260        let names: Vec<String> = public_items(&file)
261            .into_iter()
262            .map(|item| item.name)
263            .collect();
264
265        assert_eq!(names, ["Kept", "method", "LIMIT"]);
266    }
267}