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