perl_semantic_analyzer/analysis/
export_analyzer.rs1use crate::ast::{Node, NodeKind};
30use perl_semantic_facts::{AnchorId, Confidence, ExportSet, ExportTag, Provenance};
31use std::collections::{HashMap, HashSet};
32
33#[derive(Debug, Clone, Default)]
35pub struct ExportInfo {
36 pub default_export: HashSet<String>,
38 pub optional_export: HashSet<String>,
40 pub export_tags: HashMap<String, Vec<String>>,
42 pub module_name: Option<String>,
44 pub anchor_id: Option<AnchorId>,
46}
47
48impl ExportInfo {
49 #[must_use]
51 pub fn to_export_set(&self) -> ExportSet {
52 let mut default_exports: Vec<String> = self.default_export.iter().cloned().collect();
53 default_exports.sort();
54
55 let mut optional_exports: Vec<String> = self.optional_export.iter().cloned().collect();
56 optional_exports.sort();
57
58 let mut tags: Vec<ExportTag> = self
59 .export_tags
60 .iter()
61 .map(|(name, members)| {
62 let mut members = members.clone();
63 members.sort();
64 members.dedup();
65 ExportTag { name: name.clone(), members }
66 })
67 .collect();
68 tags.sort_by(|left, right| left.name.cmp(&right.name));
69
70 ExportSet {
71 default_exports,
72 optional_exports,
73 tags,
74 provenance: Provenance::ImportExportInference,
75 confidence: Confidence::High,
76 module_name: self.module_name.clone(),
77 anchor_id: self.anchor_id,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ExporterDetector {
85 UseExporterImport,
87 UseParentExporter,
89 UseBaseExporter,
91 OurIsaExporter,
93}
94
95pub struct ExportSymbolExtractor;
101
102impl ExportSymbolExtractor {
103 pub fn extract(ast: &Node) -> Option<ExportInfo> {
109 let detector = Self::detect_exporter_inheritance(ast)?;
110
111 let mut info = ExportInfo {
112 module_name: Self::find_package_name(ast),
114 anchor_id: Self::find_first_export_anchor(ast),
116 ..Default::default()
117 };
118
119 Self::walk_and_extract_exports(ast, &detector, &mut info);
121
122 Some(info)
123 }
124
125 fn detect_exporter_inheritance(ast: &Node) -> Option<ExporterDetector> {
133 Self::walk_for_exporter_detection(ast)
134 }
135
136 fn find_package_name(ast: &Node) -> Option<String> {
138 match &ast.kind {
139 NodeKind::Package { name, .. } => Some(name.clone()),
140 _ => {
141 for child in ast.children() {
142 if let Some(name) = Self::find_package_name(child) {
143 return Some(name);
144 }
145 }
146 None
147 }
148 }
149 }
150
151 fn find_first_export_anchor(ast: &Node) -> Option<AnchorId> {
154 Self::walk_for_first_export_anchor(ast)
155 }
156
157 fn walk_for_first_export_anchor(node: &Node) -> Option<AnchorId> {
159 match &node.kind {
160 NodeKind::VariableDeclaration { variable, initializer: Some(_), .. } => {
162 if let NodeKind::Variable { sigil, name } = &variable.kind {
163 let is_export_var = (sigil == "@" && (name == "EXPORT" || name == "EXPORT_OK"))
164 || (sigil == "%" && name == "EXPORT_TAGS");
165 if is_export_var {
166 return Some(AnchorId(node.location.start as u64));
167 }
168 }
169 }
170 NodeKind::Assignment { lhs, .. } => {
172 if let NodeKind::Variable { sigil, name } = &lhs.kind {
173 let is_export_var = (sigil == "@" && (name == "EXPORT" || name == "EXPORT_OK"))
174 || (sigil == "%" && name == "EXPORT_TAGS");
175 if is_export_var {
176 return Some(AnchorId(node.location.start as u64));
177 }
178 }
179 }
180 _ => {}
181 }
182
183 for child in node.children() {
184 if let Some(anchor) = Self::walk_for_first_export_anchor(child) {
185 return Some(anchor);
186 }
187 }
188
189 None
190 }
191
192 fn walk_for_exporter_detection(ast: &Node) -> Option<ExporterDetector> {
194 match &ast.kind {
195 NodeKind::Use { module, args, .. } if module == "Exporter" => {
202 if args.is_empty()
204 || args.iter().any(|arg| {
205 let arg_stripped = arg.trim_matches('\'');
206 arg_stripped == "import" || arg == "import"
207 })
208 {
209 return Some(ExporterDetector::UseExporterImport);
210 }
211 }
212 NodeKind::Use { module, args, .. } if module == "parent" => {
217 if args.iter().any(|arg| Self::arg_contains_exporter(arg)) {
218 return Some(ExporterDetector::UseParentExporter);
219 }
220 }
221 NodeKind::Use { module, args, .. } if module == "base" => {
226 if args.iter().any(|arg| Self::arg_contains_exporter(arg)) {
227 return Some(ExporterDetector::UseBaseExporter);
228 }
229 }
230 NodeKind::VariableDeclaration { variable, initializer: Some(init), .. } => {
232 if let NodeKind::Variable { sigil, name } = &variable.kind {
233 if sigil == "@" && name == "ISA" && Self::initializer_contains_exporter(init) {
234 return Some(ExporterDetector::OurIsaExporter);
235 }
236 }
237 }
238 NodeKind::Assignment { lhs, rhs, .. } => {
240 if let NodeKind::Variable { sigil, name } = &lhs.kind {
241 if sigil == "@" && name == "ISA" && Self::initializer_contains_exporter(rhs) {
242 return Some(ExporterDetector::OurIsaExporter);
243 }
244 }
245 }
246 _ => {}
247 }
248
249 for child in ast.children() {
253 if let Some(detector) = Self::walk_for_exporter_detection(child) {
254 return Some(detector);
255 }
256 }
257
258 None
259 }
260
261 fn arg_contains_exporter(arg: &str) -> bool {
266 let arg = arg.trim();
267 if arg.trim_matches('\'').trim_matches('"') == "Exporter" {
269 return true;
270 }
271 if arg.starts_with("qw") {
273 let open_pos = arg.find(|c: char| !c.is_alphanumeric()).unwrap_or(arg.len());
275 let close = match arg[open_pos..].chars().next() {
276 Some('(') => ')',
277 Some('{') => '}',
278 Some('[') => ']',
279 Some('<') => '>',
280 Some(c) => c,
281 None => return false,
282 };
283 if let (Some(start), Some(end)) =
284 (arg[open_pos..].find(|c: char| !c.is_whitespace()), arg.rfind(close))
285 {
286 let content = &arg[open_pos + start + 1..end];
287 return content.split_whitespace().any(|w| w == "Exporter");
288 }
289 }
290 false
291 }
292
293 fn initializer_contains_exporter(init: &Node) -> bool {
295 match &init.kind {
296 NodeKind::ArrayLiteral { elements } => elements.iter().any(Self::node_is_exporter),
298 NodeKind::String { value, .. } => {
300 let s_stripped = value.trim_matches('\'');
301 s_stripped == "Exporter" || value == "Exporter"
302 }
303 _ => false,
304 }
305 }
306
307 fn node_is_exporter(node: &Node) -> bool {
309 match &node.kind {
310 NodeKind::String { value, .. } => {
311 let s_stripped = value.trim_matches('\'');
312 s_stripped == "Exporter" || value == "Exporter"
313 }
314 NodeKind::ArrayLiteral { elements } => elements.iter().any(Self::node_is_exporter),
315 _ => false,
316 }
317 }
318
319 fn walk_and_extract_exports(ast: &Node, _detector: &ExporterDetector, info: &mut ExportInfo) {
325 match &ast.kind {
326 NodeKind::VariableDeclaration { variable, initializer: Some(init), .. } => {
328 if let NodeKind::Variable { sigil, name } = &variable.kind {
329 if sigil == "@" {
330 match name.as_str() {
331 "EXPORT" => {
332 let symbols = Self::parse_qw_array(init);
333 info.default_export.extend(symbols);
334 }
335 "EXPORT_OK" => {
336 let symbols = Self::parse_qw_array(init);
337 info.optional_export.extend(symbols);
338 }
339 _ => {}
340 }
341 } else if sigil == "%" && name == "EXPORT_TAGS" {
342 let tags = Self::parse_export_tags(init);
343 info.export_tags.extend(tags);
344 }
345 }
346
347 Self::walk_and_extract_exports(init, _detector, info);
349 }
350 NodeKind::Assignment { lhs, rhs, .. } => {
352 if let NodeKind::Variable { sigil, name } = &lhs.kind {
353 if sigil == "@" {
354 match name.as_str() {
355 "EXPORT" => {
356 let symbols = Self::parse_qw_array(rhs);
357 info.default_export.extend(symbols);
358 }
359 "EXPORT_OK" => {
360 let symbols = Self::parse_qw_array(rhs);
361 info.optional_export.extend(symbols);
362 }
363 _ => {}
364 }
365 } else if sigil == "%" && name == "EXPORT_TAGS" {
366 let tags = Self::parse_export_tags(rhs);
367 info.export_tags.extend(tags);
368 }
369 }
370 Self::walk_and_extract_exports(rhs, _detector, info);
372 }
373 _ => {
374 for child in ast.children() {
376 Self::walk_and_extract_exports(child, _detector, info);
377 }
378 }
379 }
380 }
381
382 fn parse_qw_array(node: &Node) -> Vec<String> {
392 match &node.kind {
393 NodeKind::ArrayLiteral { elements } => {
395 if elements.is_empty() {
396 return Vec::new();
397 }
398 if elements.len() == 1 {
402 if let NodeKind::ArrayLiteral { .. } = &elements[0].kind {
403 return Self::parse_qw_array(&elements[0]);
405 }
406 }
407 elements
409 .iter()
410 .filter_map(|elem| {
411 if let NodeKind::String { value, .. } = &elem.kind {
413 Some(value.clone())
414 } else {
415 None
416 }
417 })
418 .collect()
419 }
420 NodeKind::Binary { op, left, right } if op == "." => {
422 let mut result = Vec::new();
424 if let NodeKind::String { value, .. } = &left.kind {
425 result.push(value.clone());
426 }
427 if let NodeKind::String { value, .. } = &right.kind {
428 result.push(value.clone());
429 }
430 result
431 }
432 _ => {
435 let mut symbols = Vec::new();
437 for child in node.children() {
438 symbols.extend(Self::parse_qw_array(child));
439 }
440 symbols
441 }
442 }
443 }
444
445 fn parse_export_tags(node: &Node) -> HashMap<String, Vec<String>> {
457 let mut tags: HashMap<String, Vec<String>> = HashMap::new();
458
459 match &node.kind {
460 NodeKind::HashLiteral { pairs } => {
462 for (key_node, value_node) in pairs {
463 if let Some(tag_name) = Self::extract_string_value(key_node) {
464 let symbols = Self::parse_qw_array(value_node);
465 if !symbols.is_empty() {
466 tags.insert(tag_name, symbols);
467 }
468 }
469 }
470 }
471 _ => {
473 Self::walk_and_extract_export_tags(node, &mut tags);
474 }
475 }
476
477 tags
478 }
479
480 fn walk_and_extract_export_tags(node: &Node, tags: &mut HashMap<String, Vec<String>>) {
482 match &node.kind {
483 NodeKind::HashLiteral { pairs } => {
484 for (key_node, value_node) in pairs {
485 if let Some(tag_name) = Self::extract_string_value(key_node) {
486 let symbols = Self::parse_qw_array(value_node);
487 if !symbols.is_empty() {
488 tags.insert(tag_name, symbols);
489 }
490 }
491 }
492 }
493 _ => {
494 for child in node.children() {
495 Self::walk_and_extract_export_tags(child, tags);
496 }
497 }
498 }
499 }
500
501 fn extract_string_value(node: &Node) -> Option<String> {
503 match &node.kind {
504 NodeKind::String { value, .. } => Some(value.clone()),
505 NodeKind::Identifier { name } => Some(name.clone()),
506 _ => None,
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use crate::Parser;
515
516 fn parse_and_extract(code: &str) -> Option<ExportInfo> {
517 let mut parser = Parser::new(code);
518 let ast = parser.parse().ok()?;
519 ExportSymbolExtractor::extract(&ast)
520 }
521
522 #[test]
523 fn test_detect_use_exporter_import() {
524 let code = r#"
525package MyUtils;
526use Exporter 'import';
527our @EXPORT = qw(foo bar);
5281;
529"#;
530 let info = parse_and_extract(code);
531 assert!(info.is_some(), "Should detect Exporter, got {:?}", info);
532 let info = info.unwrap();
533 assert!(info.default_export.contains("foo"));
534 assert!(info.default_export.contains("bar"));
535 }
536
537 #[test]
538 fn test_detect_use_parent_exporter() {
539 let code = r#"
540package MyModule;
541use parent 'Exporter';
542our @EXPORT = qw(default_func);
5431;
544"#;
545 let info = parse_and_extract(code);
546 assert!(info.is_some(), "Should detect parent Exporter");
547 let info = info.unwrap();
548 assert!(info.default_export.contains("default_func"));
549 }
550
551 #[test]
552 fn test_detect_use_parent_exporter_qw_form() {
553 let code = r#"
555package MyModule;
556use parent qw(Exporter);
557our @EXPORT = qw(qw_parent_func);
5581;
559"#;
560 let info = parse_and_extract(code);
561 assert!(info.is_some(), "Should detect `use parent qw(Exporter)` as Exporter-based");
562 let info = info.unwrap();
563 assert!(info.default_export.contains("qw_parent_func"));
564 }
565
566 #[test]
567 fn test_detect_use_base_exporter() {
568 let code = r#"
570package Legacy;
571use base 'Exporter';
572our @EXPORT = qw(legacy_func);
5731;
574"#;
575 let info = parse_and_extract(code);
576 assert!(info.is_some(), "Should detect `use base 'Exporter'` as Exporter-based");
577 let info = info.unwrap();
578 assert!(info.default_export.contains("legacy_func"));
579 }
580
581 #[test]
582 fn test_detect_use_base_exporter_qw_form() {
583 let code = r#"
584package Legacy;
585use base qw(Exporter SomeOtherBase);
586our @EXPORT = qw(base_qw_func);
5871;
588"#;
589 let info = parse_and_extract(code);
590 assert!(info.is_some(), "Should detect `use base qw(Exporter ...)` as Exporter-based");
591 let info = info.unwrap();
592 assert!(info.default_export.contains("base_qw_func"));
593 }
594
595 #[test]
596 fn test_detect_our_isa_exporter() {
597 let code = r#"
598package MyClass;
599our @ISA = qw(Exporter);
600our @EXPORT = qw(inherited_func);
6011;
602"#;
603 let info = parse_and_extract(code);
604 assert!(info.is_some(), "Should detect @ISA Exporter");
605 let info = info.unwrap();
606 assert!(info.default_export.contains("inherited_func"));
607 }
608
609 #[test]
610 fn test_detect_bare_isa_assignment() {
611 let code = r#"
613package OldStyle;
614@ISA = qw(Exporter);
615@EXPORT = qw(old_func);
6161;
617"#;
618 let info = parse_and_extract(code);
619 assert!(info.is_some(), "Should detect bare `@ISA = qw(Exporter)` form");
620 let info = info.unwrap();
621 assert!(
622 info.default_export.contains("old_func"),
623 "Should extract @EXPORT from bare assignment form"
624 );
625 }
626
627 #[test]
628 fn test_export_ok() {
629 let code = r#"
630package MyLib;
631use Exporter 'import';
632our @EXPORT_OK = qw(optional_a optional_b);
6331;
634"#;
635 let info = parse_and_extract(code).unwrap();
636 assert!(info.optional_export.contains("optional_a"));
637 assert!(info.optional_export.contains("optional_b"));
638 }
639
640 #[test]
641 fn test_export_tags() {
642 let code = r#"
643package Color;
644use Exporter 'import';
645our @EXPORT_OK = qw(red green blue rgb hex);
646our %EXPORT_TAGS = (
647 primary => [qw(red green blue)],
648 formats => [qw(rgb hex)],
649);
6501;
651"#;
652 let info = parse_and_extract(code).unwrap();
653 let primary = info.export_tags.get("primary");
654 assert!(primary.is_some());
655 let primary = primary.unwrap();
656 assert!(primary.contains(&"red".to_string()));
657 assert!(primary.contains(&"green".to_string()));
658 assert!(primary.contains(&"blue".to_string()));
659
660 let formats = info.export_tags.get("formats").unwrap();
661 assert!(formats.contains(&"rgb".to_string()));
662 assert!(formats.contains(&"hex".to_string()));
663 }
664
665 #[test]
666 fn test_no_exporter_no_extraction() {
667 let code = r#"
670package MyModule;
671our @EXPORT = qw(not_exported);
6721;
673"#;
674 let info = parse_and_extract(code);
675 assert!(
676 info.is_none(),
677 "Should return None when no Exporter inheritance is detected, got {:?}",
678 info
679 );
680 }
681
682 #[test]
683 fn test_empty_export_arrays() {
684 let code = r#"
685package MyModule;
686use Exporter 'import';
687our @EXPORT = ();
688our @EXPORT_OK = ();
689our %EXPORT_TAGS = ();
6901;
691"#;
692 let info = parse_and_extract(code).unwrap();
693 assert!(info.default_export.is_empty());
694 assert!(info.optional_export.is_empty());
695 assert!(info.export_tags.is_empty());
696 }
697
698 #[test]
699 fn test_multiple_arrays() {
700 let code = r#"
701package MyModule;
702use Exporter 'import';
703our @EXPORT = qw(default_a default_b);
704our @EXPORT_OK = qw(optional_c optional_d);
705our %EXPORT_TAGS = (
706 tag1 => [qw(tag_a tag_b)],
707);
7081;
709"#;
710 let info = parse_and_extract(code).unwrap();
711 assert_eq!(info.default_export.len(), 2);
712 assert!(info.default_export.contains("default_a"));
713 assert!(info.default_export.contains("default_b"));
714
715 assert_eq!(info.optional_export.len(), 2);
716 assert!(info.optional_export.contains("optional_c"));
717 assert!(info.optional_export.contains("optional_d"));
718
719 assert_eq!(info.export_tags.len(), 1);
720 }
721
722 #[test]
723 fn test_detect_use_exporter_no_args() {
724 let code = r#"
727package MyUtils;
728use Exporter;
729our @EXPORT = qw(legacy_func);
7301;
731"#;
732 let info = parse_and_extract(code);
733 assert!(info.is_some(), "Should detect bare `use Exporter;` as Exporter-based module");
734 let info = info.unwrap();
735 assert!(
736 info.default_export.contains("legacy_func"),
737 "Should extract @EXPORT symbols from bare use Exporter; module"
738 );
739 }
740
741 #[test]
742 fn test_isa_with_multiple_parents_includes_exporter() {
743 let code = r#"
745package Multi;
746our @ISA = qw(SomeBase Exporter OtherBase);
747our @EXPORT = qw(multi_func);
7481;
749"#;
750 let info = parse_and_extract(code);
751 assert!(info.is_some(), "Should detect Exporter even when mixed with other @ISA parents");
752 let info = info.unwrap();
753 assert!(info.default_export.contains("multi_func"));
754 }
755 #[test]
756 fn test_regression_exporter_visibility_fixture() {
757 let code = r#"
758package MyLib;
759use Exporter 'import';
760our @EXPORT = qw(foo);
761our @EXPORT_OK = qw(bar baz);
762our %EXPORT_TAGS = (
763 all => [qw(foo bar baz)],
764);
7651;
766"#;
767 let info = parse_and_extract(code).unwrap();
768
769 assert_eq!(info.default_export.len(), 1);
770 assert!(info.default_export.contains("foo"));
771
772 assert_eq!(info.optional_export.len(), 2);
773 assert!(info.optional_export.contains("bar"));
774 assert!(info.optional_export.contains("baz"));
775
776 let all = info.export_tags.get("all").unwrap();
777 assert_eq!(all, &vec!["foo".to_string(), "bar".to_string(), "baz".to_string()]);
778 }
779
780 #[test]
781 fn test_regression_merges_export_assignments_across_statements() {
782 let code = r#"
783package MyLib;
784use Exporter 'import';
785our @EXPORT = qw(foo);
786our @EXPORT_OK = qw(bar);
787our @EXPORT_OK = qw(bar baz);
788our %EXPORT_TAGS = (core => [qw(foo bar)]);
789our %EXPORT_TAGS = (all => [qw(foo bar baz)]);
7901;
791"#;
792 let info = parse_and_extract(code).unwrap();
793
794 assert!(info.default_export.contains("foo"));
795 assert!(info.optional_export.contains("bar"));
796 assert!(info.optional_export.contains("baz"));
797 assert_eq!(
798 info.export_tags.get("core").unwrap(),
799 &vec!["foo".to_string(), "bar".to_string()]
800 );
801 assert_eq!(
802 info.export_tags.get("all").unwrap(),
803 &vec!["foo".to_string(), "bar".to_string(), "baz".to_string()]
804 );
805 }
806
807 #[test]
808 fn test_module_name_populated_from_package_declaration() -> Result<(), String> {
809 let code = r#"
810package My::Utils;
811use Exporter 'import';
812our @EXPORT = qw(helper);
8131;
814"#;
815 let info = parse_and_extract(code).ok_or("Expected Some(ExportInfo)")?;
816 assert_eq!(
817 info.module_name.as_deref(),
818 Some("My::Utils"),
819 "module_name should be extracted from the package declaration"
820 );
821 Ok(())
822 }
823
824 #[test]
825 fn test_module_name_propagated_to_export_set() -> Result<(), String> {
826 let code = r#"
827package Data::Formatter;
828use parent 'Exporter';
829our @EXPORT_OK = qw(format_csv);
8301;
831"#;
832 let info = parse_and_extract(code).ok_or("Expected Some(ExportInfo)")?;
833 let export_set = info.to_export_set();
834 assert_eq!(
835 export_set.module_name.as_deref(),
836 Some("Data::Formatter"),
837 "ExportSet.module_name should carry the package name"
838 );
839 Ok(())
840 }
841
842 #[test]
843 fn test_anchor_id_populated_from_first_export_declaration() -> Result<(), String> {
844 let code = r#"
845package MyLib;
846use Exporter 'import';
847our @EXPORT = qw(foo);
8481;
849"#;
850 let info = parse_and_extract(code).ok_or("Expected Some(ExportInfo)")?;
851 assert!(
852 info.anchor_id.is_some(),
853 "anchor_id should be populated from the first export declaration"
854 );
855 Ok(())
856 }
857
858 #[test]
859 fn test_anchor_id_propagated_to_export_set() -> Result<(), String> {
860 let code = r#"
861package MyLib;
862use Exporter 'import';
863our @EXPORT_OK = qw(bar baz);
8641;
865"#;
866 let info = parse_and_extract(code).ok_or("Expected Some(ExportInfo)")?;
867 let export_set = info.to_export_set();
868 assert!(
869 export_set.anchor_id.is_some(),
870 "ExportSet.anchor_id should carry the first export declaration anchor"
871 );
872 Ok(())
873 }
874
875 #[test]
876 fn test_anchor_id_none_when_no_export_arrays() -> Result<(), String> {
877 let code = r#"
879package EmptyExporter;
880use Exporter 'import';
8811;
882"#;
883 let info = parse_and_extract(code).ok_or("Expected Some(ExportInfo)")?;
884 assert!(
885 info.anchor_id.is_none(),
886 "anchor_id should be None when no export arrays are declared"
887 );
888 Ok(())
889 }
890
891 #[test]
892 fn test_module_name_and_anchor_id_with_bare_assignment() -> Result<(), String> {
893 let code = r#"
894package OldStyle::Lib;
895@ISA = qw(Exporter);
896@EXPORT = qw(old_func);
8971;
898"#;
899 let info = parse_and_extract(code).ok_or("Expected Some(ExportInfo)")?;
900 assert_eq!(
901 info.module_name.as_deref(),
902 Some("OldStyle::Lib"),
903 "module_name should work with bare assignment style"
904 );
905 assert!(
906 info.anchor_id.is_some(),
907 "anchor_id should be populated from bare @EXPORT assignment"
908 );
909 Ok(())
910 }
911
912 #[test]
913 fn test_export_set_completeness_with_module_and_anchor() -> Result<(), String> {
914 let code = r#"
915package Full::Module;
916use base 'Exporter';
917our @EXPORT = qw(alpha beta);
918our @EXPORT_OK = qw(gamma);
919our %EXPORT_TAGS = (all => [qw(alpha beta gamma)]);
9201;
921"#;
922 let info = parse_and_extract(code).ok_or("Expected Some(ExportInfo)")?;
923 let export_set = info.to_export_set();
924
925 assert_eq!(export_set.module_name.as_deref(), Some("Full::Module"));
927 assert!(export_set.anchor_id.is_some());
928
929 assert_eq!(export_set.default_exports, vec!["alpha", "beta"]);
931 assert_eq!(export_set.optional_exports, vec!["gamma"]);
932 assert_eq!(export_set.tags.len(), 1);
933 assert_eq!(export_set.tags[0].name, "all");
934 assert_eq!(export_set.tags[0].members, vec!["alpha", "beta", "gamma"]);
935 Ok(())
936 }
937}