1use crate::facts::{
11 Declaration, DeclarationKind, Facts, Import, ImportBinding, Reference, ReferenceKind, Span,
12};
13use crate::syntax::Language;
14use crate::token::{Mode, Token, TokenKind, Tokenizer};
15
16#[must_use]
18pub fn extract(source: &str, language: Language) -> Facts {
19 let tokens = Tokenizer::new(source, language)
20 .mode(Mode::Lite)
21 .collect::<Vec<_>>();
22 let mut state = Extractor {
23 source,
24 tokens: &tokens,
25 language,
26 rules: Rules::of(language),
27 facts: Facts::default(),
28 scopes: Vec::new(),
29 depth: 0,
30 };
31 state.run();
32 state.facts
33}
34
35struct Rules {
37 declarations: &'static [(&'static str, DeclarationKind)],
39 imports: &'static [&'static str],
41 modifiers: &'static [&'static str],
43 braced_members: bool,
45 grouped_declarations: bool,
48 typed_functions: bool,
51 exported_keyword: Option<&'static str>,
53 scope_keywords: &'static [&'static str],
57}
58
59impl Rules {
60 #[allow(clippy::too_many_lines)]
64 const fn of(language: Language) -> Self {
65 match language {
66 Language::Rust => Self {
67 declarations: &[
68 ("fn", DeclarationKind::Function),
69 ("struct", DeclarationKind::Struct),
70 ("enum", DeclarationKind::Enum),
71 ("trait", DeclarationKind::Trait),
72 ("type", DeclarationKind::TypeAlias),
73 ("const", DeclarationKind::Constant),
74 ("static", DeclarationKind::Constant),
75 ("mod", DeclarationKind::Module),
76 ],
77 imports: &["use", "mod"],
78 modifiers: &["pub", "async", "unsafe", "extern", "default"],
79 braced_members: false,
80 grouped_declarations: false,
81 typed_functions: false,
82 exported_keyword: Some("pub"),
83 scope_keywords: &["impl"],
84 },
85 Language::Swift => Self {
86 declarations: &[
87 ("func", DeclarationKind::Function),
88 ("class", DeclarationKind::Class),
89 ("struct", DeclarationKind::Struct),
90 ("actor", DeclarationKind::Class),
91 ("enum", DeclarationKind::Enum),
92 ("protocol", DeclarationKind::Interface),
93 ("typealias", DeclarationKind::TypeAlias),
94 ("associatedtype", DeclarationKind::TypeAlias),
95 ("let", DeclarationKind::Constant),
96 ("var", DeclarationKind::Variable),
97 ("init", DeclarationKind::Method),
98 ("subscript", DeclarationKind::Method),
99 ],
100 imports: &["import"],
101 modifiers: &[
102 "public",
103 "private",
104 "internal",
105 "fileprivate",
106 "open",
107 "static",
108 "final",
109 "override",
110 "mutating",
111 "nonmutating",
112 "lazy",
113 "weak",
114 "unowned",
115 "required",
116 "convenience",
117 "indirect",
118 "dynamic",
119 "optional",
120 "async",
121 "throws",
122 ],
123 braced_members: false,
124 grouped_declarations: false,
125 typed_functions: false,
126 exported_keyword: Some("public"),
129 scope_keywords: &["extension"],
130 },
131 Language::Go => Self {
132 declarations: &[
133 ("func", DeclarationKind::Function),
134 ("type", DeclarationKind::Struct),
135 ("const", DeclarationKind::Constant),
136 ("var", DeclarationKind::Variable),
137 ],
138 imports: &["import"],
139 modifiers: &[],
140 braced_members: false,
141 grouped_declarations: true,
142 typed_functions: false,
143 exported_keyword: None,
144 scope_keywords: &[],
145 },
146 Language::Java | Language::CSharp => Self {
147 declarations: &[
148 ("class", DeclarationKind::Class),
149 ("interface", DeclarationKind::Interface),
150 ("enum", DeclarationKind::Enum),
151 ("record", DeclarationKind::Struct),
152 ("struct", DeclarationKind::Struct),
153 ],
154 imports: &["import", "using"],
155 modifiers: &[
156 "public",
157 "private",
158 "protected",
159 "static",
160 "final",
161 "abstract",
162 "sealed",
163 "internal",
164 "override",
165 "async",
166 "virtual",
167 "readonly",
168 ],
169 braced_members: true,
170 grouped_declarations: false,
171 typed_functions: false,
172 exported_keyword: Some("public"),
173 scope_keywords: &[],
174 },
175 Language::Solidity => Self {
176 declarations: &[
177 ("contract", DeclarationKind::Class),
178 ("library", DeclarationKind::Class),
179 ("interface", DeclarationKind::Interface),
180 ("struct", DeclarationKind::Struct),
181 ("enum", DeclarationKind::Enum),
182 ("function", DeclarationKind::Function),
183 ("constructor", DeclarationKind::Method),
184 ("modifier", DeclarationKind::Function),
185 ("event", DeclarationKind::Field),
186 ("error", DeclarationKind::Struct),
187 ],
188 imports: &["import"],
189 modifiers: &[
190 "abstract", "virtual", "override", "public", "private", "internal", "external",
191 "pure", "view", "payable",
192 ],
193 braced_members: false,
194 grouped_declarations: false,
195 typed_functions: false,
196 exported_keyword: Some("public"),
199 scope_keywords: &[],
200 },
201 _ => Self {
202 declarations: &[
203 ("struct", DeclarationKind::Struct),
204 ("class", DeclarationKind::Class),
205 ("enum", DeclarationKind::Enum),
206 ("namespace", DeclarationKind::Module),
207 ],
208 imports: &["#include", "include"],
209 modifiers: &["static", "inline", "extern", "const", "virtual"],
210 braced_members: true,
211 grouped_declarations: false,
212 typed_functions: true,
213 exported_keyword: None,
214 scope_keywords: &[],
215 },
216 }
217 }
218}
219
220struct Scope {
221 name: String,
222 depth: Option<i32>,
223 type_body: bool,
224 test_only: bool,
225}
226
227struct Extractor<'source, 'tokens> {
228 source: &'source str,
229 tokens: &'tokens [Token],
230 language: Language,
231 rules: Rules,
232 facts: Facts,
233 scopes: Vec<Scope>,
234 depth: i32,
235}
236
237impl Extractor<'_, '_> {
238 fn run(&mut self) {
239 let mut index = 0;
240 while index < self.tokens.len() {
241 self.close_scopes();
242 index = self.step(index);
243 }
244 }
245
246 fn text(&self, index: usize) -> &str {
247 self.tokens
248 .get(index)
249 .map_or("", |token| token.text(self.source))
250 }
251
252 fn kind(&self, index: usize) -> Option<TokenKind> {
253 self.tokens.get(index).map(|token| token.kind)
254 }
255
256 fn punct(&self, index: usize, mark: &str) -> bool {
257 self.kind(index) == Some(TokenKind::Punctuation) && self.text(index) == mark
258 }
259
260 fn span(&self, start: usize, end: usize) -> Span {
261 let last_index = self.tokens.len().saturating_sub(1);
262 let first = &self.tokens[start.min(last_index)];
263 let last = &self.tokens[end.min(last_index)];
264 Span {
265 start: first.start,
266 end: last.end,
267 line: first.line,
268 column: first.column,
269 end_line: last.line,
270 end_column: last.column,
271 }
272 }
273
274 fn owner(&self) -> Option<String> {
275 self.scopes.last().map(|scope| scope.name.clone())
276 }
277
278 fn test_only_at(&self, index: usize) -> bool {
279 if self.language != Language::Rust {
280 return false;
281 }
282 self.scopes.last().is_some_and(|scope| scope.test_only)
283 || self.rust_test_attribute_before(index)
284 }
285
286 fn record_test_only_declaration(&mut self, test_only: bool, span: Span) {
287 if test_only {
288 self.facts.test_only_declarations.push(span);
289 }
290 }
291
292 fn rust_test_attribute_before(&self, index: usize) -> bool {
297 let mut cursor = index;
298 while cursor > 0 && self.punct(cursor - 1, "]") {
299 let end = cursor - 1;
300 let mut start = end;
301 let mut depth = 1_i32;
302 while start > 0 {
303 start -= 1;
304 if self.punct(start, "]") {
305 depth += 1;
306 } else if self.punct(start, "[") {
307 depth -= 1;
308 if depth == 0 {
309 break;
310 }
311 }
312 }
313 if depth != 0 || start == 0 || !self.punct(start - 1, "#") {
314 break;
315 }
316 if self.rust_attribute_is_test(start + 1, end) {
317 return true;
318 }
319 cursor = start - 1;
320 }
321 false
322 }
323
324 fn rust_attribute_is_test(&self, start: usize, end: usize) -> bool {
325 let Some(first) =
326 (start..end).find(|&index| self.kind(index) == Some(TokenKind::Identifier))
327 else {
328 return false;
329 };
330 if self.text(first) == "cfg" {
331 let Some(open) = (first + 1..end).find(|&index| self.punct(index, "(")) else {
332 return false;
333 };
334 return self.cfg_has_positive_test(open + 1, end, false);
335 }
336 let path_end = (first..end)
337 .find(|&index| self.punct(index, "("))
338 .unwrap_or(end);
339 (first..path_end)
340 .rfind(|&index| self.kind(index) == Some(TokenKind::Identifier))
341 .is_some_and(|index| {
342 matches!(
343 self.text(index),
344 "test" | "rstest" | "proptest" | "wasm_bindgen_test" | "test_case"
345 )
346 })
347 }
348
349 fn cfg_has_positive_test(&self, start: usize, end: usize, negated: bool) -> bool {
350 let mut cursor = start;
351 while cursor < end {
352 if self.kind(cursor) == Some(TokenKind::Identifier) {
353 if self.text(cursor) == "not" && self.punct(cursor + 1, "(") {
354 let close = self.matching_paren(cursor + 1, end);
355 if self.cfg_has_positive_test(cursor + 2, close, !negated) {
356 return true;
357 }
358 cursor = close.saturating_add(1);
359 continue;
360 }
361 if self.text(cursor) == "test" && !negated {
362 return true;
363 }
364 }
365 cursor += 1;
366 }
367 false
368 }
369
370 fn matching_paren(&self, open: usize, end: usize) -> usize {
371 let mut depth = 0_i32;
372 for cursor in open..end {
373 if self.punct(cursor, "(") {
374 depth += 1;
375 } else if self.punct(cursor, ")") {
376 depth -= 1;
377 if depth == 0 {
378 return cursor;
379 }
380 }
381 }
382 end
383 }
384
385 fn close_scopes(&mut self) {
386 while self
387 .scopes
388 .last()
389 .is_some_and(|scope| scope.depth.is_some_and(|depth| self.depth < depth))
390 {
391 self.scopes.pop();
392 }
393 }
394
395 fn drop_waiting(&mut self) {
402 if self
403 .scopes
404 .last()
405 .is_some_and(|scope| scope.depth.is_none())
406 {
407 self.scopes.pop();
408 }
409 }
410
411 fn open_body(&mut self) {
412 let depth = self.depth;
413 if let Some(scope) = self.scopes.last_mut()
414 && scope.depth.is_none()
415 {
416 scope.depth = Some(depth);
417 }
418 }
419
420 fn step(&mut self, index: usize) -> usize {
421 if self.punct(index, "{") {
422 self.depth += 1;
423 self.open_body();
424 return index + 1;
425 }
426 if self.punct(index, "}") {
427 self.depth -= 1;
428 return index + 1;
429 }
430 if self.punct(index, ";") {
431 if self
435 .scopes
436 .last()
437 .is_some_and(|scope| scope.depth.is_none())
438 {
439 self.scopes.pop();
440 }
441 return index + 1;
442 }
443 if self.kind(index) != Some(TokenKind::Identifier) {
444 return index + 1;
445 }
446 if let Some(next) = self.import(index) {
447 return next;
448 }
449 if let Some(next) = self.declaration(index) {
450 return next;
451 }
452 if let Some(next) = self.call(index) {
453 return next;
454 }
455 index + 1
456 }
457
458 #[allow(clippy::too_many_lines)]
463 fn import(&mut self, start: usize) -> Option<usize> {
464 let mut index = start;
467 let forwarding = self.rules.exported_keyword == Some(self.text(start));
471 while self.rules.modifiers.contains(&self.text(index)) {
472 index += 1;
473 if self.punct(index, "(") {
475 while index < self.tokens.len() && !self.punct(index, ")") {
476 index += 1;
477 }
478 index += 1;
479 }
480 }
481 let word = self.text(index);
482 if !self.rules.imports.contains(&word) {
483 return None;
484 }
485 let mut bindings = Vec::new();
486 if self.punct(index + 1, "(") {
488 let mut cursor = index + 2;
489 let limit = (index + 512).min(self.tokens.len());
490 while cursor < limit && !self.punct(cursor, ")") {
491 if self.kind(cursor) == Some(TokenKind::String) {
492 let specifier = self.text(cursor).trim_matches(['"', '`']).to_owned();
493 let bindings = self.package_import_bindings(cursor, &specifier);
494 let names = bindings
495 .iter()
496 .map(|binding| binding.local.clone())
497 .collect();
498 self.facts.imports.push(Import {
499 specifier,
500 span: self.span(cursor, cursor),
501 type_only: false,
502 reexport: false,
503 names,
504 bindings,
505 });
506 }
507 cursor += 1;
508 }
509 return Some(cursor + 1);
510 }
511 if word == "mod" {
514 let name = self.text(index + 1);
515 if name.is_empty() || !self.punct(index + 2, ";") {
516 return None;
517 }
518 self.facts.imports.push(Import {
519 specifier: format!("self::{name}"),
520 span: self.span(index, index + 1),
521 type_only: false,
522 reexport: false,
523 names: Vec::new(),
524 bindings: Vec::new(),
525 });
526 return Some(index + 2);
527 }
528 let line = self.tokens[index].line;
531 let mut cursor = index + 1;
532 let mut specifier = String::new();
533 let limit = (index + 128).min(self.tokens.len());
534 while cursor < limit {
535 if self.kind(cursor) == Some(TokenKind::String) {
536 specifier.clear();
539 specifier.push_str(self.text(cursor).trim_matches(['"', '`', '\'']));
540 if word == "import"
541 && bindings.is_empty()
542 && self.text(cursor.wrapping_sub(1)) != "from"
543 {
544 bindings = self.package_import_bindings(cursor, &specifier);
545 }
546 cursor += 1;
547 break;
548 }
549 if self.punct(cursor, ";") {
550 break;
551 }
552 if self.punct(cursor, "{") {
553 let mut close = cursor + 1;
557 while close < limit && !self.punct(close, "}") {
558 close += 1;
559 }
560 bindings.extend(self.named_import_bindings(cursor + 1, close));
561 if !specifier.is_empty() {
562 cursor = close.saturating_add(1);
563 break;
564 }
565 cursor = close.saturating_add(1);
566 continue;
567 }
568 if word == "use"
569 && self.text(cursor) == "as"
570 && self.kind(cursor + 1) == Some(TokenKind::Identifier)
571 {
572 let imported = specifier
573 .trim_end_matches(':')
574 .rsplit("::")
575 .next()
576 .unwrap_or(specifier.as_str())
577 .to_owned();
578 bindings.push(ImportBinding {
579 imported,
580 local: self.text(cursor + 1).to_owned(),
581 });
582 cursor += 2;
583 continue;
584 }
585 if self.tokens[cursor].line != line && !specifier.is_empty() {
591 break;
592 }
593 if matches!(
594 self.kind(cursor),
595 Some(TokenKind::Identifier | TokenKind::Punctuation)
596 ) {
597 specifier.push_str(self.text(cursor));
598 }
599 cursor += 1;
600 }
601 let specifier = specifier.trim_end_matches([':', '.']).to_owned();
603 if specifier.is_empty() {
604 return None;
605 }
606 if word == "use" && bindings.is_empty() {
607 let imported = specifier
608 .rsplit("::")
609 .next()
610 .unwrap_or(specifier.as_str())
611 .to_owned();
612 if imported != "*" {
613 bindings.push(ImportBinding {
614 local: imported.clone(),
615 imported,
616 });
617 }
618 }
619 let names = bindings
620 .iter()
621 .map(|binding| binding.local.clone())
622 .collect();
623 self.facts.imports.push(Import {
624 specifier,
625 span: self.span(index, cursor.saturating_sub(1)),
626 type_only: false,
627 reexport: forwarding,
628 names,
629 bindings,
630 });
631 Some(cursor)
632 }
633
634 fn named_import_bindings(&self, start: usize, end: usize) -> Vec<ImportBinding> {
635 let mut bindings = Vec::new();
636 let mut cursor = start;
637 while cursor < end {
638 if self.kind(cursor) != Some(TokenKind::Identifier)
639 || matches!(self.text(cursor), "as" | "type")
640 || self.text(cursor.wrapping_sub(1)) == "as"
641 {
642 cursor += 1;
643 continue;
644 }
645 let imported = self.text(cursor).to_owned();
646 let local = if self.text(cursor + 1) == "as"
647 && self.kind(cursor + 2) == Some(TokenKind::Identifier)
648 {
649 self.text(cursor + 2).to_owned()
650 } else {
651 imported.clone()
652 };
653 bindings.push(ImportBinding { imported, local });
654 cursor += 1;
655 }
656 bindings
657 }
658
659 fn package_import_bindings(&self, path: usize, specifier: &str) -> Vec<ImportBinding> {
660 let imported = specifier
661 .trim_end_matches('/')
662 .rsplit('/')
663 .next()
664 .unwrap_or(specifier)
665 .to_owned();
666 let previous = path.wrapping_sub(1);
667 let same_line = self
668 .tokens
669 .get(previous)
670 .is_some_and(|token| token.line == self.tokens[path].line);
671 let local = if same_line
672 && self.kind(previous) == Some(TokenKind::Identifier)
673 && !matches!(self.text(previous), "import" | "from")
674 {
675 self.text(previous).to_owned()
676 } else if same_line && self.punct(previous, ".") {
677 "*".to_owned()
678 } else {
679 imported.clone()
680 };
681 if local == "_" {
682 Vec::new()
683 } else {
684 vec![ImportBinding { imported, local }]
685 }
686 }
687
688 fn declaration(&mut self, index: usize) -> Option<usize> {
689 let mut cursor = index;
690 let mut exported = false;
691 loop {
692 let word = self.text(cursor);
693 if self.rules.exported_keyword == Some(word) {
694 exported = true;
695 }
696 if self.rules.modifiers.contains(&word) {
697 cursor += 1;
698 if self.punct(cursor, "(") {
700 while cursor < self.tokens.len() && !self.punct(cursor, ")") {
701 cursor += 1;
702 }
703 cursor += 1;
704 }
705 continue;
706 }
707 break;
708 }
709 if self.rules.scope_keywords.contains(&self.text(cursor))
710 && let Some(next) = self.open_named_scope(cursor)
711 {
712 return Some(next);
713 }
714 let keyword = self.text(cursor);
715 if keyword == "const" && self.punct(cursor.wrapping_sub(1), "*") {
716 return None;
720 }
721 let Some((_, kind)) = self
722 .rules
723 .declarations
724 .iter()
725 .find(|(word, _)| *word == keyword)
726 else {
727 return self
728 .typed_function(cursor, exported)
729 .or_else(|| self.braced_member(cursor, exported));
730 };
731 if self.rules.grouped_declarations && self.punct(cursor + 1, "(") {
734 return Some(self.grouped_declarations(index, cursor + 2, *kind));
735 }
736 let name_index = cursor + 1;
737 if self.kind(name_index) != Some(TokenKind::Identifier) {
738 return None;
739 }
740 let name = self.text(name_index).to_owned();
741 let exported = exported || name.starts_with(char::is_uppercase);
743 let test_only = self.test_only_at(index);
744 let declaration_span = self.span(index, name_index);
745 self.drop_waiting();
746 self.record_test_only_declaration(test_only, declaration_span);
747 self.facts.declarations.push(Declaration {
748 name: name.clone(),
749 kind: *kind,
750 span: declaration_span,
751 owner: self.owner(),
752 exported,
753 });
754 self.heritage(name_index + 1, &name);
755 self.scopes.push(Scope {
756 name,
757 depth: None,
758 type_body: matches!(
759 kind,
760 DeclarationKind::Class
761 | DeclarationKind::Struct
762 | DeclarationKind::Interface
763 | DeclarationKind::Trait
764 | DeclarationKind::Enum
765 ),
766 test_only,
767 });
768 Some(name_index + 1)
769 }
770
771 fn heritage(&mut self, start: usize, owner: &str) {
777 let limit = (start + 48).min(self.tokens.len());
778 let mut cursor = start;
779 let mut kind = None;
780 while cursor < limit && !self.punct(cursor, "{") && !self.punct(cursor, ";") {
781 match self.text(cursor) {
782 "extends" | "is" => kind = Some(ReferenceKind::Inherits),
785 "implements" => kind = Some(ReferenceKind::Implements),
786 _ => {
787 if let Some(kind) = kind
788 && self.kind(cursor) == Some(TokenKind::Identifier)
789 && !self.punct(cursor.wrapping_sub(1), ".")
790 {
791 self.facts.references.push(Reference {
792 name: self.text(cursor).to_owned(),
793 kind,
794 receiver: None,
795 span: self.span(cursor, cursor),
796 owner: Some(owner.to_owned()),
797 string_arguments: Vec::new(),
798 name_arguments: Vec::new(),
799 });
800 }
801 }
802 }
803 cursor += 1;
804 }
805 }
806
807 fn braced_field(&mut self, index: usize, exported: bool) -> Option<usize> {
813 let limit = (index + 32).min(self.tokens.len());
814 let mut cursor = index;
815 let mut name = None;
816 while cursor < limit {
817 if self.punct(cursor, ";") || self.punct(cursor, "=") {
818 break;
819 }
820 if self.punct(cursor, "(") || self.punct(cursor, "{") {
822 return None;
823 }
824 if self.kind(cursor) == Some(TokenKind::Identifier) {
825 name = Some((self.text(cursor).to_owned(), cursor));
826 }
827 cursor += 1;
828 }
829 let (name, at) = name?;
830 if at == index {
833 return None;
834 }
835 let declaration_span = self.span(index, at);
836 let test_only = self.test_only_at(index);
837 self.record_test_only_declaration(test_only, declaration_span);
838 self.facts.declarations.push(Declaration {
839 name,
840 kind: DeclarationKind::Field,
841 span: declaration_span,
842 owner: self.owner(),
843 exported,
844 });
845 Some(cursor)
846 }
847
848 fn grouped_declarations(&mut self, start: usize, open: usize, kind: DeclarationKind) -> usize {
853 let limit = self.tokens.len();
854 let mut cursor = open;
855 let mut line = 0;
856 let mut found = false;
857 let mut closed = false;
858 let mut parentheses = 1_u32;
859 let mut braces = 0_u32;
860 let mut brackets = 0_u32;
861 while cursor < limit && parentheses != 0 {
862 if self.punct(cursor, "(") {
863 parentheses = parentheses.saturating_add(1);
864 cursor += 1;
865 continue;
866 }
867 if self.punct(cursor, ")") {
868 parentheses = parentheses.saturating_sub(1);
869 cursor += 1;
870 if parentheses == 0 {
871 closed = true;
872 }
873 continue;
874 }
875 if self.punct(cursor, "{") {
876 braces = braces.saturating_add(1);
877 cursor += 1;
878 continue;
879 }
880 if self.punct(cursor, "}") {
881 braces = braces.saturating_sub(1);
882 cursor += 1;
883 continue;
884 }
885 if self.punct(cursor, "[") {
886 brackets = brackets.saturating_add(1);
887 cursor += 1;
888 continue;
889 }
890 if self.punct(cursor, "]") {
891 brackets = brackets.saturating_sub(1);
892 cursor += 1;
893 continue;
894 }
895 if parentheses == 1
896 && braces == 0
897 && brackets == 0
898 && self.kind(cursor) == Some(TokenKind::Identifier)
899 && self.tokens[cursor].line != line
900 && !cursor.checked_sub(1).is_some_and(|previous| {
901 self.kind(previous) == Some(TokenKind::Punctuation)
902 && matches!(
903 self.text(previous),
904 "=" | ","
905 | "."
906 | "+"
907 | "-"
908 | "*"
909 | "/"
910 | "%"
911 | "&"
912 | "|"
913 | "^"
914 | "!"
915 | "<"
916 | ">"
917 | ":"
918 )
919 })
920 {
921 line = self.tokens[cursor].line;
922 let name = self.text(cursor).to_owned();
923 let exported = name.starts_with(char::is_uppercase);
924 let declaration_span = self.span(cursor, cursor);
925 let test_only = self.test_only_at(cursor);
926 self.record_test_only_declaration(test_only, declaration_span);
927 self.facts.declarations.push(Declaration {
928 name,
929 kind,
930 span: declaration_span,
931 owner: self.owner(),
932 exported,
933 });
934 found = true;
935 }
936 if self.kind(cursor) == Some(TokenKind::Identifier)
941 && let Some(next) = self.call(cursor)
942 {
943 cursor = next;
944 continue;
945 }
946 cursor += 1;
947 }
948 if closed && found {
949 cursor
950 } else {
951 start + 1
954 }
955 }
956
957 fn open_named_scope(&mut self, keyword: usize) -> Option<usize> {
963 self.drop_waiting();
964 let limit = (keyword + 64).min(self.tokens.len());
965 let mut cursor = keyword + 1;
966 let mut name = None;
967 let mut generic = 0_i32;
968 while cursor < limit && !self.punct(cursor, "{") {
969 if self.punct(cursor, ";") {
970 return None;
971 }
972 if self.punct(cursor, "<") {
974 generic += 1;
975 } else if self.punct(cursor, ">") {
976 generic -= 1;
977 } else if generic == 0 && self.kind(cursor) == Some(TokenKind::Identifier) {
978 name = Some(self.text(cursor).to_owned());
979 }
980 cursor += 1;
981 }
982 let name = name?;
983 if !self.punct(cursor, "{") {
984 return None;
985 }
986 let test_only = self.test_only_at(keyword);
987 self.scopes.push(Scope {
988 name,
989 depth: None,
990 type_body: true,
991 test_only,
992 });
993 Some(cursor)
994 }
995
996 fn typed_function(&mut self, index: usize, exported: bool) -> Option<usize> {
1004 if !self.rules.typed_functions || !self.punct(index + 1, "(") {
1005 return None;
1006 }
1007 let name = self.text(index);
1008 if matches!(
1011 name,
1012 "if" | "for" | "while" | "switch" | "return" | "catch" | "sizeof" | "do"
1013 ) {
1014 return None;
1015 }
1016 let (owner, type_index) = if self.punct(index - 1, ":")
1019 && self.punct(index.checked_sub(2)?, ":")
1020 && self.kind(index.checked_sub(3)?) == Some(TokenKind::Identifier)
1021 {
1022 (Some(self.text(index - 3).to_owned()), index.checked_sub(4)?)
1023 } else {
1024 (self.owner(), index.checked_sub(1)?)
1025 };
1026 let preceded_by_type = self.kind(type_index) == Some(TokenKind::Identifier)
1027 && !matches!(self.text(type_index), "return" | "else" | "case" | "goto")
1028 || self.punct(type_index, "*")
1029 || self.punct(type_index, "&");
1030 if !preceded_by_type {
1031 return None;
1032 }
1033 let mut cursor = index + 2;
1037 let mut depth = 1_i32;
1038 let limit = (index + 512).min(self.tokens.len());
1039 while cursor < limit && depth > 0 {
1040 if self.punct(cursor, "(") {
1041 depth += 1;
1042 } else if self.punct(cursor, ")") {
1043 depth -= 1;
1044 }
1045 cursor += 1;
1046 }
1047 while cursor < limit && self.kind(cursor) == Some(TokenKind::Identifier) {
1049 cursor += 1;
1050 }
1051 if !self.punct(cursor, "{") {
1052 return None;
1053 }
1054 let name = name.to_owned();
1055 let test_only = self.test_only_at(index);
1056 let declaration_span = self.span(index, index);
1057 self.record_test_only_declaration(test_only, declaration_span);
1058 self.facts.declarations.push(Declaration {
1059 name: name.clone(),
1060 kind: if owner.is_some() {
1061 DeclarationKind::Method
1062 } else {
1063 DeclarationKind::Function
1064 },
1065 span: declaration_span,
1066 owner,
1067 exported: exported || !self.is_static(index),
1070 });
1071 self.scopes.push(Scope {
1072 name,
1073 depth: None,
1074 type_body: false,
1075 test_only,
1076 });
1077 Some(index + 1)
1078 }
1079
1080 fn type_argument_span(&self, index: usize) -> usize {
1092 if !self.punct(index, "<") {
1093 return 0;
1094 }
1095 let limit = (index + 32).min(self.tokens.len());
1096 let mut cursor = index + 1;
1097 let mut depth = 1_i32;
1098 while cursor < limit && depth > 0 {
1099 if self.punct(cursor, "<") {
1100 depth += 1;
1101 } else if self.punct(cursor, ">") {
1102 depth -= 1;
1103 } else if self.punct(cursor, ";") || self.punct(cursor, "{") {
1104 return 0;
1106 }
1107 cursor += 1;
1108 }
1109 if depth > 0 { 0 } else { cursor - index }
1110 }
1111
1112 fn type_argument_names(&self, index: usize, length: usize) -> Vec<String> {
1114 (index..index + length)
1115 .filter(|cursor| self.kind(*cursor) == Some(TokenKind::Identifier))
1116 .map(|cursor| self.text(cursor).to_owned())
1117 .collect()
1118 }
1119
1120 fn is_static(&self, index: usize) -> bool {
1122 let start = index.saturating_sub(4);
1123 (start..index).any(|cursor| self.text(cursor) == "static")
1124 }
1125
1126 fn braced_member(&mut self, index: usize, exported: bool) -> Option<usize> {
1129 if !self.rules.braced_members {
1130 return None;
1131 }
1132 let inside_type = self.scopes.last().is_some_and(|scope| {
1133 scope.type_body && scope.depth.is_some_and(|depth| self.depth == depth)
1134 });
1135 if !inside_type {
1136 return None;
1137 }
1138 if self.punct(index.wrapping_sub(1), "@") || self.punct(index.wrapping_sub(1), "[") {
1143 return None;
1144 }
1145 let mut cursor = index;
1150 let limit = (index + 16).min(self.tokens.len());
1151 while cursor < limit && !self.punct(cursor + 1, "(") {
1152 if self.punct(cursor, ";") || self.punct(cursor, "{") || self.punct(cursor, "=") {
1153 return self.braced_field(index, exported);
1154 }
1155 cursor += 1;
1156 }
1157 if cursor >= limit || !self.punct(cursor + 1, "(") {
1161 return self.braced_field(index, exported);
1162 }
1163 if self.kind(cursor) != Some(TokenKind::Identifier) {
1164 return None;
1165 }
1166 let name = self.text(cursor).to_owned();
1167 if matches!(name.as_str(), "if" | "for" | "while" | "switch" | "return") {
1168 return None;
1169 }
1170 let test_only = self.test_only_at(index);
1171 let declaration_span = self.span(index, cursor);
1172 self.record_test_only_declaration(test_only, declaration_span);
1173 self.facts.declarations.push(Declaration {
1174 name: name.clone(),
1175 kind: DeclarationKind::Method,
1176 span: declaration_span,
1177 owner: self.owner(),
1178 exported,
1179 });
1180 self.scopes.push(Scope {
1181 name,
1182 depth: None,
1183 type_body: false,
1184 test_only,
1185 });
1186 Some(cursor + 1)
1187 }
1188
1189 fn call(&mut self, index: usize) -> Option<usize> {
1190 let type_arguments = self.type_argument_span(index + 1);
1194 let open = index + 1 + type_arguments;
1195 if !self.punct(open, "(") {
1196 return None;
1197 }
1198 let name = self.text(index).to_owned();
1199 if matches!(
1200 name.as_str(),
1201 "if" | "for" | "while" | "switch" | "match" | "return" | "catch" | "sizeof" | "fn"
1202 ) {
1203 return None;
1204 }
1205 let receiver = (index >= 2
1206 && (self.punct(index - 1, ".") || self.punct(index - 1, ":"))
1207 && self.kind(index - 2) == Some(TokenKind::Identifier))
1208 .then(|| self.text(index - 2).to_owned());
1209 for argument in self.type_argument_names(index + 1, type_arguments) {
1210 self.facts.references.push(Reference {
1211 name: argument,
1212 kind: ReferenceKind::Uses,
1213 receiver: None,
1214 span: self.span(index, index),
1215 owner: self.owner(),
1216 string_arguments: Vec::new(),
1217 name_arguments: Vec::new(),
1218 });
1219 }
1220 let mut arguments = Vec::new();
1221 let mut scan = open + 1;
1222 let mut depth = 1_i32;
1223 let limit = (index + 256).min(self.tokens.len());
1224 while scan < limit && depth > 0 {
1225 if self.punct(scan, "(") {
1226 depth += 1;
1227 } else if self.punct(scan, ")") {
1228 depth -= 1;
1229 } else if depth == 1 && self.kind(scan) == Some(TokenKind::String) {
1230 arguments.push(self.text(scan).trim_matches(['"', '`', '\'']).to_owned());
1231 }
1232 scan += 1;
1233 }
1234 self.facts.references.push(Reference {
1235 kind: ReferenceKind::Call,
1236 name,
1237 receiver,
1238 span: self.span(index, index),
1239 owner: self.owner(),
1240 string_arguments: arguments,
1241 name_arguments: Vec::new(),
1242 });
1243 Some(index + 1)
1244 }
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249 use super::extract;
1250 use crate::facts::{DeclarationKind, ImportBinding, ReferenceKind};
1251 use crate::syntax::Language;
1252
1253 fn declared(
1254 source: &str,
1255 language: Language,
1256 ) -> Vec<(String, DeclarationKind, Option<String>)> {
1257 extract(source, language)
1258 .declarations
1259 .into_iter()
1260 .map(|item| (item.name, item.kind, item.owner))
1261 .collect()
1262 }
1263
1264 #[test]
1265 fn rust_module_declarations_are_dependencies_but_inline_modules_are_not() {
1266 let source = "pub mod engine;\nmod helper;\nuse crate::engine::Driver;\n\
1267 mod inline { pub fn nested() {} }\npub fn run() { Driver::start(); }\n";
1268 let specifiers = extract(source, Language::Rust)
1269 .imports
1270 .into_iter()
1271 .map(|import| import.specifier)
1272 .collect::<Vec<_>>();
1273 assert_eq!(
1274 specifiers,
1275 ["self::engine", "self::helper", "crate::engine::Driver"],
1276 "a mod with a body defines the module here rather than including a file"
1277 );
1278 }
1279
1280 #[test]
1281 fn rust_test_attributes_classify_the_declaration_and_nested_scope() {
1282 let source = r#"
1283 fn production() {}
1284 #[cfg(not(test))]
1285 fn production_without_tests() {}
1286 #[cfg(any(test, feature = "test-support"))]
1287 #[allow(dead_code)]
1288 mod tests {
1289 #[test]
1290 fn embedded_test() {}
1291 fn helper_for_test() {}
1292 }
1293 #[tokio::test]
1294 async fn async_test() {}
1295 #[cfg(not(not(test)))]
1296 fn double_negated_test() {}
1297 "#;
1298 let facts = extract(source, Language::Rust);
1299 let test_only = |name: &str| {
1300 facts
1301 .declarations
1302 .iter()
1303 .find(|declaration| declaration.name == name)
1304 .is_some_and(|declaration| facts.declaration_is_test_only(declaration.span))
1305 };
1306 for production in ["production", "production_without_tests"] {
1307 assert!(
1308 !test_only(production),
1309 "{production} is available to production"
1310 );
1311 }
1312 for test in [
1313 "tests",
1314 "embedded_test",
1315 "helper_for_test",
1316 "async_test",
1317 "double_negated_test",
1318 ] {
1319 assert!(test_only(test), "{test} is test-only syntax");
1320 }
1321 }
1322
1323 #[test]
1324 fn a_character_literal_holding_a_quote_does_not_shift_the_rest_of_the_file() {
1325 let source = "fn classify<'a>(head: &'a str) -> bool {\n\
1329 \x20 head.contains(['.', '\"', '+']) || head.starts_with('@')\n\
1330 }\n\
1331 mod tests {\n\
1332 \x20 use super::classify;\n\
1333 }\n";
1334 let facts = extract(source, Language::Rust);
1335 assert_eq!(
1336 facts
1337 .imports
1338 .iter()
1339 .map(|import| import.specifier.as_str())
1340 .collect::<Vec<_>>(),
1341 ["super::classify"],
1342 "the import after the quote character is still reachable"
1343 );
1344 assert!(
1345 facts
1346 .declarations
1347 .iter()
1348 .any(|item| item.name == "classify"),
1349 "the lifetime is punctuation, not an unterminated literal"
1350 );
1351 }
1352
1353 #[test]
1354 fn a_restricted_visibility_still_leads_to_the_import() {
1355 assert_eq!(
1356 extract("pub(crate) use transport::serve_stdio;\n", Language::Rust)
1357 .imports
1358 .len(),
1359 1,
1360 "the parenthesised scope after pub must not hide the use"
1361 );
1362 }
1363
1364 #[test]
1365 fn a_grouped_use_names_the_module_without_its_separator() {
1366 let import = extract("use super::support::{one as first, two};\n", Language::Rust)
1367 .imports
1368 .remove(0);
1369 assert_eq!(import.specifier, "super::support");
1370 assert_eq!(import.names, ["first", "two"]);
1371 assert_eq!(
1372 import.bindings,
1373 [
1374 ImportBinding {
1375 imported: "one".to_owned(),
1376 local: "first".to_owned(),
1377 },
1378 ImportBinding {
1379 imported: "two".to_owned(),
1380 local: "two".to_owned(),
1381 },
1382 ]
1383 );
1384 }
1385
1386 #[test]
1387 fn rust_declarations_carry_their_kind_and_visibility() {
1388 let facts = extract(
1389 "pub struct Engine;\npub(crate) fn build() {}\nfn private() {}\ntrait Run {}\n",
1390 Language::Rust,
1391 );
1392 let items = facts
1393 .declarations
1394 .iter()
1395 .map(|item| (item.name.as_str(), item.kind, item.exported))
1396 .collect::<Vec<_>>();
1397 assert!(
1398 items.contains(&("Engine", DeclarationKind::Struct, true)),
1399 "got {items:?}"
1400 );
1401 assert!(
1402 items.contains(&("build", DeclarationKind::Function, true)),
1403 "got {items:?}"
1404 );
1405 assert!(
1406 items.contains(&("private", DeclarationKind::Function, false)),
1407 "got {items:?}"
1408 );
1409 assert!(
1410 items.contains(&("Run", DeclarationKind::Trait, true)),
1411 "got {items:?}"
1412 );
1413 }
1414
1415 #[test]
1416 fn rust_function_pointer_types_do_not_invent_declarations_or_calls() {
1417 let facts = extract(
1418 "type Callback = unsafe extern \"system\" fn(\n\
1419 \x20 *mut c_void,\n\
1420 \x20 *const u16,\n\
1421 ) -> *mut c_void;\n",
1422 Language::Rust,
1423 );
1424 assert!(
1425 facts
1426 .declarations
1427 .iter()
1428 .any(|item| item.name == "Callback" && item.kind == DeclarationKind::TypeAlias),
1429 "the actual alias must survive, got {:?}",
1430 facts.declarations
1431 );
1432 for false_positive in ["mut", "const", "u16", "c_void"] {
1433 assert!(
1434 !facts
1435 .declarations
1436 .iter()
1437 .any(|item| item.name == false_positive),
1438 "{false_positive} is part of a pointer type, got {:?}",
1439 facts.declarations
1440 );
1441 }
1442 assert!(
1443 !facts
1444 .references
1445 .iter()
1446 .any(|reference| reference.name == "fn" && reference.kind == ReferenceKind::Call),
1447 "the function-pointer marker is a type, not a call"
1448 );
1449 }
1450
1451 #[test]
1452 fn go_groups_imports_and_capitalisation_marks_export() {
1453 let source = "package main\n\nimport (\n\tf \"fmt\"\n\t\"edgehawk.com/app/reader\"\n)\n\n\
1454 func Exported() {}\nfunc internal() {}\n";
1455 let facts = extract(source, Language::Go);
1456 assert_eq!(
1457 facts
1458 .imports
1459 .iter()
1460 .map(|import| import.specifier.as_str())
1461 .collect::<Vec<_>>(),
1462 ["fmt", "edgehawk.com/app/reader"],
1463 "a grouped import block yields one fact per path"
1464 );
1465 assert_eq!(
1466 facts.imports[0].bindings,
1467 [ImportBinding {
1468 imported: "fmt".to_owned(),
1469 local: "f".to_owned(),
1470 }]
1471 );
1472 assert_eq!(
1473 facts.imports[1].bindings,
1474 [ImportBinding {
1475 imported: "reader".to_owned(),
1476 local: "reader".to_owned(),
1477 }]
1478 );
1479 let items = facts
1480 .declarations
1481 .iter()
1482 .map(|item| (item.name.as_str(), item.exported))
1483 .collect::<Vec<_>>();
1484 assert!(items.contains(&("Exported", true)), "got {items:?}");
1485 assert!(items.contains(&("internal", false)), "got {items:?}");
1486 }
1487
1488 #[test]
1489 fn go_const_and_var_groups_declare_each_line() {
1490 let source = r#"package main
1491const (
1492 EventAdd = "added"
1493 eventDelete = "deleted"
1494)
1495var (
1496 endpoint = flag.String("endpoint", "/events", "endpoint")
1497 topics = []string{EventAdd, eventDelete}
1498)
1499"#;
1500 let facts = extract(source, Language::Go);
1501 let items = facts
1502 .declarations
1503 .iter()
1504 .map(|item| (item.name.as_str(), item.kind, item.span.line))
1505 .collect::<Vec<_>>();
1506 for expected in [
1507 ("EventAdd", DeclarationKind::Constant, 3),
1508 ("eventDelete", DeclarationKind::Constant, 4),
1509 ("endpoint", DeclarationKind::Variable, 7),
1510 ("topics", DeclarationKind::Variable, 8),
1511 ] {
1512 assert!(
1513 items.contains(&expected),
1514 "missing {expected:?}; got {items:?}"
1515 );
1516 }
1517 }
1518
1519 #[test]
1520 fn go_grouped_initializers_keep_their_call_references() {
1521 let facts = extract(
1522 "package main\nvar (\n flagName = config.String(\"name\")\n)\n",
1523 Language::Go,
1524 );
1525 assert!(
1526 facts
1527 .declarations
1528 .iter()
1529 .any(|item| item.name == "flagName" && item.kind == DeclarationKind::Variable),
1530 "the grouped declaration must survive"
1531 );
1532 assert!(
1533 facts.references.iter().any(|reference| {
1534 reference.name == "String"
1535 && reference.kind == ReferenceKind::Call
1536 && reference.receiver.as_deref() == Some("config")
1537 }),
1538 "the initializer call must survive, got {:?}",
1539 facts.references
1540 );
1541 }
1542
1543 #[test]
1544 fn go_grouped_values_do_not_turn_continuation_lines_into_declarations() {
1545 let source = r#"package main
1546var (
1547 config = Config{
1548 Name: "primary",
1549 }
1550 continued =
1551 buildValue
1552 next = 1
1553)
1554"#;
1555 let facts = extract(source, Language::Go);
1556 let names = facts
1557 .declarations
1558 .iter()
1559 .map(|item| item.name.as_str())
1560 .collect::<Vec<_>>();
1561 for expected in ["config", "continued", "next"] {
1562 assert!(
1563 names.contains(&expected),
1564 "missing {expected}; got {names:?}"
1565 );
1566 }
1567 for false_positive in ["Name", "buildValue"] {
1568 assert!(
1569 !names.contains(&false_positive),
1570 "{false_positive} is an initializer expression, got {names:?}"
1571 );
1572 }
1573 }
1574
1575 #[test]
1576 fn a_large_go_group_does_not_swallow_following_functions() {
1577 use std::fmt::Write as _;
1578
1579 let mut source = String::from("package main\nvar (\n");
1580 for index in 0..1_100 {
1581 let _ = writeln!(source, "value{index} = {index}");
1582 }
1583 source.push_str(")\nfunc AfterGroup() {}\n");
1584 let facts = extract(&source, Language::Go);
1585 assert!(
1586 facts
1587 .declarations
1588 .iter()
1589 .any(|item| item.name == "AfterGroup" && item.kind == DeclarationKind::Function),
1590 "the closing group delimiter must return scanning to the following function"
1591 );
1592 }
1593
1594 #[test]
1595 fn java_methods_belong_to_their_class() {
1596 let source = "package com.x;\nimport com.x.Helper;\n\
1597 public class Service {\n\
1598 \x20 private final Helper helper = null;\n\
1599 \x20 public void run() {\n\
1600 \x20 items.forEach(item -> {});\n\
1601 \x20 }\n\
1602 \x20 private int score(String value) { return 1; }\n\
1603 }\n";
1604 let items = declared(source, Language::Java);
1605 assert!(
1606 items.iter().any(|(name, kind, owner)| name == "Service"
1607 && *kind == DeclarationKind::Class
1608 && owner.is_none()),
1609 "got {items:?}"
1610 );
1611 for method in ["run", "score"] {
1612 assert!(
1613 items.iter().any(|(name, kind, owner)| name == method
1614 && *kind == DeclarationKind::Method
1615 && owner.as_deref() == Some("Service")),
1616 "{method} must be a method of Service, got {items:?}"
1617 );
1618 }
1619 assert!(
1620 !items.iter().any(|(name, ..)| name == "forEach"),
1621 "a call chain inside a body is not a declaration, got {items:?}"
1622 );
1623 }
1624
1625 #[test]
1626 fn java_route_annotations_keep_their_structural_owner_and_literal() {
1627 let source = "@RequestMapping(\"warehouse\")\n\
1628 public class Service {\n\
1629 \x20 @GetMapping(\"/stock\")\n\
1630 \x20 public void stock() {}\n\
1631 }\n";
1632 let facts = extract(source, Language::Java);
1633 let class_mapping = facts
1634 .calls()
1635 .find(|call| call.name == "RequestMapping")
1636 .expect("class mapping call");
1637 assert_eq!(class_mapping.owner, None);
1638 assert_eq!(class_mapping.string_arguments, ["warehouse"]);
1639
1640 let method_mapping = facts
1641 .calls()
1642 .find(|call| call.name == "GetMapping")
1643 .expect("method mapping call");
1644 assert_eq!(method_mapping.owner.as_deref(), Some("Service"));
1645 assert_eq!(method_mapping.string_arguments, ["/stock"]);
1646 }
1647
1648 #[test]
1649 fn solidity_contracts_own_their_functions_and_name_their_dependencies() {
1650 let source = "// SPDX-License-Identifier: MIT\n\
1651 pragma solidity ^0.8.20;\n\
1652 import \"./Ownable.sol\";\n\
1653 import {IERC20, SafeMath} from \"@openzeppelin/contracts/token/IERC20.sol\";\n\
1654 \n\
1655 contract Vault is Ownable {\n\
1656 \x20 event Deposited(address indexed who, uint256 amount);\n\
1657 \x20 function deposit(uint256 amount) public payable {\n\
1658 \x20 token.transferFrom(msg.sender, address(this), amount);\n\
1659 \x20 }\n\
1660 \x20 function _sweep() internal {}\n\
1661 }\n";
1662 let facts = extract(source, Language::Solidity);
1663 assert_eq!(
1664 facts
1665 .imports
1666 .iter()
1667 .map(|import| import.specifier.as_str())
1668 .collect::<Vec<_>>(),
1669 ["./Ownable.sol", "@openzeppelin/contracts/token/IERC20.sol"],
1670 "the names listed before `from` are bindings, not the path"
1671 );
1672 let items = declared(source, Language::Solidity);
1673 assert!(
1674 items.iter().any(|(name, kind, owner)| name == "Vault"
1675 && *kind == DeclarationKind::Class
1676 && owner.is_none()),
1677 "got {items:?}"
1678 );
1679 for method in ["deposit", "_sweep"] {
1680 assert!(
1681 items.iter().any(|(name, kind, owner)| name == method
1682 && *kind == DeclarationKind::Function
1683 && owner.as_deref() == Some("Vault")),
1684 "{method} belongs to Vault, got {items:?}"
1685 );
1686 }
1687 assert!(
1688 facts.references.iter().any(
1689 |call| call.name == "transferFrom" && call.receiver.as_deref() == Some("token")
1690 ),
1691 "a call through a receiver keeps the receiver"
1692 );
1693 }
1694
1695 #[test]
1696 fn swift_members_belong_to_the_type_their_extension_names() {
1697 let source = "import Foundation\n\
1698 import UIKit\n\
1699 \n\
1700 public struct Engine {\n\
1701 \x20 let name: String\n\
1702 \x20 public func start() { boot() }\n\
1703 }\n\
1704 \n\
1705 extension Engine {\n\
1706 \x20 func restart() { start() }\n\
1707 }\n\
1708 \n\
1709 private func boot() {}\n";
1710 let facts = extract(source, Language::Swift);
1711 assert_eq!(
1712 facts
1713 .imports
1714 .iter()
1715 .map(|import| import.specifier.as_str())
1716 .collect::<Vec<_>>(),
1717 ["Foundation", "UIKit"]
1718 );
1719 let items = declared(source, Language::Swift);
1720 assert!(
1721 items.iter().any(|(name, kind, owner)| name == "start"
1722 && *kind == DeclarationKind::Function
1723 && owner.as_deref() == Some("Engine")),
1724 "got {items:?}"
1725 );
1726 assert!(
1727 items
1728 .iter()
1729 .any(|(name, _, owner)| name == "restart" && owner.as_deref() == Some("Engine")),
1730 "an extension names what its members belong to, got {items:?}"
1731 );
1732 assert!(
1733 !items.iter().any(|(name, ..)| name == "extension"),
1734 "and declares nothing itself, got {items:?}"
1735 );
1736 assert!(
1737 items
1738 .iter()
1739 .any(|(name, _, owner)| name == "boot" && owner.is_none()),
1740 "the file-level function is back outside, got {items:?}"
1741 );
1742 }
1743
1744 #[test]
1745 fn a_rust_impl_gives_its_methods_an_owner() {
1746 let source = "struct Engine;\n\
1747 impl Engine {\n\
1748 \x20 pub fn start(&self) {}\n\
1749 }\n\
1750 impl Display for Engine {\n\
1751 \x20 fn fmt(&self) {}\n\
1752 }\n";
1753 let items = declared(source, Language::Rust);
1754 for method in ["start", "fmt"] {
1755 assert!(
1756 items
1757 .iter()
1758 .any(|(name, _, owner)| name == method && owner.as_deref() == Some("Engine")),
1759 "{method} belongs to Engine, not to the trait, got {items:?}"
1760 );
1761 }
1762 }
1763
1764 #[test]
1765 fn c_functions_are_declarations_rather_than_calls_to_themselves() {
1766 let source = "#include <stdio.h>\n\
1770 int add(int a, int b) { return a + b; }\n\
1771 static void run(void) { add(1, 2); }\n\
1772 int main(void) { run(); return 0; }\n";
1773 let facts = extract(source, Language::C);
1774 let declared = facts
1775 .declarations
1776 .iter()
1777 .map(|item| (item.name.as_str(), item.kind, item.exported))
1778 .collect::<Vec<_>>();
1779 assert_eq!(
1780 declared,
1781 [
1782 ("add", DeclarationKind::Function, true),
1783 ("run", DeclarationKind::Function, false),
1784 ("main", DeclarationKind::Function, true),
1785 ],
1786 "a static function is file-local; the rest are linkable"
1787 );
1788 assert_eq!(
1789 facts
1790 .references
1791 .iter()
1792 .map(|reference| reference.name.as_str())
1793 .collect::<Vec<_>>(),
1794 ["add", "run"],
1795 "only the two real call sites, and no definition among them"
1796 );
1797 }
1798
1799 #[test]
1800 fn an_include_does_not_swallow_the_line_beneath_it() {
1801 let facts = extract(
1802 "#include <stdio.h>\nint first(void) { return 0; }\n",
1803 Language::C,
1804 );
1805 assert_eq!(
1806 facts.imports.len(),
1807 1,
1808 "the include is one dependency, not a run-on statement"
1809 );
1810 assert!(
1811 facts.declarations.iter().any(|item| item.name == "first"),
1812 "the function under the include survives, got {:?}",
1813 facts.declarations
1814 );
1815 }
1816
1817 #[test]
1818 fn an_out_of_line_cpp_definition_belongs_to_its_class() {
1819 let facts = extract(
1820 "int helper(int x) { return x; }\nvoid Engine::start() { helper(1); }\n",
1821 Language::Cpp,
1822 );
1823 let start = facts
1824 .declarations
1825 .iter()
1826 .find(|item| item.name == "start")
1827 .expect("the qualified definition is a declaration");
1828 assert_eq!(start.kind, DeclarationKind::Method);
1829 assert_eq!(start.owner.as_deref(), Some("Engine"));
1830 }
1831
1832 #[test]
1833 fn a_type_argument_reaches_the_name_a_mapper_configures() {
1834 let facts = extract(
1838 "modelBuilder.Entity<Order>().ToTable(\"orders\");\nif (a < b && c > d) {}\n",
1839 Language::CSharp,
1840 );
1841 let seen = facts
1842 .references
1843 .iter()
1844 .map(|reference| {
1845 (
1846 reference.name.as_str(),
1847 reference.kind,
1848 reference.string_arguments.clone(),
1849 )
1850 })
1851 .collect::<Vec<_>>();
1852 assert!(
1853 seen.contains(&("Order", ReferenceKind::Uses, Vec::new())),
1854 "the entity type is reachable, got {seen:?}"
1855 );
1856 assert!(
1857 seen.contains(&("ToTable", ReferenceKind::Call, vec!["orders".to_owned()])),
1858 "and so is the table it maps to, got {seen:?}"
1859 );
1860 assert!(
1861 !seen.iter().any(|(name, ..)| *name == "b"),
1862 "a comparison is not a type argument list, got {seen:?}"
1863 );
1864 }
1865
1866 #[test]
1867 fn a_control_structure_is_not_a_function_definition() {
1868 let source = "int pick(int x) {\n\
1869 \x20 if (x) { return 1; }\n\
1870 \x20 else if (x > 2) { return 2; }\n\
1871 \x20 while (x) { x--; }\n\
1872 \x20 return helper(x);\n\
1873 }\n";
1874 let names = declared(source, Language::C)
1875 .into_iter()
1876 .map(|(name, ..)| name)
1877 .collect::<Vec<_>>();
1878 assert_eq!(
1879 names,
1880 ["pick"],
1881 "an else-if has an identifier before it and still declares nothing"
1882 );
1883 }
1884
1885 #[test]
1886 fn a_comment_never_declares_anything_in_any_brace_language() {
1887 for (source, language) in [
1888 (
1889 "// pub fn ghost() {}\n/* struct Ghost; */\npub fn real() {}\n",
1890 Language::Rust,
1891 ),
1892 ("// func Ghost() {}\nfunc Real() {}\n", Language::Go),
1893 ] {
1894 let items = declared(source, language);
1895 assert_eq!(
1896 items.len(),
1897 1,
1898 "only the real declaration counts: {items:?}"
1899 );
1900 }
1901 }
1902}