1use proc_macro2::{Span, TokenStream, TokenTree};
5use syn::{
6 Attribute, File, ImplItem, ItemConst, ItemEnum, ItemFn, ItemImpl, ItemMacro, ItemMod,
7 ItemStatic, ItemStruct, ItemTrait, ItemType, TraitItem, Visibility as SynVisibility,
8 spanned::Spanned, visit::Visit,
9};
10
11use crate::types::{LineSpan, SemanticUnit, SemanticUnitKind, Visibility};
12
13fn cfg_predicate_enables_test(tokens: TokenStream, inside_not: bool) -> bool {
20 let mut last_ident: Option<String> = None;
21
22 for tree in tokens {
23 match tree {
24 TokenTree::Ident(ident) => {
25 let name = ident.to_string();
26 if name == "test" && !inside_not {
27 return true;
28 }
29 last_ident = Some(name);
30 }
31 TokenTree::Group(group) => {
32 let inner_not = inside_not || last_ident.as_deref() == Some("not");
33 if cfg_predicate_enables_test(group.stream(), inner_not) {
34 return true;
35 }
36 last_ident = None;
37 }
38 _ => {}
39 }
40 }
41
42 false
43}
44
45fn collect_cfg_features(tokens: TokenStream, inside_not: bool, out: &mut Vec<String>) {
48 let mut last_ident: Option<String> = None;
49 let mut after_feature_eq = false;
50
51 for tree in tokens {
52 match tree {
53 TokenTree::Ident(ident) => {
54 last_ident = Some(ident.to_string());
55 after_feature_eq = false;
56 }
57 TokenTree::Punct(punct) => {
58 after_feature_eq =
59 punct.as_char() == '=' && last_ident.as_deref() == Some("feature");
60 }
61 TokenTree::Literal(literal) => {
62 if after_feature_eq && !inside_not {
63 let raw = literal.to_string();
64 out.push(raw.trim_matches('"').to_string());
65 }
66 after_feature_eq = false;
67 last_ident = None;
68 }
69 TokenTree::Group(group) => {
70 let inner_not = inside_not || last_ident.as_deref() == Some("not");
71 collect_cfg_features(group.stream(), inner_not, out);
72 last_ident = None;
73 after_feature_eq = false;
74 }
75 }
76 }
77}
78
79pub struct SemanticUnitVisitor {
81 units: Vec<SemanticUnit>,
82 in_test_module: bool,
83 current_impl_name: Option<String>,
84 current_trait_visibility: Option<Visibility>,
85}
86
87impl SemanticUnitVisitor {
88 pub fn new() -> Self {
102 Self {
103 units: Vec::new(),
104 in_test_module: false,
105 current_impl_name: None,
106 current_trait_visibility: None,
107 }
108 }
109
110 pub fn extract(file: &File) -> Vec<SemanticUnit> {
131 let mut visitor = Self::new();
132 visitor.visit_file(file);
133 visitor.units
134 }
135
136 fn span_to_line_span(&self, span: Span) -> LineSpan {
137 let start = span.start();
138 let end = span.end();
139 LineSpan::new(start.line, end.line)
140 }
141
142 fn convert_visibility(&self, vis: &SynVisibility) -> Visibility {
143 match vis {
144 SynVisibility::Public(_) => Visibility::Public,
145 SynVisibility::Restricted(r) => {
146 if r.path.is_ident("crate") {
147 Visibility::Crate
148 } else {
149 Visibility::Restricted
150 }
151 }
152 SynVisibility::Inherited => Visibility::Private,
153 }
154 }
155
156 fn extract_attributes(&self, attrs: &[Attribute]) -> Vec<String> {
157 let mut attributes = Vec::new();
158
159 for attr in attrs {
160 let path = attr.path();
161 let name = path
162 .segments
163 .iter()
164 .map(|s| s.ident.to_string())
165 .collect::<Vec<_>>()
166 .join("::");
167 if !name.is_empty() {
168 attributes.push(name);
169 }
170
171 if path.is_ident("cfg")
172 && let Ok(meta) = attr.meta.require_list()
173 {
174 let mut features = Vec::new();
175 collect_cfg_features(meta.tokens.clone(), false, &mut features);
176 for feature in features {
177 attributes.push(format!("cfg_feature:{}", feature));
178 }
179 }
180 }
181
182 attributes
183 }
184
185 fn has_test_attribute(&self, attrs: &[Attribute]) -> bool {
186 attrs.iter().any(|attr| {
187 let path = attr.path();
188 let last_is_test = path
189 .segments
190 .last()
191 .map(|s| s.ident == "test" || s.ident == "bench")
192 .unwrap_or(false);
193 if last_is_test && !path.is_ident("cfg") {
194 return true;
195 }
196 if path.is_ident("cfg")
197 && let Ok(meta) = attr.meta.require_list()
198 {
199 return cfg_predicate_enables_test(meta.tokens.clone(), false);
200 }
201 false
202 })
203 }
204
205 fn is_test_module(&self, attrs: &[Attribute]) -> bool {
206 attrs.iter().any(|attr| {
207 if attr.path().is_ident("cfg")
208 && let Ok(meta) = attr.meta.require_list()
209 {
210 return cfg_predicate_enables_test(meta.tokens.clone(), false);
211 }
212 false
213 })
214 }
215
216 fn add_unit(
217 &mut self,
218 kind: SemanticUnitKind,
219 name: String,
220 visibility: Visibility,
221 span: Span,
222 attrs: &[Attribute],
223 ) {
224 let mut attributes = self.extract_attributes(attrs);
225
226 if self.in_test_module && !attributes.iter().any(|a| a == "cfg_test") {
227 attributes.push("cfg_test".to_string());
228 }
229
230 if self.has_test_attribute(attrs) && !attributes.iter().any(|a| a == "test") {
231 attributes.push("test".to_string());
232 }
233
234 let unit = match &self.current_impl_name {
235 Some(impl_name) => SemanticUnit::with_impl(
236 kind,
237 name,
238 impl_name.clone(),
239 visibility,
240 self.span_to_line_span(span),
241 attributes,
242 ),
243 None => SemanticUnit::new(
244 kind,
245 name,
246 visibility,
247 self.span_to_line_span(span),
248 attributes,
249 ),
250 };
251 self.units.push(unit);
252 }
253}
254
255impl Default for SemanticUnitVisitor {
256 fn default() -> Self {
257 Self::new()
258 }
259}
260
261impl<'ast> Visit<'ast> for SemanticUnitVisitor {
262 fn visit_item_fn(&mut self, node: &'ast ItemFn) {
263 self.add_unit(
264 SemanticUnitKind::Function,
265 node.sig.ident.to_string(),
266 self.convert_visibility(&node.vis),
267 node.span(),
268 &node.attrs,
269 );
270 syn::visit::visit_item_fn(self, node);
271 }
272
273 fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
274 self.add_unit(
275 SemanticUnitKind::Struct,
276 node.ident.to_string(),
277 self.convert_visibility(&node.vis),
278 node.span(),
279 &node.attrs,
280 );
281 syn::visit::visit_item_struct(self, node);
282 }
283
284 fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
285 self.add_unit(
286 SemanticUnitKind::Enum,
287 node.ident.to_string(),
288 self.convert_visibility(&node.vis),
289 node.span(),
290 &node.attrs,
291 );
292 syn::visit::visit_item_enum(self, node);
293 }
294
295 fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
296 let visibility = self.convert_visibility(&node.vis);
297 self.add_unit(
298 SemanticUnitKind::Trait,
299 node.ident.to_string(),
300 visibility.clone(),
301 node.span(),
302 &node.attrs,
303 );
304
305 let previous_visibility = self.current_trait_visibility.replace(visibility);
306 syn::visit::visit_item_trait(self, node);
307 self.current_trait_visibility = previous_visibility;
308 }
309
310 fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
311 let impl_name = if let Some((_, path, _)) = &node.trait_ {
312 format!(
313 "{} for {}",
314 path.segments
315 .last()
316 .map(|s| s.ident.to_string())
317 .unwrap_or_default(),
318 type_to_string(&node.self_ty)
319 )
320 } else {
321 type_to_string(&node.self_ty)
322 };
323
324 self.add_unit(
325 SemanticUnitKind::Impl,
326 impl_name.clone(),
327 Visibility::Private,
328 node.span(),
329 &node.attrs,
330 );
331
332 let previous_impl_name = self.current_impl_name.take();
333 self.current_impl_name = Some(impl_name);
334
335 let was_in_test = self.in_test_module;
336 if self.is_test_module(&node.attrs) {
337 self.in_test_module = true;
338 }
339
340 for item in &node.items {
341 match item {
342 ImplItem::Fn(method) => {
343 self.add_unit(
344 SemanticUnitKind::Function,
345 method.sig.ident.to_string(),
346 self.convert_visibility(&method.vis),
347 method.span(),
348 &method.attrs,
349 );
350 }
351 ImplItem::Const(c) => {
352 self.add_unit(
353 SemanticUnitKind::Const,
354 c.ident.to_string(),
355 self.convert_visibility(&c.vis),
356 c.span(),
357 &c.attrs,
358 );
359 }
360 ImplItem::Type(t) => {
361 self.add_unit(
362 SemanticUnitKind::TypeAlias,
363 t.ident.to_string(),
364 self.convert_visibility(&t.vis),
365 t.span(),
366 &t.attrs,
367 );
368 }
369 _ => {}
370 }
371 }
372
373 self.in_test_module = was_in_test;
374 self.current_impl_name = previous_impl_name;
375 }
376
377 fn visit_item_const(&mut self, node: &'ast ItemConst) {
378 self.add_unit(
379 SemanticUnitKind::Const,
380 node.ident.to_string(),
381 self.convert_visibility(&node.vis),
382 node.span(),
383 &node.attrs,
384 );
385 }
386
387 fn visit_item_static(&mut self, node: &'ast ItemStatic) {
388 self.add_unit(
389 SemanticUnitKind::Static,
390 node.ident.to_string(),
391 self.convert_visibility(&node.vis),
392 node.span(),
393 &node.attrs,
394 );
395 }
396
397 fn visit_item_type(&mut self, node: &'ast ItemType) {
398 self.add_unit(
399 SemanticUnitKind::TypeAlias,
400 node.ident.to_string(),
401 self.convert_visibility(&node.vis),
402 node.span(),
403 &node.attrs,
404 );
405 }
406
407 fn visit_item_macro(&mut self, node: &'ast ItemMacro) {
408 if let Some(ident) = &node.ident {
409 self.add_unit(
410 SemanticUnitKind::Macro,
411 ident.to_string(),
412 Visibility::Private,
413 node.span(),
414 &node.attrs,
415 );
416 }
417 }
418
419 fn visit_item_mod(&mut self, node: &'ast ItemMod) {
420 let is_test = self.is_test_module(&node.attrs) || node.ident == "tests";
421
422 self.add_unit(
423 SemanticUnitKind::Module,
424 node.ident.to_string(),
425 self.convert_visibility(&node.vis),
426 node.span(),
427 &node.attrs,
428 );
429
430 if let Some((_, items)) = &node.content {
431 let was_in_test = self.in_test_module;
432 self.in_test_module = is_test || was_in_test;
433
434 for item in items {
435 self.visit_item(item);
436 }
437
438 self.in_test_module = was_in_test;
439 }
440 }
441
442 fn visit_trait_item(&mut self, node: &'ast TraitItem) {
443 let visibility = self
444 .current_trait_visibility
445 .clone()
446 .unwrap_or(Visibility::Public);
447
448 match node {
449 TraitItem::Fn(method) => {
450 self.add_unit(
451 SemanticUnitKind::Function,
452 method.sig.ident.to_string(),
453 visibility,
454 method.span(),
455 &method.attrs,
456 );
457 }
458 TraitItem::Const(c) => {
459 self.add_unit(
460 SemanticUnitKind::Const,
461 c.ident.to_string(),
462 visibility,
463 c.span(),
464 &c.attrs,
465 );
466 }
467 TraitItem::Type(t) => {
468 self.add_unit(
469 SemanticUnitKind::TypeAlias,
470 t.ident.to_string(),
471 visibility,
472 t.span(),
473 &t.attrs,
474 );
475 }
476 _ => {}
477 }
478 syn::visit::visit_trait_item(self, node);
479 }
480}
481
482fn type_to_string(ty: &syn::Type) -> String {
483 match ty {
484 syn::Type::Path(p) => p
485 .path
486 .segments
487 .last()
488 .map(|s| s.ident.to_string())
489 .unwrap_or_else(|| "Unknown".to_string()),
490 _ => "Unknown".to_string(),
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
499 fn test_extract_function() {
500 let code = "pub fn hello() {}";
501 let file = syn::parse_file(code).expect("parse failed");
502 let units = SemanticUnitVisitor::extract(&file);
503
504 assert_eq!(units.len(), 1);
505 assert_eq!(units[0].name, "hello");
506 assert!(matches!(units[0].kind, SemanticUnitKind::Function));
507 assert!(matches!(units[0].visibility, Visibility::Public));
508 }
509
510 #[test]
511 fn test_extract_struct() {
512 let code = "struct Point { x: i32, y: i32 }";
513 let file = syn::parse_file(code).expect("parse failed");
514 let units = SemanticUnitVisitor::extract(&file);
515
516 assert_eq!(units.len(), 1);
517 assert_eq!(units[0].name, "Point");
518 assert!(matches!(units[0].kind, SemanticUnitKind::Struct));
519 }
520
521 #[test]
522 fn test_extract_test_function() {
523 let code = r#"
524 #[test]
525 fn test_something() {}
526 "#;
527 let file = syn::parse_file(code).expect("parse failed");
528 let units = SemanticUnitVisitor::extract(&file);
529
530 assert_eq!(units.len(), 1);
531 assert!(units[0].has_attribute("test"));
532 }
533
534 #[test]
535 fn test_extract_impl_block() {
536 let code = r#"
537 struct Foo;
538 impl Foo {
539 pub fn new() -> Self { Foo }
540 }
541 "#;
542 let file = syn::parse_file(code).expect("parse failed");
543 let units = SemanticUnitVisitor::extract(&file);
544
545 assert_eq!(units.len(), 3);
546 assert!(
547 units
548 .iter()
549 .any(|u| u.name == "Foo" && matches!(u.kind, SemanticUnitKind::Struct))
550 );
551 assert!(
552 units
553 .iter()
554 .any(|u| u.name == "Foo" && matches!(u.kind, SemanticUnitKind::Impl))
555 );
556 assert!(
557 units
558 .iter()
559 .any(|u| u.name == "new" && matches!(u.kind, SemanticUnitKind::Function))
560 );
561 }
562
563 #[test]
564 fn test_extract_test_module() {
565 let code = r#"
566 fn production() {}
567
568 #[cfg(test)]
569 mod tests {
570 fn helper() {}
571
572 #[test]
573 fn test_it() {}
574 }
575 "#;
576 let file = syn::parse_file(code).expect("parse failed");
577 let units = SemanticUnitVisitor::extract(&file);
578
579 let prod_fn = units
580 .iter()
581 .find(|u| u.name == "production")
582 .expect("production not found");
583 assert!(!prod_fn.has_attribute("cfg_test"));
584
585 let helper_fn = units
586 .iter()
587 .find(|u| u.name == "helper")
588 .expect("helper not found");
589 assert!(helper_fn.has_attribute("cfg_test"));
590
591 let test_fn = units
592 .iter()
593 .find(|u| u.name == "test_it")
594 .expect("test_it not found");
595 assert!(test_fn.has_attribute("test"));
596 assert!(test_fn.has_attribute("cfg_test"));
597 }
598
599 #[test]
600 fn test_cfg_not_test_is_production() {
601 let code = r#"
602 #[cfg(not(test))]
603 pub fn prod_only() {}
604 "#;
605 let file = syn::parse_file(code).expect("parse failed");
606 let units = SemanticUnitVisitor::extract(&file);
607
608 assert_eq!(units.len(), 1);
609 assert!(!units[0].has_attribute("test"));
610 }
611
612 #[test]
613 fn test_cfg_feature_latest_is_not_test() {
614 let code = r#"
615 #[cfg(feature = "latest")]
616 pub fn latest_api() {}
617 "#;
618 let file = syn::parse_file(code).expect("parse failed");
619 let units = SemanticUnitVisitor::extract(&file);
620
621 assert!(!units[0].has_attribute("test"));
622 assert!(units[0].has_attribute("cfg_feature:latest"));
623 }
624
625 #[test]
626 fn test_cfg_any_test_is_test() {
627 let code = r#"
628 #[cfg(any(test, feature = "slow"))]
629 fn helper() {}
630 "#;
631 let file = syn::parse_file(code).expect("parse failed");
632 let units = SemanticUnitVisitor::extract(&file);
633
634 assert!(units[0].has_attribute("test"));
635 }
636
637 #[test]
638 fn test_multi_segment_test_attribute() {
639 let code = r#"
640 #[tokio::test]
641 async fn async_test() {}
642 "#;
643 let file = syn::parse_file(code).expect("parse failed");
644 let units = SemanticUnitVisitor::extract(&file);
645
646 assert!(units[0].has_attribute("test"));
647 assert!(units[0].has_attribute("tokio::test"));
648 }
649
650 #[test]
651 fn test_cfg_feature_marker_recorded() {
652 let code = r#"
653 #[cfg(feature = "mock")]
654 pub fn mock_helper() {}
655 "#;
656 let file = syn::parse_file(code).expect("parse failed");
657 let units = SemanticUnitVisitor::extract(&file);
658
659 assert!(units[0].has_attribute("cfg_feature:mock"));
660 }
661
662 #[test]
663 fn test_cfg_not_feature_marker_skipped() {
664 let code = r#"
665 #[cfg(not(feature = "mock"))]
666 pub fn real_impl() {}
667 "#;
668 let file = syn::parse_file(code).expect("parse failed");
669 let units = SemanticUnitVisitor::extract(&file);
670
671 assert!(!units[0].has_attribute("cfg_feature:mock"));
672 }
673
674 #[test]
675 fn test_cfg_test_impl_marks_methods() {
676 let code = r#"
677 struct Foo;
678
679 #[cfg(test)]
680 impl Foo {
681 fn helper() {}
682 }
683 "#;
684 let file = syn::parse_file(code).expect("parse failed");
685 let units = SemanticUnitVisitor::extract(&file);
686
687 let helper = units
688 .iter()
689 .find(|u| u.name == "helper")
690 .expect("helper not found");
691 assert!(helper.has_attribute("cfg_test"));
692
693 let foo = units
694 .iter()
695 .find(|u| matches!(u.kind, SemanticUnitKind::Struct))
696 .expect("struct not found");
697 assert!(!foo.has_attribute("cfg_test"));
698 }
699
700 #[test]
701 fn test_private_trait_items_inherit_visibility() {
702 let code = r#"
703 trait Internal {
704 fn hidden(&self);
705 }
706
707 pub trait External {
708 fn visible(&self);
709 }
710 "#;
711 let file = syn::parse_file(code).expect("parse failed");
712 let units = SemanticUnitVisitor::extract(&file);
713
714 let hidden = units
715 .iter()
716 .find(|u| u.name == "hidden")
717 .expect("hidden not found");
718 assert!(matches!(hidden.visibility, Visibility::Private));
719
720 let visible = units
721 .iter()
722 .find(|u| u.name == "visible")
723 .expect("visible not found");
724 assert!(matches!(visible.visibility, Visibility::Public));
725 }
726}