1use crate::facts::{
13 Declaration, DeclarationKind, Facts, Import, ImportBinding, Reference, ReferenceKind, Span,
14};
15use crate::syntax::Language;
16use crate::token::{Mode, Token, TokenKind, Tokenizer};
17use std::collections::BTreeMap;
18
19#[must_use]
21pub fn extract(source: &str, language: Language) -> Facts {
22 let tokens = Tokenizer::new(source, language)
23 .mode(Mode::Lite)
24 .collect::<Vec<_>>();
25 Extractor {
26 source,
27 tokens: &tokens,
28 language,
29 facts: Facts::default(),
30 scopes: Vec::new(),
31 import_bindings: BTreeMap::new(),
32 depth: 0,
33 paren_depth: 0,
34 bracket_depth: 0,
35 }
36 .run()
37}
38
39struct Scope {
41 name: String,
42 depth: Option<i32>,
46 member_body: bool,
48 fields: bool,
50 paren_depth: i32,
52 bracket_depth: i32,
53}
54
55struct Extractor<'source, 'tokens> {
56 source: &'source str,
57 tokens: &'tokens [Token],
58 language: Language,
59 facts: Facts,
60 scopes: Vec<Scope>,
61 import_bindings: BTreeMap<String, (String, bool, String)>,
62 depth: i32,
63 paren_depth: i32,
64 bracket_depth: i32,
65}
66
67impl Extractor<'_, '_> {
68 fn run(mut self) -> Facts {
69 let mut index = 0;
70 while index < self.tokens.len() {
71 self.close_scopes();
72 index = self.step(index);
73 }
74 self.facts
75 }
76
77 fn text(&self, index: usize) -> &str {
78 self.tokens
79 .get(index)
80 .map_or("", |token| token.text(self.source))
81 }
82
83 fn kind(&self, index: usize) -> Option<TokenKind> {
84 self.tokens.get(index).map(|token| token.kind)
85 }
86
87 fn is(&self, index: usize, word: &str) -> bool {
88 self.kind(index) == Some(TokenKind::Identifier) && self.text(index) == word
89 }
90
91 fn punct(&self, index: usize, mark: &str) -> bool {
92 self.kind(index) == Some(TokenKind::Punctuation) && self.text(index) == mark
93 }
94
95 fn span(&self, start: usize, end: usize) -> Span {
96 let first = &self.tokens[start.min(self.tokens.len() - 1)];
97 let last = &self.tokens[end.min(self.tokens.len() - 1)];
98 Span {
99 start: first.start,
100 end: last.end,
101 line: first.line,
102 column: first.column,
103 end_line: last.line,
104 end_column: last.column,
105 }
106 }
107
108 fn owner(&self) -> Option<String> {
109 self.scopes.last().map(|scope| scope.name.clone())
110 }
111
112 fn close_scopes(&mut self) {
113 while self
114 .scopes
115 .last()
116 .is_some_and(|scope| scope.depth.is_some_and(|depth| self.depth < depth))
117 {
118 self.scopes.pop();
119 }
120 }
121
122 fn open_body(&mut self) {
124 let depth = self.depth;
125 if let Some(scope) = self.scopes.last_mut()
126 && scope.depth.is_none()
127 && scope.paren_depth == self.paren_depth
128 && scope.bracket_depth == self.bracket_depth
129 {
130 scope.depth = Some(depth);
131 scope.paren_depth = self.paren_depth;
132 scope.bracket_depth = self.bracket_depth;
133 }
134 }
135
136 fn step(&mut self, index: usize) -> usize {
138 if self.punct(index, "{") {
139 let object_owner = self.object_literal_owner(index);
140 let waiting_scope = self
141 .scopes
142 .last()
143 .is_some_and(|scope| scope.depth.is_none());
144 self.depth += 1;
145 self.open_body();
146 if !waiting_scope && let Some(name) = object_owner {
147 self.scopes.push(Scope {
148 name,
149 depth: Some(self.depth),
150 member_body: true,
151 fields: false,
152 paren_depth: self.paren_depth,
153 bracket_depth: self.bracket_depth,
154 });
155 }
156 return index + 1;
157 }
158 if self.punct(index, "}") {
159 self.depth -= 1;
160 return index + 1;
161 }
162 if self.punct(index, "(") {
163 self.paren_depth += 1;
164 return index + 1;
165 }
166 if self.punct(index, ")") {
167 self.paren_depth -= 1;
168 return index + 1;
169 }
170 if self.punct(index, "[") {
171 self.bracket_depth += 1;
172 return index + 1;
173 }
174 if self.punct(index, "]") {
175 self.bracket_depth -= 1;
176 return index + 1;
177 }
178 if (self.is(index, "import") || self.is(index, "export"))
179 && let Some(next) = self.module_statement(index)
180 {
181 return next;
182 }
183 if self.kind(index) == Some(TokenKind::String) {
184 self.template_references(index);
185 if let Some(next) = self.route_table(index) {
186 return next;
187 }
188 }
189 if self.kind(index) == Some(TokenKind::Identifier) {
190 if let Some(next) = self.declaration(index) {
191 return next;
192 }
193 if let Some(next) = self.call(index) {
194 return next;
195 }
196 }
197 index + 1
198 }
199
200 fn template_references(&mut self, index: usize) {
206 let token = &self.tokens[index];
207 let template = token.text(self.source);
208 if !template.starts_with('`') {
209 return;
210 }
211 let owner = self.owner();
212 for (start, end) in template_interpolation_ranges(template, self.language) {
213 let Some(expression) = template.get(start..end) else {
214 continue;
215 };
216 let base = token.start + start;
217 let (base_line, base_column) = position_at(self.source, base);
218 let mut references = extract(expression, self.language).references;
219 for reference in &mut references {
220 reference.span.start += base;
221 reference.span.end += base;
222 reference.span.line = base_line.saturating_add(reference.span.line - 1);
223 if reference.span.line == base_line {
224 reference.span.column = base_column.saturating_add(reference.span.column - 1);
225 }
226 reference.span.end_line = base_line.saturating_add(reference.span.end_line - 1);
227 if reference.span.end_line == base_line {
228 reference.span.end_column =
229 base_column.saturating_add(reference.span.end_column - 1);
230 }
231 reference.owner.clone_from(&owner);
232 }
233 self.facts.references.extend(references);
234 }
235 }
236
237 fn module_statement(&mut self, index: usize) -> Option<usize> {
241 let exporting = self.is(index, "export");
242 let mut cursor = index + 1;
243 let mut type_only = self.is(cursor, "type");
244 if type_only {
245 cursor += 1;
246 }
247 if exporting && !self.leads_to_from(cursor) {
250 return self.local_reexport(index, cursor);
251 }
252 if !exporting && self.punct(cursor, "(") {
253 let specifier = self.string_at(cursor + 1)?;
255 self.facts.imports.push(Import {
256 specifier,
257 span: self.span(index, cursor + 1),
258 type_only: false,
259 reexport: false,
260 names: Vec::new(),
261 bindings: Vec::new(),
262 });
263 return Some(cursor + 2);
264 }
265 let mut scan = cursor;
266 let limit = (index + 512).min(self.tokens.len());
267 while scan < limit {
268 if self.kind(scan) == Some(TokenKind::String) {
269 let specifier = self.string_at(scan)?;
270 type_only = type_only || self.braced_types_only(cursor, scan);
272 let bindings = self.clause_bindings(cursor, scan);
273 let names = bindings
274 .iter()
275 .map(|binding| binding.local.clone())
276 .collect::<Vec<_>>();
277 if !exporting {
278 for binding in &bindings {
279 self.import_bindings.insert(
280 binding.local.clone(),
281 (specifier.clone(), type_only, binding.imported.clone()),
282 );
283 }
284 }
285 self.facts.imports.push(Import {
286 specifier,
287 span: self.span(index, scan),
288 type_only,
289 reexport: exporting,
290 names,
291 bindings,
292 });
293 return Some(scan + 1);
294 }
295 if self.punct(scan, ";") {
304 break;
305 }
306 if self.kind(scan) == Some(TokenKind::Punctuation)
307 && !matches!(self.text(scan), "{" | "}" | "," | "*")
308 {
309 break;
310 }
311 scan += 1;
312 }
313 None
314 }
315
316 fn local_reexport(&mut self, start: usize, open: usize) -> Option<usize> {
319 if !self.punct(open, "{") {
320 return None;
321 }
322 let limit = (open + 512).min(self.tokens.len());
323 let mut cursor = open + 1;
324 let mut by_target = BTreeMap::<(String, bool), Vec<ImportBinding>>::new();
325 while cursor < limit && !self.punct(cursor, "}") {
326 if self.kind(cursor) == Some(TokenKind::Identifier)
327 && !matches!(self.text(cursor), "as" | "type")
328 && !self.is(cursor.wrapping_sub(1), "as")
329 && let Some((target, type_only, imported)) =
330 self.import_bindings.get(self.text(cursor))
331 {
332 let local = if self.is(cursor + 1, "as")
333 && self.kind(cursor + 2) == Some(TokenKind::Identifier)
334 {
335 self.text(cursor + 2).to_owned()
336 } else {
337 self.text(cursor).to_owned()
338 };
339 by_target
340 .entry((target.clone(), *type_only))
341 .or_default()
342 .push(ImportBinding {
343 imported: imported.clone(),
344 local,
345 });
346 }
347 cursor += 1;
348 }
349 if !self.punct(cursor, "}") {
350 return None;
351 }
352 for ((specifier, type_only), bindings) in by_target {
353 let names = bindings
354 .iter()
355 .map(|binding| binding.local.clone())
356 .collect();
357 self.facts.imports.push(Import {
358 specifier,
359 span: self.span(start, cursor),
360 type_only,
361 reexport: true,
362 names,
363 bindings,
364 });
365 }
366 Some(cursor + 1)
367 }
368
369 fn route_table(&mut self, index: usize) -> Option<usize> {
375 let path = self.string_at(index)?;
376 if !path.starts_with('/') || !self.punct(index + 1, ":") || !self.punct(index + 2, "{") {
377 return None;
378 }
379 let limit = (index + 128).min(self.tokens.len());
380 let mut cursor = index + 3;
381 let mut depth = 1_i32;
382 while cursor < limit && depth > 0 {
383 if self.punct(cursor, "{") {
384 depth += 1;
385 } else if self.punct(cursor, "}") {
386 depth -= 1;
387 } else if depth == 1
388 && self.kind(cursor) == Some(TokenKind::Identifier)
389 && self.punct(cursor + 1, ":")
390 && is_method(self.text(cursor))
391 {
392 self.facts.references.push(Reference {
393 name: self.text(cursor).to_owned(),
394 kind: ReferenceKind::Call,
395 receiver: None,
396 span: self.span(index, cursor),
397 owner: self.owner(),
398 string_arguments: vec![path.clone()],
399 name_arguments: Vec::new(),
400 });
401 }
402 cursor += 1;
403 }
404 Some(cursor)
405 }
406
407 fn clause_bindings(&self, start: usize, end: usize) -> Vec<ImportBinding> {
409 let mut bindings = Vec::new();
410 let mut scan = start;
411 let mut braced = false;
412 let mut default_seen = false;
413 while scan < end {
414 if self.punct(scan, "{") {
415 braced = true;
416 scan += 1;
417 continue;
418 }
419 if self.punct(scan, "}") {
420 braced = false;
421 scan += 1;
422 continue;
423 }
424 if self.punct(scan, "*")
425 && self.is(scan + 1, "as")
426 && self.kind(scan + 2) == Some(TokenKind::Identifier)
427 {
428 bindings.push(ImportBinding {
429 imported: "*".to_owned(),
430 local: self.text(scan + 2).to_owned(),
431 });
432 scan += 3;
433 continue;
434 }
435 if self.kind(scan) == Some(TokenKind::Identifier) {
436 let text = self.text(scan);
437 if matches!(text, "from" | "type" | "as") || self.is(scan.wrapping_sub(1), "as") {
438 scan += 1;
439 continue;
440 }
441 if self.is(scan + 1, "as") && self.kind(scan + 2) == Some(TokenKind::Identifier) {
442 bindings.push(ImportBinding {
443 imported: text.to_owned(),
444 local: self.text(scan + 2).to_owned(),
445 });
446 scan += 3;
447 continue;
448 }
449 if braced {
450 bindings.push(ImportBinding {
451 imported: text.to_owned(),
452 local: text.to_owned(),
453 });
454 } else if !default_seen {
455 bindings.push(ImportBinding {
456 imported: "default".to_owned(),
457 local: text.to_owned(),
458 });
459 default_seen = true;
460 }
461 }
462 scan += 1;
463 }
464 bindings
465 }
466
467 fn leads_to_from(&self, index: usize) -> bool {
469 let limit = (index + 512).min(self.tokens.len());
470 let mut scan = index;
471 while scan < limit {
472 if self.is(scan, "from") {
473 return true;
474 }
475 if self.punct(scan, ";") || self.punct(scan, "=") {
476 return false;
477 }
478 scan += 1;
479 }
480 false
481 }
482
483 fn braced_types_only(&self, start: usize, end: usize) -> bool {
485 let mut scan = start;
486 let mut inside = false;
487 let mut names = 0_usize;
488 let mut typed = 0_usize;
489 while scan < end {
490 if self.punct(scan, "{") {
491 inside = true;
492 } else if self.punct(scan, "}") {
493 inside = false;
494 } else if inside && self.kind(scan) == Some(TokenKind::Identifier) {
495 if self.text(scan) == "type" {
496 typed += 1;
497 } else if !self.is(scan, "as") {
498 names += 1;
499 }
500 }
501 scan += 1;
502 }
503 names > 0 && typed >= names
504 }
505
506 fn string_at(&self, index: usize) -> Option<String> {
507 if self.kind(index) != Some(TokenKind::String) {
508 return None;
509 }
510 let raw = self.text(index);
511 let trimmed = raw
512 .strip_prefix(['"', '\'', '`'])
513 .and_then(|value| value.strip_suffix(['"', '\'', '`']))
514 .unwrap_or(raw);
515 Some(trimmed.to_owned())
516 }
517
518 fn declaration(&mut self, index: usize) -> Option<usize> {
520 let mut cursor = index;
521 let mut exported = false;
522 if self.is(cursor, "export") {
523 exported = true;
524 cursor += 1;
525 if self.is(cursor, "default") {
526 cursor += 1;
527 }
528 }
529 for keyword in [
530 "async", "declare", "abstract", "static", "public", "private",
531 ] {
532 if self.is(cursor, keyword) {
533 cursor += 1;
534 }
535 }
536 let kind = match self.text(cursor) {
537 "function" => DeclarationKind::Function,
538 "class" => DeclarationKind::Class,
539 "interface" => DeclarationKind::Interface,
540 "enum" => DeclarationKind::Enum,
541 "type" => DeclarationKind::TypeAlias,
542 "const" => DeclarationKind::Constant,
543 "let" | "var" => DeclarationKind::Variable,
544 _ => return self.class_member(cursor, exported),
548 };
549 if self.kind(cursor) != Some(TokenKind::Identifier) {
550 return None;
551 }
552 let name_index = if self.punct(cursor + 1, "*") {
553 cursor + 2
554 } else {
555 cursor + 1
556 };
557 if self.kind(name_index) != Some(TokenKind::Identifier) {
558 return None;
559 }
560 let name = self.text(name_index).to_owned();
561 let kind = if matches!(kind, DeclarationKind::Constant | DeclarationKind::Variable)
563 && self.is_arrow_function(name_index)
564 {
565 DeclarationKind::Function
566 } else {
567 kind
568 };
569 self.facts.declarations.push(Declaration {
570 name: name.clone(),
571 kind,
572 span: self.span(index, name_index),
573 owner: self.owner(),
574 exported,
575 });
576 if matches!(
577 kind,
578 DeclarationKind::Class | DeclarationKind::Interface | DeclarationKind::Enum
579 ) {
580 self.scopes.push(Scope {
581 name,
582 depth: None,
583 member_body: true,
584 fields: true,
585 paren_depth: self.paren_depth,
586 bracket_depth: self.bracket_depth,
587 });
588 } else if matches!(kind, DeclarationKind::Function) {
589 self.scopes.push(Scope {
590 name,
591 depth: None,
592 member_body: false,
593 fields: false,
594 paren_depth: self.paren_depth,
595 bracket_depth: self.bracket_depth,
596 });
597 }
598 Some(name_index + 1)
599 }
600
601 fn is_arrow_function(&self, name_index: usize) -> bool {
604 let limit = (name_index + 64).min(self.tokens.len());
605 let mut scan = name_index + 1;
606 let mut nesting = 0_i32;
607 let mut assignment = false;
608 let mut value_started = false;
609 let declaration_line = self.tokens[name_index].line;
610 while scan < limit {
611 if nesting == 0 && self.punct(scan, "=") && self.punct(scan + 1, ">") {
612 return true;
613 }
614 if nesting == 0
615 && self.tokens[scan].line > declaration_line
616 && assignment
617 && value_started
618 {
619 return false;
620 }
621 if nesting == 0 && self.punct(scan, "=") {
622 assignment = true;
623 scan += 1;
624 continue;
625 }
626 if self.punct(scan, "(") || self.punct(scan, "[") {
627 nesting += 1;
628 } else if self.punct(scan, ")") || self.punct(scan, "]") {
629 nesting -= 1;
630 } else if self.punct(scan, ";") || self.punct(scan, "{") {
631 return false;
632 }
633 if assignment {
634 value_started = true;
635 }
636 scan += 1;
637 }
638 false
639 }
640
641 fn class_member(&mut self, index: usize, exported: bool) -> Option<usize> {
643 let inside_class = self.scopes.last().is_some_and(|scope| {
644 scope.member_body
645 && scope.depth.is_some_and(|depth| self.depth == depth)
646 && scope.paren_depth == self.paren_depth
647 && scope.bracket_depth == self.bracket_depth
648 });
649 if !inside_class || self.kind(index) != Some(TokenKind::Identifier) {
650 return None;
651 }
652 let previous = self.text(index.wrapping_sub(1));
653 let starts_member = matches!(
654 previous,
655 "{" | "}"
656 | ","
657 | ";"
658 | "public"
659 | "private"
660 | "protected"
661 | "static"
662 | "async"
663 | "get"
664 | "set"
665 | "readonly"
666 | "abstract"
667 | "declare"
668 );
669 if !starts_member {
670 return None;
671 }
672 let name = self.text(index).to_owned();
673 if matches!(name.as_str(), "return" | "if" | "for" | "while" | "switch") {
674 return None;
675 }
676 let kind = if self.punct(index + 1, "(") || self.punct(index + 1, "<") {
679 DeclarationKind::Method
680 } else if self.punct(index + 1, ":") || self.punct(index + 1, "=") {
681 if !self.scopes.last().is_some_and(|scope| scope.fields) {
682 return None;
683 }
684 DeclarationKind::Field
685 } else {
686 return None;
687 };
688 self.facts.declarations.push(Declaration {
689 name: name.clone(),
690 kind,
691 span: self.span(index, index),
692 owner: self.owner(),
693 exported,
694 });
695 if kind == DeclarationKind::Method {
696 self.scopes.push(Scope {
697 name,
698 depth: None,
699 member_body: false,
700 fields: false,
701 paren_depth: self.paren_depth,
702 bracket_depth: self.bracket_depth,
703 });
704 return Some(index + 1);
705 }
706 Some(self.skip_initializer(index + 1))
709 }
710
711 fn object_literal_owner(&self, open: usize) -> Option<String> {
712 if self.is(open.wrapping_sub(1), "return") {
713 return self.owner();
714 }
715 if self.punct(open.wrapping_sub(1), "=")
716 && self.kind(open.wrapping_sub(2)) == Some(TokenKind::Identifier)
717 {
718 return Some(self.text(open - 2).to_owned());
719 }
720 if self.punct(open.wrapping_sub(1), ":")
721 && self.kind(open.wrapping_sub(2)) == Some(TokenKind::Identifier)
722 {
723 return Some(self.text(open - 2).to_owned());
724 }
725 if self.punct(open.wrapping_sub(1), "(") {
730 let boundary = open.saturating_sub(24);
731 let mut scan = open - 1;
732 while scan > boundary {
733 scan -= 1;
734 if self.is(scan, "return") {
735 return self.owner();
736 }
737 if self.punct(scan, "=")
738 && self.kind(scan.wrapping_sub(1)) == Some(TokenKind::Identifier)
739 {
740 return Some(self.text(scan - 1).to_owned());
741 }
742 if self.punct(scan, ";") || self.punct(scan, "{") || self.punct(scan, "}") {
743 break;
744 }
745 }
746 }
747 None
748 }
749
750 fn skip_initializer(&self, start: usize) -> usize {
753 let mut scan = start;
754 let mut nesting = 0_i32;
755 let limit = (start + 512).min(self.tokens.len());
756 while scan < limit {
757 if self.punct(scan, "(") || self.punct(scan, "[") || self.punct(scan, "{") {
758 nesting += 1;
759 } else if self.punct(scan, ")") || self.punct(scan, "]") || self.punct(scan, "}") {
760 if nesting == 0 {
761 return scan;
762 }
763 nesting -= 1;
764 } else if nesting == 0 && self.punct(scan, ";") {
765 return scan + 1;
766 }
767 scan += 1;
768 }
769 start
770 }
771
772 fn call(&mut self, index: usize) -> Option<usize> {
774 let type_arguments = self.type_argument_span(index + 1);
775 let open = index + 1 + type_arguments;
776 if !self.punct(open, "(") {
777 return None;
778 }
779 let name = self.text(index).to_owned();
780 if matches!(
781 name.as_str(),
782 "if" | "for" | "while" | "switch" | "catch" | "function" | "return" | "typeof"
783 ) {
784 return None;
785 }
786 let receiver = (self.punct(index.wrapping_sub(1), ".")
787 && self.kind(index.wrapping_sub(2)) == Some(TokenKind::Identifier))
788 .then(|| self.text(index - 2).to_owned());
789 let mut arguments = Vec::new();
790 let mut names = Vec::new();
791 let mut scan = open + 1;
792 let mut depth = 1_i32;
793 let mut nested = 0_i32;
794 let limit = (index + 256).min(self.tokens.len());
795 while scan < limit && depth > 0 {
796 if self.punct(scan, "(") {
797 depth += 1;
798 } else if self.punct(scan, ")") {
799 depth -= 1;
800 } else if self.punct(scan, "{") || self.punct(scan, "[") {
801 nested += 1;
802 } else if self.punct(scan, "}") || self.punct(scan, "]") {
803 nested -= 1;
804 } else if depth == 1
805 && nested == 0
806 && self.kind(scan) == Some(TokenKind::String)
807 && let Some(value) = self.string_at(scan)
808 {
809 arguments.push(value);
810 } else if depth == 1 && nested == 0 && self.kind(scan) == Some(TokenKind::Identifier) {
811 if !self.punct(scan.wrapping_sub(1), ".") {
816 names.push(self.text(scan).to_owned());
817 }
818 }
819 scan += 1;
820 }
821 if name == "require"
824 && receiver.is_none()
825 && let Some(specifier) = arguments.first()
826 {
827 let bound = if self.punct(index.wrapping_sub(1), "=")
831 && self.kind(index.wrapping_sub(2)) == Some(TokenKind::Identifier)
832 {
833 vec![self.text(index - 2).to_owned()]
834 } else {
835 Vec::new()
836 };
837 self.facts.imports.push(Import {
838 specifier: specifier.clone(),
839 span: self.span(index, index),
840 type_only: false,
841 reexport: false,
842 bindings: bound
843 .iter()
844 .map(|local| ImportBinding {
845 imported: "*".to_owned(),
846 local: local.clone(),
847 })
848 .collect(),
849 names: bound,
850 });
851 }
852 self.facts.references.push(Reference {
853 kind: ReferenceKind::Call,
854 name,
855 receiver,
856 span: self.span(index, index),
857 owner: self.owner(),
858 string_arguments: arguments,
859 name_arguments: names,
860 });
861 Some(index + 1)
862 }
863
864 fn type_argument_span(&self, index: usize) -> usize {
866 if !self.punct(index, "<") {
867 return 0;
868 }
869 let limit = (index + 64).min(self.tokens.len());
870 let mut cursor = index + 1;
871 let mut depth = 1_i32;
872 while cursor < limit && depth > 0 {
873 if self.punct(cursor, "<") {
874 depth += 1;
875 } else if self.punct(cursor, ">") {
876 depth -= 1;
877 } else if depth == 1 && matches!(self.text(cursor), ";" | "{" | "}") {
878 return 0;
879 }
880 cursor += 1;
881 }
882 if depth == 0 && self.punct(cursor, "(") {
883 cursor - index
884 } else {
885 0
886 }
887 }
888}
889
890fn is_method(name: &str) -> bool {
892 matches!(
893 name,
894 "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "ALL"
895 )
896}
897
898fn template_interpolation_ranges(template: &str, language: Language) -> Vec<(usize, usize)> {
902 let bytes = template.as_bytes();
903 let mut ranges = Vec::new();
904 let mut cursor = usize::from(bytes.first() == Some(&b'`'));
905 while cursor + 1 < bytes.len() {
906 if bytes[cursor] == b'\\' {
907 cursor = (cursor + 2).min(bytes.len());
908 continue;
909 }
910 if bytes[cursor] == b'`' {
911 break;
912 }
913 if bytes[cursor] != b'$' || bytes[cursor + 1] != b'{' {
914 cursor += 1;
915 continue;
916 }
917 let expression_start = cursor + 2;
918 let tail = &template[expression_start..];
919 let tokens = Tokenizer::new(tail, language)
920 .mode(Mode::Lite)
921 .collect::<Vec<_>>();
922 let mut depth = 1_i32;
923 let mut expression_end = None;
924 for token in tokens {
925 if token.kind != TokenKind::Punctuation {
926 continue;
927 }
928 match token.text(tail) {
929 "{" => depth += 1,
930 "}" => {
931 depth -= 1;
932 if depth == 0 {
933 expression_end = Some(expression_start + token.start);
934 cursor = expression_start + token.end;
935 break;
936 }
937 }
938 _ => {}
939 }
940 }
941 let Some(expression_end) = expression_end else {
942 break;
943 };
944 ranges.push((expression_start, expression_end));
945 }
946 ranges
947}
948
949fn position_at(source: &str, offset: usize) -> (u32, u32) {
950 let prefix = source.get(..offset).unwrap_or(source);
951 let line = u32::try_from(prefix.bytes().filter(|byte| *byte == b'\n').count())
952 .unwrap_or(u32::MAX)
953 .saturating_add(1);
954 let column = u32::try_from(
955 prefix
956 .rsplit_once('\n')
957 .map_or(prefix, |(_, suffix)| suffix)
958 .chars()
959 .count(),
960 )
961 .unwrap_or(u32::MAX)
962 .saturating_add(1);
963 (line, column)
964}
965
966#[cfg(test)]
967mod tests {
968 #[test]
969 fn a_default_import_alongside_named_ones_is_still_an_import() {
970 use crate::syntax::Language;
971
972 let source = "import './sideEffect.js';\n\
973 import express from 'express';\n\
974 import logger, { logRequest, logAction } from './logger.js';\n\
975 import * as tty from 'node:tty';\n\
976 import type { Config } from './config';\n\
977 export { helper } from './helper.js';\n\
978 const meta = import.meta.url;\n";
979 let facts = super::extract(source, Language::TypeScript);
980 assert_eq!(
981 facts
982 .imports
983 .iter()
984 .map(|import| import.specifier.as_str())
985 .collect::<Vec<_>>(),
986 [
987 "./sideEffect.js",
988 "express",
989 "./logger.js",
990 "node:tty",
991 "./config",
992 "./helper.js",
993 ],
994 "a name before the brace must not end the clause, and import.meta \
995 is not a module statement"
996 );
997 }
998
999 #[test]
1004 fn jsx_does_not_derail_the_token_stream() {
1005 use crate::syntax::Language;
1006 use crate::token::{TokenKind, tokenize};
1007
1008 let source = "import React from 'react';\n\
1009 import { Button } from './Button';\n\
1010 export function Panel({ items, onPick }) {\n\
1011 \x20 const ratio = items.length / 2;\n\
1012 \x20 return (\n\
1013 \x20 <div className=\"panel\" data-count={items.length}>\n\
1014 \x20 {items.map((item) => (\n\
1015 \x20 <Button key={item.id} onClick={() => onPick(item)}>\n\
1016 \x20 {item.label} / {ratio}\n\
1017 \x20 </Button>\n\
1018 \x20 ))}\n\
1019 \x20 </div>\n\
1020 \x20 );\n\
1021 }\n\
1022 export const Footer = () => <footer>done</footer>;\n";
1023 let tokens = tokenize(source, Language::TypeScript);
1024 assert_eq!(
1025 tokens
1026 .iter()
1027 .map(|token| token.text(source))
1028 .collect::<String>(),
1029 source,
1030 "the stream must still reproduce the file"
1031 );
1032 assert!(
1033 !tokens
1034 .iter()
1035 .any(|token| matches!(token.kind, TokenKind::Regex | TokenKind::Unterminated)),
1036 "no division in JSX may be read as a regular expression"
1037 );
1038 let facts = super::extract(source, Language::TypeScript);
1039 assert_eq!(
1040 facts
1041 .imports
1042 .iter()
1043 .map(|import| import.specifier.as_str())
1044 .collect::<Vec<_>>(),
1045 ["react", "./Button"]
1046 );
1047 for name in ["Panel", "Footer"] {
1048 assert!(
1049 facts.declarations.iter().any(|item| item.name == name),
1050 "{name} must survive the markup, got {:?}",
1051 facts
1052 .declarations
1053 .iter()
1054 .map(|item| item.name.as_str())
1055 .collect::<Vec<_>>()
1056 );
1057 }
1058 }
1059
1060 use super::extract;
1061 use crate::facts::{DeclarationKind, ImportBinding, ReferenceKind};
1062 use crate::syntax::Language;
1063
1064 fn specifiers(source: &str) -> Vec<(String, bool, bool)> {
1065 extract(source, Language::TypeScript)
1066 .imports
1067 .into_iter()
1068 .map(|import| (import.specifier, import.type_only, import.reexport))
1069 .collect()
1070 }
1071
1072 #[test]
1073 fn reads_every_module_form_including_multi_line() {
1074 let source = "import defaultExport from './a';\n\
1075 import {\n first,\n second,\n} from './b';\n\
1076 import type { Shape } from './c';\n\
1077 import { type Only } from './d';\n\
1078 import * as everything from './e';\n\
1079 import './f';\n\
1080 const legacy = require('./g');\n\
1081 const lazy = await import('./h');\n\
1082 export { thing } from './i';\n\
1083 export * from './j';\n";
1084 assert_eq!(
1085 specifiers(source),
1086 [
1087 ("./a".to_owned(), false, false),
1088 ("./b".to_owned(), false, false),
1089 ("./c".to_owned(), true, false),
1090 ("./d".to_owned(), true, false),
1091 ("./e".to_owned(), false, false),
1092 ("./f".to_owned(), false, false),
1093 ("./g".to_owned(), false, false),
1094 ("./h".to_owned(), false, false),
1095 ("./i".to_owned(), false, true),
1096 ("./j".to_owned(), false, true),
1097 ],
1098 "a multi-line import is one fact, and type-only is distinguished"
1099 );
1100 }
1101
1102 #[test]
1103 fn a_comment_or_string_never_becomes_a_fact() {
1104 let source = "// import { fake } from './nope';\n\
1105 const text = \"import { alsoFake } from './nope2'\";\n\
1106 /* app.get('/commented-route', handler); */\n\
1107 import { real } from './yes';\n\
1108 app.get('/real-route', handler);\n";
1109 assert_eq!(specifiers(source), [("./yes".to_owned(), false, false)]);
1110 let facts = extract(source, Language::TypeScript);
1111 let routes = facts
1112 .references
1113 .iter()
1114 .flat_map(|call| call.string_arguments.clone())
1115 .collect::<Vec<_>>();
1116 assert_eq!(
1117 routes,
1118 ["/real-route"],
1119 "the commented route is not a call argument"
1120 );
1121 }
1122
1123 #[test]
1124 fn class_bodies_yield_methods_and_fields_with_their_owner() {
1125 let source = "export class Service {\n\
1126 \x20 private cache = new Map();\n\
1127 \x20 readonly limit: number = 10;\n\
1128 \x20 async run(input: string) {\n\
1129 \x20 return this.helper(input);\n\
1130 \x20 }\n\
1131 \x20 helper(value: string) { return value; }\n\
1132 }\n";
1133 let facts = extract(source, Language::TypeScript);
1134 let declared = facts
1135 .declarations
1136 .iter()
1137 .map(|item| (item.name.as_str(), item.kind, item.owner.as_deref()))
1138 .collect::<Vec<_>>();
1139 assert!(
1140 declared.contains(&("Service", DeclarationKind::Class, None)),
1141 "got {declared:?}"
1142 );
1143 assert!(
1144 declared.contains(&("run", DeclarationKind::Method, Some("Service"))),
1145 "a class method is a declaration owned by its class, got {declared:?}"
1146 );
1147 assert!(
1148 declared.contains(&("helper", DeclarationKind::Method, Some("Service"))),
1149 "got {declared:?}"
1150 );
1151 assert!(
1152 declared.contains(&("cache", DeclarationKind::Field, Some("Service"))),
1153 "got {declared:?}"
1154 );
1155 let call = facts
1156 .references
1157 .iter()
1158 .find(|call| call.name == "helper")
1159 .expect("the call inside run is recorded");
1160 assert_eq!(call.receiver.as_deref(), Some("this"));
1161 assert_eq!(call.owner.as_deref(), Some("run"));
1162 }
1163
1164 #[test]
1165 fn arrow_constants_are_functions_and_plain_constants_are_not() {
1166 let source = "export const load = async () => { return 1; };\n\
1167 const multiline =\n(value) => value;\n\
1168 const limit = 10;\n";
1169 let facts = extract(source, Language::TypeScript);
1170 let kinds = facts
1171 .declarations
1172 .iter()
1173 .map(|item| (item.name.as_str(), item.kind, item.exported))
1174 .collect::<Vec<_>>();
1175 assert!(
1176 kinds.contains(&("load", DeclarationKind::Function, true)),
1177 "got {kinds:?}"
1178 );
1179 assert!(
1180 kinds.contains(&("multiline", DeclarationKind::Function, false)),
1181 "got {kinds:?}"
1182 );
1183 assert!(
1184 kinds.contains(&("limit", DeclarationKind::Constant, false)),
1185 "got {kinds:?}"
1186 );
1187 }
1188
1189 #[test]
1190 fn regexes_and_collection_initializers_are_not_arrow_functions() {
1191 let source = "const SAFE_SCRIPT = /^(?:test(?::|$)|[^:]+:(?:test|check)(?::|$))/i\n\
1192 const UNSAFE_SHELL_ARG = /[\\0\\r\\n&|<>^%!`\\\"]/ \n\
1193 const byId = new Map((graph.nodes || []).map((node) => [String(node.id), node]))\n\
1194 const files = new Set((graph.nodes || []).filter((node) => node.id))\n\
1195 const adjacency = new Map([...files].map((file) => [file, new Set()]))\n";
1196 let facts = extract(source, Language::JavaScript);
1197 for name in [
1198 "SAFE_SCRIPT",
1199 "UNSAFE_SHELL_ARG",
1200 "byId",
1201 "files",
1202 "adjacency",
1203 ] {
1204 let declaration = facts
1205 .declarations
1206 .iter()
1207 .find(|item| item.name == name)
1208 .unwrap_or_else(|| panic!("missing {name}: {facts:?}"));
1209 assert_eq!(
1210 declaration.kind,
1211 DeclarationKind::Constant,
1212 "{name} is a value initializer"
1213 );
1214 }
1215 }
1216
1217 #[test]
1218 fn exported_functions_and_returned_object_methods_are_declarations() {
1219 let source = "export function runCommand(command, args = [], options = {}) {}\n\
1220 export function createGate() {\n\
1221 \x20 return {\n\
1222 \x20 shouldShow({ force = false } = {}) { return force },\n\
1223 \x20 reset() {},\n\
1224 \x20 }\n\
1225 }\n\
1226 export function createClassifier() {\n\
1227 \x20 return { explain(path, options = {}) { return path } }\n\
1228 }\n";
1229 let facts = extract(source, Language::JavaScript);
1230 let declared = facts
1231 .declarations
1232 .iter()
1233 .map(|item| (item.name.as_str(), item.kind, item.owner.as_deref()))
1234 .collect::<Vec<_>>();
1235 assert!(
1236 declared.contains(&("runCommand", DeclarationKind::Function, None)),
1237 "got {declared:?}"
1238 );
1239 for (name, owner) in [
1240 ("shouldShow", "createGate"),
1241 ("reset", "createGate"),
1242 ("explain", "createClassifier"),
1243 ] {
1244 assert!(
1245 declared.contains(&(name, DeclarationKind::Method, Some(owner))),
1246 "missing {owner}.{name}; got {declared:?}"
1247 );
1248 }
1249 }
1250
1251 #[test]
1252 fn exporting_an_imported_binding_keeps_its_origin() {
1253 let source =
1254 "import { safeRead, MAX_FILE_BYTES } from '../util.js';\nexport { safeRead };\n";
1255 let facts = extract(source, Language::JavaScript);
1256 let forwarded = facts
1257 .imports
1258 .iter()
1259 .find(|item| item.reexport)
1260 .expect("local export of imported binding");
1261 assert_eq!(forwarded.specifier, "../util.js");
1262 assert_eq!(forwarded.names, ["safeRead"]);
1263 assert_eq!(
1264 forwarded.bindings,
1265 [ImportBinding {
1266 imported: "safeRead".to_owned(),
1267 local: "safeRead".to_owned(),
1268 }]
1269 );
1270 }
1271
1272 #[test]
1273 fn aliased_imports_preserve_original_and_local_names() {
1274 let facts = extract(
1275 "import Default, {\n\
1276 \x20 architectureViolation as violation,\n\
1277 \x20 matchComponentSelector as matches,\n\
1278 } from './architecture.js';\n\
1279 import * as catalog from './catalog.js';\n",
1280 Language::JavaScript,
1281 );
1282 let architecture = facts
1283 .imports
1284 .iter()
1285 .find(|item| item.specifier == "./architecture.js")
1286 .expect("architecture import");
1287 assert_eq!(architecture.names, ["Default", "violation", "matches"]);
1288 assert_eq!(
1289 architecture.bindings,
1290 [
1291 ImportBinding {
1292 imported: "default".to_owned(),
1293 local: "Default".to_owned(),
1294 },
1295 ImportBinding {
1296 imported: "architectureViolation".to_owned(),
1297 local: "violation".to_owned(),
1298 },
1299 ImportBinding {
1300 imported: "matchComponentSelector".to_owned(),
1301 local: "matches".to_owned(),
1302 },
1303 ]
1304 );
1305 let catalog = facts
1306 .imports
1307 .iter()
1308 .find(|item| item.specifier == "./catalog.js")
1309 .expect("namespace import");
1310 assert_eq!(
1311 catalog.bindings,
1312 [ImportBinding {
1313 imported: "*".to_owned(),
1314 local: "catalog".to_owned(),
1315 }]
1316 );
1317 }
1318
1319 #[test]
1320 fn nested_template_text_never_becomes_a_call() {
1321 let source = r"function exactUsage(files) {
1322 return `${files ? ` in ${plural(files)} file(s)` : ''}`
1323}
1324";
1325 let facts = extract(source, Language::JavaScript);
1326 assert!(
1327 !facts
1328 .references
1329 .iter()
1330 .any(|reference| reference.name == "file"),
1331 "`file(s)` is literal template text, got {:?}",
1332 facts.references
1333 );
1334 let plural = facts
1335 .references
1336 .iter()
1337 .find(|reference| reference.name == "plural")
1338 .expect("call in a nested interpolation");
1339 assert_eq!(plural.kind, ReferenceKind::Call);
1340 assert_eq!(plural.owner.as_deref(), Some("exactUsage"));
1341 assert_eq!(plural.span.line, 2);
1342 }
1343
1344 #[test]
1345 fn template_interpolations_keep_all_calls_and_exact_spans() {
1346 let source = r"function describe(blob, name, edge, graph) {
1347 const mentioned = new RegExp(`x${escRe(name)}y`).test(blob)
1348 return `${compileKind(edge) ? labelOf(graph, edge.id) : ''}`
1349}
1350";
1351 let facts = extract(source, Language::JavaScript);
1352 let calls = facts
1353 .references
1354 .iter()
1355 .filter(|reference| reference.kind == ReferenceKind::Call)
1356 .collect::<Vec<_>>();
1357 for (name, line) in [
1358 ("RegExp", 2),
1359 ("escRe", 2),
1360 ("test", 2),
1361 ("compileKind", 3),
1362 ("labelOf", 3),
1363 ] {
1364 let reference = calls
1365 .iter()
1366 .find(|reference| reference.name == name)
1367 .unwrap_or_else(|| panic!("missing {name}, got {calls:?}"));
1368 assert_eq!(reference.span.line, line);
1369 assert_eq!(
1370 &source[reference.span.start..reference.span.end],
1371 name,
1372 "relocated span must name the original call"
1373 );
1374 assert_eq!(reference.owner.as_deref(), Some("describe"));
1375 }
1376 }
1377
1378 #[test]
1379 fn nested_call_arguments_are_not_object_methods() {
1380 let source = r"function build(makeClient, session) {
1381 return {
1382 make: () => makeClient({ timeoutMs: Math.max(100, remaining(session)) }),
1383 }
1384}
1385";
1386 let facts = extract(source, Language::JavaScript);
1387 let remaining = facts
1388 .references
1389 .iter()
1390 .filter(|reference| {
1391 reference.name == "remaining" && reference.kind == ReferenceKind::Call
1392 })
1393 .collect::<Vec<_>>();
1394 assert_eq!(remaining.len(), 1, "got {:?}", facts.references);
1395 assert_eq!(remaining[0].span.line, 3);
1396 assert!(
1397 !facts.declarations.iter().any(|declaration| {
1398 declaration.name == "remaining" && declaration.kind == DeclarationKind::Method
1399 }),
1400 "a nested argument is not an object method: {:?}",
1401 facts.declarations
1402 );
1403 }
1404
1405 #[test]
1406 fn default_object_parameter_is_not_the_function_body() {
1407 let source = r"export function runCommand(command, args = [], options = {}) {
1408 return spawn(command, args, { env: childProcessEnv(options.env || {}) })
1409}
1410";
1411 let facts = extract(source, Language::JavaScript);
1412 let declaration = facts
1413 .declarations
1414 .iter()
1415 .find(|declaration| declaration.name == "runCommand")
1416 .expect("exported function declaration");
1417 assert_eq!(declaration.kind, DeclarationKind::Function);
1418 assert!(declaration.exported);
1419 let environment = facts
1420 .references
1421 .iter()
1422 .find(|reference| reference.name == "childProcessEnv")
1423 .expect("call inside function body");
1424 assert_eq!(environment.owner.as_deref(), Some("runCommand"));
1425 }
1426
1427 #[test]
1428 fn call_in_returned_object_property_is_retained() {
1429 let source = r"function withGraph(graph) {
1430 const root = mkdtempSync('prefix')
1431 const graphPath = join(root, 'graph.json')
1432 return {root, graphPath, graph: loadGraph(graphPath)}
1433}
1434";
1435 let facts = extract(source, Language::JavaScript);
1436 let load = facts
1437 .references
1438 .iter()
1439 .find(|reference| {
1440 reference.name == "loadGraph" && reference.kind == ReferenceKind::Call
1441 })
1442 .unwrap_or_else(|| panic!("missing loadGraph, got {facts:?}"));
1443 assert_eq!(load.owner.as_deref(), Some("withGraph"));
1444 assert_eq!(load.span.line, 4);
1445 }
1446
1447 #[test]
1448 fn object_method_names_are_not_calls_but_their_bodies_are() {
1449 let source = r"function wrap(client) {
1450 return Object.freeze({
1451 fromUri(uri) { return client.normalizer.fromUri(uri) },
1452 kill() { client.kill() },
1453 })
1454}
1455";
1456 let facts = extract(source, Language::JavaScript);
1457 let calls = facts
1458 .references
1459 .iter()
1460 .filter(|reference| reference.kind == ReferenceKind::Call)
1461 .collect::<Vec<_>>();
1462 assert_eq!(
1463 calls
1464 .iter()
1465 .filter(|reference| reference.name == "fromUri")
1466 .count(),
1467 1,
1468 "only the body call is a reference, got {calls:?}"
1469 );
1470 let from_uri = calls
1471 .iter()
1472 .find(|reference| reference.name == "fromUri")
1473 .expect("body call");
1474 assert_eq!(from_uri.receiver.as_deref(), Some("normalizer"));
1475 assert_eq!(from_uri.span.line, 3);
1476 assert!(
1477 from_uri.span.column > 30,
1478 "the call must point inside the body, got {:?}",
1479 from_uri.span
1480 );
1481 assert_eq!(
1482 calls
1483 .iter()
1484 .filter(|reference| reference.name == "kill")
1485 .count(),
1486 1,
1487 "only client.kill() is a call, got {calls:?}"
1488 );
1489 }
1490
1491 #[test]
1492 fn typescript_generic_calls_keep_their_call_fact() {
1493 let facts = extract(
1494 "export async function loadUser() { return get<User>('/users/1'); }\n",
1495 Language::TypeScript,
1496 );
1497 let call = facts
1498 .references
1499 .iter()
1500 .find(|reference| reference.name == "get" && reference.kind == ReferenceKind::Call)
1501 .expect("generic call");
1502 assert_eq!(call.string_arguments, ["/users/1"]);
1503 }
1504
1505 #[test]
1506 fn calls_inside_object_fields_and_nested_arguments_are_all_retained() {
1507 let source = "function score(entry, count, total) {\n\
1508 \x20 return {...entry, hotspotScore: round(Math.sqrt(entry.value))}\n\
1509 }\n\
1510 function pair(pairs, count, total) {\n\
1511 \x20 pairs.push({jaccard: round(count / total), lift: round(Math.max(count, total))})\n\
1512 }\n";
1513 let facts = extract(source, Language::JavaScript);
1514 let calls = facts
1515 .references
1516 .iter()
1517 .filter(|reference| reference.kind == ReferenceKind::Call)
1518 .map(|reference| (reference.name.as_str(), reference.span.line))
1519 .collect::<Vec<_>>();
1520 assert_eq!(
1521 calls.iter().filter(|(name, _)| *name == "round").count(),
1522 3,
1523 "every aliased round call must survive, got {calls:?}"
1524 );
1525 for expected in [
1526 ("round", 2),
1527 ("sqrt", 2),
1528 ("push", 5),
1529 ("round", 5),
1530 ("max", 5),
1531 ] {
1532 assert!(
1533 calls.contains(&expected),
1534 "missing {expected:?}; got {calls:?}"
1535 );
1536 }
1537 for false_method in ["round", "sqrt", "max"] {
1538 assert!(
1539 !facts.declarations.iter().any(|declaration| {
1540 declaration.name == false_method && declaration.kind == DeclarationKind::Method
1541 }),
1542 "a call used as an object value is not a method declaration"
1543 );
1544 }
1545 }
1546}