1use std::collections::HashMap;
4use std::collections::HashSet;
5use std::collections::VecDeque;
6
7use proc_macro2::Span;
8use proc_macro2::TokenTree;
9use syn::Item;
10use syn::spanned::Spanned;
11
12#[derive(Clone, Copy, Debug, strum::Display, Eq, PartialEq)]
13#[strum(serialize_all = "snake_case")]
14pub enum ItemGroup {
15 ExternCrate,
16 Use,
17 Modules,
18 GlobalAsm,
19 Constants,
20 Aliases,
21 Items,
22}
23
24#[derive(Clone, Copy, Debug, Eq, strum::IntoStaticStr, PartialEq)]
25#[strum(serialize_all = "snake_case")]
26pub enum ItemKind {
27 ExternCrate,
28 Use,
29 ForeignMod,
30 Mod,
31 GlobalAsm,
32 Const,
33 Static,
34 #[strum(to_string = "ty_alias")]
35 TypeAlias,
36 Macro,
37 Enum,
38 Struct,
39 Union,
40 Trait,
41 TraitAlias,
42 Impl,
43 Fn,
44}
45
46impl ItemKind {
47 pub fn label(self) -> &'static str {
48 self.into()
49 }
50
51 pub const fn group(self) -> ItemGroup {
52 match self {
53 Self::ExternCrate => ItemGroup::ExternCrate,
54 Self::Use => ItemGroup::Use,
55 Self::ForeignMod | Self::Mod => ItemGroup::Modules,
56 Self::GlobalAsm => ItemGroup::GlobalAsm,
57 Self::Const | Self::Static => ItemGroup::Constants,
58 Self::TypeAlias => ItemGroup::Aliases,
59 Self::Macro
60 | Self::Enum
61 | Self::Struct
62 | Self::Union
63 | Self::Trait
64 | Self::TraitAlias
65 | Self::Impl
66 | Self::Fn => ItemGroup::Items,
67 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, strum::Display, Eq, Ord, PartialEq, PartialOrd)]
72pub enum VisibilityClass {
73 #[strum(to_string = "pub")]
74 Public,
75 #[strum(to_string = "pub(crate)")]
76 Crate,
77 #[strum(to_string = "restricted")]
78 Restricted,
79 #[strum(to_string = "private")]
80 Private,
81}
82
83impl From<&syn::Visibility> for VisibilityClass {
84 fn from(visibility: &syn::Visibility) -> Self {
85 match visibility {
86 syn::Visibility::Public(_) => Self::Public,
87 syn::Visibility::Restricted(restricted) => {
88 if restricted.path.is_ident("crate") {
89 Self::Crate
90 } else if restricted.path.is_ident("self") {
91 Self::Private
92 } else {
93 Self::Restricted
94 }
95 }
96 syn::Visibility::Inherited => Self::Private,
97 }
98 }
99}
100
101#[derive(Clone, Debug)]
102pub struct ClassifiedItem {
103 pub kind: ItemKind,
104 pub span: Span,
105}
106
107pub fn type_definition(item: &Item) -> Option<(String, ItemKind, VisibilityClass)> {
108 match item {
109 Item::Enum(item) => Some((item.ident.to_string(), ItemKind::Enum, VisibilityClass::from(&item.vis))),
110 Item::Struct(item) => Some((
111 item.ident.to_string(),
112 ItemKind::Struct,
113 VisibilityClass::from(&item.vis),
114 )),
115 Item::Union(item) => Some((
116 item.ident.to_string(),
117 ItemKind::Union,
118 VisibilityClass::from(&item.vis),
119 )),
120 Item::Const(_)
121 | Item::ExternCrate(_)
122 | Item::Fn(_)
123 | Item::ForeignMod(_)
124 | Item::Impl(_)
125 | Item::Macro(_)
126 | Item::Mod(_)
127 | Item::Static(_)
128 | Item::Trait(_)
129 | Item::TraitAlias(_)
130 | Item::Type(_)
131 | Item::Use(_)
132 | Item::Verbatim(_)
133 | _ => None,
134 }
135}
136
137pub fn impl_target_name(item_impl: &syn::ItemImpl) -> Option<String> {
138 let syn::Type::Path(type_path) = item_impl.self_ty.as_ref() else {
139 return None;
140 };
141 if type_path.qself.is_some() || type_path.path.leading_colon.is_some() || type_path.path.segments.len() != 1 {
142 return None;
143 }
144 type_path.path.segments.first().map(|segment| segment.ident.to_string())
145}
146
147pub fn item_visibility(item: &Item) -> Option<VisibilityClass> {
148 let visibility = match item {
149 Item::Const(item) => &item.vis,
150 Item::Enum(item) => &item.vis,
151 Item::ExternCrate(item) => &item.vis,
152 Item::Fn(item) => &item.vis,
153 Item::Mod(item) => &item.vis,
154 Item::Static(item) => &item.vis,
155 Item::Struct(item) => &item.vis,
156 Item::Trait(item) => &item.vis,
157 Item::TraitAlias(item) => &item.vis,
158 Item::Type(item) => &item.vis,
159 Item::Union(item) => &item.vis,
160 Item::Use(item) => &item.vis,
161 Item::Macro(_) | Item::Impl(_) | Item::Verbatim(_) | _ => {
162 return None;
163 }
164 };
165 Some(VisibilityClass::from(visibility))
166}
167
168pub fn item_label(item: &Item, kind: ItemKind) -> String {
169 let name = match item {
170 Item::Const(item) => Some(item.ident.to_string()),
171 Item::Enum(item) => Some(item.ident.to_string()),
172 Item::ExternCrate(item) => Some(item.ident.to_string()),
173 Item::Fn(item) => Some(item.sig.ident.to_string()),
174 Item::Mod(item) => Some(item.ident.to_string()),
175 Item::Static(item) => Some(item.ident.to_string()),
176 Item::Struct(item) => Some(item.ident.to_string()),
177 Item::Trait(item) => Some(item.ident.to_string()),
178 Item::TraitAlias(item) => Some(item.ident.to_string()),
179 Item::Type(item) => Some(item.ident.to_string()),
180 Item::Union(item) => Some(item.ident.to_string()),
181 Item::Use(_) | Item::ForeignMod(_) | Item::Macro(_) | Item::Impl(_) | Item::Verbatim(_) | _ => None,
182 };
183 name.map_or_else(|| kind.label().to_owned(), |name| format!("{} {name}", kind.label()))
184}
185
186pub fn impl_order_label(item: &Item) -> String {
187 if let Item::Impl(item_impl) = item {
188 let target = self::impl_target_name(item_impl).unwrap_or_else(|| "type".to_owned());
189 if item_impl.trait_.is_some() {
190 format!("trait impl {target}")
191 } else {
192 format!("inherent impl {target}")
193 }
194 } else {
195 self::classify_item(item).map_or_else(
196 || "item".to_owned(),
197 |classified| self::item_label(item, classified.kind),
198 )
199 }
200}
201
202pub fn item_span(item: &Item) -> Span {
203 match item {
204 Item::Const(item) => item.const_token.span(),
205 Item::Enum(item) => item.enum_token.span(),
206 Item::ExternCrate(item) => item.extern_token.span(),
207 Item::Fn(item) => item.sig.fn_token.span(),
208 Item::ForeignMod(item) => item.abi.extern_token.span(),
209 Item::Impl(item) => item.impl_token.span(),
210 Item::Macro(item) => item.mac.path.span(),
211 Item::Mod(item) => item.mod_token.span(),
212 Item::Static(item) => item.static_token.span(),
213 Item::Struct(item) => item.struct_token.span(),
214 Item::Trait(item) => item.trait_token.span(),
215 Item::TraitAlias(item) => item.trait_token.span(),
216 Item::Type(item) => item.type_token.span(),
217 Item::Union(item) => item.union_token.span(),
218 Item::Use(item) => item.use_token.span(),
219 Item::Verbatim(tokens) => self::explicit_macro_span(tokens).unwrap_or_else(|| tokens.span()),
220 _ => item.span(),
221 }
222}
223
224pub fn classify_item(item: &Item) -> Option<ClassifiedItem> {
225 let kind = match item {
226 Item::ExternCrate(_) => ItemKind::ExternCrate,
227 Item::Use(_) => ItemKind::Use,
228 Item::ForeignMod(_) => ItemKind::ForeignMod,
229 Item::Mod(_) => ItemKind::Mod,
230 Item::Macro(item_macro) if self::is_global_asm(item_macro) => ItemKind::GlobalAsm,
231 Item::Const(_) => ItemKind::Const,
232 Item::Static(_) => ItemKind::Static,
233 Item::Type(_) => ItemKind::TypeAlias,
234 Item::Macro(item_macro) if item_macro.ident.is_some() => ItemKind::Macro,
235 Item::Enum(_) => ItemKind::Enum,
236 Item::Struct(_) => ItemKind::Struct,
237 Item::Union(_) => ItemKind::Union,
238 Item::Trait(_) => ItemKind::Trait,
239 Item::TraitAlias(_) => ItemKind::TraitAlias,
240 Item::Impl(_) => ItemKind::Impl,
241 Item::Fn(_) => ItemKind::Fn,
242 Item::Verbatim(tokens) if self::explicit_macro_span(tokens).is_some() => ItemKind::Macro,
243 Item::Macro(_) | Item::Verbatim(_) | _ => return None,
244 };
245
246 Some(ClassifiedItem {
247 kind,
248 span: self::item_span(item),
249 })
250}
251
252pub(super) fn is_test_module(item: &Item) -> bool {
253 let Item::Mod(module) = item else {
254 return false;
255 };
256
257 self::is_test_module_declaration(module)
258}
259
260pub(super) fn is_test_module_declaration(module: &syn::ItemMod) -> bool {
261 module.ident == "tests"
262 && module.attrs.iter().any(|attribute| {
263 let syn::Meta::List(meta) = &attribute.meta else {
264 return false;
265 };
266 meta.path.is_ident("cfg")
267 && syn::parse2::<syn::Path>(meta.tokens.clone()).is_ok_and(|path| path.is_ident("test"))
268 })
269}
270
271fn is_global_asm(item: &syn::ItemMacro) -> bool {
272 item.mac
273 .path
274 .segments
275 .last()
276 .is_some_and(|segment| segment.ident == "global_asm")
277}
278
279fn explicit_macro_span(tokens: &proc_macro2::TokenStream) -> Option<Span> {
280 let mut tokens = tokens.clone().into_iter().peekable();
281 loop {
282 match tokens.next()? {
283 TokenTree::Punct(punct) if punct.as_char() == '#' => {
284 if !matches!(tokens.next(), Some(TokenTree::Group(group)) if group.delimiter() == proc_macro2::Delimiter::Bracket)
285 {
286 return None;
287 }
288 }
289 TokenTree::Ident(ident) if ident == "pub" => {
290 if matches!(tokens.peek(), Some(TokenTree::Group(group)) if group.delimiter() == proc_macro2::Delimiter::Parenthesis)
291 {
292 tokens.next();
293 }
294 }
295 TokenTree::Ident(ident) if matches!(ident.to_string().as_str(), "crate" | "self" | "super") => {}
296 TokenTree::Ident(ident) if ident == "macro" => {
297 let Some(TokenTree::Ident(_)) = tokens.next() else {
298 return None;
299 };
300 if matches!(tokens.peek(), Some(TokenTree::Group(group)) if group.delimiter() == proc_macro2::Delimiter::Parenthesis)
301 {
302 tokens.next();
303 }
304 return matches!(tokens.next(), Some(TokenTree::Group(group)) if group.delimiter() == proc_macro2::Delimiter::Brace)
305 .then_some(ident.span());
306 }
307 TokenTree::Group(_) | TokenTree::Ident(_) | TokenTree::Punct(_) | TokenTree::Literal(_) => return None,
308 }
309 }
310}
311
312#[derive(Clone, Debug)]
313pub(super) struct OrderNode {
314 pub(super) source_idx: usize,
315 pub(super) span: Span,
316 pub(super) group: Option<ItemGroup>,
317 pub(super) visibility: Option<VisibilityClass>,
318 pub(super) label: String,
319}
320
321#[derive(Clone, Debug)]
322pub(super) struct ModuleNode {
323 pub(super) order: OrderNode,
324 pub(super) idxs: Vec<usize>,
325}
326
327#[derive(Clone, Debug)]
328struct TypeCluster {
329 type_idx: usize,
330 name: String,
331 kind: ItemKind,
332 visibility: VisibilityClass,
333 inherent_impls: Vec<usize>,
334 trait_impls: Vec<usize>,
335}
336
337#[derive(Clone, Copy, Debug)]
338pub(super) enum ItemVisibility {
339 Known(VisibilityClass),
340 NotApplicable,
341}
342
343#[derive(Debug)]
344pub(super) enum ItemMetadata {
345 Unclassified,
346 Classified {
347 item: ClassifiedItem,
348 visibility: ItemVisibility,
349 },
350 TestModule {
351 item: ClassifiedItem,
352 visibility: VisibilityClass,
353 },
354}
355
356impl ItemMetadata {
357 fn from_item(item: &Item) -> Self {
358 let Some(classified) = self::classify_item(item) else {
359 return Self::Unclassified;
360 };
361 if self::is_test_module(item)
362 && let Item::Mod(module) = item
363 {
364 return Self::TestModule {
365 item: classified,
366 visibility: VisibilityClass::from(&module.vis),
367 };
368 }
369
370 let visibility = self::item_visibility(item).map_or(ItemVisibility::NotApplicable, ItemVisibility::Known);
371 Self::Classified {
372 item: classified,
373 visibility,
374 }
375 }
376
377 pub(super) const fn classified(&self) -> Option<&ClassifiedItem> {
378 match self {
379 Self::Unclassified => None,
380 Self::Classified { item, .. } | Self::TestModule { item, .. } => Some(item),
381 }
382 }
383
384 pub(super) const fn is_test_module(&self) -> bool {
385 matches!(self, Self::TestModule { .. })
386 }
387
388 pub(super) const fn visibility(&self) -> Option<VisibilityClass> {
389 match self {
390 Self::Unclassified => None,
391 Self::Classified { visibility, .. } => visibility.value(),
392 Self::TestModule { visibility, .. } => Some(*visibility),
393 }
394 }
395}
396
397impl ItemVisibility {
398 const fn value(self) -> Option<VisibilityClass> {
399 match self {
400 Self::Known(visibility) => Some(visibility),
401 Self::NotApplicable => None,
402 }
403 }
404}
405
406pub(super) struct ModuleItem<'ast> {
407 item: &'ast Item,
408 metadata: ItemMetadata,
409}
410
411impl<'ast> ModuleItem<'ast> {
412 pub(super) const fn item(&self) -> &'ast Item {
413 self.item
414 }
415
416 pub(super) const fn metadata(&self) -> &ItemMetadata {
417 &self.metadata
418 }
419}
420
421pub(super) fn module_item_lists(file: &syn::File) -> Vec<Vec<ModuleItem<'_>>> {
422 let mut pending = VecDeque::from([file.items.as_slice()]);
423 let mut scopes = Vec::new();
424
425 while let Some(items) = pending.pop_front() {
426 let module_items = items
427 .iter()
428 .map(|item| ModuleItem {
429 item,
430 metadata: ItemMetadata::from_item(item),
431 })
432 .collect();
433 scopes.push(module_items);
434 for item in items {
435 if let Item::Mod(module) = item
436 && let Some((_, nested_items)) = &module.content
437 {
438 pending.push_back(nested_items);
439 }
440 }
441 }
442
443 scopes
444}
445
446pub(super) fn module_nodes(items: &[ModuleItem<'_>]) -> Vec<ModuleNode> {
447 let mut type_idxs = HashMap::new();
448 let mut clusters = HashMap::new();
449
450 for (idx, module_item) in items.iter().enumerate() {
451 let item = module_item.item();
452 let Some((name, kind, visibility)) = self::type_definition(item) else {
453 continue;
454 };
455 if type_idxs.insert(name.clone(), Some(idx)).is_some() {
456 type_idxs.insert(name, None);
457 continue;
458 }
459 clusters.insert(
460 idx,
461 TypeCluster {
462 type_idx: idx,
463 name,
464 kind,
465 visibility,
466 inherent_impls: Vec::new(),
467 trait_impls: Vec::new(),
468 },
469 );
470 }
471
472 for (idx, module_item) in items.iter().enumerate() {
473 let item = module_item.item();
474 let Item::Impl(item_impl) = item else {
475 continue;
476 };
477 let Some(target_name) = self::impl_target_name(item_impl) else {
478 continue;
479 };
480 let Some(Some(type_idx)) = type_idxs.get(&target_name) else {
481 continue;
482 };
483 let Some(cluster) = clusters.get_mut(type_idx) else {
484 continue;
485 };
486 if item_impl.trait_.is_some() {
487 cluster.trait_impls.push(idx);
488 } else {
489 cluster.inherent_impls.push(idx);
490 }
491 }
492
493 let mut item_to_cluster = HashMap::new();
494 for (&type_idx, cluster) in &clusters {
495 item_to_cluster.insert(type_idx, type_idx);
496 for &idx in cluster.inherent_impls.iter().chain(&cluster.trait_impls) {
497 item_to_cluster.insert(idx, type_idx);
498 }
499 }
500
501 let mut emitted_clusters = HashSet::new();
502 let mut nodes = Vec::new();
503 for (idx, module_item) in items.iter().enumerate() {
504 let item = module_item.item();
505 if let Some(&type_idx) = item_to_cluster.get(&idx) {
506 if !emitted_clusters.insert(type_idx) {
507 continue;
508 }
509 let Some(cluster) = clusters.get(&type_idx) else {
510 continue;
511 };
512 let Some(type_item) = items.get(cluster.type_idx) else {
513 continue;
514 };
515 let mut idxs = vec![cluster.type_idx];
516 idxs.extend(&cluster.inherent_impls);
517 idxs.extend(&cluster.trait_impls);
518 let source_idx = idxs.iter().copied().min().unwrap_or(cluster.type_idx);
519 nodes.push(ModuleNode {
520 order: OrderNode {
521 source_idx,
522 span: self::item_span(type_item.item()),
523 group: Some(cluster.kind.group()),
524 visibility: Some(cluster.visibility),
525 label: format!("{} {}", cluster.kind.label(), cluster.name),
526 },
527 idxs,
528 });
529 } else if let Some(classified) = module_item.metadata().classified() {
530 nodes.push(ModuleNode {
531 order: OrderNode {
532 source_idx: idx,
533 span: classified.span,
534 group: Some(classified.kind.group()),
535 visibility: module_item.metadata().visibility(),
536 label: self::item_label(item, classified.kind),
537 },
538 idxs: vec![idx],
539 });
540 }
541 }
542
543 nodes.sort_unstable_by_key(|node| node.order.source_idx);
544 nodes
545}
546
547pub(super) fn impl_nodes(item_impl: &syn::ItemImpl) -> Vec<OrderNode> {
548 item_impl
549 .items
550 .iter()
551 .enumerate()
552 .filter_map(|(source_idx, item)| {
553 let (kind, visibility) = match item {
554 syn::ImplItem::Const(item) => (ItemKind::Const, Some(VisibilityClass::from(&item.vis))),
555 syn::ImplItem::Fn(item) => (ItemKind::Fn, Some(VisibilityClass::from(&item.vis))),
556 syn::ImplItem::Type(item) => (ItemKind::TypeAlias, Some(VisibilityClass::from(&item.vis))),
557 syn::ImplItem::Macro(_) | syn::ImplItem::Verbatim(_) | _ => return None,
558 };
559 Some(OrderNode {
560 source_idx,
561 span: self::impl_item_span(item),
562 group: None,
563 visibility,
564 label: self::impl_item_label(item, kind),
565 })
566 })
567 .collect()
568}
569
570fn impl_item_label(item: &syn::ImplItem, kind: ItemKind) -> String {
571 let name = match item {
572 syn::ImplItem::Const(item) => Some(item.ident.to_string()),
573 syn::ImplItem::Fn(item) => Some(item.sig.ident.to_string()),
574 syn::ImplItem::Type(item) => Some(item.ident.to_string()),
575 syn::ImplItem::Macro(_) | syn::ImplItem::Verbatim(_) | _ => None,
576 };
577 name.map_or_else(|| kind.label().to_owned(), |name| format!("{} {name}", kind.label()))
578}
579
580fn impl_item_span(item: &syn::ImplItem) -> Span {
581 match item {
582 syn::ImplItem::Const(item) => item.const_token.span,
583 syn::ImplItem::Fn(item) => item.sig.fn_token.span,
584 syn::ImplItem::Type(item) => item.type_token.span,
585 syn::ImplItem::Macro(item) => item.mac.path.span(),
586 syn::ImplItem::Verbatim(tokens) => tokens.span(),
587 _ => item.span(),
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 #[test]
596 fn test_module_item_lists_when_metadata_is_requested_returns_cached_item_details() {
597 let file = syn::parse_file(
598 r"
599 fn run() {}
600 #[cfg(test)]
601 mod tests {}
602 ",
603 )
604 .expect("source should parse");
605
606 let item_lists = module_item_lists(&file);
607 let root = item_lists.first().expect("root item list should exist");
608 let first_metadata = root.first().expect("root item should exist").metadata();
609
610 assert_eq!(root.len(), 2);
611 assert_eq!(first_metadata.classified().map(|item| item.kind), Some(ItemKind::Fn));
612 assert!(!first_metadata.is_test_module());
613 assert_eq!(first_metadata.visibility(), Some(VisibilityClass::Private));
614 let second_item = root.get(1).expect("second root item should exist");
615 assert_eq!(
616 second_item.metadata().classified().map(|item| item.kind),
617 Some(ItemKind::Mod)
618 );
619 assert!(second_item.metadata().is_test_module());
620 }
621
622 #[test]
623 fn test_item_metadata_when_item_has_no_module_visibility_marks_it_not_applicable() {
624 let file = syn::parse_file(
625 r"
626 struct Data;
627 impl Data {}
628 ",
629 )
630 .expect("source should parse");
631
632 let item_lists = module_item_lists(&file);
633 let root = item_lists.first().expect("root item list should exist");
634 let metadata = root.get(1).expect("impl item should exist").metadata();
635
636 assert!(matches!(
637 metadata,
638 ItemMetadata::Classified {
639 visibility: ItemVisibility::NotApplicable,
640 ..
641 }
642 ));
643 }
644}