1use 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#[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 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}