1use std::cell::OnceCell;
24use std::collections::{HashMap, HashSet};
25use std::fs;
26use std::path::Path;
27use std::process;
28use std::rc::Rc;
29
30use serde::{Deserialize, Serialize};
31use tree_sitter::{Node, Parser};
32
33const MAX_PURPOSE: usize = 200;
34
35const TYPE_KINDS: &[(&str, &str)] = &[
38 ("class_declaration", "class"),
39 ("interface_declaration", "interface"),
40 ("struct_declaration", "struct"),
41 ("record_declaration", "record"),
42 ("enum_declaration", "enum"),
43];
44
45const NAMESPACE_NODES: &[&str] = &["namespace_declaration", "file_scoped_namespace_declaration"];
46
47fn type_kind_label(kind: &str) -> Option<&'static str> {
48 TYPE_KINDS
49 .iter()
50 .find(|(k, _)| *k == kind)
51 .map(|(_, label)| *label)
52}
53
54fn new_parser() -> Parser {
55 let mut parser = Parser::new();
56 parser
57 .set_language(&tree_sitter_c_sharp::LANGUAGE.into())
58 .expect("failed to load C# grammar");
59 parser
60}
61
62fn named_children<'a>(node: Node<'a>) -> Vec<Node<'a>> {
63 let mut cursor = node.walk();
64 node.named_children(&mut cursor).collect()
65}
66
67fn text(node: Node, src: &[u8]) -> String {
68 crate::parse::node_text(node, src)
69}
70
71fn declared_name(node: Node, src: &[u8]) -> String {
72 node.child_by_field_name("name")
73 .map(|n| text(n, src))
74 .unwrap_or_default()
75}
76
77fn namespace_level_types<'a>(root: Node<'a>) -> Vec<Node<'a>> {
82 let mut types = Vec::new();
83 collect_types(root, &mut types);
84 types
85}
86
87fn collect_types<'a>(node: Node<'a>, types: &mut Vec<Node<'a>>) {
88 for child in named_children(node) {
89 if NAMESPACE_NODES.contains(&child.kind()) {
90 let body = child.child_by_field_name("body").unwrap_or(child);
91 collect_types(body, types);
92 continue;
93 }
94 if type_kind_label(child.kind()).is_some() {
95 types.push(child);
96 }
97 }
98}
99
100fn base_list_text(node: Node, src: &[u8]) -> String {
101 let Some(bases) = named_children(node)
102 .into_iter()
103 .find(|c| c.kind() == "base_list")
104 else {
105 return String::new();
106 };
107 let raw = text(bases, src);
108 let after_colon = match raw.strip_prefix(':') {
109 Some(rest) => rest.trim_start(),
110 None => raw.as_str(),
111 };
112 collapse_whitespace(after_colon).trim().to_string()
113}
114
115fn collapse_whitespace(s: &str) -> String {
116 let mut out = String::with_capacity(s.len());
117 let mut in_ws = false;
118 for ch in s.chars() {
119 if ch.is_whitespace() {
120 if !in_ws {
121 out.push(' ');
122 in_ws = true;
123 }
124 } else {
125 out.push(ch);
126 in_ws = false;
127 }
128 }
129 out
130}
131
132fn is_public(node: Node, src: &[u8]) -> bool {
133 named_children(node)
134 .into_iter()
135 .any(|c| c.kind() == "modifier" && text(c, src) == "public")
136}
137
138fn strip_async_suffix(name: &str) -> String {
139 name.strip_suffix("Async").unwrap_or(name).to_string()
140}
141
142fn public_method_names(node: Node, src: &[u8], kind: &str) -> Vec<String> {
147 let Some(body) = node.child_by_field_name("body") else {
148 return Vec::new();
149 };
150 named_children(body)
151 .into_iter()
152 .filter(|c| c.kind() == "method_declaration" && (kind == "interface" || is_public(*c, src)))
153 .map(|c| strip_async_suffix(&declared_name(c, src)))
154 .filter(|n| !n.is_empty())
155 .collect()
156}
157
158type SegmentParts = (usize, &'static str, String, String, Vec<String>);
164
165fn type_segment_parts(node: Node, src: &[u8]) -> Option<SegmentParts> {
166 let kind = type_kind_label(node.kind())?;
167 let name = declared_name(node, src);
168 if name.is_empty() {
169 return None;
170 }
171 let bases = base_list_text(node, src);
172 let methods = public_method_names(node, src, kind);
173 Some((node.start_position().row + 1, kind, name, bases, methods))
174}
175
176fn format_segments(parts: Vec<SegmentParts>) -> Option<String> {
183 let mut seen_methods: HashSet<String> = HashSet::new();
184 let mut segments: Vec<String> = Vec::new();
185 for (_line, kind, name, bases, raw_methods) in parts {
186 let mut methods = Vec::new();
187 for method in raw_methods {
188 if seen_methods.contains(&method) {
189 continue;
190 }
191 seen_methods.insert(method.clone());
192 methods.push(method);
193 }
194 let header = if bases.is_empty() {
195 format!("{kind} {name}")
196 } else {
197 format!("{kind} {name} : {bases}")
198 };
199 let segment = if methods.is_empty() {
200 header
201 } else {
202 format!("{header}; {}", methods.join(", "))
203 };
204 segments.push(segment);
205 }
206 if segments.is_empty() {
207 None
208 } else {
209 Some(truncate(&segments.join(" | ")))
210 }
211}
212
213fn truncate(s: &str) -> String {
221 let units: Vec<u16> = s.encode_utf16().collect();
222 if units.len() > MAX_PURPOSE {
223 let mut truncated = String::from_utf16_lossy(&units[..MAX_PURPOSE - 3]);
224 truncated.push_str("...");
225 truncated
226 } else {
227 s.to_string()
228 }
229}
230
231pub struct DefRecord {
242 pub id: String,
244 pub name: String,
246 pub namespace: String,
248 pub kind: String,
250 pub line: usize,
252 pub methods: Vec<String>,
254 pub properties: Vec<String>,
262 pub fields: Vec<String>,
268 pub method_returns: Vec<(String, String)>,
278 pub extension_methods: Vec<ExtensionMethod>,
288 pub bases: Vec<String>,
296 pub type_params: Vec<String>,
302 pub base_generic_args: Vec<(String, Vec<String>)>,
310 pub test_methods: Vec<String>,
317 pub property_types: Vec<(String, Fact)>,
325 pub end_line: usize,
327}
328
329#[derive(Debug, Clone, PartialEq)]
333pub struct ExtensionMethod {
334 pub name: String,
336 pub this_type: String,
338 pub arity_min: usize,
341 pub arity_max: i64,
345 pub this_args: Option<Vec<String>>,
350}
351
352pub struct RefRecord {
354 pub kind: String,
356 pub name: String,
358 pub qualified: Option<String>,
360 pub member: Option<String>,
362 pub line: usize,
364 pub namespace: Option<String>,
369 pub type_arg_count: Option<usize>,
371 pub generic: bool,
377 pub receiver_type: Option<String>,
385 pub arg_count: Option<usize>,
392 pub receiver_args: Option<Vec<String>>,
399 pub outer_types: Vec<String>,
404 pub args: Option<Vec<String>>,
412 pub receiver_property_owner: Option<String>,
421 pub receiver_call_owner: Option<String>,
426 pub receiver_call_member: Option<String>,
428}
429
430pub enum UsingRecord {
432 Alias {
434 alias: String,
436 target: String,
438 global: bool,
440 },
441 Plain {
443 text: String,
445 global: bool,
447 },
448}
449
450pub struct Extraction {
452 pub purpose: Option<String>,
454 pub defs: Vec<DefRecord>,
456 pub usings: Vec<UsingRecord>,
458 pub refs: Vec<RefRecord>,
460 pub names: Vec<NameRecord>,
463}
464
465#[derive(Debug, Clone, PartialEq)]
475pub struct NameRecord {
476 pub name: String,
478 pub kind: String,
480 pub line: usize,
482 pub owner: String,
484}
485
486fn outer_type_name(node: Node, src: &[u8]) -> Option<String> {
493 match node.kind() {
494 "nullable_type" | "array_type" => match node.child_by_field_name("type") {
495 Some(inner) => outer_type_name(inner, src),
496 None => None,
497 },
498 "generic_name" => named_children(node)
499 .into_iter()
500 .find(|c| c.kind() == "identifier")
501 .map(|id| text(id, src)),
502 "qualified_name" => {
503 let tail = node.child_by_field_name("name")?;
504 let full = text(node, src);
505 let tail_text = text(tail, src);
506 let normalized_tail = outer_type_name(tail, src)?;
507 full.strip_suffix(&tail_text)
508 .map(|prefix| format!("{prefix}{normalized_tail}"))
509 }
510 "alias_qualified_name" => match node.child_by_field_name("name") {
511 Some(n) => outer_type_name(n, src),
512 None => None,
513 },
514 "identifier" => Some(text(node, src)),
515 _ => None,
516 }
517}
518
519fn base_type_identifier(node: Option<Node>, src: &[u8], keep_predefined: bool) -> Option<String> {
536 let node = node?;
537 let name = match node.kind() {
538 "nullable_type" | "array_type" => {
539 base_type_identifier(node.child_by_field_name("type"), src, keep_predefined)
540 }
541 "generic_name" => named_children(node)
542 .into_iter()
543 .find(|c| c.kind() == "identifier")
544 .map(|id| text(id, src)),
545 "qualified_name" | "alias_qualified_name" => {
546 base_type_identifier(node.child_by_field_name("name"), src, keep_predefined)
547 }
548 "identifier" => Some(text(node, src)),
549 "predefined_type" => {
550 if keep_predefined {
551 Some(text(node, src))
552 } else {
553 None
554 }
555 }
556 _ => None,
557 };
558 name.filter(|n| !n.is_empty())
559}
560
561fn top_level_generic_name(node: Option<Node>) -> Option<Node> {
568 let node = node?;
569 match node.kind() {
570 "nullable_type" | "array_type" => top_level_generic_name(node.child_by_field_name("type")),
571 "qualified_name" | "alias_qualified_name" => {
572 top_level_generic_name(node.child_by_field_name("name"))
573 }
574 "generic_name" => Some(node),
575 _ => None,
576 }
577}
578
579fn type_argument_arity(node: Option<Node>, src: &[u8]) -> Option<usize> {
580 let generic = top_level_generic_name(node)?;
581 let list = named_children(generic)
582 .into_iter()
583 .find(|c| c.kind() == "type_argument_list")?;
584 Some(text(list, src).bytes().filter(|b| *b == b',').count() + 1)
585}
586
587fn generic_arg_descriptors(
599 node: Option<Node>,
600 src: &[u8],
601 type_params: &HashSet<String>,
602) -> Option<Vec<String>> {
603 let generic = top_level_generic_name(node)?;
604 let list = named_children(generic)
605 .into_iter()
606 .find(|c| c.kind() == "type_argument_list")?;
607 let args = named_children(list);
608 if args.is_empty() {
609 return None;
610 }
611 let mut descriptors = Vec::with_capacity(args.len());
612 for arg in args {
613 let base = base_type_identifier(Some(arg), src, true)?;
614 descriptors.push(if type_params.contains(&base) {
615 "*".to_string()
616 } else {
617 base
618 });
619 }
620 Some(descriptors)
621}
622
623fn type_parameter_names(node: Node, src: &[u8]) -> HashSet<String> {
626 let mut names = HashSet::new();
627 let Some(list) = named_children(node)
628 .into_iter()
629 .find(|c| c.kind() == "type_parameter_list")
630 else {
631 return names;
632 };
633 for p in named_children(list) {
634 if p.kind() != "type_parameter" {
635 continue;
636 }
637 let name = match p.child_by_field_name("name") {
638 Some(n) => text(n, src),
639 None => text(p, src),
640 };
641 if !name.is_empty() {
642 names.insert(name);
643 }
644 }
645 names
646}
647
648fn type_parameter_names_ordered(node: Node, src: &[u8]) -> Vec<String> {
655 let mut names: Vec<String> = Vec::new();
656 let Some(list) = named_children(node)
657 .into_iter()
658 .find(|c| c.kind() == "type_parameter_list")
659 else {
660 return names;
661 };
662 for p in named_children(list) {
663 if p.kind() != "type_parameter" {
664 continue;
665 }
666 let name = match p.child_by_field_name("name") {
667 Some(n) => text(n, src),
668 None => text(p, src),
669 };
670 if !name.is_empty() && !names.contains(&name) {
671 names.push(name);
672 }
673 }
674 names
675}
676
677fn member_qualifier_info(node: Option<Node>, src: &[u8]) -> Option<(String, bool)> {
698 let node = node?;
699 match node.kind() {
700 "identifier" | "qualified_name" => Some((text(node, src), false)),
701 "generic_name" => named_children(node)
702 .into_iter()
703 .find(|c| c.kind() == "identifier")
704 .map(|id| (text(id, src), true)),
705 "member_access_expression" => {
706 let inner = member_qualifier_info(node.child_by_field_name("expression"), src);
707 let name_node = node.child_by_field_name("name")?;
708 let (inner_text, inner_generic) = inner?;
709 if name_node.kind() == "generic_name" {
710 return named_children(name_node)
711 .into_iter()
712 .find(|c| c.kind() == "identifier")
713 .map(|id| (format!("{inner_text}.{}", text(id, src)), true));
714 }
715 let name = text(name_node, src);
716 if name.is_empty() {
717 return None;
718 }
719 Some((format!("{inner_text}.{name}"), inner_generic))
720 }
721 _ => None,
722 }
723}
724
725fn invocation_arg_count(node: Node) -> Option<usize> {
745 let parent = node.parent()?;
746 if parent.kind() != "invocation_expression" {
747 return None;
748 }
749 let function = parent.child_by_field_name("function")?;
750 if function.id() != node.id() {
751 return None;
752 }
753 let args = parent.child_by_field_name("arguments")?;
754 if args.kind() != "argument_list" {
755 return None;
756 }
757 Some(named_children(args).len())
758}
759
760fn push_ref(
761 refs: &mut Vec<RefRecord>,
762 kind: &str,
763 name: String,
764 line: usize,
765 ns: Option<String>,
766 qualified: Option<String>,
767 type_stack: &[String],
768) {
769 refs.push(RefRecord {
770 kind: kind.to_string(),
771 name,
772 qualified,
773 member: None,
774 line,
775 namespace: ns,
776 type_arg_count: None,
777 generic: false,
778 receiver_type: None,
779 arg_count: None,
780 receiver_args: None,
781 outer_types: type_stack.to_vec(),
782 args: None,
783 receiver_property_owner: None,
784 receiver_call_owner: None,
785 receiver_call_member: None,
786 });
787}
788
789fn push_ctor_param_ref(
794 refs: &mut Vec<RefRecord>,
795 name: String,
796 line: usize,
797 ns: String,
798 args: Option<Vec<String>>,
799 type_stack: &[String],
800) {
801 refs.push(RefRecord {
802 kind: "ctor-param".to_string(),
803 name,
804 qualified: None,
805 member: None,
806 line,
807 namespace: Some(ns),
808 type_arg_count: Some(args.as_ref().map_or(0, Vec::len)),
809 generic: false,
810 receiver_type: None,
811 arg_count: None,
812 receiver_args: None,
813 outer_types: type_stack.to_vec(),
814 args,
815 receiver_property_owner: None,
816 receiver_call_owner: None,
817 receiver_call_member: None,
818 });
819}
820
821fn push_member_ref(
843 refs: &mut Vec<RefRecord>,
844 qualifier_text: &str,
845 member: String,
846 line: usize,
847 ns: String,
848 generic: bool,
849 receiver: Option<Fact>,
850 arg_count: Option<usize>,
851 type_stack: &[String],
852 property_owner: Option<String>,
853) {
854 let (receiver_type, receiver_args, receiver_call_owner, receiver_call_member) = match receiver {
858 Some(Fact {
859 type_name,
860 call: Some(member),
861 ..
862 }) => (None, None, Some(type_name), Some(member)),
863 Some(Fact {
864 type_name,
865 args,
866 call: None,
867 }) => (Some(type_name), args, None, None),
868 None => (None, None, None, None),
869 };
870 match qualifier_text.rfind('.') {
871 Some(dot) => refs.push(RefRecord {
872 kind: "uses-member".to_string(),
873 name: qualifier_text[dot + 1..].to_string(),
874 qualified: Some(qualifier_text.to_string()),
875 member: Some(member),
876 line,
877 namespace: Some(ns),
878 type_arg_count: None,
879 generic,
880 receiver_type,
881 arg_count,
882 receiver_args,
883 outer_types: type_stack.to_vec(),
884 args: None,
885 receiver_property_owner: property_owner.clone(),
886 receiver_call_owner: receiver_call_owner.clone(),
887 receiver_call_member: receiver_call_member.clone(),
888 }),
889 None => refs.push(RefRecord {
890 kind: "uses-member".to_string(),
891 name: qualifier_text.to_string(),
892 qualified: None,
893 member: Some(member),
894 line,
895 namespace: Some(ns),
896 type_arg_count: None,
897 generic,
898 receiver_type,
899 arg_count,
900 receiver_args,
901 outer_types: type_stack.to_vec(),
902 args: None,
903 receiver_property_owner: property_owner,
904 receiver_call_owner,
905 receiver_call_member,
906 }),
907 }
908}
909
910fn record_single_type(
915 node: Option<Node>,
916 kind: &str,
917 ns: &str,
918 type_stack: &[String],
919 src: &[u8],
920 refs: &mut Vec<RefRecord>,
921) {
922 let Some(node) = node else {
923 return;
924 };
925 if node.kind() == "tuple_type" {
926 for el in named_children(node) {
927 if el.kind() != "tuple_element" {
928 continue;
929 }
930 record_single_type(
931 el.child_by_field_name("type"),
932 kind,
933 ns,
934 type_stack,
935 src,
936 refs,
937 );
938 }
939 return;
940 }
941 let Some(raw) = outer_type_name(node, src) else {
942 return;
943 };
944 let line = node.start_position().row + 1;
945 let arity = Some(type_argument_arity(Some(node), src).unwrap_or(0));
946 match raw.rfind('.') {
947 Some(dot) => push_ref(
948 refs,
949 kind,
950 raw[dot + 1..].to_string(),
951 line,
952 Some(ns.to_string()),
953 Some(raw.clone()),
954 type_stack,
955 ),
956 None => push_ref(
957 refs,
958 kind,
959 raw.clone(),
960 line,
961 Some(ns.to_string()),
962 None,
963 type_stack,
964 ),
965 }
966 if let Some(last) = refs.last_mut() {
967 last.type_arg_count = arity;
968 }
969}
970
971fn record_base_list(
972 node: Node,
973 ns: &str,
974 type_stack: &[String],
975 src: &[u8],
976 refs: &mut Vec<RefRecord>,
977) {
978 let Some(bl) = named_children(node)
979 .into_iter()
980 .find(|c| c.kind() == "base_list")
981 else {
982 return;
983 };
984 for child in named_children(bl) {
985 if child.kind() == "argument_list" {
986 continue;
987 }
988 if child.kind() == "primary_constructor_base_type" {
989 record_single_type(
990 child.child_by_field_name("type"),
991 "inherits",
992 ns,
993 type_stack,
994 src,
995 refs,
996 );
997 continue;
998 }
999 record_single_type(Some(child), "inherits", ns, type_stack, src, refs);
1000 }
1001}
1002
1003fn raw_method_names(node: Node, src: &[u8], kind: &str) -> Vec<String> {
1007 let Some(body) = node.child_by_field_name("body") else {
1008 return Vec::new();
1009 };
1010 named_children(body)
1011 .into_iter()
1012 .filter(|c| is_recorded_method(*c, src, kind))
1013 .map(|c| declared_name(c, src))
1014 .filter(|n| !n.is_empty())
1015 .collect()
1016}
1017
1018fn is_recorded_method(node: Node, src: &[u8], kind: &str) -> bool {
1022 node.kind() == "method_declaration" && (kind == "interface" || is_public(node, src))
1023}
1024
1025fn raw_property_names(node: Node, src: &[u8]) -> Vec<String> {
1031 let Some(body) = node.child_by_field_name("body") else {
1032 return Vec::new();
1033 };
1034 let mut seen: HashSet<String> = HashSet::new();
1035 let mut names = Vec::new();
1036 for c in named_children(body) {
1037 if c.kind() != "property_declaration" {
1038 continue;
1039 }
1040 let name = declared_name(c, src);
1041 if name.is_empty() || !seen.insert(name.clone()) {
1042 continue;
1043 }
1044 names.push(name);
1045 }
1046 names
1047}
1048
1049fn raw_property_types(
1055 node: Node,
1056 src: &[u8],
1057 type_params: &HashSet<String>,
1058) -> Vec<(String, Fact)> {
1059 let Some(body) = node.child_by_field_name("body") else {
1060 return Vec::new();
1061 };
1062 let mut seen: HashSet<String> = HashSet::new();
1063 let mut pairs = Vec::new();
1064 for c in named_children(body) {
1065 if c.kind() != "property_declaration" {
1066 continue;
1067 }
1068 let name = declared_name(c, src);
1069 if name.is_empty() || !seen.insert(name.clone()) {
1070 continue;
1071 }
1072 if let Some(fact) = type_fact(c.child_by_field_name("type"), src, type_params) {
1073 pairs.push((name, fact));
1074 }
1075 }
1076 pairs
1077}
1078
1079fn raw_field_names(node: Node, src: &[u8]) -> Vec<String> {
1084 let Some(body) = node.child_by_field_name("body") else {
1085 return Vec::new();
1086 };
1087 let mut seen: HashSet<String> = HashSet::new();
1088 let mut names = Vec::new();
1089 for c in named_children(body) {
1090 if c.kind() != "field_declaration" {
1091 continue;
1092 }
1093 let Some(vd) = named_children(c)
1094 .into_iter()
1095 .find(|k| k.kind() == "variable_declaration")
1096 else {
1097 continue;
1098 };
1099 for decl in named_children(vd) {
1100 if decl.kind() != "variable_declarator" {
1101 continue;
1102 }
1103 let Some(name) = decl.child_by_field_name("name").map(|n| text(n, src)) else {
1104 continue;
1105 };
1106 if name.is_empty() || !seen.insert(name.clone()) {
1107 continue;
1108 }
1109 names.push(name);
1110 }
1111 }
1112 names
1113}
1114
1115fn raw_method_returns(node: Node, src: &[u8], kind: &str) -> Vec<(String, String)> {
1123 let Some(body) = node.child_by_field_name("body") else {
1124 return Vec::new();
1125 };
1126 let mut seen: HashSet<String> = HashSet::new();
1127 let mut pairs = Vec::new();
1128 for c in named_children(body) {
1129 if !is_recorded_method(c, src, kind) {
1130 continue;
1131 }
1132 let name = declared_name(c, src);
1133 if name.is_empty() || !seen.insert(name.clone()) {
1134 continue;
1135 }
1136 if let Some(returns) = base_type_identifier(c.child_by_field_name("returns"), src, false) {
1137 pairs.push((name, returns));
1138 }
1139 }
1140 pairs
1141}
1142
1143fn extension_arity_range(parameters: Node, src: &[u8]) -> (usize, i64) {
1163 let mut total: i64 = 0;
1164 let mut min: usize = 0;
1165 let mut unbounded = false;
1166 let mut seen_this = false;
1167 let mut cursor = parameters.walk();
1168 for c in parameters.children(&mut cursor) {
1169 if c.kind() == "params" {
1170 unbounded = true;
1171 continue;
1172 }
1173 if c.kind() != "parameter" {
1174 continue;
1175 }
1176 if !seen_this {
1180 seen_this = true;
1181 continue;
1182 }
1183 total += 1;
1184 let is_params = named_children(c)
1185 .iter()
1186 .any(|m| m.kind() == "modifier" && text(*m, src) == "params");
1187 if is_params {
1188 unbounded = true;
1189 } else if !has_default_value(c) {
1190 min += 1;
1191 }
1192 }
1193 (min, if unbounded { -1 } else { total })
1194}
1195
1196fn has_default_value(parameter: Node) -> bool {
1198 (0..parameter.child_count() as u32).any(|i| parameter.child(i).map(|c| c.kind()) == Some("="))
1199}
1200
1201fn raw_extension_methods(node: Node, src: &[u8]) -> Vec<ExtensionMethod> {
1233 let Some(body) = node.child_by_field_name("body") else {
1234 return Vec::new();
1235 };
1236 let class_type_params = type_parameter_names(node, src);
1237 let mut seen: HashSet<String> = HashSet::new();
1238 let mut entries = Vec::new();
1239 for c in named_children(body) {
1240 if c.kind() != "method_declaration" {
1241 continue;
1242 }
1243 let Some(parameters) = c.child_by_field_name("parameters") else {
1244 continue;
1245 };
1246 let params = named_children(parameters);
1247 let Some(first) = params.first().copied() else {
1248 continue;
1249 };
1250 if first.kind() != "parameter" {
1251 continue;
1252 }
1253 if !named_children(first)
1254 .iter()
1255 .any(|m| m.kind() == "modifier" && text(*m, src) == "this")
1256 {
1257 continue;
1258 }
1259 let name = declared_name(c, src);
1260 let type_node = first.child_by_field_name("type");
1265 let Some(this_type) = base_type_identifier(type_node, src, true) else {
1266 continue;
1267 };
1268 if name.is_empty() {
1269 continue;
1270 }
1271 let (arity_min, arity_max) = extension_arity_range(parameters, src);
1272 if !seen.insert(format!("{name} {this_type} {arity_min} {arity_max}")) {
1273 continue;
1274 }
1275 let mut type_params = class_type_params.clone();
1276 type_params.extend(type_parameter_names(c, src));
1277 let this_args = generic_arg_descriptors(type_node, src, &type_params);
1278 entries.push(ExtensionMethod {
1279 name,
1280 this_type,
1281 arity_min,
1282 arity_max,
1283 this_args,
1284 });
1285 }
1286 entries
1287}
1288
1289fn raw_base_names(node: Node, src: &[u8]) -> Vec<String> {
1302 let Some(bl) = named_children(node)
1303 .into_iter()
1304 .find(|c| c.kind() == "base_list")
1305 else {
1306 return Vec::new();
1307 };
1308 let mut names: Vec<String> = Vec::new();
1309 for child in named_children(bl) {
1310 if child.kind() == "argument_list" {
1311 continue;
1312 }
1313 let type_node = if child.kind() == "primary_constructor_base_type" {
1314 child.child_by_field_name("type")
1315 } else {
1316 Some(child)
1317 };
1318 if let Some(name) = base_type_identifier(type_node, src, false) {
1319 if !names.contains(&name) {
1320 names.push(name);
1321 }
1322 }
1323 }
1324 names
1325}
1326
1327fn raw_base_generic_args(
1337 node: Node,
1338 src: &[u8],
1339 type_params: &HashSet<String>,
1340) -> Vec<(String, Vec<String>)> {
1341 let Some(bl) = named_children(node)
1342 .into_iter()
1343 .find(|c| c.kind() == "base_list")
1344 else {
1345 return Vec::new();
1346 };
1347 let mut out: Vec<(String, Vec<String>)> = Vec::new();
1348 for child in named_children(bl) {
1349 if child.kind() == "argument_list" {
1350 continue;
1351 }
1352 let type_node = if child.kind() == "primary_constructor_base_type" {
1353 child.child_by_field_name("type")
1354 } else {
1355 Some(child)
1356 };
1357 let Some(name) = base_type_identifier(type_node, src, false) else {
1358 continue;
1359 };
1360 if out.iter().any(|(k, _)| k == &name) {
1361 continue;
1362 }
1363 if let Some(args) = generic_arg_descriptors(type_node, src, type_params) {
1364 out.push((name, args));
1365 }
1366 }
1367 out
1368}
1369
1370const DIRECT_TEST_ATTRIBUTES: &[&str] = &["Fact", "Theory", "Test", "TestCase", "TestCaseSource"];
1381const MSTEST_TEST_ATTRIBUTES: &[&str] = &["TestMethod", "DataTestMethod"];
1382const MSTEST_CLASS_ATTRIBUTE: &str = "TestClass";
1383const TEST_METHOD_KINDS: &[&str] = &["class", "struct", "record"];
1384const ATTRIBUTE_SUFFIX: &str = "Attribute";
1385
1386fn attribute_names(node: Node, src: &[u8]) -> HashSet<String> {
1397 let mut names = HashSet::new();
1398 for list in named_children(node) {
1399 if list.kind() != "attribute_list" {
1400 continue;
1401 }
1402 for attr in named_children(list) {
1403 if attr.kind() != "attribute" {
1404 continue;
1405 }
1406 let Some(name_node) = attr.child_by_field_name("name") else {
1407 continue;
1408 };
1409 let text = text(name_node, src);
1410 if text.is_empty() {
1411 continue;
1412 }
1413 let last = match text.rfind('.') {
1414 Some(i) => &text[i + 1..],
1415 None => &text[..],
1416 };
1417 if last.is_empty() {
1418 continue;
1419 }
1420 names.insert(last.to_string());
1421 if last.len() > ATTRIBUTE_SUFFIX.len() && last.ends_with(ATTRIBUTE_SUFFIX) {
1422 names.insert(last[..last.len() - ATTRIBUTE_SUFFIX.len()].to_string());
1423 }
1424 }
1425 }
1426 names
1427}
1428
1429fn raw_test_methods(node: Node, src: &[u8], kind: &str) -> Vec<String> {
1439 if !TEST_METHOD_KINDS.contains(&kind) {
1440 return Vec::new();
1441 }
1442 let Some(body) = node.child_by_field_name("body") else {
1443 return Vec::new();
1444 };
1445 let mstest = attribute_names(node, src).contains(MSTEST_CLASS_ATTRIBUTE);
1446 let mut seen: HashSet<String> = HashSet::new();
1447 let mut names = Vec::new();
1448 for c in named_children(body) {
1449 if c.kind() != "method_declaration" {
1450 continue;
1451 }
1452 let attrs = attribute_names(c, src);
1453 let marked = attrs.iter().any(|a| {
1454 DIRECT_TEST_ATTRIBUTES.contains(&a.as_str())
1455 || (mstest && MSTEST_TEST_ATTRIBUTES.contains(&a.as_str()))
1456 });
1457 if !marked {
1458 continue;
1459 }
1460 let name = declared_name(c, src);
1461 if name.is_empty() || !seen.insert(name.clone()) {
1462 continue;
1463 }
1464 names.push(name);
1465 }
1466 names
1467}
1468
1469fn type_id(name: &str, ns: &str, type_stack: &[String]) -> String {
1475 if !type_stack.is_empty() {
1476 let prefix = if ns.is_empty() {
1477 String::new()
1478 } else {
1479 format!("{ns}.")
1480 };
1481 format!("{prefix}{}+{name}", type_stack.join("+"))
1482 } else if !ns.is_empty() {
1483 format!("{ns}.{name}")
1484 } else {
1485 name.to_string()
1486 }
1487}
1488
1489fn record_declared_members(node: Node, owner_id: &str, src: &[u8], names: &mut Vec<NameRecord>) {
1494 let Some(body) = node.child_by_field_name("body") else {
1495 return;
1496 };
1497 for c in named_children(body) {
1498 match c.kind() {
1499 "method_declaration" => push_declared_name(
1500 c.child_by_field_name("name"),
1501 "method",
1502 owner_id,
1503 src,
1504 names,
1505 ),
1506 "property_declaration" => push_declared_name(
1507 c.child_by_field_name("name"),
1508 "property",
1509 owner_id,
1510 src,
1511 names,
1512 ),
1513 "event_declaration" => {
1514 push_declared_name(c.child_by_field_name("name"), "event", owner_id, src, names)
1515 }
1516 "field_declaration" => push_declarators(c, "field", owner_id, src, names),
1517 "event_field_declaration" => push_declarators(c, "event", owner_id, src, names),
1518 _ => {}
1519 }
1520 }
1521}
1522
1523fn push_declared_name(
1524 name_node: Option<Node>,
1525 kind: &str,
1526 owner_id: &str,
1527 src: &[u8],
1528 names: &mut Vec<NameRecord>,
1529) {
1530 let Some(name_node) = name_node else {
1531 return;
1532 };
1533 let name = text(name_node, src);
1534 if name.is_empty() {
1535 return;
1536 }
1537 names.push(NameRecord {
1538 name,
1539 kind: kind.to_string(),
1540 line: name_node.start_position().row + 1,
1541 owner: owner_id.to_string(),
1542 });
1543}
1544
1545fn push_declarators(
1546 node: Node,
1547 kind: &str,
1548 owner_id: &str,
1549 src: &[u8],
1550 names: &mut Vec<NameRecord>,
1551) {
1552 let Some(vd) = named_children(node)
1553 .into_iter()
1554 .find(|k| k.kind() == "variable_declaration")
1555 else {
1556 return;
1557 };
1558 for decl in named_children(vd) {
1559 if decl.kind() != "variable_declarator" {
1560 continue;
1561 }
1562 push_declared_name(decl.child_by_field_name("name"), kind, owner_id, src, names);
1563 }
1564}
1565
1566fn record_type_def(
1571 node: Node,
1572 ns: &str,
1573 kind: &str,
1574 type_stack: &[String],
1575 src: &[u8],
1576 defs: &mut Vec<DefRecord>,
1577 names: &mut Vec<NameRecord>,
1578 type_params: &HashSet<String>,
1579) {
1580 let name = declared_name(node, src);
1581 if name.is_empty() {
1582 return;
1583 }
1584 let id = type_id(&name, ns, type_stack);
1585 record_declared_members(node, &id, src, names);
1586 defs.push(DefRecord {
1594 id,
1595 name,
1596 namespace: ns.to_string(),
1597 kind: kind.to_string(),
1598 line: node.start_position().row + 1,
1599 methods: raw_method_names(node, src, kind),
1600 properties: raw_property_names(node, src),
1601 fields: raw_field_names(node, src),
1602 method_returns: raw_method_returns(node, src, kind),
1603 extension_methods: raw_extension_methods(node, src),
1604 bases: raw_base_names(node, src),
1605 type_params: type_parameter_names_ordered(node, src),
1606 base_generic_args: raw_base_generic_args(node, src, type_params),
1607 test_methods: raw_test_methods(node, src, kind),
1608 property_types: raw_property_types(node, src, type_params),
1609 end_line: node.end_position().row + 1,
1610 });
1611}
1612
1613fn record_enum_members(
1618 node: Node,
1619 ns: &str,
1620 type_stack: &[String],
1621 src: &[u8],
1622 defs: &mut Vec<DefRecord>,
1623) {
1624 let name = declared_name(node, src);
1625 if name.is_empty() {
1626 return;
1627 }
1628 let enum_id = type_id(&name, ns, type_stack);
1629 let Some(body) = node.child_by_field_name("body") else {
1630 return;
1631 };
1632 for member in named_children(body) {
1633 if member.kind() != "enum_member_declaration" {
1634 continue;
1635 }
1636 let member_name = declared_name(member, src);
1637 if member_name.is_empty() {
1638 continue;
1639 }
1640 defs.push(DefRecord {
1641 id: format!("{enum_id}.{member_name}"),
1642 name: member_name,
1643 namespace: ns.to_string(),
1644 kind: "enum-member".to_string(),
1645 line: member.start_position().row + 1,
1646 methods: Vec::new(),
1647 properties: Vec::new(),
1648 fields: Vec::new(),
1649 method_returns: Vec::new(),
1650 extension_methods: Vec::new(),
1651 bases: Vec::new(),
1652 type_params: Vec::new(),
1653 base_generic_args: Vec::new(),
1654 test_methods: Vec::new(),
1655 property_types: Vec::new(),
1656 end_line: member.end_position().row + 1,
1657 });
1658 }
1659}
1660
1661fn record_using(node: Node, src: &[u8], usings: &mut Vec<UsingRecord>, refs: &mut Vec<RefRecord>) {
1662 let is_global = node.child(0).map(|n| n.kind()) == Some("global");
1665 let kids = named_children(node);
1672 let Some(target) = kids.last() else {
1673 return;
1674 };
1675 let text_val = text(*target, src).trim().to_string();
1676 if text_val.is_empty() {
1677 return;
1678 }
1679 if kids.len() == 2 {
1680 let alias_name = text(kids[0], src).trim().to_string();
1681 if !alias_name.is_empty() {
1682 usings.push(UsingRecord::Alias {
1683 alias: alias_name,
1684 target: text_val.clone(),
1685 global: is_global,
1686 });
1687 }
1688 } else {
1689 usings.push(UsingRecord::Plain {
1690 text: text_val.clone(),
1691 global: is_global,
1692 });
1693 }
1694 push_ref(
1695 refs,
1696 "imports",
1697 text_val,
1698 node.start_position().row + 1,
1699 None,
1700 None,
1701 &[],
1702 );
1703}
1704
1705#[derive(Clone, PartialEq, Eq)]
1729pub struct Fact {
1730 pub type_name: String,
1732 pub args: Option<Vec<String>>,
1734 pub call: Option<String>,
1740}
1741
1742type FactTable = HashMap<String, Option<Fact>>;
1746
1747fn add_fact(table: &mut FactTable, name: Option<String>, fact: Option<Fact>) {
1748 let Some(name) = name.filter(|n| !n.is_empty()) else {
1749 return;
1750 };
1751 match table.get(&name) {
1752 None => {
1753 table.insert(name, fact);
1754 }
1755 Some(existing) => {
1756 if *existing != fact {
1757 table.insert(name, None);
1758 }
1759 }
1760 }
1761}
1762
1763fn type_fact(type_node: Option<Node>, src: &[u8], type_params: &HashSet<String>) -> Option<Fact> {
1766 let type_name = base_type_identifier(type_node, src, false)?;
1767 let args = generic_arg_descriptors(type_node, src, type_params);
1768 Some(Fact {
1769 type_name,
1770 args,
1771 call: None,
1772 })
1773}
1774
1775fn new_expression_fact(
1780 declarator: Node,
1781 src: &[u8],
1782 type_params: &HashSet<String>,
1783) -> Option<Fact> {
1784 let init = named_children(declarator)
1785 .into_iter()
1786 .find(|c| c.kind() == "object_creation_expression")?;
1787 type_fact(init.child_by_field_name("type"), src, type_params)
1788}
1789
1790fn collect_type_facts(node: Node, src: &[u8], type_params: &HashSet<String>) -> FactTable {
1794 let mut table = FactTable::new();
1795 for kid in named_children(node) {
1796 if kid.kind() != "parameter_list" {
1797 continue;
1798 }
1799 for p in named_children(kid) {
1800 if p.kind() != "parameter" {
1801 continue;
1802 }
1803 add_fact(
1804 &mut table,
1805 p.child_by_field_name("name").map(|n| text(n, src)),
1806 type_fact(p.child_by_field_name("type"), src, type_params),
1807 );
1808 }
1809 }
1810 let Some(body) = node.child_by_field_name("body") else {
1811 return table;
1812 };
1813 collect_declared_member_facts(&named_children(body), src, type_params, &mut table);
1814 table
1815}
1816
1817fn collect_declared_member_facts(
1824 members: &[Node],
1825 src: &[u8],
1826 type_params: &HashSet<String>,
1827 table: &mut FactTable,
1828) {
1829 for c in members {
1830 if c.kind() == "property_declaration" {
1831 let name = declared_name(*c, src);
1832 add_fact(
1833 table,
1834 Some(name),
1835 type_fact(c.child_by_field_name("type"), src, type_params),
1836 );
1837 continue;
1838 }
1839 if c.kind() != "field_declaration" {
1840 continue;
1841 }
1842 let Some(vd) = named_children(*c)
1843 .into_iter()
1844 .find(|k| k.kind() == "variable_declaration")
1845 else {
1846 continue;
1847 };
1848 let fact = type_fact(vd.child_by_field_name("type"), src, type_params);
1849 for decl in named_children(vd) {
1850 if decl.kind() != "variable_declarator" {
1851 continue;
1852 }
1853 add_fact(
1854 table,
1855 decl.child_by_field_name("name").map(|n| text(n, src)),
1856 fact.clone(),
1857 );
1858 }
1859 }
1860}
1861
1862fn collect_member_facts(
1876 node: Node,
1877 src: &[u8],
1878 class_type_params: &HashSet<String>,
1879 type_facts: &FactTable,
1880) -> FactTable {
1881 let mut type_params = class_type_params.clone();
1882 type_params.extend(type_parameter_names(node, src));
1883 let mut table = FactTable::new();
1884 let mut deferred: Vec<DeferredCall> = Vec::new();
1889 let mut deferred_foreach: Vec<DeferredForeach> = Vec::new();
1893 visit_member_facts(
1894 node,
1895 src,
1896 &type_params,
1897 &mut table,
1898 &mut deferred,
1899 &mut deferred_foreach,
1900 );
1901 if !deferred.is_empty() {
1902 let pending: HashSet<&str> = deferred.iter().map(|d| d.name.as_str()).collect();
1903 for d in &deferred {
1904 let owner = if pending.contains(d.qualifier.as_str()) {
1909 None
1910 } else {
1911 qualifier_type_name(&table, type_facts, &d.qualifier)
1912 };
1913 let fact = owner.map(|type_name| Fact {
1914 type_name,
1915 args: None,
1916 call: Some(d.member.clone()),
1917 });
1918 add_fact(&mut table, Some(d.name.clone()), fact);
1919 }
1920 }
1921 for d in &deferred_foreach {
1922 let fact = collection_element_fact(&table, type_facts, &d.collection);
1931 add_fact(&mut table, Some(d.name.clone()), fact);
1932 }
1933 table
1934}
1935
1936struct DeferredCall {
1939 name: String,
1940 qualifier: String,
1941 member: String,
1942}
1943
1944struct DeferredForeach {
1948 name: String,
1949 collection: String,
1950}
1951
1952fn visit_member_facts(
1953 n: Node,
1954 src: &[u8],
1955 type_params: &HashSet<String>,
1956 table: &mut FactTable,
1957 deferred: &mut Vec<DeferredCall>,
1958 deferred_foreach: &mut Vec<DeferredForeach>,
1959) {
1960 if n.kind() == "parameter" {
1961 add_fact(
1962 table,
1963 n.child_by_field_name("name").map(|x| text(x, src)),
1964 type_fact(n.child_by_field_name("type"), src, type_params),
1965 );
1966 } else if n.kind() == "variable_declaration" {
1967 let type_node = n.child_by_field_name("type");
1968 let is_var = type_node.map(|t| t.kind()) == Some("implicit_type");
1969 let declared = if is_var {
1970 None
1971 } else {
1972 type_fact(type_node, src, type_params)
1973 };
1974 for decl in named_children(n) {
1975 if decl.kind() != "variable_declarator" {
1976 continue;
1977 }
1978 let name = decl.child_by_field_name("name").map(|x| text(x, src));
1979 let fact = if is_var {
1980 new_expression_fact(decl, src, type_params)
1981 } else {
1982 declared.clone()
1983 };
1984 let call = match (is_var && fact.is_none(), name.as_deref()) {
1985 (true, Some(n)) if !n.is_empty() => invocation_call(decl, src),
1986 _ => None,
1987 };
1988 match call {
1989 Some((qualifier, member)) => deferred.push(DeferredCall {
1990 name: name.unwrap_or_default(),
1991 qualifier,
1992 member,
1993 }),
1994 None => add_fact(table, name, fact),
1995 }
1996 }
1997 } else if n.kind() == "foreach_statement" {
1998 let left = n.child_by_field_name("left");
2008 let name = left
2009 .filter(|l| l.kind() == "identifier")
2010 .map(|l| text(l, src));
2011 if let Some(name) = name {
2012 let type_node = n.child_by_field_name("type");
2013 let is_var = type_node
2014 .map(|t| t.kind() == "implicit_type")
2015 .unwrap_or(true);
2016 if !is_var {
2017 add_fact(table, Some(name), type_fact(type_node, src, type_params));
2018 } else {
2019 let right = n.child_by_field_name("right");
2020 match right.filter(|r| r.kind() == "identifier") {
2021 Some(r) => deferred_foreach.push(DeferredForeach {
2022 name,
2023 collection: text(r, src),
2024 }),
2025 None => add_fact(table, Some(name), None),
2026 }
2027 }
2028 }
2029 }
2030 for c in named_children(n) {
2031 visit_member_facts(c, src, type_params, table, deferred, deferred_foreach);
2032 }
2033}
2034
2035fn invocation_call(declarator: Node, src: &[u8]) -> Option<(String, String)> {
2041 let init = named_children(declarator)
2042 .into_iter()
2043 .find(|c| c.kind() == "invocation_expression")?;
2044 let function = init.child_by_field_name("function")?;
2045 if function.kind() != "member_access_expression" {
2046 return None;
2047 }
2048 let (qualifier, generic) =
2049 member_qualifier_info(function.child_by_field_name("expression"), src)?;
2050 if generic || qualifier.contains('.') {
2051 return None;
2052 }
2053 let name_node = function.child_by_field_name("name")?;
2054 let member = if name_node.kind() == "generic_name" {
2055 named_children(name_node)
2056 .into_iter()
2057 .find(|c| c.kind() == "identifier")
2058 .map(|id| text(id, src))?
2059 } else {
2060 text(name_node, src)
2061 };
2062 if member.is_empty() {
2063 return None;
2064 }
2065 Some((qualifier, member))
2066}
2067
2068fn qualifier_type_name(locals: &FactTable, type_facts: &FactTable, name: &str) -> Option<String> {
2075 match locals.get(name).or_else(|| type_facts.get(name)) {
2076 None => Some(name.to_string()),
2077 Some(Some(fact)) if fact.call.is_none() => Some(fact.type_name.clone()),
2078 Some(_) => None,
2079 }
2080}
2081
2082fn collection_element_fact(locals: &FactTable, type_facts: &FactTable, name: &str) -> Option<Fact> {
2090 match locals.get(name).or_else(|| type_facts.get(name)) {
2091 Some(Some(fact)) => match fact.args.as_deref() {
2092 Some([arg]) if arg != "*" => Some(Fact {
2093 type_name: arg.clone(),
2094 args: None,
2095 call: None,
2096 }),
2097 _ => None,
2098 },
2099 _ => None,
2100 }
2101}
2102
2103const MEMBER_SCOPE_NODES: &[&str] = &[
2108 "method_declaration",
2109 "constructor_declaration",
2110 "destructor_declaration",
2111 "operator_declaration",
2112 "conversion_operator_declaration",
2113 "property_declaration",
2114 "indexer_declaration",
2115];
2116
2117struct Scope<'a> {
2120 type_facts: Rc<FactTable>,
2123 type_params: Rc<HashSet<String>>,
2127 node: Option<Node<'a>>,
2129 member_facts: OnceCell<Option<FactTable>>,
2134}
2135
2136impl<'a> Scope<'a> {
2137 fn root() -> Self {
2138 Scope {
2139 type_facts: Rc::new(FactTable::new()),
2140 type_params: Rc::new(HashSet::new()),
2141 node: None,
2142 member_facts: OnceCell::new(),
2143 }
2144 }
2145
2146 fn for_type(node: Node<'a>, src: &[u8]) -> Self {
2147 let type_params = type_parameter_names(node, src);
2148 Scope {
2149 type_facts: Rc::new(collect_type_facts(node, src, &type_params)),
2150 type_params: Rc::new(type_params),
2151 node: None,
2152 member_facts: OnceCell::new(),
2153 }
2154 }
2155
2156 fn for_member(&self, node: Node<'a>) -> Self {
2157 Scope {
2158 type_facts: Rc::clone(&self.type_facts),
2159 type_params: Rc::clone(&self.type_params),
2160 node: Some(node),
2161 member_facts: OnceCell::new(),
2162 }
2163 }
2164
2165 fn receiver_fact_for(&self, name: &str, src: &[u8]) -> Option<Fact> {
2169 let locals = self.member_facts.get_or_init(|| {
2170 self.node
2171 .map(|n| collect_member_facts(n, src, &self.type_params, &self.type_facts))
2172 });
2173 if let Some(table) = locals {
2174 if let Some(found) = table.get(name) {
2175 return found.clone();
2176 }
2177 }
2178 self.type_facts.get(name).cloned().flatten()
2179 }
2180}
2181
2182fn walk_list<'a>(
2190 nodes: Vec<Node<'a>>,
2191 mut ns: String,
2192 type_stack: &[String],
2193 src: &[u8],
2194 out: &mut Extraction,
2195 scope: &Scope<'a>,
2196) {
2197 for node in nodes {
2198 if node.kind() == "file_scoped_namespace_declaration" {
2199 let name = node
2200 .child_by_field_name("name")
2201 .map(|n| text(n, src))
2202 .unwrap_or_default();
2203 ns = if ns.is_empty() {
2204 name
2205 } else {
2206 format!("{ns}.{name}")
2207 };
2208 continue;
2209 }
2210 walk(node, &ns, type_stack, src, out, scope);
2211 }
2212}
2213
2214fn walk<'a>(
2218 node: Node<'a>,
2219 ns: &str,
2220 type_stack: &[String],
2221 src: &[u8],
2222 out: &mut Extraction,
2223 scope: &Scope<'a>,
2224) {
2225 let member_scope;
2229 let scope: &Scope<'a> = if MEMBER_SCOPE_NODES.contains(&node.kind()) {
2230 member_scope = scope.for_member(node);
2231 &member_scope
2232 } else {
2233 scope
2234 };
2235 match node.kind() {
2236 "namespace_declaration" => {
2237 let name = node
2238 .child_by_field_name("name")
2239 .map(|n| text(n, src))
2240 .unwrap_or_default();
2241 let new_ns = if ns.is_empty() {
2242 name
2243 } else {
2244 format!("{ns}.{name}")
2245 };
2246 walk_list(named_children(node), new_ns, type_stack, src, out, scope);
2247 }
2248 "using_directive" => {
2249 record_using(node, src, &mut out.usings, &mut out.refs);
2250 }
2251 "class_declaration"
2252 | "interface_declaration"
2253 | "struct_declaration"
2254 | "record_declaration" => {
2255 let kind = type_kind_label(node.kind()).expect("matched TYPE_KINDS arm");
2256 let type_params = type_parameter_names(node, src);
2261 record_type_def(
2262 node,
2263 ns,
2264 kind,
2265 type_stack,
2266 src,
2267 &mut out.defs,
2268 &mut out.names,
2269 &type_params,
2270 );
2271 record_base_list(node, ns, type_stack, src, &mut out.refs);
2272 let name = declared_name(node, src);
2273 let new_stack: Vec<String> = if name.is_empty() {
2274 type_stack.to_vec()
2275 } else {
2276 let mut s = type_stack.to_vec();
2277 s.push(name);
2278 s
2279 };
2280 let type_scope = Scope::for_type(node, src);
2284 walk_list(
2285 named_children(node),
2286 ns.to_string(),
2287 &new_stack,
2288 src,
2289 out,
2290 &type_scope,
2291 );
2292 }
2293 "enum_declaration" => {
2294 record_type_def(
2295 node,
2296 ns,
2297 "enum",
2298 type_stack,
2299 src,
2300 &mut out.defs,
2301 &mut out.names,
2302 &HashSet::new(),
2303 );
2304 record_enum_members(node, ns, type_stack, src, &mut out.defs);
2305 }
2308 "delegate_declaration" => {
2309 record_type_def(
2310 node,
2311 ns,
2312 "delegate",
2313 type_stack,
2314 src,
2315 &mut out.defs,
2316 &mut out.names,
2317 &HashSet::new(),
2318 );
2319 record_single_type(
2320 node.child_by_field_name("type"),
2321 "uses-type",
2322 ns,
2323 type_stack,
2324 src,
2325 &mut out.refs,
2326 );
2327 walk_list(
2328 named_children(node),
2329 ns.to_string(),
2330 type_stack,
2331 src,
2332 out,
2333 scope,
2334 );
2335 }
2336 "field_declaration" => {
2337 let vd = named_children(node)
2338 .into_iter()
2339 .find(|c| c.kind() == "variable_declaration");
2340 record_single_type(
2341 vd.and_then(|v| v.child_by_field_name("type")),
2342 "uses-type",
2343 ns,
2344 type_stack,
2345 src,
2346 &mut out.refs,
2347 );
2348 walk_list(
2349 named_children(node),
2350 ns.to_string(),
2351 type_stack,
2352 src,
2353 out,
2354 scope,
2355 );
2356 }
2357 "property_declaration" => {
2358 record_single_type(
2359 node.child_by_field_name("type"),
2360 "uses-type",
2361 ns,
2362 type_stack,
2363 src,
2364 &mut out.refs,
2365 );
2366 walk_list(
2367 named_children(node),
2368 ns.to_string(),
2369 type_stack,
2370 src,
2371 out,
2372 scope,
2373 );
2374 }
2375 "parameter" => {
2376 record_single_type(
2377 node.child_by_field_name("type"),
2378 "uses-type",
2379 ns,
2380 type_stack,
2381 src,
2382 &mut out.refs,
2383 );
2384 walk_list(
2385 named_children(node),
2386 ns.to_string(),
2387 type_stack,
2388 src,
2389 out,
2390 scope,
2391 );
2392 }
2393 "constructor_declaration" => {
2405 if let Some(params) = node.child_by_field_name("parameters") {
2406 for p in named_children(params) {
2407 if p.kind() != "parameter" {
2408 continue;
2409 }
2410 let type_node = p.child_by_field_name("type");
2411 if let Some(fact) = type_fact(type_node, src, &scope.type_params) {
2412 let line = type_node.map(|t| t.start_position().row + 1).unwrap_or(0);
2413 push_ctor_param_ref(
2414 &mut out.refs,
2415 fact.type_name,
2416 line,
2417 ns.to_string(),
2418 fact.args,
2419 type_stack,
2420 );
2421 }
2422 }
2423 }
2424 walk_list(
2425 named_children(node),
2426 ns.to_string(),
2427 type_stack,
2428 src,
2429 out,
2430 scope,
2431 );
2432 }
2433 "declaration_expression" => {
2439 record_single_type(
2440 node.child_by_field_name("type"),
2441 "uses-type",
2442 ns,
2443 type_stack,
2444 src,
2445 &mut out.refs,
2446 );
2447 walk_list(
2448 named_children(node),
2449 ns.to_string(),
2450 type_stack,
2451 src,
2452 out,
2453 scope,
2454 );
2455 }
2456 "method_declaration" => {
2457 record_single_type(
2458 node.child_by_field_name("returns"),
2459 "uses-type",
2460 ns,
2461 type_stack,
2462 src,
2463 &mut out.refs,
2464 );
2465 walk_list(
2466 named_children(node),
2467 ns.to_string(),
2468 type_stack,
2469 src,
2470 out,
2471 scope,
2472 );
2473 }
2474 "object_creation_expression" => {
2475 record_single_type(
2476 node.child_by_field_name("type"),
2477 "uses-type",
2478 ns,
2479 type_stack,
2480 src,
2481 &mut out.refs,
2482 );
2483 walk_list(
2484 named_children(node),
2485 ns.to_string(),
2486 type_stack,
2487 src,
2488 out,
2489 scope,
2490 );
2491 }
2492 "typeof_expression" => {
2493 record_single_type(
2494 node.child_by_field_name("type"),
2495 "uses-type",
2496 ns,
2497 type_stack,
2498 src,
2499 &mut out.refs,
2500 );
2501 walk_list(
2502 named_children(node),
2503 ns.to_string(),
2504 type_stack,
2505 src,
2506 out,
2507 scope,
2508 );
2509 }
2510 "member_access_expression" => {
2515 let expr_field = node.child_by_field_name("expression");
2516 let member = node.child_by_field_name("name").map(|n| {
2520 if n.kind() == "generic_name" {
2521 named_children(n)
2522 .into_iter()
2523 .find(|c| c.kind() == "identifier")
2524 .map(|id| text(id, src))
2525 .unwrap_or_default()
2526 } else {
2527 text(n, src)
2528 }
2529 });
2530 let normal_qualifier = member_qualifier_info(expr_field, src);
2531 if let (Some((qt, generic)), Some(m)) = (&normal_qualifier, &member) {
2532 if !m.is_empty() {
2533 let dot_at = if *generic { None } else { qt.find('.') };
2539 let bare = dot_at.is_none() && !*generic;
2540 let receiver = if bare {
2541 scope.receiver_fact_for(qt, src)
2542 } else {
2543 None
2544 };
2545 let property_owner = dot_at
2551 .filter(|d| !qt[d + 1..].contains('.'))
2552 .and_then(|d| scope.receiver_fact_for(&qt[..d], src))
2553 .filter(|fact| fact.call.is_none())
2554 .map(|fact| fact.type_name);
2555 push_member_ref(
2556 &mut out.refs,
2557 qt,
2558 m.clone(),
2559 node.start_position().row + 1,
2560 ns.to_string(),
2561 *generic,
2562 receiver,
2563 invocation_arg_count(node),
2566 type_stack,
2567 property_owner,
2568 );
2569 }
2570 }
2571 walk_list(
2572 named_children(node),
2573 ns.to_string(),
2574 type_stack,
2575 src,
2576 out,
2577 scope,
2578 );
2579 }
2580 "type_argument_list" => {
2581 for arg in named_children(node) {
2582 record_single_type(Some(arg), "uses-type", ns, type_stack, src, &mut out.refs);
2583 }
2584 walk_list(
2585 named_children(node),
2586 ns.to_string(),
2587 type_stack,
2588 src,
2589 out,
2590 scope,
2591 );
2592 }
2593 _ => {
2594 walk_list(
2595 named_children(node),
2596 ns.to_string(),
2597 type_stack,
2598 src,
2599 out,
2600 scope,
2601 );
2602 }
2603 }
2604}
2605
2606pub fn extract(source: &str) -> Extraction {
2613 let mut parser = new_parser();
2614 let units = crate::parse::utf16_units(source);
2615 let utf16 = crate::parse::utf16_bytes(&units);
2616 let tree = parser
2617 .parse_utf16_le(&units, None)
2618 .expect("parse returned no tree");
2619 let root = tree.root_node();
2620 let src = &utf16[..];
2621
2622 let purpose_parts: Vec<SegmentParts> = namespace_level_types(root)
2623 .into_iter()
2624 .filter_map(|n| type_segment_parts(n, src))
2625 .collect();
2626
2627 let mut out = Extraction {
2628 purpose: None,
2629 defs: Vec::new(),
2630 usings: Vec::new(),
2631 refs: Vec::new(),
2632 names: Vec::new(),
2633 };
2634 walk_list(
2635 named_children(root),
2636 String::new(),
2637 &[],
2638 src,
2639 &mut out,
2640 &Scope::root(),
2641 );
2642
2643 out.purpose = format_segments(purpose_parts);
2644
2645 out
2646}
2647
2648pub fn run_extract_dump(path: &str) {
2654 let source = fs::read_to_string(path).unwrap_or_else(|err| {
2655 eprintln!("failed to read {path}: {err}");
2656 process::exit(1);
2657 });
2658 if let Some(grammar) = crate::parse::ts_grammar_for(path) {
2664 let file_path = Path::new(path);
2669 let root = file_path
2670 .parent()
2671 .filter(|p| !p.as_os_str().is_empty())
2672 .unwrap_or_else(|| Path::new("."));
2673 let rel = file_path
2674 .file_name()
2675 .and_then(|n| n.to_str())
2676 .unwrap_or(path);
2677 match extract_ts_file(root, rel, &source, grammar) {
2678 Some(ts) => println!("{}", ts_extraction_to_json(&ts.purpose, &ts.fragment)),
2679 None => println!("{}", ts_extraction_to_json(&None, &TsFragment::default())),
2682 }
2683 return;
2684 }
2685 let result = extract(&source);
2686 println!("{}", extraction_to_json(&result));
2687}
2688
2689enum Json {
2697 Null,
2698 Bool(bool),
2699 Num(usize),
2700 Int(i64),
2704 Str(String),
2705 Arr(Vec<Json>),
2706 Obj(Vec<(&'static str, Json)>),
2707 Map(Vec<(String, Json)>),
2712}
2713
2714impl Json {
2715 fn to_pretty_string(&self) -> String {
2716 let mut out = String::new();
2717 self.write(&mut out, 0);
2718 out
2719 }
2720
2721 fn write(&self, out: &mut String, indent: usize) {
2722 match self {
2723 Json::Null => out.push_str("null"),
2724 Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
2725 Json::Num(n) => out.push_str(&n.to_string()),
2726 Json::Int(n) => out.push_str(&n.to_string()),
2727 Json::Str(s) => out.push_str(&json_string(s)),
2728 Json::Arr(items) => {
2729 if items.is_empty() {
2730 out.push_str("[]");
2731 return;
2732 }
2733 out.push_str("[\n");
2734 let pad = " ".repeat(indent + 1);
2735 let last = items.len() - 1;
2736 for (i, item) in items.iter().enumerate() {
2737 out.push_str(&pad);
2738 item.write(out, indent + 1);
2739 if i != last {
2740 out.push(',');
2741 }
2742 out.push('\n');
2743 }
2744 out.push_str(&" ".repeat(indent));
2745 out.push(']');
2746 }
2747 Json::Obj(fields) => {
2748 write_object(out, indent, fields.iter().map(|(k, v)| (*k, v)));
2749 }
2750 Json::Map(entries) => {
2751 write_object(out, indent, entries.iter().map(|(k, v)| (k.as_str(), v)));
2752 }
2753 }
2754 }
2755}
2756
2757fn write_object<'a>(
2758 out: &mut String,
2759 indent: usize,
2760 fields: impl ExactSizeIterator<Item = (&'a str, &'a Json)>,
2761) {
2762 if fields.len() == 0 {
2763 out.push_str("{}");
2764 return;
2765 }
2766 out.push_str("{\n");
2767 let pad = " ".repeat(indent + 1);
2768 let last = fields.len() - 1;
2769 for (i, (k, v)) in fields.enumerate() {
2770 out.push_str(&pad);
2771 out.push_str(&json_string(k));
2772 out.push_str(": ");
2773 v.write(out, indent + 1);
2774 if i != last {
2775 out.push(',');
2776 }
2777 out.push('\n');
2778 }
2779 out.push_str(&" ".repeat(indent));
2780 out.push('}');
2781}
2782
2783fn json_string(s: &str) -> String {
2784 let mut out = String::with_capacity(s.len() + 2);
2785 out.push('"');
2786 for ch in s.chars() {
2787 match ch {
2788 '"' => out.push_str("\\\""),
2789 '\\' => out.push_str("\\\\"),
2790 '\n' => out.push_str("\\n"),
2791 '\r' => out.push_str("\\r"),
2792 '\t' => out.push_str("\\t"),
2793 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
2794 c => out.push(c),
2795 }
2796 }
2797 out.push('"');
2798 out
2799}
2800
2801fn def_to_json(d: &DefRecord) -> Json {
2802 let mut fields: Vec<(&'static str, Json)> = vec![
2803 ("id", Json::Str(d.id.clone())),
2804 ("name", Json::Str(d.name.clone())),
2805 ("namespace", Json::Str(d.namespace.clone())),
2806 ("kind", Json::Str(d.kind.clone())),
2807 ("line", Json::Num(d.line)),
2808 (
2809 "methods",
2810 Json::Arr(d.methods.iter().map(|m| Json::Str(m.clone())).collect()),
2811 ),
2812 ];
2813 if !d.properties.is_empty() {
2815 fields.push((
2816 "properties",
2817 Json::Arr(d.properties.iter().map(|p| Json::Str(p.clone())).collect()),
2818 ));
2819 }
2820 if !d.fields.is_empty() {
2821 fields.push((
2822 "fields",
2823 Json::Arr(d.fields.iter().map(|f| Json::Str(f.clone())).collect()),
2824 ));
2825 }
2826 if !d.method_returns.is_empty() {
2827 fields.push((
2828 "methodReturns",
2829 Json::Map(
2830 d.method_returns
2831 .iter()
2832 .map(|(k, v)| (k.clone(), Json::Str(v.clone())))
2833 .collect(),
2834 ),
2835 ));
2836 }
2837 if !d.extension_methods.is_empty() {
2842 fields.push((
2843 "extensionMethods",
2844 Json::Arr(
2845 d.extension_methods
2846 .iter()
2847 .map(|e| {
2848 let mut kv: Vec<(&'static str, Json)> = vec![
2849 ("name", Json::Str(e.name.clone())),
2850 ("thisType", Json::Str(e.this_type.clone())),
2851 ("arityMin", Json::Num(e.arity_min)),
2852 ("arityMax", Json::Int(e.arity_max)),
2853 ];
2854 if let Some(args) = &e.this_args {
2855 kv.push((
2856 "thisArgs",
2857 Json::Arr(args.iter().map(|a| Json::Str(a.clone())).collect()),
2858 ));
2859 }
2860 Json::Obj(kv)
2861 })
2862 .collect(),
2863 ),
2864 ));
2865 }
2866 if !d.bases.is_empty() {
2868 fields.push((
2869 "bases",
2870 Json::Arr(d.bases.iter().map(|b| Json::Str(b.clone())).collect()),
2871 ));
2872 }
2873 if !d.type_params.is_empty() {
2875 fields.push((
2876 "typeParams",
2877 Json::Arr(d.type_params.iter().map(|t| Json::Str(t.clone())).collect()),
2878 ));
2879 }
2880 if !d.base_generic_args.is_empty() {
2881 fields.push((
2882 "baseGenericArgs",
2883 Json::Map(
2884 d.base_generic_args
2885 .iter()
2886 .map(|(k, v)| {
2887 (
2888 k.clone(),
2889 Json::Arr(v.iter().map(|a| Json::Str(a.clone())).collect()),
2890 )
2891 })
2892 .collect(),
2893 ),
2894 ));
2895 }
2896 if !d.test_methods.is_empty() {
2898 fields.push((
2899 "testMethods",
2900 Json::Arr(
2901 d.test_methods
2902 .iter()
2903 .map(|t| Json::Str(t.clone()))
2904 .collect(),
2905 ),
2906 ));
2907 }
2908 if !d.property_types.is_empty() {
2910 fields.push((
2911 "propertyTypes",
2912 Json::Map(
2913 d.property_types
2914 .iter()
2915 .map(|(name, fact)| (name.clone(), fact_to_json(fact)))
2916 .collect(),
2917 ),
2918 ));
2919 }
2920 Json::Obj(fields)
2921}
2922
2923fn fact_to_json(fact: &Fact) -> Json {
2927 let mut fields: Vec<(&'static str, Json)> = vec![("type", Json::Str(fact.type_name.clone()))];
2928 if let Some(args) = &fact.args {
2929 fields.push((
2930 "args",
2931 Json::Arr(args.iter().map(|a| Json::Str(a.clone())).collect()),
2932 ));
2933 }
2934 Json::Obj(fields)
2935}
2936
2937fn using_to_json(u: &UsingRecord) -> Json {
2938 match u {
2939 UsingRecord::Alias {
2940 alias,
2941 target,
2942 global,
2943 } => Json::Obj(vec![
2944 ("alias", Json::Str(alias.clone())),
2945 ("target", Json::Str(target.clone())),
2946 ("global", Json::Bool(*global)),
2947 ]),
2948 UsingRecord::Plain { text, global } => Json::Obj(vec![
2949 ("text", Json::Str(text.clone())),
2950 ("global", Json::Bool(*global)),
2951 ]),
2952 }
2953}
2954
2955fn ref_to_json(r: &RefRecord) -> Json {
2956 let mut fields: Vec<(&'static str, Json)> = vec![
2957 ("kind", Json::Str(r.kind.clone())),
2958 ("name", Json::Str(r.name.clone())),
2959 ];
2960 if let Some(q) = &r.qualified {
2961 fields.push(("qualified", Json::Str(q.clone())));
2962 }
2963 if let Some(m) = &r.member {
2964 fields.push(("member", Json::Str(m.clone())));
2965 }
2966 fields.push(("line", Json::Num(r.line)));
2967 fields.push((
2968 "namespace",
2969 match &r.namespace {
2970 Some(ns) => Json::Str(ns.clone()),
2971 None => Json::Null,
2972 },
2973 ));
2974 if let Some(arity) = r.type_arg_count {
2975 fields.push(("typeArgCount", Json::Num(arity)));
2976 }
2977 if r.generic {
2979 fields.push(("generic", Json::Bool(true)));
2980 }
2981 if let Some(rt) = &r.receiver_type {
2982 fields.push(("receiverType", Json::Str(rt.clone())));
2983 }
2984 if let Some(ac) = r.arg_count {
2987 fields.push(("argCount", Json::Num(ac)));
2988 }
2989 if let Some(args) = &r.receiver_args {
2992 fields.push((
2993 "receiverArgs",
2994 Json::Arr(args.iter().map(|a| Json::Str(a.clone())).collect()),
2995 ));
2996 }
2997 if !r.outer_types.is_empty() {
3000 fields.push((
3001 "outerTypes",
3002 Json::Arr(r.outer_types.iter().map(|t| Json::Str(t.clone())).collect()),
3003 ));
3004 }
3005 if let Some(args) = &r.args {
3008 fields.push((
3009 "args",
3010 Json::Arr(args.iter().map(|a| Json::Str(a.clone())).collect()),
3011 ));
3012 }
3013 if let Some(owner) = &r.receiver_property_owner {
3016 fields.push(("receiverPropertyOwner", Json::Str(owner.clone())));
3017 }
3018 if let Some(owner) = &r.receiver_call_owner {
3020 fields.push(("receiverCallOwner", Json::Str(owner.clone())));
3021 }
3022 if let Some(member) = &r.receiver_call_member {
3023 fields.push(("receiverCallMember", Json::Str(member.clone())));
3024 }
3025 Json::Obj(fields)
3026}
3027
3028fn name_to_json(n: &NameRecord) -> Json {
3031 let mut fields: Vec<(&'static str, Json)> = vec![
3032 ("name", Json::Str(n.name.clone())),
3033 ("kind", Json::Str(n.kind.clone())),
3034 ("line", Json::Num(n.line)),
3035 ];
3036 if !n.owner.is_empty() {
3037 fields.push(("owner", Json::Str(n.owner.clone())));
3038 }
3039 Json::Obj(fields)
3040}
3041
3042pub fn extraction_to_json(e: &Extraction) -> String {
3044 let purpose_json = match &e.purpose {
3045 Some(p) => Json::Str(p.clone()),
3046 None => Json::Null,
3047 };
3048 let root = Json::Obj(vec![
3049 ("purpose", purpose_json),
3050 ("defs", Json::Arr(e.defs.iter().map(def_to_json).collect())),
3051 (
3052 "usings",
3053 Json::Arr(e.usings.iter().map(using_to_json).collect()),
3054 ),
3055 ("refs", Json::Arr(e.refs.iter().map(ref_to_json).collect())),
3056 (
3057 "names",
3058 Json::Arr(e.names.iter().map(name_to_json).collect()),
3059 ),
3060 ]);
3061 root.to_pretty_string()
3062}
3063
3064struct TsEntry {
3081 kind: &'static str,
3082 name: String,
3083 bases: String,
3084 methods: Vec<String>,
3085}
3086
3087fn ts_declared_name(node: Node, src: &[u8]) -> String {
3092 match node.child_by_field_name("name") {
3093 Some(n) if n.kind() != "computed_property_name" => text(n, src),
3094 _ => String::new(),
3095 }
3096}
3097
3098fn ts_accessibility(node: Node, src: &[u8]) -> Option<String> {
3099 named_children(node)
3100 .into_iter()
3101 .find(|c| c.kind() == "accessibility_modifier")
3102 .map(|m| text(m, src))
3103}
3104
3105fn ts_member_is_public(node: Node, src: &[u8]) -> bool {
3110 match ts_accessibility(node, src) {
3111 None => true,
3112 Some(a) => a == "public",
3113 }
3114}
3115
3116fn class_bases(node: Node, src: &[u8]) -> String {
3117 let Some(heritage) = named_children(node)
3118 .into_iter()
3119 .find(|c| c.kind() == "class_heritage")
3120 else {
3121 return String::new();
3122 };
3123 let mut parts: Vec<String> = Vec::new();
3124 if let Some(extends_clause) = named_children(heritage)
3125 .into_iter()
3126 .find(|c| c.kind() == "extends_clause")
3127 {
3128 if let Some(value) = extends_clause.child_by_field_name("value") {
3129 parts.push(text(value, src));
3130 }
3131 }
3132 if let Some(implements_clause) = named_children(heritage)
3133 .into_iter()
3134 .find(|c| c.kind() == "implements_clause")
3135 {
3136 for t in named_children(implements_clause) {
3137 parts.push(text(t, src));
3138 }
3139 }
3140 parts.join(", ")
3141}
3142
3143fn class_method_names(node: Node, src: &[u8]) -> Vec<String> {
3144 let Some(body) = node.child_by_field_name("body") else {
3145 return Vec::new();
3146 };
3147 named_children(body)
3148 .into_iter()
3149 .filter(|c| c.kind() == "method_definition" && ts_member_is_public(*c, src))
3150 .map(|c| ts_declared_name(c, src))
3151 .filter(|name| !name.is_empty() && name != "constructor")
3152 .collect()
3153}
3154
3155fn interface_bases(node: Node, src: &[u8]) -> String {
3156 let Some(ext) = named_children(node)
3157 .into_iter()
3158 .find(|c| c.kind() == "extends_type_clause")
3159 else {
3160 return String::new();
3161 };
3162 named_children(ext)
3163 .into_iter()
3164 .map(|c| text(c, src))
3165 .collect::<Vec<_>>()
3166 .join(", ")
3167}
3168
3169fn interface_method_names(node: Node, src: &[u8]) -> Vec<String> {
3170 let Some(body) = node.child_by_field_name("body") else {
3171 return Vec::new();
3172 };
3173 named_children(body)
3174 .into_iter()
3175 .filter(|c| c.kind() == "method_signature")
3176 .map(|c| ts_declared_name(c, src))
3177 .filter(|n| !n.is_empty())
3178 .collect()
3179}
3180
3181fn ts_pattern_names(node: Option<Node>, src: &[u8]) -> Vec<String> {
3187 let Some(node) = node else {
3188 return Vec::new();
3189 };
3190 match node.kind() {
3191 "identifier" => vec![text(node, src)],
3192 "object_pattern" => {
3193 let mut names = Vec::new();
3194 for c in named_children(node) {
3195 match c.kind() {
3196 "shorthand_property_identifier_pattern" => names.push(text(c, src)),
3197 "pair_pattern" => {
3198 names.extend(ts_pattern_names(c.child_by_field_name("value"), src))
3199 }
3200 "rest_pattern" => {
3201 names.extend(ts_pattern_names(named_children(c).into_iter().next(), src))
3202 }
3203 "object_assignment_pattern" => {
3204 names.extend(ts_pattern_names(c.child_by_field_name("left"), src))
3205 }
3206 _ => {}
3207 }
3208 }
3209 names
3210 }
3211 "array_pattern" => named_children(node)
3212 .into_iter()
3213 .flat_map(|c| ts_pattern_names(Some(c), src))
3214 .collect(),
3215 "rest_pattern" => ts_pattern_names(named_children(node).into_iter().next(), src),
3216 _ => Vec::new(),
3217 }
3218}
3219
3220fn ts_default_expression_entry(value_node: Node, src: &[u8]) -> TsEntry {
3227 let name = if value_node.kind() == "identifier" {
3228 text(value_node, src)
3229 } else {
3230 "(anonymous)".to_string()
3231 };
3232 TsEntry {
3233 kind: "default",
3234 name,
3235 bases: String::new(),
3236 methods: Vec::new(),
3237 }
3238}
3239
3240fn ts_entries_for_declaration(decl_node: Node, is_default: bool, src: &[u8]) -> Vec<TsEntry> {
3241 match decl_node.kind() {
3242 "class_declaration" => {
3243 let name = ts_declared_name(decl_node, src);
3244 if name.is_empty() {
3245 Vec::new()
3246 } else {
3247 vec![TsEntry {
3248 kind: if is_default { "default" } else { "class" },
3249 name,
3250 bases: class_bases(decl_node, src),
3251 methods: class_method_names(decl_node, src),
3252 }]
3253 }
3254 }
3255 "function_declaration" => {
3256 let name = ts_declared_name(decl_node, src);
3257 if name.is_empty() {
3258 Vec::new()
3259 } else {
3260 vec![TsEntry {
3261 kind: if is_default { "default" } else { "function" },
3262 name,
3263 bases: String::new(),
3264 methods: Vec::new(),
3265 }]
3266 }
3267 }
3268 "interface_declaration" => {
3269 let name = ts_declared_name(decl_node, src);
3270 if name.is_empty() {
3271 Vec::new()
3272 } else {
3273 vec![TsEntry {
3274 kind: "interface",
3275 name,
3276 bases: interface_bases(decl_node, src),
3277 methods: interface_method_names(decl_node, src),
3278 }]
3279 }
3280 }
3281 "type_alias_declaration" => {
3282 let name = ts_declared_name(decl_node, src);
3283 if name.is_empty() {
3284 Vec::new()
3285 } else {
3286 vec![TsEntry {
3287 kind: "type",
3288 name,
3289 bases: String::new(),
3290 methods: Vec::new(),
3291 }]
3292 }
3293 }
3294 "enum_declaration" => {
3295 let name = ts_declared_name(decl_node, src);
3296 if name.is_empty() {
3297 Vec::new()
3298 } else {
3299 vec![TsEntry {
3300 kind: "enum",
3301 name,
3302 bases: String::new(),
3303 methods: Vec::new(),
3304 }]
3305 }
3306 }
3307 "lexical_declaration" | "variable_declaration" => {
3311 let mut entries = Vec::new();
3312 for decl in named_children(decl_node) {
3313 if decl.kind() != "variable_declarator" {
3314 continue;
3315 }
3316 for name in ts_pattern_names(decl.child_by_field_name("name"), src) {
3317 entries.push(TsEntry {
3318 kind: "const",
3319 name,
3320 bases: String::new(),
3321 methods: Vec::new(),
3322 });
3323 }
3324 }
3325 entries
3326 }
3327 _ => Vec::new(),
3328 }
3329}
3330
3331fn is_default_export_statement(node: Node) -> bool {
3335 let mut cursor = node.walk();
3336 let found = node.children(&mut cursor).any(|c| c.kind() == "default");
3337 found
3338}
3339
3340fn ts_esm_entries(export_stmts: &[Node], src: &[u8]) -> Vec<TsEntry> {
3341 let mut entries = Vec::new();
3342 for stmt in export_stmts {
3343 if let Some(decl_node) = stmt.child_by_field_name("declaration") {
3344 entries.extend(ts_entries_for_declaration(
3345 decl_node,
3346 is_default_export_statement(*stmt),
3347 src,
3348 ));
3349 continue;
3350 }
3351 if let Some(value_node) = stmt.child_by_field_name("value") {
3352 entries.push(ts_default_expression_entry(value_node, src));
3353 }
3354 }
3359 entries
3360}
3361
3362enum CjsTarget {
3363 Whole,
3364 Prop(String),
3365}
3366
3367fn common_js_export_target(left: Option<Node>, src: &[u8]) -> Option<CjsTarget> {
3371 let left = left?;
3372 if left.kind() != "member_expression" {
3373 return None;
3374 }
3375 let obj = left.child_by_field_name("object")?;
3376 let prop = left.child_by_field_name("property")?;
3377 if obj.kind() == "identifier" && text(obj, src) == "module" && text(prop, src) == "exports" {
3378 return Some(CjsTarget::Whole);
3379 }
3380 if obj.kind() == "identifier" && text(obj, src) == "exports" {
3381 return Some(CjsTarget::Prop(text(prop, src)));
3382 }
3383 if obj.kind() == "member_expression" {
3384 let outer_obj = obj.child_by_field_name("object");
3385 let outer_prop = obj.child_by_field_name("property");
3386 if let (Some(oo), Some(op)) = (outer_obj, outer_prop) {
3387 if oo.kind() == "identifier" && text(oo, src) == "module" && text(op, src) == "exports"
3388 {
3389 return Some(CjsTarget::Prop(text(prop, src)));
3390 }
3391 }
3392 }
3393 None
3394}
3395
3396struct LocalKind {
3397 kind: &'static str,
3398 bases: String,
3399 methods: Vec<String>,
3400}
3401
3402fn ts_local_declaration_kinds(program_node: Node, src: &[u8]) -> HashMap<String, LocalKind> {
3408 let mut kinds = HashMap::new();
3409 for c in named_children(program_node) {
3410 match c.kind() {
3411 "function_declaration" => {
3412 let name = ts_declared_name(c, src);
3413 if !name.is_empty() {
3414 kinds.insert(
3415 name,
3416 LocalKind {
3417 kind: "function",
3418 bases: String::new(),
3419 methods: Vec::new(),
3420 },
3421 );
3422 }
3423 }
3424 "class_declaration" => {
3425 let name = ts_declared_name(c, src);
3426 if !name.is_empty() {
3427 kinds.insert(
3428 name,
3429 LocalKind {
3430 kind: "class",
3431 bases: class_bases(c, src),
3432 methods: class_method_names(c, src),
3433 },
3434 );
3435 }
3436 }
3437 "lexical_declaration" | "variable_declaration" => {
3438 for decl in named_children(c) {
3439 if decl.kind() != "variable_declarator" {
3440 continue;
3441 }
3442 if let Some(name_node) = decl.child_by_field_name("name") {
3443 if name_node.kind() == "identifier" {
3444 kinds.insert(
3445 text(name_node, src),
3446 LocalKind {
3447 kind: "const",
3448 bases: String::new(),
3449 methods: Vec::new(),
3450 },
3451 );
3452 }
3453 }
3454 }
3455 }
3456 _ => {}
3457 }
3458 }
3459 kinds
3460}
3461
3462fn property_export_entry(name: String, local_kinds: &HashMap<String, LocalKind>) -> TsEntry {
3463 match local_kinds.get(&name) {
3464 Some(local) => TsEntry {
3465 kind: local.kind,
3466 name,
3467 bases: local.bases.clone(),
3468 methods: local.methods.clone(),
3469 },
3470 None => TsEntry {
3471 kind: "const",
3472 name,
3473 bases: String::new(),
3474 methods: Vec::new(),
3475 },
3476 }
3477}
3478
3479fn object_literal_export_entries(
3480 obj_node: Node,
3481 local_kinds: &HashMap<String, LocalKind>,
3482 src: &[u8],
3483) -> Vec<TsEntry> {
3484 let mut entries = Vec::new();
3485 for c in named_children(obj_node) {
3486 match c.kind() {
3487 "shorthand_property_identifier" => {
3488 entries.push(property_export_entry(text(c, src), local_kinds))
3489 }
3490 "pair" => {
3491 let Some(key) = c.child_by_field_name("key") else {
3492 continue;
3493 };
3494 let name = if key.kind() == "string" {
3495 named_children(key).into_iter().next().map(|n| text(n, src))
3496 } else {
3497 Some(text(key, src))
3498 };
3499 if let Some(name) = name {
3500 entries.push(property_export_entry(name, local_kinds));
3501 }
3502 }
3503 _ => {}
3504 }
3505 }
3506 entries
3507}
3508
3509fn ts_cjs_entries(program_node: Node, src: &[u8]) -> Vec<TsEntry> {
3510 let local_kinds = ts_local_declaration_kinds(program_node, src);
3511 let mut entries = Vec::new();
3512 for stmt in named_children(program_node) {
3513 if stmt.kind() != "expression_statement" {
3514 continue;
3515 }
3516 let Some(assign) = named_children(stmt)
3517 .into_iter()
3518 .find(|n| n.kind() == "assignment_expression")
3519 else {
3520 continue;
3521 };
3522 let Some(target) = common_js_export_target(assign.child_by_field_name("left"), src) else {
3523 continue;
3524 };
3525 let right = assign.child_by_field_name("right");
3526 match target {
3527 CjsTarget::Whole => match right {
3528 Some(r) if r.kind() == "object" => {
3529 entries.extend(object_literal_export_entries(r, &local_kinds, src))
3530 }
3531 Some(r) => entries.push(ts_default_expression_entry(r, src)),
3532 None => {}
3535 },
3536 CjsTarget::Prop(name) => entries.push(property_export_entry(name, &local_kinds)),
3537 }
3538 }
3539 entries
3540}
3541
3542fn ts_export_entries(program_node: Node, src: &[u8]) -> Vec<TsEntry> {
3547 let export_stmts: Vec<Node> = named_children(program_node)
3548 .into_iter()
3549 .filter(|c| c.kind() == "export_statement")
3550 .collect();
3551 if export_stmts.is_empty() {
3552 ts_cjs_entries(program_node, src)
3553 } else {
3554 ts_esm_entries(&export_stmts, src)
3555 }
3556}
3557
3558fn push_reexport_name(name: String, names: &mut Vec<String>, seen: &mut HashSet<String>) {
3571 if seen.insert(name.clone()) {
3572 names.push(name);
3573 }
3574}
3575
3576fn ts_reexport_names(program_node: Node, src: &[u8]) -> Vec<String> {
3577 let mut names = Vec::new();
3578 let mut seen: HashSet<String> = HashSet::new();
3579 for stmt in named_children(program_node) {
3580 if stmt.kind() != "export_statement" {
3581 continue;
3582 }
3583 if stmt.child_by_field_name("source").is_none() {
3584 continue; }
3586 if let Some(ns_export) = named_children(stmt)
3587 .into_iter()
3588 .find(|c| c.kind() == "namespace_export")
3589 {
3590 if let Some(id) = named_children(ns_export)
3591 .into_iter()
3592 .find(|c| c.kind() == "identifier")
3593 {
3594 push_reexport_name(text(id, src), &mut names, &mut seen);
3595 }
3596 continue;
3597 }
3598 if let Some(clause) = named_children(stmt)
3599 .into_iter()
3600 .find(|c| c.kind() == "export_clause")
3601 {
3602 for spec in named_children(clause) {
3603 if spec.kind() != "export_specifier" {
3604 continue;
3605 }
3606 let alias = spec.child_by_field_name("alias").map(|n| text(n, src));
3607 let name = spec.child_by_field_name("name").map(|n| text(n, src));
3608 if let Some(chosen) = alias.or(name) {
3609 push_reexport_name(chosen, &mut names, &mut seen);
3610 }
3611 }
3612 continue;
3613 }
3614 push_reexport_name("*".to_string(), &mut names, &mut seen);
3616 }
3617 names
3618}
3619
3620fn reexports_bucket_segment(program_node: Node, src: &[u8]) -> Option<String> {
3621 let names = ts_reexport_names(program_node, src);
3622 if names.is_empty() {
3623 None
3624 } else {
3625 Some(format!("reexports {}", names.join(", ")))
3626 }
3627}
3628
3629const TS_BUCKET_ORDER: &[&str] = &[
3630 "class",
3631 "function",
3632 "const",
3633 "interface",
3634 "type",
3635 "enum",
3636 "default",
3637];
3638
3639fn ts_purpose_segments(program_node: Node, src: &[u8]) -> Option<String> {
3647 let entries = ts_export_entries(program_node, src);
3648 let mut sorted: Vec<&TsEntry> = Vec::with_capacity(entries.len());
3649 for kind in TS_BUCKET_ORDER {
3650 sorted.extend(entries.iter().filter(|e| e.kind == *kind));
3651 }
3652 let reexports_segment = reexports_bucket_segment(program_node, src);
3653 if sorted.is_empty() && reexports_segment.is_none() {
3654 return None;
3655 }
3656 let mut seen_methods: HashSet<String> = HashSet::new();
3662 let mut segments: Vec<String> = Vec::new();
3663 for e in sorted {
3664 let mut methods = Vec::new();
3665 for m in &e.methods {
3666 if seen_methods.contains(m) {
3667 continue;
3668 }
3669 seen_methods.insert(m.clone());
3670 methods.push(m.clone());
3671 }
3672 let header = if e.bases.is_empty() {
3673 format!("{} {}", e.kind, e.name)
3674 } else {
3675 format!("{} {} : {}", e.kind, e.name, e.bases)
3676 };
3677 segments.push(if methods.is_empty() {
3678 header
3679 } else {
3680 format!("{header}; {}", methods.join(", "))
3681 });
3682 }
3683 if let Some(seg) = reexports_segment {
3687 segments.push(seg);
3688 }
3689 Some(segments.join(" | "))
3690}
3691
3692fn compose_ts_purpose(program_node: Node, src: &[u8]) -> Option<String> {
3693 ts_purpose_segments(program_node, src).map(|s| truncate(&s))
3694}
3695
3696pub fn compose_hybrid_ts_purpose(comment_text: &str, raw_segments: &str) -> String {
3703 truncate(&format!("{comment_text} — {raw_segments}"))
3704}
3705
3706pub fn extract_ts_purpose(source: &str, grammar: crate::parse::TsGrammar) -> Option<String> {
3713 let units = crate::parse::utf16_units(source);
3714 let tree = crate::parse::parse_ts_js(&units, grammar)?;
3715 compose_ts_purpose(tree.root_node(), &crate::parse::utf16_bytes(&units))
3716}
3717
3718pub fn extract_ts_purpose_with_heuristic(
3727 root: &Path,
3728 rel: &str,
3729 source: &str,
3730 grammar: crate::parse::TsGrammar,
3731) -> Option<String> {
3732 let units = crate::parse::utf16_units(source);
3733 let tree = crate::parse::parse_ts_js(&units, grammar)?;
3734 let raw = ts_purpose_segments(tree.root_node(), &crate::parse::utf16_bytes(&units))?;
3735 let detail = crate::walk::default_purpose_detailed(root, rel);
3736 Some(if detail.is_comment {
3737 compose_hybrid_ts_purpose(&detail.text, &raw)
3738 } else {
3739 truncate(&raw)
3740 })
3741}
3742
3743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3765pub struct TsFragmentDef {
3766 pub name: String,
3768 pub kind: String,
3770 pub line: usize,
3772 #[serde(rename = "endLine")]
3773 pub end_line: usize,
3775}
3776
3777#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3781pub struct TsBinding {
3782 pub local: String,
3784 pub imported: String,
3786}
3787
3788#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3789pub struct TsImport {
3791 pub spec: String,
3793 pub line: usize,
3795 pub bindings: Vec<TsBinding>,
3797}
3798
3799#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3803pub struct TsReexportName {
3804 pub exported: String,
3806 pub imported: String,
3808}
3809
3810#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3811pub struct TsReexport {
3813 pub spec: String,
3815 pub line: usize,
3817 pub star: bool,
3819 pub names: Vec<TsReexportName>,
3821}
3822
3823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3826pub struct TsRef {
3827 pub kind: String,
3829 pub name: String,
3831 #[serde(default, skip_serializing_if = "Option::is_none")]
3835 pub member: Option<String>,
3836 pub line: usize,
3838}
3839
3840#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3845pub struct TsFragment {
3846 pub ts: u8,
3853 pub defs: Vec<TsFragmentDef>,
3855 pub imports: Vec<TsImport>,
3857 pub reexports: Vec<TsReexport>,
3859 pub refs: Vec<TsRef>,
3861 #[serde(default, skip_serializing_if = "Option::is_none")]
3864 pub default: Option<String>,
3865}
3866
3867impl Default for TsFragment {
3868 fn default() -> Self {
3869 TsFragment {
3870 ts: 1,
3871 defs: Vec::new(),
3872 imports: Vec::new(),
3873 reexports: Vec::new(),
3874 refs: Vec::new(),
3875 default: None,
3876 }
3877 }
3878}
3879
3880const TS_DISPATCH_CALLEES: &[&str] = &["dispatch", "ofType"];
3887
3888const TS_REFERENCE_NODES: &[&str] = &[
3891 "call_expression",
3892 "new_expression",
3893 "jsx_opening_element",
3894 "jsx_self_closing_element",
3895];
3896
3897fn ts_string_literal(node: Option<Node>, src: &[u8]) -> Option<String> {
3900 let node = node?;
3901 if node.kind() != "string" {
3902 return None;
3903 }
3904 Some(
3905 match named_children(node)
3906 .into_iter()
3907 .find(|c| c.kind() == "string_fragment")
3908 {
3909 Some(frag) => text(frag, src),
3910 None => String::new(),
3911 },
3912 )
3913}
3914
3915fn ts_line(node: Node) -> usize {
3916 node.start_position().row + 1
3917}
3918
3919fn is_component_tag_name(name: &str) -> bool {
3924 matches!(name.chars().next(), Some(c) if c.is_ascii_uppercase())
3925}
3926
3927fn ts_import_bindings(stmt: Node, src: &[u8]) -> Vec<TsBinding> {
3928 let Some(clause) = named_children(stmt)
3929 .into_iter()
3930 .find(|c| c.kind() == "import_clause")
3931 else {
3932 return Vec::new();
3933 };
3934 let mut out = Vec::new();
3935 for c in named_children(clause) {
3936 match c.kind() {
3937 "identifier" => out.push(TsBinding {
3938 local: text(c, src),
3939 imported: "default".to_string(),
3940 }),
3941 "namespace_import" => {
3942 if let Some(id) = named_children(c)
3943 .into_iter()
3944 .find(|n| n.kind() == "identifier")
3945 {
3946 out.push(TsBinding {
3947 local: text(id, src),
3948 imported: "*".to_string(),
3949 });
3950 }
3951 }
3952 "named_imports" => {
3953 for spec in named_children(c) {
3954 if spec.kind() != "import_specifier" {
3955 continue;
3956 }
3957 let Some(name) = spec.child_by_field_name("name") else {
3958 continue;
3959 };
3960 let alias = spec.child_by_field_name("alias");
3961 out.push(TsBinding {
3962 local: text(alias.unwrap_or(name), src),
3963 imported: text(name, src),
3964 });
3965 }
3966 }
3967 _ => {}
3968 }
3969 }
3970 out
3971}
3972
3973fn ts_reexport_entry(stmt: Node, src: &[u8]) -> Option<TsReexport> {
3979 let spec = ts_string_literal(stmt.child_by_field_name("source"), src)?;
3980 let line = ts_line(stmt);
3981 if named_children(stmt)
3982 .iter()
3983 .any(|c| c.kind() == "namespace_export")
3984 {
3985 return Some(TsReexport {
3986 spec,
3987 line,
3988 star: false,
3989 names: Vec::new(),
3990 });
3991 }
3992 let Some(clause) = named_children(stmt)
3993 .into_iter()
3994 .find(|c| c.kind() == "export_clause")
3995 else {
3996 return Some(TsReexport {
3997 spec,
3998 line,
3999 star: true,
4000 names: Vec::new(),
4001 });
4002 };
4003 let mut names = Vec::new();
4004 for s in named_children(clause) {
4005 if s.kind() != "export_specifier" {
4006 continue;
4007 }
4008 let Some(name) = s.child_by_field_name("name") else {
4009 continue;
4010 };
4011 let alias = s.child_by_field_name("alias");
4012 names.push(TsReexportName {
4013 exported: text(alias.unwrap_or(name), src),
4014 imported: text(name, src),
4015 });
4016 }
4017 Some(TsReexport {
4018 spec,
4019 line,
4020 star: false,
4021 names,
4022 })
4023}
4024
4025fn ts_decl_kind(node: Node) -> Option<&'static str> {
4030 match node.kind() {
4031 "class_declaration" | "abstract_class_declaration" => Some("class"),
4032 "function_declaration" | "generator_function_declaration" => Some("function"),
4033 "interface_declaration" => Some("interface"),
4034 "type_alias_declaration" => Some("type"),
4035 "enum_declaration" => Some("enum"),
4036 "lexical_declaration" | "variable_declaration" => Some("const"),
4037 _ => None,
4038 }
4039}
4040
4041#[derive(Debug, Clone)]
4042struct TsDecl {
4043 kind: &'static str,
4044 line: usize,
4045 end_line: usize,
4046}
4047
4048fn ts_top_level_decls(program_node: Node, src: &[u8]) -> HashMap<String, TsDecl> {
4052 let mut decls: HashMap<String, TsDecl> = HashMap::new();
4053 fn add(
4054 decls: &mut HashMap<String, TsDecl>,
4055 name: String,
4056 kind: &'static str,
4057 line: usize,
4058 end_line: usize,
4059 ) {
4060 if name.is_empty() {
4061 return;
4062 }
4063 decls.entry(name).or_insert(TsDecl {
4064 kind,
4065 line,
4066 end_line,
4067 });
4068 }
4069 fn from_declaration(node: Node, src: &[u8], decls: &mut HashMap<String, TsDecl>) {
4070 let Some(kind) = ts_decl_kind(node) else {
4071 return;
4072 };
4073 if kind == "const" {
4074 for d in named_children(node) {
4075 if d.kind() != "variable_declarator" {
4076 continue;
4077 }
4078 let name_node = d.child_by_field_name("name");
4079 let line = ts_line(name_node.unwrap_or(d));
4080 for n in ts_pattern_names(name_node, src) {
4081 add(decls, n, "const", line, d.end_position().row + 1);
4082 }
4083 }
4084 return;
4085 }
4086 add(
4087 decls,
4088 ts_declared_name(node, src),
4089 kind,
4090 ts_line(node),
4091 node.end_position().row + 1,
4092 );
4093 }
4094 for c in named_children(program_node) {
4095 if c.kind() == "export_statement" {
4096 if let Some(decl) = c.child_by_field_name("declaration") {
4097 from_declaration(decl, src, &mut decls);
4098 }
4099 continue;
4100 }
4101 from_declaration(c, src, &mut decls);
4102 }
4103 decls
4104}
4105
4106fn ts_default_export_name(program_node: Node, src: &[u8]) -> Option<String> {
4107 for stmt in named_children(program_node) {
4108 if stmt.kind() != "export_statement" || !is_default_export_statement(stmt) {
4109 continue;
4110 }
4111 if let Some(decl) = stmt.child_by_field_name("declaration") {
4112 let name = ts_declared_name(decl, src);
4113 if !name.is_empty() {
4114 return Some(name);
4115 }
4116 continue;
4117 }
4118 if let Some(value) = stmt.child_by_field_name("value") {
4119 if value.kind() == "identifier" {
4120 return Some(text(value, src));
4121 }
4122 }
4123 }
4124 None
4125}
4126
4127fn ts_exported_names(
4132 program_node: Node,
4133 decls: &HashMap<String, TsDecl>,
4134 src: &[u8],
4135) -> Vec<String> {
4136 let mut names: Vec<String> = Vec::new();
4137 let mut seen: HashSet<String> = HashSet::new();
4138 macro_rules! add {
4139 ($name:expr) => {{
4140 let name: String = $name;
4141 if !name.is_empty() && decls.contains_key(&name) && seen.insert(name.clone()) {
4142 names.push(name);
4143 }
4144 }};
4145 }
4146 let export_stmts: Vec<Node> = named_children(program_node)
4147 .into_iter()
4148 .filter(|c| c.kind() == "export_statement")
4149 .collect();
4150 if !export_stmts.is_empty() {
4151 for stmt in export_stmts {
4152 if stmt.child_by_field_name("source").is_some() {
4153 continue; }
4155 if let Some(decl) = stmt.child_by_field_name("declaration") {
4156 if ts_decl_kind(decl) == Some("const") {
4157 for d in named_children(decl) {
4158 if d.kind() != "variable_declarator" {
4159 continue;
4160 }
4161 for n in ts_pattern_names(d.child_by_field_name("name"), src) {
4162 add!(n);
4163 }
4164 }
4165 } else {
4166 add!(ts_declared_name(decl, src));
4167 }
4168 continue;
4169 }
4170 if let Some(clause) = named_children(stmt)
4171 .into_iter()
4172 .find(|c| c.kind() == "export_clause")
4173 {
4174 for s in named_children(clause) {
4175 if s.kind() != "export_specifier" {
4176 continue;
4177 }
4178 if let Some(name) = s.child_by_field_name("name") {
4183 add!(text(name, src));
4184 }
4185 }
4186 }
4187 }
4188 return names;
4189 }
4190 for stmt in named_children(program_node) {
4192 if stmt.kind() != "expression_statement" {
4193 continue;
4194 }
4195 let Some(assign) = named_children(stmt)
4196 .into_iter()
4197 .find(|n| n.kind() == "assignment_expression")
4198 else {
4199 continue;
4200 };
4201 let Some(target) = common_js_export_target(assign.child_by_field_name("left"), src) else {
4202 continue;
4203 };
4204 let right = assign.child_by_field_name("right");
4205 match target {
4206 CjsTarget::Prop(prop) => {
4207 add!(prop);
4208 }
4209 CjsTarget::Whole => match right {
4210 Some(r) if r.kind() == "object" => {
4211 for c in named_children(r) {
4212 if c.kind() == "shorthand_property_identifier" {
4213 add!(text(c, src));
4214 } else if c.kind() == "pair" {
4215 if let Some(value) = c.child_by_field_name("value") {
4216 if value.kind() == "identifier" {
4217 add!(text(value, src));
4218 }
4219 }
4220 }
4221 }
4222 }
4223 Some(r) if r.kind() == "identifier" => {
4224 add!(text(r, src));
4225 }
4226 _ => {}
4227 },
4228 }
4229 }
4230 names
4231}
4232
4233fn ts_require_import(decl_node: Node, src: &[u8]) -> Vec<TsImport> {
4238 let mut out = Vec::new();
4239 for d in named_children(decl_node) {
4240 if d.kind() != "variable_declarator" {
4241 continue;
4242 }
4243 let Some(value) = d.child_by_field_name("value") else {
4244 continue;
4245 };
4246 if value.kind() != "call_expression" {
4247 continue;
4248 }
4249 let Some(fnode) = value.child_by_field_name("function") else {
4250 continue;
4251 };
4252 if fnode.kind() != "identifier" || text(fnode, src) != "require" {
4253 continue;
4254 }
4255 let args = value.child_by_field_name("arguments");
4256 let first = args.and_then(|a| named_children(a).into_iter().next());
4257 let Some(spec) = ts_string_literal(first, src) else {
4258 continue;
4259 };
4260 let name_node = d.child_by_field_name("name");
4261 let bindings = match name_node {
4262 Some(n) if n.kind() == "identifier" => {
4263 vec![TsBinding {
4264 local: text(n, src),
4265 imported: "*".to_string(),
4266 }]
4267 }
4268 _ => ts_pattern_names(name_node, src)
4269 .into_iter()
4270 .map(|n| TsBinding {
4271 local: n.clone(),
4272 imported: n,
4273 })
4274 .collect(),
4275 };
4276 out.push(TsImport {
4277 spec,
4278 line: ts_line(d),
4279 bindings,
4280 });
4281 }
4282 out
4283}
4284
4285fn ts_record_ref(
4286 node: Node,
4287 src: &[u8],
4288 refs: &mut Vec<TsRef>,
4289 consumed: &mut HashSet<usize>,
4290 known: &HashSet<String>,
4291) {
4292 match node.kind() {
4293 "call_expression" => {
4294 let fnode = node.child_by_field_name("function");
4295 let callee = match fnode {
4296 Some(f) if f.kind() == "identifier" => Some(text(f, src)),
4297 Some(f) if f.kind() == "member_expression" => {
4298 f.child_by_field_name("property").map(|p| text(p, src))
4299 }
4300 _ => None,
4301 };
4302 let dispatching = callee
4303 .as_deref()
4304 .is_some_and(|c| TS_DISPATCH_CALLEES.contains(&c));
4305 if dispatching {
4306 let args = node
4312 .child_by_field_name("arguments")
4313 .map(named_children)
4314 .unwrap_or_default();
4315 for arg in args {
4316 if arg.kind() == "identifier" {
4317 let name = text(arg, src);
4318 if known.contains(&name) {
4319 refs.push(TsRef {
4320 kind: "dispatch".to_string(),
4321 name,
4322 member: None,
4323 line: ts_line(arg),
4324 });
4325 }
4326 consumed.insert(arg.id());
4327 } else if arg.kind() == "call_expression" {
4328 if let Some(inner) = arg.child_by_field_name("function") {
4329 if inner.kind() == "identifier" {
4330 let name = text(inner, src);
4331 if known.contains(&name) {
4332 refs.push(TsRef {
4333 kind: "dispatch".to_string(),
4334 name,
4335 member: None,
4336 line: ts_line(inner),
4337 });
4338 }
4339 consumed.insert(arg.id());
4340 }
4341 }
4342 }
4343 }
4344 } else if !consumed.contains(&node.id()) {
4345 match fnode {
4346 Some(f) if f.kind() == "identifier" => {
4347 let name = text(f, src);
4348 if known.contains(&name) {
4349 refs.push(TsRef {
4350 kind: "call".to_string(),
4351 name,
4352 member: None,
4353 line: ts_line(f),
4354 });
4355 }
4356 }
4357 Some(f) if f.kind() == "member_expression" => {
4358 let obj = f.child_by_field_name("object");
4359 let prop = f.child_by_field_name("property");
4360 if let (Some(obj), Some(prop)) = (obj, prop) {
4365 let name = text(obj, src);
4366 if obj.kind() == "identifier" && known.contains(&name) {
4367 refs.push(TsRef {
4368 kind: "call".to_string(),
4369 name,
4370 member: Some(text(prop, src)),
4371 line: ts_line(obj),
4372 });
4373 }
4374 }
4375 }
4376 _ => {}
4377 }
4378 }
4379 }
4380 "new_expression" => {
4381 if let Some(ctor) = node.child_by_field_name("constructor") {
4382 let name = text(ctor, src);
4383 if ctor.kind() == "identifier" && known.contains(&name) {
4384 refs.push(TsRef {
4385 kind: "call".to_string(),
4386 name,
4387 member: None,
4388 line: ts_line(ctor),
4389 });
4390 }
4391 }
4392 }
4393 "jsx_opening_element" | "jsx_self_closing_element" => {
4394 let name_node = node.child_by_field_name("name");
4395 match name_node {
4396 Some(n) if n.kind() == "identifier" => {
4397 let name = text(n, src);
4398 if is_component_tag_name(&name) && known.contains(&name) {
4399 refs.push(TsRef {
4400 kind: "jsx-use".to_string(),
4401 name,
4402 member: None,
4403 line: ts_line(n),
4404 });
4405 }
4406 }
4407 Some(n) if n.kind() == "member_expression" || n.kind() == "nested_identifier" => {
4408 let obj = n.child_by_field_name("object");
4409 let prop = n.child_by_field_name("property");
4410 if let (Some(obj), Some(prop)) = (obj, prop) {
4411 let name = text(obj, src);
4412 if obj.kind() == "identifier"
4413 && is_component_tag_name(&name)
4414 && known.contains(&name)
4415 {
4416 refs.push(TsRef {
4417 kind: "jsx-use".to_string(),
4418 name,
4419 member: Some(text(prop, src)),
4420 line: ts_line(obj),
4421 });
4422 }
4423 }
4424 }
4425 _ => {}
4426 }
4427 }
4428 _ => {}
4429 }
4430}
4431
4432fn ts_ref_walk(program_node: Node, src: &[u8], refs: &mut Vec<TsRef>, known: &HashSet<String>) {
4437 let mut consumed: HashSet<usize> = HashSet::new();
4438 let mut cursor = program_node.walk();
4439 loop {
4440 if TS_REFERENCE_NODES.contains(&cursor.node().kind()) {
4441 ts_record_ref(cursor.node(), src, refs, &mut consumed, known);
4442 }
4443 if cursor.goto_first_child() {
4444 continue;
4445 }
4446 let mut advanced = false;
4447 while cursor.depth() > 0 {
4448 if cursor.goto_next_sibling() {
4449 advanced = true;
4450 break;
4451 }
4452 if !cursor.goto_parent() {
4453 break;
4454 }
4455 }
4456 if !advanced && !cursor.goto_next_sibling() {
4457 break;
4458 }
4459 }
4460}
4461
4462pub fn extract_ts_fragment(program_node: Node, src: &[u8]) -> TsFragment {
4465 let decls = ts_top_level_decls(program_node, src);
4466 let mut imports: Vec<TsImport> = Vec::new();
4467 let mut reexports: Vec<TsReexport> = Vec::new();
4468 for stmt in named_children(program_node) {
4469 if stmt.kind() == "import_statement" {
4470 let Some(spec) = ts_string_literal(stmt.child_by_field_name("source"), src) else {
4471 continue;
4472 };
4473 imports.push(TsImport {
4474 spec,
4475 line: ts_line(stmt),
4476 bindings: ts_import_bindings(stmt, src),
4477 });
4478 } else if stmt.kind() == "export_statement" && stmt.child_by_field_name("source").is_some()
4479 {
4480 if let Some(entry) = ts_reexport_entry(stmt, src) {
4481 reexports.push(entry);
4482 }
4483 } else if stmt.kind() == "lexical_declaration" || stmt.kind() == "variable_declaration" {
4484 imports.extend(ts_require_import(stmt, src));
4485 }
4486 }
4487 let defs: Vec<TsFragmentDef> = ts_exported_names(program_node, &decls, src)
4488 .into_iter()
4489 .map(|name| {
4490 let d = &decls[&name];
4491 TsFragmentDef {
4492 name,
4493 kind: d.kind.to_string(),
4494 line: d.line,
4495 end_line: d.end_line,
4496 }
4497 })
4498 .collect();
4499 let mut known: HashSet<String> = defs.iter().map(|d| d.name.clone()).collect();
4505 for imp in &imports {
4506 for b in &imp.bindings {
4507 known.insert(b.local.clone());
4508 }
4509 }
4510 let mut refs: Vec<TsRef> = Vec::new();
4511 ts_ref_walk(program_node, src, &mut refs, &known);
4512 let default = ts_default_export_name(program_node, src).filter(|d| decls.contains_key(d));
4515 TsFragment {
4516 ts: 1,
4517 defs,
4518 imports,
4519 reexports,
4520 refs,
4521 default,
4522 }
4523}
4524
4525pub struct TsFileExtraction {
4529 pub purpose: Option<String>,
4531 pub fragment: TsFragment,
4533}
4534
4535pub fn extract_ts_file(
4539 root: &Path,
4540 rel: &str,
4541 source: &str,
4542 grammar: crate::parse::TsGrammar,
4543) -> Option<TsFileExtraction> {
4544 let units = crate::parse::utf16_units(source);
4545 let tree = crate::parse::parse_ts_js(&units, grammar)?;
4546 let src = crate::parse::utf16_bytes(&units);
4547 let root_node = tree.root_node();
4548 let purpose = ts_purpose_segments(root_node, &src).map(|raw| {
4549 let detail = crate::walk::default_purpose_detailed(root, rel);
4550 if detail.is_comment {
4551 compose_hybrid_ts_purpose(&detail.text, &raw)
4552 } else {
4553 truncate(&raw)
4554 }
4555 });
4556 Some(TsFileExtraction {
4557 purpose,
4558 fragment: extract_ts_fragment(root_node, &src),
4559 })
4560}
4561
4562pub fn ts_extraction_to_json(purpose: &Option<String>, fragment: &TsFragment) -> String {
4567 let purpose_json = match purpose {
4568 Some(p) => Json::Str(p.clone()),
4569 None => Json::Null,
4570 };
4571 let root = Json::Obj(vec![
4572 ("purpose", purpose_json),
4573 (
4574 "defs",
4575 Json::Arr(
4576 fragment
4577 .defs
4578 .iter()
4579 .map(|d| {
4580 Json::Obj(vec![
4581 ("name", Json::Str(d.name.clone())),
4582 ("kind", Json::Str(d.kind.clone())),
4583 ("line", Json::Num(d.line)),
4584 ])
4585 })
4586 .collect(),
4587 ),
4588 ),
4589 (
4590 "refs",
4591 Json::Arr(
4592 fragment
4593 .refs
4594 .iter()
4595 .map(|r| {
4596 let mut fields = vec![
4597 ("kind", Json::Str(r.kind.clone())),
4598 ("name", Json::Str(r.name.clone())),
4599 ];
4600 if let Some(m) = &r.member {
4601 fields.push(("member", Json::Str(m.clone())));
4602 }
4603 fields.push(("line", Json::Num(r.line)));
4604 Json::Obj(fields)
4605 })
4606 .collect(),
4607 ),
4608 ),
4609 ]);
4610 root.to_pretty_string()
4611}
4612
4613#[cfg(test)]
4614mod tests {
4615 use super::*;
4616
4617 fn extract_src(src: &str) -> Extraction {
4618 extract(src)
4619 }
4620
4621 fn find_def<'a>(e: &'a Extraction, id: &str) -> Option<&'a DefRecord> {
4622 e.defs.iter().find(|d| d.id == id)
4623 }
4624
4625 #[test]
4628 fn namespace_level_type_fqn_is_dotted() {
4629 let e = extract_src("namespace Fixtures.Widgets { public class Gadget {} }");
4630 assert!(find_def(&e, "Fixtures.Widgets.Gadget").is_some());
4631 }
4632
4633 #[test]
4634 fn top_level_type_fqn_has_no_namespace_prefix() {
4635 let e = extract_src("public class Gadget {}");
4636 let d = find_def(&e, "Gadget").expect("Gadget def present");
4637 assert_eq!(d.namespace, "");
4638 }
4639
4640 #[test]
4641 fn nested_type_fqn_uses_plus_not_dot() {
4642 let e = extract_src(
4643 "namespace Fixtures.Widgets { public class Outer { public class Inner {} } }",
4644 );
4645 assert!(find_def(&e, "Fixtures.Widgets.Outer+Inner").is_some());
4646 assert!(find_def(&e, "Fixtures.Widgets.Outer.Inner").is_none());
4648 }
4649
4650 #[test]
4651 fn doubly_nested_type_fqn_chains_plus_joins() {
4652 let e = extract_src(
4653 "namespace Fixtures.Widgets { public class A { public class B { public class C {} } } }",
4654 );
4655 assert!(find_def(&e, "Fixtures.Widgets.A+B+C").is_some());
4656 }
4657
4658 #[test]
4659 fn enum_member_id_appends_dot_even_under_nested_type() {
4660 let e = extract_src(
4661 "namespace Fixtures.Gadgets { public class Controller { public enum State { Off, On } } }",
4662 );
4663 assert!(find_def(&e, "Fixtures.Gadgets.Controller+State").is_some());
4664 assert!(find_def(&e, "Fixtures.Gadgets.Controller+State.Off").is_some());
4665 assert!(find_def(&e, "Fixtures.Gadgets.Controller+State.On").is_some());
4666 }
4667
4668 #[test]
4669 fn file_scoped_namespace_siblings_get_the_namespace() {
4670 let e = extract_src("namespace Fixtures.Gadgets;\npublic class Registry {}\npublic enum State { Off, On }\n");
4671 assert!(find_def(&e, "Fixtures.Gadgets.Registry").is_some());
4672 assert!(find_def(&e, "Fixtures.Gadgets.State").is_some());
4673 assert!(find_def(&e, "Fixtures.Gadgets.State.Off").is_some());
4674 }
4675
4676 #[test]
4677 fn nested_regular_namespace_dots_accumulate() {
4678 let e = extract_src("namespace A { namespace B { public class Widget {} } }");
4679 assert!(find_def(&e, "A.B.Widget").is_some());
4680 }
4681
4682 #[test]
4685 fn type_def_end_line_is_the_closing_brace_line_of_the_whole_declaration() {
4686 let e = extract_src("namespace N;\npublic class Widget\n{\n int n;\n}\n");
4687 let d = find_def(&e, "N.Widget").unwrap();
4688 assert_eq!((d.line, d.end_line), (2, 5));
4689 }
4690
4691 #[test]
4692 fn enum_member_end_line_is_its_own_single_line() {
4693 let e = extract_src("public enum State\n{\n Off,\n On,\n}\n");
4694 assert_eq!(
4695 (
4696 find_def(&e, "State.Off").unwrap().line,
4697 find_def(&e, "State.Off").unwrap().end_line
4698 ),
4699 (3, 3)
4700 );
4701 assert_eq!(
4702 (
4703 find_def(&e, "State.On").unwrap().line,
4704 find_def(&e, "State.On").unwrap().end_line
4705 ),
4706 (4, 4)
4707 );
4708 }
4709
4710 #[test]
4711 fn nested_type_span_stays_within_its_own_node() {
4712 let e = extract_src("public class Outer\n{\n public class Inner { }\n}\n");
4713 assert_eq!(
4714 (
4715 find_def(&e, "Outer").unwrap().line,
4716 find_def(&e, "Outer").unwrap().end_line
4717 ),
4718 (1, 4)
4719 );
4720 assert_eq!(
4721 (
4722 find_def(&e, "Outer+Inner").unwrap().line,
4723 find_def(&e, "Outer+Inner").unwrap().end_line
4724 ),
4725 (3, 3)
4726 );
4727 }
4728
4729 #[test]
4730 fn generic_name_records_base_and_each_type_argument() {
4731 let e = extract_src(
4732 "using System.Collections.Generic;\nnamespace Fixtures.Generics { public class Store { public Dictionary<Key, Value> Items { get; set; } } public class Key {} public class Value {} }",
4733 );
4734 let uses_type_names: Vec<&str> = e
4735 .refs
4736 .iter()
4737 .filter(|r| r.kind == "uses-type")
4738 .map(|r| r.name.as_str())
4739 .collect();
4740 assert!(uses_type_names.contains(&"Dictionary"));
4744 assert!(uses_type_names.contains(&"Key"));
4745 assert!(uses_type_names.contains(&"Value"));
4746 }
4747
4748 #[test]
4749 fn nested_generic_type_arguments_are_all_recorded() {
4750 let e = extract_src(
4751 "using System.Collections.Generic;\nnamespace Fixtures.Generics { public class Store { public List<Dictionary<string, Gadget>> Items { get; set; } } public class Gadget {} }",
4752 );
4753 let uses_type_names: Vec<&str> = e
4754 .refs
4755 .iter()
4756 .filter(|r| r.kind == "uses-type")
4757 .map(|r| r.name.as_str())
4758 .collect();
4759 assert!(uses_type_names.contains(&"List"));
4760 assert!(uses_type_names.contains(&"Dictionary"));
4761 assert!(uses_type_names.contains(&"Gadget"));
4762 assert!(!uses_type_names.contains(&"string"));
4764 }
4765
4766 #[test]
4769 fn using_alias_directive_captures_alias_and_target() {
4770 let e = extract_src("using Widgets = Fixtures.Widgets.Catalog;\n");
4771 assert_eq!(e.usings.len(), 1);
4772 match &e.usings[0] {
4773 UsingRecord::Alias {
4774 alias,
4775 target,
4776 global,
4777 } => {
4778 assert_eq!(alias, "Widgets");
4779 assert_eq!(target, "Fixtures.Widgets.Catalog");
4780 assert!(!global);
4781 }
4782 UsingRecord::Plain { .. } => panic!("expected alias form"),
4783 }
4784 }
4785
4786 #[test]
4787 fn plain_using_directive_has_no_alias() {
4788 let e = extract_src("using System.Collections.Generic;\n");
4789 match &e.usings[0] {
4790 UsingRecord::Plain { text, global } => {
4791 assert_eq!(text, "System.Collections.Generic");
4792 assert!(!global);
4793 }
4794 UsingRecord::Alias { .. } => panic!("expected plain form"),
4795 }
4796 }
4797
4798 #[test]
4799 fn global_using_directive_sets_global_flag() {
4800 let e = extract_src("global using System;\n");
4801 match &e.usings[0] {
4802 UsingRecord::Plain { text, global } => {
4803 assert_eq!(text, "System");
4804 assert!(*global);
4805 }
4806 UsingRecord::Alias { .. } => panic!("expected plain form"),
4807 }
4808 }
4809
4810 #[test]
4811 fn static_using_directive_is_plain_form_not_flagged() {
4812 let e = extract_src("using static System.Math;\n");
4815 match &e.usings[0] {
4816 UsingRecord::Plain { text, global } => {
4817 assert_eq!(text, "System.Math");
4818 assert!(!global);
4819 }
4820 UsingRecord::Alias { .. } => panic!("expected plain form"),
4821 }
4822 }
4823
4824 #[test]
4825 fn using_directive_also_pushes_an_imports_ref_with_null_namespace() {
4826 let e = extract_src("using System.Collections.Generic;\n");
4827 let import_ref = e
4828 .refs
4829 .iter()
4830 .find(|r| r.kind == "imports")
4831 .expect("imports ref present");
4832 assert_eq!(import_ref.name, "System.Collections.Generic");
4833 assert!(import_ref.namespace.is_none());
4834 }
4835
4836 #[test]
4839 fn simple_identifier_qualifier_is_captured_without_dot() {
4840 let e = extract_src(
4841 "namespace Fixtures.Orders { public enum Priority { Low, High } public class Probe { public bool F() { var x = Priority.High; return true; } } }",
4842 );
4843 let m = e
4844 .refs
4845 .iter()
4846 .find(|r| r.kind == "uses-member" && r.member.as_deref() == Some("High"))
4847 .expect("member ref present");
4848 assert_eq!(m.name, "Priority");
4849 assert!(m.qualified.is_none());
4850 }
4851
4852 #[test]
4853 fn deep_member_chain_captures_every_window_qualifier_flattened_at_each_level() {
4854 let e = extract_src(
4865 "namespace Fixtures.Orders { public class Probe { public void F() { var x = Fixtures.Orders.Priority.High; } } }",
4866 );
4867 let members: Vec<(&str, &str)> = e
4868 .refs
4869 .iter()
4870 .filter(|r| r.kind == "uses-member")
4871 .map(|r| (r.name.as_str(), r.member.as_deref().unwrap_or("")))
4872 .collect();
4873 assert_eq!(
4874 members,
4875 vec![
4876 ("Priority", "High"),
4877 ("Orders", "Priority"),
4878 ("Fixtures", "Orders")
4879 ]
4880 );
4881 let outer = e
4882 .refs
4883 .iter()
4884 .find(|r| r.member.as_deref() == Some("High"))
4885 .expect("outer window present");
4886 assert_eq!(outer.qualified.as_deref(), Some("Fixtures.Orders.Priority"));
4887 }
4888
4889 #[test]
4890 fn nested_enum_dotted_qualifier_is_captured_as_a_two_part_qualified_text() {
4891 let e = extract_src(
4896 "namespace Fixtures.Widgets { public class Outer { public enum Inner { Off, On } } public class Probe { public void F() { var x = Outer.Inner.On; } } }",
4897 );
4898 let m = e
4899 .refs
4900 .iter()
4901 .find(|r| r.kind == "uses-member" && r.member.as_deref() == Some("On"))
4902 .expect("outer window present");
4903 assert_eq!(m.name, "Inner");
4904 assert_eq!(m.qualified.as_deref(), Some("Outer.Inner"));
4905 }
4906
4907 #[test]
4910 fn inline_out_declaration_emits_a_uses_type_ref_for_its_type() {
4911 let e = extract_src(
4915 "namespace Fixtures.Orders { public class Probe { public void F() { TryGet(out SomeEnum x); } } }",
4916 );
4917 let uses_type: Vec<&str> = e
4918 .refs
4919 .iter()
4920 .filter(|r| r.kind == "uses-type")
4921 .map(|r| r.name.as_str())
4922 .collect();
4923 assert!(
4924 uses_type.contains(&"SomeEnum"),
4925 "expected a uses-type ref for the out-declared type, got {uses_type:?}"
4926 );
4927 }
4928
4929 #[test]
4930 fn inline_out_declaration_with_var_type_yields_no_candidate() {
4931 let e = extract_src("namespace Fixtures.Orders { public class Probe { public void F() { TryGet(out var x); } } }");
4934 assert!(e.refs.iter().all(|r| r.kind != "uses-type"));
4935 }
4936
4937 #[test]
4940 fn purpose_signature_composes_kind_name_bases_and_public_methods() {
4941 let e = extract_src(
4942 "namespace Fixtures.Orders { public interface IReader { int Count { get; } } public class Order : IReader { public int Count { get; set; } public void SaveAsync() {} private void Hidden() {} } }",
4943 );
4944 assert_eq!(
4945 e.purpose.as_deref(),
4946 Some("interface IReader | class Order : IReader; Save")
4947 );
4948 }
4949
4950 #[test]
4951 fn purpose_signature_is_none_when_no_namespace_level_types() {
4952 let e = extract_src("// just a comment\n");
4953 assert!(e.purpose.is_none());
4954 }
4955
4956 #[test]
4957 fn purpose_signature_truncates_at_200_utf16_units() {
4958 let long_bases = (0..40)
4959 .map(|i| format!("IFace{i}"))
4960 .collect::<Vec<_>>()
4961 .join(", ");
4962 let src = format!("public class Wide : {long_bases} {{}}");
4963 let e = extract_src(&src);
4964 let p = e.purpose.expect("purpose present");
4965 assert_eq!(p.encode_utf16().count(), MAX_PURPOSE);
4966 assert!(p.ends_with("..."));
4967 }
4968
4969 #[test]
4972 fn primary_constructor_base_type_records_inherits_ref() {
4973 let e = extract_src(
4974 "namespace Fixtures.Misc { public class GadgetBase { public GadgetBase(string name) {} } public record GadgetRecord(string Name) : GadgetBase(Name); }",
4975 );
4976 let inherits: Vec<&str> = e
4977 .refs
4978 .iter()
4979 .filter(|r| r.kind == "inherits")
4980 .map(|r| r.name.as_str())
4981 .collect();
4982 assert!(inherits.contains(&"GadgetBase"));
4983 }
4984
4985 #[test]
4988 fn tuple_return_type_records_each_element_type() {
4989 let e = extract_src(
4990 "namespace Fixtures.Misc { public class GadgetBase {} public class Gadget { public (Gadget Primary, GadgetBase Fallback) Describe() { return (this, null); } } }",
4991 );
4992 let uses_type_names: Vec<&str> = e
4993 .refs
4994 .iter()
4995 .filter(|r| r.kind == "uses-type")
4996 .map(|r| r.name.as_str())
4997 .collect();
4998 assert!(uses_type_names.contains(&"Gadget"));
4999 assert!(uses_type_names.contains(&"GadgetBase"));
5000 }
5001
5002 const IF_DIRECTIVE_CHAIN: &str =
5025 include_str!("../fixtures/preproc/preproc_chain_interrupt_if.cs");
5026 const IFELSE_DIRECTIVE_CHAIN: &str =
5027 include_str!("../fixtures/preproc/preproc_chain_interrupt_ifelse.cs");
5028 const NESTED_IF_DIRECTIVE_CHAIN_CONTROL: &str =
5029 include_str!("../fixtures/preproc/preproc_chain_interrupt_nested_control.cs");
5030 const WHOLESTMT_DIRECTIVE_CONTROL: &str =
5031 include_str!("../fixtures/preproc/preproc_chain_wholestmt_control.cs");
5032
5033 fn uses_member_refs(e: &Extraction) -> Vec<(&str, &str, usize)> {
5034 e.refs
5035 .iter()
5036 .filter(|r| r.kind == "uses-member")
5037 .map(|r| (r.name.as_str(), r.member.as_deref().unwrap_or(""), r.line))
5038 .collect()
5039 }
5040
5041 #[test]
5042 fn preproc_if_interrupting_chain_promotes_qualifier_at_the_resumed_identifiers_own_line() {
5043 let e = extract_src(IF_DIRECTIVE_CHAIN);
5044 assert!(uses_member_refs(&e).contains(&("WriteTo", "Debug", 31)));
5050 }
5051
5052 #[test]
5053 fn preproc_if_interrupting_chain_places_promoted_ref_after_the_original_chains_own_arguments() {
5054 let e = extract_src(IF_DIRECTIVE_CHAIN);
5055 let refs = uses_member_refs(&e);
5056 let debug_pos = refs
5057 .iter()
5058 .position(|r| *r == ("WriteTo", "Debug", 31))
5059 .expect("promoted ref present");
5060 let interval_day_pos = refs
5061 .iter()
5062 .position(|r| *r == ("Interval", "Day", 26))
5063 .expect("Interval/Day ref present");
5064 let minimum_level_pos = refs
5065 .iter()
5066 .position(|r| *r == ("Level", "Information", 33))
5067 .expect("first MinimumLevel.Override argument ref present");
5068 assert!(
5076 interval_day_pos < debug_pos,
5077 "Interval/Day ({interval_day_pos}) should precede WriteTo/Debug ({debug_pos})"
5078 );
5079 assert!(debug_pos < minimum_level_pos, "WriteTo/Debug ({debug_pos}) should precede the first MinimumLevel.Override argument ref ({minimum_level_pos})");
5080 }
5081
5082 #[test]
5083 fn preproc_ifelse_interrupting_chain_promotes_qualifier_in_both_arms_but_not_across_endif() {
5084 let e = extract_src(IFELSE_DIRECTIVE_CHAIN);
5085 let refs = uses_member_refs(&e);
5086 assert!(refs.contains(&("WriteTo", "Trace", 23)));
5089 assert!(refs.contains(&("WriteTo", "Console", 25)));
5090 assert!(!refs.iter().any(|(_, member, _)| *member == "Override"));
5094 assert!(!refs.iter().any(|(name, _, _)| *name == "MinimumLevel"));
5095 }
5096
5097 #[test]
5098 fn preproc_promotion_does_not_fire_on_nested_if_in_if_chain_interrupt() {
5099 let e = extract_src(NESTED_IF_DIRECTIVE_CHAIN_CONTROL);
5106 let refs = uses_member_refs(&e);
5107 assert!(refs.contains(&("WriteTo", "Trace", 25)));
5108 assert!(!refs
5109 .iter()
5110 .any(|(name, member, _)| *name == "WriteTo" && *member == "Debug"));
5111 }
5112
5113 #[test]
5114 fn preproc_promotion_does_not_fire_on_whole_statement_if_guard() {
5115 let e = extract_src(WHOLESTMT_DIRECTIVE_CONTROL);
5121 let refs = uses_member_refs(&e);
5122 assert_eq!(
5123 refs,
5124 vec![("registry", "Attach", 15), ("registry", "Attach", 17)]
5125 );
5126 }
5127
5128 use crate::parse::TsGrammar;
5136
5137 fn ts_purpose(src: &str, grammar: TsGrammar) -> Option<String> {
5138 extract_ts_purpose(src, grammar)
5139 }
5140
5141 const ARTICLE_CARD_TSX: &str = include_str!("../fixtures/ts-grammar/ArticleCard.tsx");
5142 const USE_WIDGET_DATA_TS: &str = include_str!("../fixtures/ts-grammar/useWidgetData.ts");
5143 const ORDER_SERVICE_TS: &str = include_str!("../fixtures/ts-grammar/OrderService.ts");
5144 const WIDGETS_SLICE_TS: &str = include_str!("../fixtures/ts-grammar/widgetsSlice.ts");
5145 const STRING_UTILS_TS: &str = include_str!("../fixtures/ts-grammar/stringUtils.ts");
5146 const ARTICLE_TYPES_TS: &str = include_str!("../fixtures/ts-grammar/articleTypes.ts");
5147 const FORMAT_CURRENCY_TS: &str = include_str!("../fixtures/ts-grammar/formatCurrency.ts");
5148 const NOTIFICATION_DIGEST_TS: &str =
5149 include_str!("../fixtures/ts-grammar/notificationDigest.ts");
5150 const PATH_HELPERS_JS: &str = include_str!("../fixtures/ts-grammar/pathHelpers.js");
5151 const REEXPORT_BARREL_TS: &str = include_str!("../fixtures/ts-grammar/reexportBarrel.ts");
5153 const WIDGET_PANEL_WITH_NOTE_TS: &str =
5154 include_str!("../fixtures/ts-grammar/widgetPanelWithNote.ts");
5155
5156 #[test]
5157 fn ts_grammar_fixture_article_card_tsx() {
5158 assert_eq!(
5159 ts_purpose(ARTICLE_CARD_TSX, TsGrammar::Tsx).as_deref(),
5160 Some("function ArticleCard | interface ArticleCardProps")
5161 );
5162 }
5163
5164 #[test]
5165 fn ts_grammar_fixture_use_widget_data_ts() {
5166 assert_eq!(
5167 ts_purpose(USE_WIDGET_DATA_TS, TsGrammar::Typescript).as_deref(),
5168 Some("function useWidgetData")
5169 );
5170 }
5171
5172 #[test]
5173 fn ts_grammar_fixture_order_service_ts() {
5174 assert_eq!(
5175 ts_purpose(ORDER_SERVICE_TS, TsGrammar::Typescript).as_deref(),
5176 Some("class OrderService : IOrderService; getOrder, cancelOrder | interface IOrderService")
5177 );
5178 }
5179
5180 #[test]
5181 fn ts_grammar_fixture_widgets_slice_ts() {
5182 assert_eq!(
5183 ts_purpose(WIDGETS_SLICE_TS, TsGrammar::Typescript).as_deref(),
5184 Some("const widgetsSlice | const setLoading | const setItems | default (anonymous)")
5185 );
5186 }
5187
5188 #[test]
5189 fn ts_grammar_fixture_string_utils_ts() {
5190 assert_eq!(
5191 ts_purpose(STRING_UTILS_TS, TsGrammar::Typescript).as_deref(),
5192 Some("function truncate | function slugify | const DEFAULT_MAX_LENGTH")
5193 );
5194 }
5195
5196 #[test]
5197 fn ts_grammar_fixture_article_types_ts() {
5198 assert_eq!(
5199 ts_purpose(ARTICLE_TYPES_TS, TsGrammar::Typescript).as_deref(),
5200 Some("interface ArticleItem | interface ArticleAuthor | type ArticleItemStatus | type ArticlePage")
5201 );
5202 }
5203
5204 #[test]
5205 fn ts_grammar_fixture_format_currency_ts() {
5206 assert_eq!(
5207 ts_purpose(FORMAT_CURRENCY_TS, TsGrammar::Typescript).as_deref(),
5208 Some("default (anonymous)")
5209 );
5210 }
5211
5212 #[test]
5213 fn ts_grammar_fixture_notification_digest_ts() {
5214 assert_eq!(
5215 ts_purpose(NOTIFICATION_DIGEST_TS, TsGrammar::Typescript).as_deref(),
5216 Some("class NotificationDigestBuilder; addEvent, build | function buildDailyDigest")
5217 );
5218 }
5219
5220 #[test]
5221 fn ts_grammar_fixture_path_helpers_js() {
5222 assert_eq!(
5223 ts_purpose(PATH_HELPERS_JS, TsGrammar::Javascript).as_deref(),
5224 Some("function joinSafe | function isAbsolute")
5225 );
5226 }
5227
5228 #[test]
5231 fn ts_grammar_fixture_reexport_barrel_ts() {
5232 assert_eq!(
5233 ts_purpose(REEXPORT_BARREL_TS, TsGrammar::Typescript).as_deref(),
5234 Some("reexports WidgetCard, WidgetList, Dialog, *, WidgetTypes")
5235 );
5236 }
5237
5238 #[test]
5239 fn reexport_bucket_mixes_with_a_real_export_and_lands_last() {
5240 let src = "export function real() {}\nexport { A, B } from './m';\n";
5241 assert_eq!(
5242 ts_purpose(src, TsGrammar::Typescript).as_deref(),
5243 Some("function real | reexports A, B")
5244 );
5245 }
5246
5247 #[test]
5248 fn local_export_clause_without_from_is_still_skipped() {
5249 let src = "const A = 1;\nexport { A };\n";
5250 assert_eq!(ts_purpose(src, TsGrammar::Typescript), None);
5251 }
5252
5253 #[test]
5254 fn bare_star_reexports_dedupe_to_one_token_regardless_of_statement_count() {
5255 let src = "export * from './a';\nexport * from './b';\n";
5256 assert_eq!(
5257 ts_purpose(src, TsGrammar::Typescript).as_deref(),
5258 Some("reexports *")
5259 );
5260 }
5261
5262 #[test]
5272 fn ts_grammar_fixture_widget_panel_with_note_pure_extractor_stays_ast_only() {
5273 assert_eq!(
5274 ts_purpose(WIDGET_PANEL_WITH_NOTE_TS, TsGrammar::Typescript).as_deref(),
5275 Some("function WidgetPanel | interface WidgetPanelProps")
5276 );
5277 }
5278
5279 fn scratch_root(label: &str) -> std::path::PathBuf {
5280 let nanos = std::time::SystemTime::now()
5281 .duration_since(std::time::UNIX_EPOCH)
5282 .unwrap()
5283 .as_nanos();
5284 let dir = std::env::temp_dir().join(format!(
5285 "scout-extract-hybrid-test-{label}-{}-{nanos}",
5286 std::process::id()
5287 ));
5288 fs::create_dir_all(&dir).expect("create scratch dir");
5289 dir
5290 }
5291
5292 #[test]
5293 fn extract_ts_purpose_with_heuristic_prefixes_a_leading_comment() {
5294 let root = scratch_root("comment");
5295 fs::write(root.join("widget.ts"), WIDGET_PANEL_WITH_NOTE_TS).unwrap();
5296 let purpose = extract_ts_purpose_with_heuristic(
5297 &root,
5298 "widget.ts",
5299 WIDGET_PANEL_WITH_NOTE_TS,
5300 TsGrammar::Typescript,
5301 );
5302 assert_eq!(
5303 purpose.as_deref(),
5304 Some("Renders the widget summary panel and its retry action — function WidgetPanel | interface WidgetPanelProps")
5305 );
5306 fs::remove_dir_all(&root).ok();
5307 }
5308
5309 #[test]
5310 fn extract_ts_purpose_with_heuristic_leaves_a_code_first_line_unprefixed() {
5311 let root = scratch_root("code-first");
5312 let src = "export function plain() { return 1; }\n";
5313 fs::write(root.join("plain.ts"), src).unwrap();
5314 let purpose =
5315 extract_ts_purpose_with_heuristic(&root, "plain.ts", src, TsGrammar::Typescript);
5316 assert_eq!(purpose.as_deref(), Some("function plain"));
5317 fs::remove_dir_all(&root).ok();
5318 }
5319
5320 #[test]
5321 fn extract_ts_purpose_with_heuristic_returns_none_for_comment_plus_zero_export() {
5322 let root = scratch_root("comment-zero-export");
5326 let src = "// just notes, nothing exported\nfunction internalOnly() { return 1; }\n";
5327 fs::write(root.join("notesOnly.ts"), src).unwrap();
5328 let purpose =
5329 extract_ts_purpose_with_heuristic(&root, "notesOnly.ts", src, TsGrammar::Typescript);
5330 assert!(purpose.is_none());
5331 fs::remove_dir_all(&root).ok();
5332 }
5333
5334 #[test]
5337 fn broken_syntax_ts_degrades_to_none_not_a_panic() {
5338 let purpose = ts_purpose(
5343 "export class {{{ not valid typescript at all @@@",
5344 TsGrammar::Typescript,
5345 );
5346 assert!(purpose.is_none());
5347 }
5348
5349 #[test]
5350 fn zero_export_file_yields_none_heuristic_fallback() {
5351 assert!(ts_purpose(
5355 "function internalOnly() { return 1; }\nconst alsoInternal = 2;\n",
5356 TsGrammar::Typescript
5357 )
5358 .is_none());
5359 assert!(ts_purpose("// just a comment, no code\n", TsGrammar::Typescript).is_none());
5360 }
5361
5362 const DUPLICATE_HEADER_HOST_SRC: &str = "
5386namespace Fixtures.Preproc
5387{
5388 public static class WidgetCompiler
5389 {
5390 private static readonly int[] _pool = new int[8];
5391
5392#if WIDGET_V2
5393 private static int[] ComputeOffsets(IWidgetProvider items)
5394 {
5395 var count = items.WidgetCount;
5396#else
5397 private static int[] ComputeOffsets(IReadOnlyList<Widget> items)
5398 {
5399 var count = items.Count;
5400#endif
5401 if (count == 0)
5402 return _pool;
5403 return _pool;
5404 }
5405
5406 private struct SlotInfo
5407 {
5408 public object Payload;
5409 }
5410
5411 [System.Flags]
5412 private enum SlotStatus : byte
5413 {
5414 Empty = 0,
5415 Filled = 1,
5416 }
5417 }
5418}
5419";
5420
5421 #[test]
5422 fn duplicate_header_keeps_nested_type_defs_at_their_real_scope() {
5423 let e = extract_src(DUPLICATE_HEADER_HOST_SRC);
5424
5425 assert!(find_def(&e, "Fixtures.Preproc.WidgetCompiler").is_some());
5426
5427 let slot_info = find_def(&e, "Fixtures.Preproc.WidgetCompiler+SlotInfo")
5428 .expect("SlotInfo kept, nested under its type");
5429 assert_eq!(slot_info.kind, "struct");
5430 assert_eq!(slot_info.namespace, "Fixtures.Preproc");
5431
5432 let slot_status =
5433 find_def(&e, "Fixtures.Preproc.WidgetCompiler+SlotStatus").expect("SlotStatus kept");
5434 assert_eq!(slot_status.kind, "enum");
5435 assert!(find_def(&e, "Fixtures.Preproc.WidgetCompiler+SlotStatus.Empty").is_some());
5436 assert!(find_def(&e, "Fixtures.Preproc.WidgetCompiler+SlotStatus.Filled").is_some());
5437
5438 assert!(e
5440 .refs
5441 .iter()
5442 .any(|r| r.kind == "uses-type" && r.name == "IWidgetProvider"));
5443 assert!(e
5444 .refs
5445 .iter()
5446 .any(|r| r.kind == "uses-type" && r.name == "IReadOnlyList"));
5447 assert!(e
5448 .refs
5449 .iter()
5450 .any(|r| r.kind == "uses-type" && r.name == "Widget"));
5451 }
5452
5453 #[test]
5454 fn defs_and_refs_stay_in_ascending_line_order_on_a_pathological_file() {
5455 let e = extract_src(DUPLICATE_HEADER_HOST_SRC);
5458 let def_lines: Vec<usize> = e.defs.iter().map(|d| d.line).collect();
5459 let mut sorted = def_lines.clone();
5460 sorted.sort();
5461 assert_eq!(
5462 def_lines, sorted,
5463 "defs must already be in ascending line order"
5464 );
5465
5466 let ref_lines: Vec<usize> = e.refs.iter().map(|r| r.line).collect();
5467 let mut sorted_refs = ref_lines.clone();
5468 sorted_refs.sort();
5469 assert_eq!(
5470 ref_lines, sorted_refs,
5471 "refs must already be in ascending line order"
5472 );
5473 }
5474
5475 #[test]
5476 fn well_formed_file_is_unaffected() {
5477 let e = extract_src(
5478 "namespace Fixtures.Widgets { public class Gadget { public int Count; } public struct Handle {} }",
5479 );
5480 assert!(find_def(&e, "Fixtures.Widgets.Gadget").is_some());
5481 assert!(find_def(&e, "Fixtures.Widgets.Handle").is_some());
5482 }
5483
5484 const DUPLICATE_HEADER_HOST_WITH_NESTED_EMITTER_SRC: &str = "
5492namespace Fixtures.Preproc
5493{
5494 public static class WidgetCompiler
5495 {
5496 private static readonly int[] _pool = new int[8];
5497
5498#if WIDGET_V2
5499 private static int[] ComputeOffsets(IWidgetProvider items)
5500 {
5501 var count = items.WidgetCount;
5502#else
5503 private static int[] ComputeOffsets(IReadOnlyList<Widget> items)
5504 {
5505 var count = items.Count;
5506#endif
5507 if (count == 0)
5508 return _pool;
5509 return _pool;
5510 }
5511
5512 private struct SlotInfo
5513 {
5514 public object Payload;
5515 }
5516
5517 [System.Flags]
5518 private enum SlotStatus : byte
5519 {
5520 Empty = 0,
5521 Filled = 1,
5522 }
5523
5524 private static class Emitter
5525 {
5526#if WIDGET_V2
5527 public static bool TryEmit(Widget expr, IWidgetProvider items, Sink sink)
5528 {
5529 var scaled = items.WidgetCount;
5530#else
5531 public static bool TryEmit(Widget expr, IReadOnlyList<Widget> items, Sink sink)
5532 {
5533 var scaled = items.Count;
5534#endif
5535 sink.Write(scaled);
5536 return true;
5537 }
5538 }
5539 }
5540}
5541";
5542
5543 #[test]
5544 fn stacked_duplicate_headers_drop_both_wrapping_types_like_the_reference() {
5545 let e = extract_src(DUPLICATE_HEADER_HOST_WITH_NESTED_EMITTER_SRC);
5546
5547 assert!(e.defs.iter().all(|d| d.name != "WidgetCompiler"));
5548 assert!(e.defs.iter().all(|d| d.name != "Emitter"));
5549
5550 let slot_info = find_def(&e, "SlotInfo").expect("SlotInfo survives, flat");
5551 assert_eq!(slot_info.kind, "struct");
5552 assert_eq!(slot_info.namespace, "");
5553 assert!(find_def(&e, "SlotStatus").is_some());
5554 assert!(find_def(&e, "SlotStatus.Empty").is_some());
5555 assert!(find_def(&e, "SlotStatus.Filled").is_some());
5556
5557 assert!(e
5560 .refs
5561 .iter()
5562 .any(|r| r.kind == "uses-type" && r.name == "Sink"));
5563 assert!(e.refs.iter().any(|r| r.kind == "uses-member"
5564 && r.name == "sink"
5565 && r.member.as_deref() == Some("Write")));
5566 }
5567
5568 fn member_facts(e: &Extraction) -> Vec<(&str, Option<&str>)> {
5571 e.refs
5572 .iter()
5573 .filter(|r| r.kind == "uses-member")
5574 .map(|r| {
5575 (
5576 r.member.as_deref().unwrap_or(""),
5577 r.receiver_type.as_deref(),
5578 )
5579 })
5580 .collect()
5581 }
5582
5583 #[test]
5584 fn stage2a_def_records_properties_fields_and_method_returns() {
5585 let e = extract_src(
5586 r#"
5587namespace App.Facts;
5588
5589public class Widget
5590{
5591 private readonly ILogger _log;
5592 private int a, b;
5593 public const int Max = 3;
5594 public static string Prefix { get; } = "p";
5595 public string Name => "n";
5596 public int this[int i] => i;
5597 public event System.EventHandler Changed;
5598
5599 public Task<Foo> GetAsync() => null;
5600 public void Nothing() { }
5601 public Some.Ns.Thing Qualified() => null;
5602 public int Num() => 1;
5603 private Hidden Secret() => null;
5604}
5605"#,
5606 );
5607 let d = find_def(&e, "App.Facts.Widget").expect("Widget def present");
5608 assert_eq!(
5609 d.properties,
5610 vec!["Prefix", "Name"],
5611 "source order; the indexer is not a property"
5612 );
5613 assert_eq!(
5614 d.fields,
5615 vec!["_log", "a", "b", "Max"],
5616 "every declarator of every field, incl. const; the event is not a field"
5617 );
5618 assert_eq!(
5619 d.method_returns,
5620 vec![("GetAsync".to_string(), "Task".to_string()), ("Qualified".to_string(), "Thing".to_string())],
5621 "generic args stripped to the base identifier, a qualified return reduced to its last segment, void/int omitted, FIRST-declaration order"
5622 );
5623 assert!(
5624 !d.methods.contains(&"Secret".to_string()),
5625 "methodReturns stays parallel to methods: a private method is in neither"
5626 );
5627 assert!(d.method_returns.iter().all(|(n, _)| n != "Secret"));
5628 }
5629
5630 #[test]
5631 fn stage2a_type_declaring_none_of_the_new_members_records_all_three_empty() {
5632 let e = extract_src("namespace App.Bare { public class Empty { public void Go() { } } }");
5633 let d = find_def(&e, "App.Bare.Empty").expect("Empty def present");
5634 assert_eq!(d.methods, vec!["Go"]);
5635 assert!(d.properties.is_empty());
5636 assert!(d.fields.is_empty());
5637 assert!(
5638 d.method_returns.is_empty(),
5639 "empty means OMITTED at serialization -- pre-stage-2 bytes preserved"
5640 );
5641 }
5642
5643 #[test]
5644 fn stage2a_method_returns_keeps_the_first_overload_and_never_backfills_a_void_first_declaration(
5645 ) {
5646 let e = extract_src(
5647 r#"
5648namespace App.Overloads;
5649
5650public class Api
5651{
5652 public Foo Get(int id) => null;
5653 public Bar Get(string key) => null;
5654 public void Send(int id) { }
5655 public Receipt Send(string key) => null;
5656}
5657"#,
5658 );
5659 let d = find_def(&e, "App.Overloads.Api").expect("Api def present");
5660 assert_eq!(
5661 d.method_returns,
5662 vec![("Get".to_string(), "Foo".to_string())],
5663 "Get keeps the first overload; Send is blocked by its void first declaration"
5664 );
5665 }
5666
5667 #[test]
5668 fn stage2a_interface_records_properties_and_returns_without_any_public_modifier() {
5669 let e = extract_src(
5670 r#"
5671namespace App.Contracts;
5672
5673public interface IRepo
5674{
5675 string Name { get; }
5676 Widget Find(int id);
5677}
5678"#,
5679 );
5680 let d = find_def(&e, "App.Contracts.IRepo").expect("IRepo def present");
5681 assert_eq!(d.properties, vec!["Name"]);
5682 assert_eq!(
5683 d.method_returns,
5684 vec![("Find".to_string(), "Widget".to_string())]
5685 );
5686 assert!(d.fields.is_empty());
5687 }
5688
5689 #[test]
5692 fn stage2b_facts_come_from_locals_var_new_params_fields_and_primary_ctor_params() {
5693 let e = extract_src(
5694 r#"
5695namespace App.Receivers;
5696
5697public class Host(IClock clock)
5698{
5699 private readonly ILogger _log;
5700
5701 public void Run(IRepo repo)
5702 {
5703 Helper h = new Helper();
5704 var svc = new Service();
5705 repo.Save();
5706 h.Help();
5707 svc.Go();
5708 _log.Info();
5709 clock.Now();
5710 }
5711}
5712"#,
5713 );
5714 assert_eq!(
5715 member_facts(&e),
5716 vec![
5717 ("Save", Some("IRepo")),
5718 ("Help", Some("Helper")),
5719 ("Go", Some("Service")),
5720 ("Info", Some("ILogger")),
5721 ("Now", Some("IClock")),
5722 ]
5723 );
5724 }
5725
5726 #[test]
5727 fn stage2b_receiver_type_is_set_only_on_refs_that_earned_a_fact() {
5728 let e = extract_src(
5729 r#"
5730namespace App.Shape;
5731
5732public class Host
5733{
5734 public void Run(IRepo repo) { repo.Save(); }
5735}
5736"#,
5737 );
5738 let r = e
5739 .refs
5740 .iter()
5741 .find(|r| r.kind == "uses-member")
5742 .expect("member ref present");
5743 assert_eq!(r.name, "repo");
5744 assert_eq!(r.qualified, None);
5745 assert!(!r.generic);
5746 assert_eq!(r.receiver_type.as_deref(), Some("IRepo"));
5747 }
5748
5749 #[test]
5750 fn stage2b_no_fact_for_var_from_call_predefined_types_or_an_implicit_lambda_parameter() {
5751 let e = extract_src(
5752 r#"
5753namespace App.NoFacts;
5754
5755public class Host
5756{
5757 public void Run(string text, System.Collections.Generic.List<Widget> items)
5758 {
5759 var computed = Compute();
5760 int count = 0;
5761 computed.Use();
5762 text.Trim();
5763 count.ToString();
5764 items.ForEach(x => x.Do());
5765 }
5766}
5767"#,
5768 );
5769 let facts: Vec<(String, Option<&str>)> = e
5770 .refs
5771 .iter()
5772 .filter(|r| r.kind == "uses-member")
5773 .map(|r| {
5774 (
5775 format!("{}.{}", r.name, r.member.as_deref().unwrap_or("")),
5776 r.receiver_type.as_deref(),
5777 )
5778 })
5779 .collect();
5780 assert_eq!(
5781 facts,
5782 vec![
5783 ("computed.Use".to_string(), None),
5785 ("text.Trim".to_string(), None),
5787 ("count.ToString".to_string(), None),
5788 ("items.ForEach".to_string(), Some("List")),
5791 ("x.Do".to_string(), None),
5793 ]
5794 );
5795 }
5796
5797 #[test]
5798 fn stage2b_two_declarations_of_one_local_with_different_types_cancel_to_no_fact() {
5799 let e = extract_src(
5800 r#"
5801namespace App.Conflict;
5802
5803public class Host
5804{
5805 public void Run(bool flag)
5806 {
5807 if (flag) { Widget x = new Widget(); x.Go(); }
5808 else { Gadget x = new Gadget(); x.Go(); }
5809 }
5810}
5811"#,
5812 );
5813 assert_eq!(
5814 member_facts(&e),
5815 vec![("Go", None), ("Go", None)],
5816 "the method-level flat table is conflicted for \"x\""
5817 );
5818 }
5819
5820 #[test]
5821 fn stage2b_same_local_name_declared_twice_with_the_same_type_still_yields_the_fact() {
5822 let e = extract_src(
5823 r#"
5824namespace App.NoConflict;
5825
5826public class Host
5827{
5828 public void Run(bool flag)
5829 {
5830 if (flag) { Widget x = new Widget(); x.Go(); }
5831 else { Widget x = new Widget(); x.Go(); }
5832 }
5833}
5834"#,
5835 );
5836 assert_eq!(
5837 member_facts(&e),
5838 vec![("Go", Some("Widget")), ("Go", Some("Widget"))]
5839 );
5840 }
5841
5842 #[test]
5843 fn stage2b_a_parameter_shadows_a_same_named_field_of_a_different_type() {
5844 let e = extract_src(
5845 r#"
5846namespace App.Shadow;
5847
5848public class Host
5849{
5850 private Widget handler;
5851
5852 public void Run(Gadget handler)
5853 {
5854 handler.Go();
5855 }
5856
5857 public void Untouched()
5858 {
5859 handler.Go();
5860 }
5861}
5862"#,
5863 );
5864 assert_eq!(
5865 member_facts(&e),
5866 vec![("Go", Some("Gadget")), ("Go", Some("Widget"))],
5867 "inside Run the parameter wins; the other method still sees the field"
5868 );
5869 }
5870
5871 #[test]
5872 fn stage2b_a_nested_type_never_inherits_the_enclosing_types_field_table() {
5873 let e = extract_src(
5874 r#"
5875namespace App.Nested;
5876
5877public class Outer
5878{
5879 private Widget handler;
5880
5881 public class Inner
5882 {
5883 public void Run() { handler.Go(); }
5884 }
5885}
5886"#,
5887 );
5888 assert_eq!(
5889 member_facts(&e),
5890 vec![("Go", None)],
5891 "a nested type cannot reach an outer instance field"
5892 );
5893 }
5894
5895 #[test]
5896 fn stage2b_a_flattened_chain_tail_never_inherits_the_heads_receiver_type() {
5897 let e = extract_src(
5901 r#"
5902namespace App.Consumers;
5903
5904public class Chain
5905{
5906 public void Run()
5907 {
5908 Widget w = new Widget();
5909 w.Inner.Tail();
5910 }
5911}
5912"#,
5913 );
5914 let head = e
5915 .refs
5916 .iter()
5917 .find(|r| r.member.as_deref() == Some("Inner"))
5918 .expect("head ref present");
5919 let tail = e
5920 .refs
5921 .iter()
5922 .find(|r| r.member.as_deref() == Some("Tail"))
5923 .expect("tail ref present");
5924 assert_eq!(
5925 head.receiver_type.as_deref(),
5926 Some("Widget"),
5927 "the head is the ref the local declaration vouches for"
5928 );
5929 assert_eq!(
5930 tail.qualified.as_deref(),
5931 Some("w.Inner"),
5932 "the tail's qualifier is the flattened chain window"
5933 );
5934 assert_eq!(
5935 tail.receiver_type, None,
5936 "the tail must NOT inherit the head's receiverType"
5937 );
5938 }
5939
5940 #[test]
5941 fn stage2b_a_generic_qualifier_never_carries_a_receiver_fact() {
5942 let e = extract_src(
5946 r#"
5947namespace App.Generic;
5948
5949public class Host
5950{
5951 public void Run()
5952 {
5953 Widget Cache = new Widget();
5954 Cache<int>.Go();
5955 }
5956}
5957"#,
5958 );
5959 let r = e
5960 .refs
5961 .iter()
5962 .find(|r| r.member.as_deref() == Some("Go"))
5963 .expect("member ref present");
5964 assert!(r.generic);
5965 assert_eq!(r.receiver_type, None);
5966 }
5967
5968 #[test]
5969 fn stage2b_a_local_declared_in_a_using_or_for_statement_is_a_fact() {
5970 let e = extract_src(
5971 r#"
5972namespace App.Statements;
5973
5974public class Host
5975{
5976 public void Run()
5977 {
5978 using (Stream s = Open()) { s.Read(); }
5979 for (Counter c = Start(); ; ) { c.Tick(); }
5980 }
5981}
5982"#,
5983 );
5984 assert_eq!(
5985 member_facts(&e),
5986 vec![("Read", Some("Stream")), ("Tick", Some("Counter"))]
5987 );
5988 }
5989
5990 #[test]
5991 fn stage2b_an_explicitly_typed_foreach_variable_is_a_fact() {
5992 let e = extract_src(
5998 r#"
5999namespace App.ForEach;
6000
6001public class Host
6002{
6003 public void Run(System.Collections.Generic.List<Widget> items)
6004 {
6005 foreach (Widget w in items) { w.Go(); }
6006 }
6007}
6008"#,
6009 );
6010 let go = e
6011 .refs
6012 .iter()
6013 .find(|r| r.member.as_deref() == Some("Go"))
6014 .expect("member ref present");
6015 assert_eq!(go.receiver_type.as_deref(), Some("Widget"));
6016 }
6017
6018 #[test]
6019 fn stage2b_a_local_function_folds_into_the_enclosing_members_flat_table() {
6020 let e = extract_src(
6021 r#"
6022namespace App.LocalFn;
6023
6024public class Host
6025{
6026 public void Run()
6027 {
6028 Widget w = new Widget();
6029 void Inner() { w.Go(); }
6030 }
6031}
6032"#,
6033 );
6034 let go = e
6035 .refs
6036 .iter()
6037 .find(|r| r.member.as_deref() == Some("Go"))
6038 .expect("member ref present");
6039 assert_eq!(
6040 go.receiver_type.as_deref(),
6041 Some("Widget"),
6042 "a local function sees the enclosing method's locals"
6043 );
6044 }
6045
6046 #[test]
6047 fn stage2b_an_explicitly_typed_declaration_never_reads_its_new_initializer() {
6048 let e = extract_src(
6049 r#"
6050namespace App.Explicit;
6051
6052public class Host
6053{
6054 public void Run()
6055 {
6056 object o = new Widget();
6057 o.Go();
6058 }
6059}
6060"#,
6061 );
6062 let go = e
6063 .refs
6064 .iter()
6065 .find(|r| r.member.as_deref() == Some("Go"))
6066 .expect("member ref present");
6067 assert_eq!(
6068 go.receiver_type, None,
6069 "`object` is a predefined type: no fact, and Widget is never substituted for it"
6070 );
6071 }
6072
6073 #[test]
6074 fn stage2b_each_duplicated_header_arm_keeps_its_own_receiver_facts() {
6075 let e = extract_src(DUPLICATE_HEADER_HOST_WITH_NESTED_EMITTER_SRC);
6083 let facts: Vec<(&str, Option<&str>)> = e
6084 .refs
6085 .iter()
6086 .filter(|r| r.kind == "uses-member")
6087 .map(|r| (r.name.as_str(), r.receiver_type.as_deref()))
6088 .collect();
6089 assert_eq!(
6090 facts,
6091 vec![
6092 ("items", Some("IWidgetProvider")),
6093 ("items", Some("IReadOnlyList")),
6094 ("items", None),
6095 ("items", None),
6096 ("sink", Some("Sink")),
6097 ]
6098 );
6099 }
6100
6101 #[test]
6104 fn stage3_extension_methods_recorded_in_source_order_deduped_by_name_this_type_and_arity() {
6105 let e = extract_src(
6106 r#"
6107namespace App.Ext;
6108
6109public static class WidgetExtensions
6110{
6111 public static Widget Copy(this Widget w) => w;
6112 public static void Render(this Widget w) { }
6113 public static void Render(this Widget w, int depth) { }
6114 public static void Render(this Widget w, string label) { }
6115 public static string Trim(this string s) => s;
6116 public static void Each(this System.Collections.Generic.List<Widget> items) { }
6117 public static void Plain(Widget w) { }
6118 public static void Late(Widget w, this Gadget g) { }
6119 internal static void Hidden(this Gadget g) { }
6120}
6121"#,
6122 );
6123 let d = find_def(&e, "App.Ext.WidgetExtensions").expect("WidgetExtensions def present");
6124 let pairs: Vec<(&str, &str, usize, i64)> = d
6125 .extension_methods
6126 .iter()
6127 .map(|x| {
6128 (
6129 x.name.as_str(),
6130 x.this_type.as_str(),
6131 x.arity_min,
6132 x.arity_max,
6133 )
6134 })
6135 .collect();
6136 assert_eq!(
6137 pairs,
6138 vec![
6139 ("Copy", "Widget", 0, 0),
6140 ("Render", "Widget", 0, 0),
6147 ("Render", "Widget", 1, 1),
6148 ("Trim", "string", 0, 0),
6150 ("Each", "List", 0, 0),
6153 ("Hidden", "Gadget", 0, 0),
6155 ]
6156 );
6157 assert_eq!(
6160 d.extension_methods
6161 .iter()
6162 .map(|x| x.this_args.clone())
6163 .collect::<Vec<_>>(),
6164 vec![
6165 None,
6166 None,
6167 None,
6168 None,
6169 Some(vec!["Widget".to_string()]),
6170 None
6171 ],
6172 "thisArgs is present ONLY on the generic this-parameter"
6173 );
6174 assert!(
6175 !pairs.iter().any(|(n, ..)| *n == "Plain"),
6176 "a static method with no this-parameter is not an extension method"
6177 );
6178 assert!(
6179 !pairs.iter().any(|(n, ..)| *n == "Late"),
6180 "`this` on a NON-first parameter is not an extension method"
6181 );
6182 assert!(!d.methods.contains(&"Hidden".to_string()));
6186 }
6187
6188 fn member_arg_counts(e: &Extraction) -> Vec<(String, Option<usize>)> {
6193 e.refs
6194 .iter()
6195 .filter(|r| r.kind == "uses-member")
6196 .map(|r| {
6197 let qualifier = r.qualified.as_deref().unwrap_or(r.name.as_str());
6198 (
6199 format!("{qualifier}.{}", r.member.as_deref().unwrap_or("")),
6200 r.arg_count,
6201 )
6202 })
6203 .collect()
6204 }
6205
6206 #[test]
6207 fn stage3_arg_count_is_recorded_only_for_a_call_and_always_from_the_refs_own_invocation() {
6208 let e = extract_src(
6209 r#"
6210namespace App.Consumers;
6211
6212public class Chain
6213{
6214 public void Run()
6215 {
6216 Widget w = new Widget();
6217 w.Inner.Tail(1, 2);
6218 Send(w.Payload);
6219 Send(w.Compute(7));
6220 var s = w.Slug;
6221 }
6222}
6223"#,
6224 );
6225 assert_eq!(
6226 member_arg_counts(&e),
6227 vec![
6228 ("w.Inner.Tail".to_string(), Some(2)),
6232 ("w.Inner".to_string(), None),
6234 ("w.Payload".to_string(), None),
6239 ("w.Compute".to_string(), Some(1)),
6240 ("w.Slug".to_string(), None),
6243 ]
6244 );
6245 }
6246
6247 #[test]
6248 fn stage3_a_kg1_promoted_qualifier_computes_arg_count_from_its_own_invocation_parent() {
6249 let e = extract_src(IF_DIRECTIVE_CHAIN);
6256 let promoted = e
6257 .refs
6258 .iter()
6259 .find(|r| r.name == "WriteTo" && r.member.as_deref() == Some("Debug"))
6260 .expect("promoted ref present");
6261 assert_eq!(promoted.arg_count, Some(0), "`.Debug()` takes no arguments; the wrapping `.File(...)` call's count must not leak in");
6262 assert_eq!(
6266 e.refs
6267 .iter()
6268 .filter(|r| r.kind == "uses-member")
6269 .map(|r| (
6270 r.name.as_str(),
6271 r.member.as_deref().unwrap_or(""),
6272 r.arg_count
6273 ))
6274 .collect::<Vec<_>>(),
6275 vec![
6276 ("e", "Level", None),
6277 ("Level", "Error", None),
6278 ("Interval", "Day", None),
6279 ("WriteTo", "Debug", Some(0)),
6280 ("Level", "Information", None),
6281 ("Level", "Information", None),
6282 ]
6283 );
6284 }
6285
6286 #[test]
6287 fn stage3_a_type_declaring_no_extension_methods_records_an_empty_list() {
6288 let e = extract_src("namespace App.Plain { public static class Utils { public static void Go(Widget w) { } } }");
6289 let d = find_def(&e, "App.Plain.Utils").expect("Utils def present");
6290 assert_eq!(d.methods, vec!["Go"]);
6291 assert!(
6292 d.extension_methods.is_empty(),
6293 "empty means OMITTED at serialization -- pre-stage-3 bytes preserved"
6294 );
6295 }
6296
6297 #[test]
6298 fn stage3_this_type_unwraps_nullable_and_array_the_same_way_a_receiver_fact_does() {
6299 let e = extract_src(
6300 r#"
6301namespace App.Ext;
6302
6303public static class Shapes
6304{
6305 public static void Each(this Widget[] items) { }
6306 public static void Maybe(this Widget? w) { }
6307 public static void Deep(this App.Other.Gadget g) { }
6308}
6309"#,
6310 );
6311 let d = find_def(&e, "App.Ext.Shapes").expect("Shapes def present");
6312 let pairs: Vec<(&str, &str)> = d
6313 .extension_methods
6314 .iter()
6315 .map(|x| (x.name.as_str(), x.this_type.as_str()))
6316 .collect();
6317 assert_eq!(
6318 pairs,
6319 vec![("Each", "Widget"), ("Maybe", "Widget"), ("Deep", "Gadget")]
6320 );
6321 }
6322
6323 #[test]
6324 fn stage3_a_this_parameter_carrying_a_second_modifier_is_still_an_extension_method() {
6325 let e = extract_src("namespace App.Ext { public static class R { public static void Bump(this ref Counter c) { } } }");
6328 let d = find_def(&e, "App.Ext.R").expect("R def present");
6329 let pairs: Vec<(&str, &str)> = d
6330 .extension_methods
6331 .iter()
6332 .map(|x| (x.name.as_str(), x.this_type.as_str()))
6333 .collect();
6334 assert_eq!(pairs, vec![("Bump", "Counter")]);
6335 }
6336
6337 #[test]
6338 fn stage3_base_type_identifier_keeps_predefined_only_for_the_this_parameter_caller() {
6339 let e = extract_src("namespace App.Ext { public static class S { public static string Trim(this string s) => s; } }");
6343 let d = find_def(&e, "App.Ext.S").expect("S def present");
6344 assert_eq!(
6345 d.extension_methods
6346 .iter()
6347 .map(|x| x.this_type.as_str())
6348 .collect::<Vec<_>>(),
6349 vec!["string"]
6350 );
6351 assert!(
6352 d.method_returns.is_empty(),
6353 "every stage-2 call site passes keep_predefined=false and is unchanged"
6354 );
6355 }
6356
6357 fn def_json_keys(d: &DefRecord) -> Vec<&'static str> {
6360 match def_to_json(d) {
6361 Json::Obj(fields) => fields.into_iter().map(|(k, _)| k).collect(),
6362 _ => panic!("def_to_json must produce an object"),
6363 }
6364 }
6365
6366 #[test]
6367 fn stage3_extension_methods_serialize_last_with_the_arity_range_and_this_args() {
6368 let e = extract_src(
6369 "namespace App.Ext { public static class W { public static Widget Copy(this Widget w) => w; public static string Trim(this string s, int n) => s; public static void Each(this List<Widget> l, params int[] xs) { } } }",
6370 );
6371 let d = find_def(&e, "App.Ext.W").expect("W def present");
6372 assert_eq!(
6373 def_json_keys(d),
6374 vec![
6375 "id",
6376 "name",
6377 "namespace",
6378 "kind",
6379 "line",
6380 "methods",
6381 "methodReturns",
6382 "extensionMethods"
6383 ],
6384 "extensionMethods lands AFTER the stage-2 additions, not among them"
6385 );
6386 let Json::Obj(fields) = def_to_json(d) else {
6387 panic!("object")
6388 };
6389 let (_, ext) = fields
6390 .into_iter()
6391 .find(|(k, _)| *k == "extensionMethods")
6392 .expect("extensionMethods present");
6393 let Json::Arr(entries) = ext else {
6394 panic!("extensionMethods is an array")
6395 };
6396 assert_eq!(entries.len(), 3);
6397 for entry in &entries[..2] {
6398 let Json::Obj(kv) = entry else {
6399 panic!("each entry is an object")
6400 };
6401 assert_eq!(
6402 kv.iter().map(|(k, _)| *k).collect::<Vec<_>>(),
6403 vec!["name", "thisType", "arityMin", "arityMax"],
6404 "entry key order is significant too"
6405 );
6406 }
6407 let Json::Obj(kv) = &entries[1] else {
6411 panic!("object")
6412 };
6413 match kv.iter().find(|(k, _)| *k == "arityMin").map(|(_, v)| v) {
6414 Some(Json::Num(n)) => {
6415 assert_eq!(*n, 1, "Trim(this string s, int n) requires one argument")
6416 }
6417 _ => panic!("arityMin must serialize as a JSON number"),
6418 }
6419 match kv.iter().find(|(k, _)| *k == "arityMax").map(|(_, v)| v) {
6420 Some(Json::Int(n)) => assert_eq!(*n, 1),
6421 _ => panic!("arityMax must serialize as a JSON number"),
6422 }
6423 let Json::Obj(kv) = &entries[2] else {
6426 panic!("object")
6427 };
6428 assert_eq!(
6429 kv.iter().map(|(k, _)| *k).collect::<Vec<_>>(),
6430 vec!["name", "thisType", "arityMin", "arityMax", "thisArgs"],
6431 "thisArgs lands after arityMax, and only on a generic this-parameter"
6432 );
6433 assert!(
6434 matches!(
6435 kv.iter().find(|(k, _)| *k == "arityMax").map(|(_, v)| v),
6436 Some(Json::Int(-1))
6437 ),
6438 "a params tail serializes arityMax as the number -1"
6439 );
6440 }
6441
6442 #[test]
6443 fn stage3_bases_serializes_after_extension_methods_and_is_absent_when_empty() {
6444 let e = extract_src(
6451 "namespace App.Other { public class Widget : Ns.BaseWidget<int>, IWidget { } }",
6452 );
6453 let d = find_def(&e, "App.Other.Widget").expect("Widget def present");
6454 assert_eq!(
6455 def_json_keys(d),
6456 vec![
6457 "id",
6458 "name",
6459 "namespace",
6460 "kind",
6461 "line",
6462 "methods",
6463 "bases",
6464 "baseGenericArgs"
6465 ],
6466 "bases lands after methods, baseGenericArgs immediately after bases"
6467 );
6468 assert_eq!(
6469 d.bases,
6470 vec!["BaseWidget".to_string(), "IWidget".to_string()],
6471 "base IDENTIFIERS: generic arguments stripped, a qualified name cut to its last segment"
6472 );
6473 assert_eq!(
6474 d.base_generic_args,
6475 vec![("BaseWidget".to_string(), vec!["int".to_string()])],
6476 "BaseWidget<int> is non-generic Widget's closed base -- int is a predefined type, KEPT (this base pass keeps predefined types the same way the this-parameter thisArgs facts do); IWidget carries no type-argument list at all, so it contributes no entry"
6477 );
6478
6479 let plain = extract_src("namespace App.Other { public class Bare { } }");
6480 let pd = find_def(&plain, "App.Other.Bare").expect("Bare def present");
6481 assert_eq!(
6482 def_json_keys(pd),
6483 vec!["id", "name", "namespace", "kind", "line", "methods"]
6484 );
6485 }
6486
6487 #[test]
6488 fn stage3_extension_methods_key_is_absent_when_the_type_declares_none() {
6489 let e = extract_src("namespace App.Plain { public static class Utils { public static void Go(Widget w) { } } }");
6490 let d = find_def(&e, "App.Plain.Utils").expect("Utils def present");
6491 assert_eq!(
6492 def_json_keys(d),
6493 vec!["id", "name", "namespace", "kind", "line", "methods"],
6494 "pre-stage-3 bytes preserved"
6495 );
6496 }
6497 #[test]
6500 fn test_coverage_xunit_fact_and_theory_land_in_test_methods_serialized_last_after_bases() {
6501 let e = extract_src(
6502 "namespace App.Tests;\n\npublic class WidgetTests : TestBase\n{\n [Fact]\n public void ComputesTotal() { }\n\n [Theory]\n [InlineData(1)]\n public void RejectsEmptyCart(int n) { }\n\n public void Helper() { }\n}\n",
6503 );
6504 let d = find_def(&e, "App.Tests.WidgetTests").expect("WidgetTests def present");
6505 assert_eq!(
6506 d.test_methods,
6507 vec!["ComputesTotal", "RejectsEmptyCart"],
6508 "source order; the unattributed helper is a method but not a test"
6509 );
6510 assert_eq!(
6511 def_json_keys(d),
6512 vec![
6513 "id",
6514 "name",
6515 "namespace",
6516 "kind",
6517 "line",
6518 "methods",
6519 "bases",
6520 "testMethods"
6521 ],
6522 "testMethods lands LAST -- after bases, which was the final fact before this stage"
6523 );
6524 }
6525
6526 #[test]
6527 fn test_coverage_suffixed_qualified_targeted_and_shared_bracket_attribute_forms_all_match() {
6528 let e = extract_src(
6529 "namespace App.Tests;\n\npublic class SpellingTests\n{\n [FactAttribute]\n public void Suffixed() { }\n\n [Xunit.Fact]\n public void Qualified() { }\n\n [method: Fact]\n public void Targeted() { }\n\n [Fact, Trait(\"speed\", \"fast\")]\n public void SharesABracket() { }\n\n [method: Xunit.FactAttribute]\n public void EveryFormAtOnce() { }\n}\n",
6530 );
6531 let d = find_def(&e, "App.Tests.SpellingTests").expect("SpellingTests def present");
6532 assert_eq!(
6533 d.test_methods,
6534 vec!["Suffixed", "Qualified", "Targeted", "SharesABracket", "EveryFormAtOnce"],
6535 "the last dotted segment is compared with and without the Attribute suffix, per attribute node, not per bracket pair"
6536 );
6537 }
6538
6539 #[test]
6540 fn test_coverage_nunit_attributes_match_without_a_test_fixture_on_the_class() {
6541 let e = extract_src(
6542 "namespace App.Tests;\n\npublic class OrderServiceTests\n{\n [Test]\n public void Renders() { }\n\n [TestCase(1, 2)]\n public void Adds(int a, int b) { }\n\n [TestCaseSource(nameof(Cases))]\n public void Divides(int a) { }\n}\n",
6543 );
6544 let d = find_def(&e, "App.Tests.OrderServiceTests").expect("OrderServiceTests def present");
6545 assert_eq!(
6546 d.test_methods,
6547 vec!["Renders", "Adds", "Divides"],
6548 "NUnit makes the class attribute optional, so requiring one would drop real tests"
6549 );
6550 }
6551
6552 #[test]
6553 fn test_coverage_lifecycle_data_source_and_class_container_attributes_never_mark_a_method() {
6554 let e = extract_src(
6555 "namespace App.Tests;\n\npublic class NotTests\n{\n [SetUp]\n public void Prepare() { }\n\n [TearDown]\n public void Cleanup() { }\n\n [OneTimeSetUp]\n public void Once() { }\n\n [OneTimeTearDown]\n public void Finally() { }\n\n [TestInitialize]\n public void Init() { }\n\n [TestCleanup]\n public void Done() { }\n\n [InlineData(1)]\n public void OnlyInline(int n) { }\n\n [MemberData(nameof(Cases))]\n public void OnlyMember(int n) { }\n\n [ClassData(typeof(Cases))]\n public void OnlyClassData(int n) { }\n\n [DataRow(1)]\n public void OnlyRow(int n) { }\n\n [DynamicData(nameof(Cases))]\n public void OnlyDynamic(int n) { }\n\n [TestFixture]\n public void FixtureOnAMethod() { }\n\n [TestClass]\n public void ClassMarkerOnAMethod() { }\n}\n",
6556 );
6557 let d = find_def(&e, "App.Tests.NotTests").expect("NotTests def present");
6558 assert!(d.test_methods.is_empty());
6559 assert_eq!(
6560 def_json_keys(d),
6561 vec!["id", "name", "namespace", "kind", "line", "methods"],
6562 "not one of these marks a test, so the key is absent entirely and the def keeps its pre-stage bytes"
6563 );
6564 }
6565
6566 #[test]
6567 fn test_coverage_mstest_methods_count_only_inside_a_test_class() {
6568 let source = |class_attribute: &str| {
6569 format!(
6570 "namespace App.Tests;\n\n{class_attribute}public class CartTests\n{{\n [TestMethod]\n public void Places() {{ }}\n\n [DataTestMethod]\n [DataRow(1)]\n public void Prices(int n) {{ }}\n}}\n"
6571 )
6572 };
6573 let ungated = extract_src(&source(""));
6574 assert!(
6575 find_def(&ungated, "App.Tests.CartTests").expect("CartTests def present").test_methods.is_empty(),
6576 "MSTest does not discover a [TestMethod] whose class lacks [TestClass] -- neither does this"
6577 );
6578
6579 let gated = extract_src(&source("[TestClass]\n"));
6580 assert_eq!(
6581 find_def(&gated, "App.Tests.CartTests")
6582 .expect("CartTests def present")
6583 .test_methods,
6584 vec!["Places", "Prices"]
6585 );
6586 }
6587
6588 #[test]
6589 fn test_coverage_a_nested_type_does_not_inherit_an_enclosing_test_class() {
6590 let e = extract_src(
6591 "namespace App.Tests;\n\n[TestClass]\npublic class OuterTests\n{\n [TestMethod]\n public void Outer() { }\n\n public class Inner\n {\n [TestMethod]\n public void Nested() { }\n }\n}\n",
6592 );
6593 assert_eq!(
6594 find_def(&e, "App.Tests.OuterTests")
6595 .expect("OuterTests def present")
6596 .test_methods,
6597 vec!["Outer"]
6598 );
6599 assert!(
6600 find_def(&e, "App.Tests.OuterTests+Inner")
6601 .expect("Inner def present")
6602 .test_methods
6603 .is_empty(),
6604 "each type computes its own list from its OWN attribute_list"
6605 );
6606 }
6607
6608 #[test]
6609 fn test_coverage_an_interface_or_enum_body_never_emits_test_methods() {
6610 let e = extract_src(
6611 "namespace App.Tests;\n\npublic interface ITestContract\n{\n [Fact]\n void Runs();\n}\n\npublic enum Speed\n{\n Fast,\n Slow,\n}\n",
6612 );
6613 assert!(find_def(&e, "App.Tests.ITestContract")
6614 .expect("interface def present")
6615 .test_methods
6616 .is_empty());
6617 assert!(find_def(&e, "App.Tests.Speed")
6618 .expect("enum def present")
6619 .test_methods
6620 .is_empty());
6621 assert!(find_def(&e, "App.Tests.Speed.Fast")
6622 .expect("enum-member def present")
6623 .test_methods
6624 .is_empty());
6625 }
6626
6627 #[test]
6628 fn test_coverage_a_struct_and_a_record_carry_test_methods_too_and_a_local_function_never_does()
6629 {
6630 let e = extract_src(
6631 "namespace App.Tests;\n\npublic struct ValueTests\n{\n [Fact]\n public void Holds() { }\n}\n\npublic record RecordTests\n{\n [Fact]\n public void Keeps()\n {\n [Fact]\n void Inner() { }\n }\n}\n",
6632 );
6633 assert_eq!(
6634 find_def(&e, "App.Tests.ValueTests")
6635 .expect("struct def present")
6636 .test_methods,
6637 vec!["Holds"]
6638 );
6639 assert_eq!(
6640 find_def(&e, "App.Tests.RecordTests")
6641 .expect("record def present")
6642 .test_methods,
6643 vec!["Keeps"],
6644 "a local function is not a method_declaration at type body level"
6645 );
6646 }
6647
6648 fn find_ref<'a>(e: &'a Extraction, kind: &str, name: &str) -> Option<&'a RefRecord> {
6651 e.refs.iter().find(|r| r.kind == kind && r.name == name)
6652 }
6653
6654 #[test]
6655 fn v8_a_ref_inside_a_nested_type_records_outer_types_outermost_first() {
6656 let e = extract_src(
6657 "namespace App.Core;\n\npublic class Outer\n{\n public class Inner\n {\n private Marker _m;\n }\n}\n\npublic class Marker { }\n",
6658 );
6659 let r = find_ref(&e, "uses-type", "Marker").expect("the field type ref is recorded");
6660 assert_eq!(
6663 r.outer_types,
6664 vec!["Outer".to_string(), "Inner".to_string()]
6665 );
6666 }
6667
6668 #[test]
6669 fn v8_a_namespace_level_ref_and_an_imports_ref_carry_no_outer_types() {
6670 let e = extract_src(
6671 "using App.Other;\n\nnamespace App.Core;\n\npublic class Widget : Marker { }\n",
6672 );
6673 assert!(find_ref(&e, "imports", "App.Other")
6674 .expect("using ref")
6675 .outer_types
6676 .is_empty());
6677 assert!(find_ref(&e, "inherits", "Marker")
6678 .expect("base ref")
6679 .outer_types
6680 .is_empty());
6681 }
6682
6683 #[test]
6684 fn v8_a_member_ref_carries_outer_types_alongside_every_other_receiver_fact() {
6685 let e = extract_src(
6686 "namespace App.Core;\n\npublic class Outer\n{\n public class Inner\n {\n private Store<int> _s;\n\n public void Go()\n {\n _s.Add(1);\n Box<int>.Make();\n }\n }\n}\n",
6687 );
6688 let with_receiver = e
6689 .refs
6690 .iter()
6691 .find(|r| r.member.as_deref() == Some("Add"))
6692 .expect("receiver-fact ref");
6693 assert_eq!(with_receiver.receiver_type.as_deref(), Some("Store"));
6694 assert_eq!(with_receiver.receiver_args, Some(vec!["int".to_string()]));
6695 assert_eq!(
6696 with_receiver.outer_types,
6697 vec!["Outer".to_string(), "Inner".to_string()]
6698 );
6699 let generic = e
6702 .refs
6703 .iter()
6704 .find(|r| r.member.as_deref() == Some("Make"))
6705 .expect("generic-qualifier ref");
6706 assert!(generic.generic);
6707 assert_eq!(
6708 generic.outer_types,
6709 vec!["Outer".to_string(), "Inner".to_string()]
6710 );
6711 }
6712
6713 #[test]
6714 fn v8_a_base_list_ref_carries_the_declaring_types_outer_stack_with_self_excluded() {
6715 let e = extract_src(
6716 "namespace App.Core;\n\npublic class Outer\n{\n public class Inner : Marker\n {\n }\n}\n",
6717 );
6718 assert_eq!(
6720 find_ref(&e, "inherits", "Marker")
6721 .expect("base ref")
6722 .outer_types,
6723 vec!["Outer".to_string()]
6724 );
6725 }
6726
6727 #[test]
6728 fn v8_a_dotted_chain_tail_ref_carries_the_sites_own_outer_types() {
6729 let e = extract_src(
6730 "namespace App.Core;\n\npublic class Outer\n{\n public class Inner\n {\n public void Go() { Alpha.Beta.Gamma(); }\n }\n}\n",
6731 );
6732 let tail = e
6733 .refs
6734 .iter()
6735 .find(|r| r.qualified.as_deref() == Some("Alpha.Beta"))
6736 .expect("chain-tail ref");
6737 assert_eq!(
6739 tail.outer_types,
6740 vec!["Outer".to_string(), "Inner".to_string()]
6741 );
6742 }
6743
6744 fn names_of(e: &Extraction) -> Vec<(&str, &str, usize, &str)> {
6747 e.names
6748 .iter()
6749 .map(|n| (n.name.as_str(), n.kind.as_str(), n.line, n.owner.as_str()))
6750 .collect()
6751 }
6752
6753 #[test]
6754 fn names_record_every_member_kind_with_no_accessibility_filter() {
6755 let e = extract_src(
6756 "namespace Ns;\npublic class Ledger\n{\n\tprivate int _a, _b;\n\tpublic string Label { get; set; }\n\tpublic event EventHandler Retired;\n\tpublic event EventHandler Sealed { add { } remove { } }\n\tprivate void Hidden() { }\n}\n",
6757 );
6758 assert_eq!(
6759 names_of(&e),
6760 vec![
6761 ("_a", "field", 4, "Ns.Ledger"),
6762 ("_b", "field", 4, "Ns.Ledger"),
6763 ("Label", "property", 5, "Ns.Ledger"),
6764 ("Retired", "event", 6, "Ns.Ledger"),
6765 ("Sealed", "event", 7, "Ns.Ledger"),
6766 ("Hidden", "method", 8, "Ns.Ledger"),
6767 ]
6768 );
6769 }
6770
6771 #[test]
6772 fn a_names_line_is_the_name_token_row_not_the_attribute_the_declaration_starts_at() {
6773 let e = extract_src(
6774 "namespace Ns;\npublic class Ledger\n{\n\t[Obsolete]\n\tpublic int Total { get; }\n}\n",
6775 );
6776 assert_eq!(
6777 names_of(&e),
6778 vec![("Total", "property", 5, "Ns.Ledger")],
6779 "line 5 is the name, line 4 is the attribute"
6780 );
6781 }
6782
6783 #[test]
6784 fn overloads_are_two_names_and_a_nested_type_owns_its_own_members() {
6785 let e = extract_src(
6786 "namespace Ns;\npublic class Ledger\n{\n\tpublic void Add() { }\n\tpublic void Add(int n) { }\n\tpublic class Tally\n\t{\n\t\tpublic void Add(long n) { }\n\t}\n}\n",
6787 );
6788 assert_eq!(
6789 names_of(&e),
6790 vec![
6791 ("Add", "method", 4, "Ns.Ledger"),
6792 ("Add", "method", 5, "Ns.Ledger"),
6793 ("Add", "method", 8, "Ns.Ledger+Tally"),
6794 ],
6795 "two overloads are two declarations at two lines; the nested type's member is owned by the nested id"
6796 );
6797 }
6798
6799 #[test]
6800 fn an_interface_body_and_an_enum_body_contribute_what_they_declare_and_nothing_else() {
6801 let e = extract_src("namespace Ns;\npublic interface ISink\n{\n\tvoid Accept();\n}\npublic enum Mode\n{\n\tOn,\n}\n");
6802 assert_eq!(
6803 names_of(&e),
6804 vec![("Accept", "method", 4, "Ns.ISink")],
6805 "an enum's members are defs already, never names rows"
6806 );
6807 }
6808
6809 fn ts_fragment(src: &str, grammar: crate::parse::TsGrammar) -> TsFragment {
6816 let units = crate::parse::utf16_units(src);
6817 let tree = crate::parse::parse_ts_js(&units, grammar).expect("fixture parses");
6818 extract_ts_fragment(tree.root_node(), &crate::parse::utf16_bytes(&units))
6819 }
6820
6821 fn ref_tuples(f: &TsFragment) -> Vec<(&str, &str, Option<&str>, usize)> {
6822 f.refs
6823 .iter()
6824 .map(|r| {
6825 (
6826 r.kind.as_str(),
6827 r.name.as_str(),
6828 r.member.as_deref(),
6829 r.line,
6830 )
6831 })
6832 .collect()
6833 }
6834
6835 #[test]
6836 fn an_import_clause_records_default_named_aliased_and_namespace_bindings() {
6837 let f = ts_fragment(
6838 "import Thing, { one, two as alias } from './m';\nimport * as ns from './n';\n",
6839 crate::parse::TsGrammar::Typescript,
6840 );
6841 assert_eq!(f.imports.len(), 2);
6842 assert_eq!(f.imports[0].spec, "./m");
6843 assert_eq!(f.imports[0].line, 1);
6844 let first: Vec<(&str, &str)> = f.imports[0]
6845 .bindings
6846 .iter()
6847 .map(|b| (b.local.as_str(), b.imported.as_str()))
6848 .collect();
6849 assert_eq!(
6850 first,
6851 vec![("Thing", "default"), ("one", "one"), ("alias", "two")]
6852 );
6853 let second: Vec<(&str, &str)> = f.imports[1]
6854 .bindings
6855 .iter()
6856 .map(|b| (b.local.as_str(), b.imported.as_str()))
6857 .collect();
6858 assert_eq!(
6859 second,
6860 vec![("ns", "*")],
6861 "a namespace clause binds '*', never a name of its own"
6862 );
6863 }
6864
6865 #[test]
6866 fn a_star_reexport_a_named_one_and_a_namespace_one_are_three_different_rows() {
6867 let f = ts_fragment(
6868 "export * from './a';\nexport { x as y } from './b';\nexport * as NS from './c';\n",
6869 crate::parse::TsGrammar::Typescript,
6870 );
6871 assert_eq!(f.reexports.len(), 3);
6872 assert!(f.reexports[0].star && f.reexports[0].names.is_empty());
6873 assert!(!f.reexports[1].star);
6874 assert_eq!(f.reexports[1].names[0].exported, "y");
6875 assert_eq!(f.reexports[1].names[0].imported, "x");
6876 assert!(
6877 !f.reexports[2].star && f.reexports[2].names.is_empty(),
6878 "`export * as NS` contributes its import edge and no name mapping"
6879 );
6880 }
6881
6882 #[test]
6883 fn only_names_this_file_declares_earn_a_def_and_the_default_name_is_recorded_separately() {
6884 let f = ts_fragment(
6885 "export function run() {}\nconst helper = 1;\nexport { helper };\nexport { missing } from './elsewhere';\nexport default run;\n",
6886 crate::parse::TsGrammar::Typescript,
6887 );
6888 let defs: Vec<(&str, &str, usize)> = f
6889 .defs
6890 .iter()
6891 .map(|d| (d.name.as_str(), d.kind.as_str(), d.line))
6892 .collect();
6893 assert_eq!(defs, vec![("run", "function", 1), ("helper", "const", 2)]);
6894 assert_eq!(
6895 f.default.as_deref(),
6896 Some("run"),
6897 "one def, two importable names -- never a second row"
6898 );
6899 }
6900
6901 #[test]
6902 fn a_commonjs_file_exports_by_name_and_binds_require_the_same_way_an_import_clause_does() {
6903 let f = ts_fragment(
6904 "const { logInfo } = require('./logger');\nconst bag = require('./logger');\nfunction report() { return logInfo(1) + bag.logInfo(2); }\nmodule.exports = { report };\n",
6905 crate::parse::TsGrammar::Javascript,
6906 );
6907 assert_eq!(
6908 f.defs.iter().map(|d| d.name.as_str()).collect::<Vec<_>>(),
6909 vec!["report"]
6910 );
6911 let bindings: Vec<(&str, &str)> = f
6912 .imports
6913 .iter()
6914 .flat_map(|i| i.bindings.iter())
6915 .map(|b| (b.local.as_str(), b.imported.as_str()))
6916 .collect();
6917 assert_eq!(bindings, vec![("logInfo", "logInfo"), ("bag", "*")]);
6918 assert_eq!(
6919 ref_tuples(&f),
6920 vec![
6921 ("call", "logInfo", None, 3),
6922 ("call", "bag", Some("logInfo"), 3)
6923 ],
6924 "a member call records the QUALIFIER and its property, never the chain's tail alone"
6925 );
6926 }
6927
6928 #[test]
6929 fn a_dispatching_call_consumes_its_argument_so_it_is_never_also_a_plain_call() {
6930 let f = ts_fragment(
6931 "import { load, reset } from './actions';\nexport function go(dispatch) {\n dispatch(load());\n dispatch(reset);\n}\n",
6932 crate::parse::TsGrammar::Typescript,
6933 );
6934 assert_eq!(
6935 ref_tuples(&f),
6936 vec![
6937 ("dispatch", "load", None, 3),
6938 ("dispatch", "reset", None, 4)
6939 ],
6940 "the argument is the action creator; the outer dispatch(...) is not itself a call ref"
6941 );
6942 }
6943
6944 #[test]
6945 fn a_jsx_tag_is_a_reference_only_when_it_names_a_component_and_that_name_is_known() {
6946 let f = ts_fragment(
6947 "import { Card, Panel } from './ui';\nexport function View() {\n return <div><Card /><Panel.Header /><section /><Unknown /></div>;\n}\n",
6948 crate::parse::TsGrammar::Tsx,
6949 );
6950 assert_eq!(
6951 ref_tuples(&f),
6952 vec![
6953 ("jsx-use", "Card", None, 3),
6954 ("jsx-use", "Panel", Some("Header"), 3)
6955 ],
6956 "lowercase tags are intrinsic elements; an unbound capitalised one is not a known name"
6957 );
6958 }
6959
6960 #[test]
6961 fn a_reference_to_a_name_this_file_neither_imports_nor_exports_is_never_recorded() {
6962 let f = ts_fragment(
6963 "export function go() {\n const local = () => 1;\n return local() + globalThing();\n}\n",
6964 crate::parse::TsGrammar::Typescript,
6965 );
6966 assert!(
6967 ref_tuples(&f).is_empty(),
6968 "the known-name filter is the contract, not an optimisation"
6969 );
6970 }
6971
6972 #[test]
6973 fn a_new_expression_records_the_constructor_as_a_call() {
6974 let f = ts_fragment(
6975 "import { Service } from './service';\nexport const make = () => new Service();\n",
6976 crate::parse::TsGrammar::Typescript,
6977 );
6978 assert_eq!(ref_tuples(&f), vec![("call", "Service", None, 2)]);
6979 }
6980
6981 #[test]
6982 fn a_file_with_no_default_export_keeps_the_shorter_fragment_shape() {
6983 let f = ts_fragment("export const x = 1;\n", crate::parse::TsGrammar::Typescript);
6984 assert_eq!(f.default, None);
6985 let json = serde_json::to_string(&f).unwrap();
6986 assert_eq!(
6987 json,
6988 r#"{"ts":1,"defs":[{"name":"x","kind":"const","line":1,"endLine":1}],"imports":[],"reexports":[],"refs":[]}"#
6989 );
6990 }
6991
6992 #[test]
6993 fn the_fragment_serializes_ts_first_and_default_last() {
6994 let f = ts_fragment(
6995 "export function run() {}\nexport default run;\n",
6996 crate::parse::TsGrammar::Typescript,
6997 );
6998 let json = serde_json::to_string(&f).unwrap();
6999 assert_eq!(
7000 json,
7001 r#"{"ts":1,"defs":[{"name":"run","kind":"function","line":1,"endLine":1}],"imports":[],"reexports":[],"refs":[],"default":"run"}"#
7002 );
7003 }
7004
7005 #[test]
7006 fn typescript_fragment_records_multiline_declaration_end_line() {
7007 let f = ts_fragment(
7008 "export interface Widget {\n id: number;\n}\n",
7009 crate::parse::TsGrammar::Typescript,
7010 );
7011 assert_eq!((f.defs[0].line, f.defs[0].end_line), (1, 3));
7012 }
7013
7014 #[test]
7017 fn ds0012_def_records_property_types_in_source_order_with_generic_args() {
7018 let e = extract_src(
7019 r#"
7020namespace App.PropTypes;
7021
7022public class Widget
7023{
7024 public Settings Config { get; set; }
7025 public string Label { get; set; }
7026 public Box<Gadget> Slots { get; set; }
7027 public Settings Config { get; set; }
7028}
7029"#,
7030 );
7031 let d = find_def(&e, "App.PropTypes.Widget").expect("Widget def present");
7032 assert_eq!(d.properties, vec!["Config", "Label", "Slots"]);
7033 let recorded: Vec<_> = d
7034 .property_types
7035 .iter()
7036 .map(|(n, f)| (n.as_str(), f.type_name.as_str(), f.args.as_ref()))
7037 .collect();
7038 assert_eq!(
7042 recorded,
7043 vec![
7044 ("Config", "Settings", None),
7045 ("Slots", "Box", Some(&vec!["Gadget".to_string()]))
7046 ]
7047 );
7048 }
7049
7050 #[test]
7051 fn ds0012_a_property_vouches_for_its_own_name_like_a_field() {
7052 let e = extract_src(
7053 r#"
7054namespace App.PropFacts;
7055
7056public class Host
7057{
7058 private Widget _field;
7059 public Widget Current { get; set; }
7060 public string Name { get; set; }
7061
7062 public void Run()
7063 {
7064 _field.Render();
7065 Current.Render();
7066 Name.Trim();
7067 }
7068}
7069"#,
7070 );
7071 assert_eq!(
7074 member_facts(&e),
7075 vec![
7076 ("Render", Some("Widget")),
7077 ("Render", Some("Widget")),
7078 ("Trim", None)
7079 ]
7080 );
7081 }
7082
7083 #[test]
7084 fn ds0012_receiver_property_owner_is_recorded_only_for_a_typed_two_segment_chain() {
7085 let e = extract_src(
7086 r#"
7087namespace App.ChainHeads;
7088
7089public class Host
7090{
7091 private Widget _widget;
7092 private string _text;
7093
7094 public void Run(Widget param)
7095 {
7096 _widget.Config.Reload();
7097 param.Config.Reload();
7098 _text.Config.Reload();
7099 unknown.Config.Reload();
7100 _widget.Config.Inner.Reload();
7101 App.Other.Thing.Reload();
7102 }
7103}
7104"#,
7105 );
7106 let owners: Vec<_> = e
7107 .refs
7108 .iter()
7109 .filter(|r| r.kind == "uses-member")
7110 .map(|r| {
7111 (
7112 r.qualified.as_deref().unwrap_or(r.name.as_str()),
7113 r.receiver_property_owner.as_deref(),
7114 )
7115 })
7116 .collect();
7117 assert_eq!(
7118 owners,
7119 vec![
7120 ("_widget.Config", Some("Widget")),
7121 ("_widget", None),
7124 ("param.Config", Some("Widget")),
7125 ("param", None),
7126 ("_text.Config", None),
7128 ("_text", None),
7129 ("unknown.Config", None),
7131 ("unknown", None),
7132 ("_widget.Config.Inner", None),
7135 ("_widget.Config", Some("Widget")),
7136 ("_widget", None),
7137 ("App.Other.Thing", None),
7139 ("App.Other", None),
7140 ("App", None),
7141 ]
7142 );
7143 }
7144
7145 #[test]
7146 fn ds0010_var_from_a_qualified_invocation_records_the_callee_owner_and_member() {
7147 let e = extract_src(
7148 r#"
7149namespace App.CallFacts;
7150
7151public class Host
7152{
7153 private Factory _factory;
7154
7155 public void Run()
7156 {
7157 var made = _factory.Make();
7158 made.Render();
7159 var stat = Factory.Create();
7160 stat.Render();
7161 var bare = Compute();
7162 bare.Render();
7163 var chained = made.Wrap();
7164 chained.Render();
7165 }
7166}
7167"#,
7168 );
7169 let facts: Vec<_> = e
7170 .refs
7171 .iter()
7172 .filter(|r| r.kind == "uses-member" && r.member.as_deref() == Some("Render"))
7173 .map(|r| {
7174 (
7175 r.name.as_str(),
7176 r.receiver_type.as_deref(),
7177 r.receiver_call_owner.as_deref(),
7178 r.receiver_call_member.as_deref(),
7179 )
7180 })
7181 .collect();
7182 assert_eq!(
7183 facts,
7184 vec![
7185 ("made", None, Some("Factory"), Some("Make")),
7188 ("stat", None, Some("Factory"), Some("Create")),
7191 ("bare", None, None, None),
7193 ("chained", None, None, None),
7196 ]
7197 );
7198 }
7199
7200 #[test]
7201 fn ds0010_a_conflicting_second_declaration_cancels_the_call_fact() {
7202 let e = extract_src(
7203 r#"
7204namespace App.CallConflict;
7205
7206public class Host
7207{
7208 private Widget made;
7209
7210 public void Run(bool flag)
7211 {
7212 if (flag) { var made = Factory.Make(); made.Render(); }
7213 else { Gadget made = new Gadget(); made.Render(); }
7214 }
7215}
7216"#,
7217 );
7218 let facts: Vec<_> = e
7219 .refs
7220 .iter()
7221 .filter(|r| r.kind == "uses-member" && r.member.as_deref() == Some("Render"))
7222 .map(|r| (r.receiver_type.as_deref(), r.receiver_call_owner.as_deref()))
7223 .collect();
7224 assert_eq!(facts, vec![(None, None), (None, None)]);
7228 }
7229
7230 #[test]
7233 fn ds0011_var_over_a_generic_collection_resolves_the_single_type_argument() {
7234 let e = extract_src(
7235 r#"
7236namespace App.ForEachVar;
7237
7238public class Host
7239{
7240 private List<Widget> _field;
7241
7242 public void Run(IEnumerable<Widget> param)
7243 {
7244 List<Widget> local = null;
7245 foreach (var a in _field) { a.Go(); }
7246 foreach (var b in param) { b.Go(); }
7247 foreach (var c in local) { c.Go(); }
7248 }
7249}
7250"#,
7251 );
7252 assert_eq!(
7256 member_facts(&e),
7257 vec![
7258 ("Go", Some("Widget")),
7259 ("Go", Some("Widget")),
7260 ("Go", Some("Widget"))
7261 ]
7262 );
7263 }
7264
7265 #[test]
7266 fn ds0011_a_var_foreach_stays_unknown_unless_the_collection_is_a_bare_identifier_with_one_type_argument(
7267 ) {
7268 let e = extract_src(
7269 r#"
7270namespace App.ForEachUnknown;
7271
7272public class Host
7273{
7274 private Widget[] _array;
7275 private Dictionary<string, Widget> _pair;
7276 private Map map;
7277
7278 public void Run<T>(List<T> generic)
7279 {
7280 foreach (var a in GetItems()) { a.Go(); }
7281 foreach (var b in map.Items) { b.Go(); }
7282 foreach (var c in _array) { c.Go(); }
7283 foreach (var d in _pair) { d.Go(); }
7284 foreach (var f in generic) { f.Go(); }
7285 }
7286
7287 private List<Widget> GetItems() { return null; }
7288}
7289"#,
7290 );
7291 let go_facts: Vec<_> = e
7301 .refs
7302 .iter()
7303 .filter(|r| r.member.as_deref() == Some("Go"))
7304 .map(|r| r.receiver_type.as_deref())
7305 .collect();
7306 assert_eq!(go_facts, vec![None, None, None, None, None]);
7307 }
7308
7309 #[test]
7310 fn ds0011_a_foreach_variable_used_as_another_foreachs_collection_is_never_a_fact() {
7311 let e = extract_src(
7312 r#"
7313namespace App.ForEachChain;
7314
7315public class Host
7316{
7317 private List<Widget> _bag;
7318
7319 public void Nested()
7320 {
7321 foreach (var outer in _bag)
7322 {
7323 outer.Go();
7324 foreach (var inner in outer) { inner.Go(); }
7325 }
7326 }
7327}
7328"#,
7329 );
7330 assert_eq!(member_facts(&e), vec![("Go", Some("Widget")), ("Go", None)]);
7337 }
7338
7339 #[test]
7340 fn ds0011_a_foreach_variable_conflicting_with_another_declaration_stays_taken_but_unknown() {
7341 let e = extract_src(
7342 r#"
7343namespace App.ForEachConflict;
7344
7345public class Host
7346{
7347 private List<Widget> made;
7348
7349 public void Run(bool flag)
7350 {
7351 if (flag) { foreach (var made in Gadgets()) { made.Go(); } }
7352 else { Widget made = new Widget(); made.Go(); }
7353 }
7354
7355 private List<Gadget> Gadgets() { return null; }
7356}
7357"#,
7358 );
7359 assert_eq!(member_facts(&e), vec![("Go", None), ("Go", None)]);
7366 }
7367
7368 #[test]
7369 fn ds0011_a_destructuring_foreach_variable_is_left_alone() {
7370 let e = extract_src(
7374 r#"
7375namespace App.ForEachTuple;
7376
7377public class Host
7378{
7379 public void Run(List<(int, int)> pairs)
7380 {
7381 Widget w = new Widget();
7382 foreach (var (a, b) in pairs) { }
7383 w.Go();
7384 }
7385}
7386"#,
7387 );
7388 assert_eq!(member_facts(&e), vec![("Go", Some("Widget"))]);
7389 }
7390}