1use sinter_core::{
2 Edge, Evidence, FieldBinding, Node, NodeId, Reference, Relation, Span, SymbolKind,
3};
4use streaming_iterator::StreamingIterator;
5use tree_sitter::{Node as TsNode, Parser, Query, QueryCursor};
6
7const DOC_CAP_BYTES: usize = 4096;
10
11use sinter_core::FileFacts;
12
13use crate::language::LanguageSpec;
14
15#[derive(Debug, thiserror::Error)]
16pub enum ExtractError {
17 #[error("bad grammar or query for {language}: {message}")]
18 Query {
19 language: &'static str,
20 message: String,
21 },
22 #[error("parser returned no tree for {0}")]
23 Parse(String),
24}
25
26pub struct Extractor {
29 spec: &'static LanguageSpec,
30 parser: Parser,
31 query: Query,
32 inline: Option<(Parser, Query)>,
36}
37
38struct RawEntry {
40 start: usize,
41 end: usize,
42 name: String,
43 kind: Option<SymbolKind>,
45 qualifier: Option<String>,
47 signature: String,
48 doc: Option<String>,
49}
50
51struct RawRef {
53 start: usize,
54 end: usize,
55 name: String,
56 path: Option<String>,
57 alias: Option<String>,
58 relation: Relation,
59}
60
61struct RawLocal {
63 start: usize,
64 end: usize,
65 name: String,
66 type_name: Option<String>,
67}
68
69#[derive(Default)]
71struct Collected {
72 refs: Vec<RawRef>,
73 locals: Vec<RawLocal>,
74 fields: Vec<(usize, usize, String, String)>,
76 embeds: Vec<(usize, usize, String)>,
78 trait_impls: Vec<(usize, usize, String)>,
80 alias_spans: Vec<(usize, usize)>,
83 def_name_spans: Vec<(usize, usize)>,
86 docs: Vec<(usize, usize, String)>,
90}
91
92impl Extractor {
93 pub fn new(spec: &'static LanguageSpec) -> Result<Self, ExtractError> {
94 let language = (spec.grammar)();
95 let mut parser = Parser::new();
96 parser
97 .set_language(&language)
98 .map_err(|e| ExtractError::Query {
99 language: spec.name,
100 message: e.to_string(),
101 })?;
102 let query = Query::new(&language, spec.query_source).map_err(|e| ExtractError::Query {
103 language: spec.name,
104 message: e.to_string(),
105 })?;
106 let inline = spec
107 .inline
108 .map(|i| {
109 let language = (i.grammar)();
110 let mut parser = Parser::new();
111 parser
112 .set_language(&language)
113 .map_err(|e| (spec.name, e.to_string()))?;
114 let query = Query::new(&language, i.query_source)
115 .map_err(|e| (spec.name, e.to_string()))?;
116 Ok((parser, query))
117 })
118 .transpose()
119 .map_err(
120 |(language, message): (&'static str, String)| ExtractError::Query {
121 language,
122 message,
123 },
124 )?;
125 Ok(Self {
126 spec,
127 parser,
128 query,
129 inline,
130 })
131 }
132
133 pub fn extract(&mut self, file: &str, source: &str) -> Result<FileFacts, ExtractError> {
135 let tree = self
136 .parser
137 .parse(source, None)
138 .ok_or_else(|| ExtractError::Parse(file.to_string()))?;
139 let root = tree.root_node();
140
141 let mut entries = Vec::new();
142 let mut collected = Collected::default();
143 collect(
144 &self.query,
145 self.spec,
146 root,
147 source,
148 &mut entries,
149 &mut collected,
150 );
151 if let (Some((parser, query)), Some(ispec)) = (&mut self.inline, self.spec.inline) {
155 let ranges = container_ranges(root, ispec.container_kinds);
156 if !ranges.is_empty() {
157 parser.set_included_ranges(&ranges).map_err(|e| {
163 ExtractError::Parse(format!("{} (inline ranges: {e})", self.spec.name))
164 })?;
165 let inline_tree = parser
166 .parse(source, None)
167 .ok_or_else(|| ExtractError::Parse(format!("{} (inline)", self.spec.name)))?;
168 collect(
169 query,
170 self.spec,
171 inline_tree.root_node(),
172 source,
173 &mut entries,
174 &mut collected,
175 );
176 }
177 }
178 entries.sort_by_key(|e| (e.start, usize::MAX - e.end));
179 let mut doc_set = vec![false; entries.len()];
184 for (d_start, d_end, text) in &collected.docs {
185 let owner = entries
186 .iter_mut()
187 .enumerate()
188 .filter(|(_, e)| e.kind.is_some() && e.start <= *d_start && *d_end <= e.end)
189 .min_by_key(|(_, e)| e.end - e.start);
190 if let Some((i, entry)) = owner {
191 let cleaned: Vec<&str> = text.lines().map(str::trim).collect();
192 let trimmed = cleaned.join("\n");
193 let trimmed = trimmed.trim_matches('\n');
194 if trimmed.is_empty() {
195 continue;
196 }
197 entry.doc = match entry.doc.take().filter(|_| doc_set[i]) {
198 Some(prev) if prev.len() >= DOC_CAP_BYTES => Some(prev),
201 Some(prev) => Some(format!("{prev}\n\n{trimmed}")),
202 None => Some(trimmed.to_string()),
203 };
204 doc_set[i] = true;
205 }
206 }
207 entries.dedup_by(|b, a| {
211 let same = a.start == b.start && a.end == b.end && a.name == b.name;
212 if same && a.kind == Some(SymbolKind::Variable) && b.kind.is_some() {
213 a.kind = b.kind;
214 }
215 same
216 });
217
218 let file_id = NodeId::new(file);
219 let mut nodes = vec![Node {
220 id: file_id.clone(),
221 kind: SymbolKind::File,
222 name: file.to_string(),
223 file: file.to_string(),
224 span: Span {
225 start: 0,
226 end: source.len().max(1) as u64,
227 },
228 signature: String::new(),
229 doc: None,
230 }];
231 let mut contains = Vec::new();
232
233 let mut stack: Vec<(usize, String, Option<NodeId>)> = Vec::new();
235 let mut def_spans: Vec<(usize, usize, NodeId, SymbolKind)> = Vec::new();
238
239 for entry in &entries {
240 while stack.last().is_some_and(|(end, _, _)| *end <= entry.start) {
241 stack.pop();
242 }
243 let mut path: Vec<&str> = stack.iter().map(|(_, name, _)| name.as_str()).collect();
244 if let Some(q) = &entry.qualifier {
245 path.push(q);
246 }
247 path.push(&entry.name);
248 let qualified = path.join("::");
249 let scope_segment = entry
251 .qualifier
252 .as_ref()
253 .map_or(entry.name.clone(), |q| format!("{q}::{}", entry.name));
254
255 let id = if let Some(kind) = entry.kind {
256 let id = NodeId::new(format!("{file}#{qualified}@{}", entry.start));
257 let parent = stack
258 .iter()
259 .rev()
260 .find_map(|(_, _, id)| id.clone())
261 .unwrap_or_else(|| file_id.clone());
262 nodes.push(Node {
263 id: id.clone(),
264 kind,
265 name: entry.name.clone(),
266 file: file.to_string(),
267 span: Span {
268 start: entry.start as u64,
269 end: entry.end as u64,
270 },
271 signature: entry.signature.clone(),
272 doc: entry.doc.clone(),
273 });
274 contains.push(Edge {
275 src: parent,
276 dst: id.clone(),
277 relation: Relation::Contains,
278 evidence: Evidence::Structural,
279 confidence: Evidence::Structural.confidence(),
280 site: None,
282 });
283 def_spans.push((entry.start, entry.end, id.clone(), kind));
284 Some(id)
285 } else {
286 None
287 };
288 stack.push((entry.end, scope_segment, id));
289 }
290
291 let mut refs = collected.refs;
294 refs.retain(|r| {
295 r.relation == Relation::Creates || !collected.def_name_spans.contains(&(r.start, r.end))
296 });
297 if self.spec.name == "rust" {
300 refs.retain(|r| {
301 r.path.is_some()
302 || !RUST_PRELUDE.contains(&r.name.as_str())
303 || entries.iter().any(|e| e.name == r.name)
304 });
305 }
306 let relation_priority = |relation: Relation| match relation {
311 Relation::Writes => 0,
312 Relation::Reads => 1,
313 _ => 2,
314 };
315 refs.sort_by_key(|r| {
316 (
317 r.start,
318 r.end,
319 r.path.is_none(),
320 relation_priority(r.relation),
321 )
322 });
323 refs.dedup_by_key(|r| (r.start, r.end));
324 let references = refs
325 .into_iter()
326 .map(|r| {
327 let enclosing = (r.relation != Relation::Creates)
331 .then(|| {
332 def_spans
333 .iter()
334 .filter(|(s, e, _, _)| *s <= r.start && r.end <= *e)
335 .min_by_key(|(s, e, _, _)| e - s)
336 .map(|(_, _, id, _)| id.clone())
337 })
338 .flatten();
339 Reference {
340 file: file.to_string(),
341 name: r.name,
342 path: r.path,
343 relation: r.relation,
344 span: Span {
345 start: r.start as u64,
346 end: r.end as u64,
347 },
348 enclosing,
349 alias: r.alias,
350 }
351 })
352 .collect();
353
354 let alias_spans = collected.alias_spans;
358 let embeds = collected
359 .embeds
360 .iter()
361 .filter_map(|(start, end, type_name)| {
362 let owner = def_spans
363 .iter()
364 .filter(|(s, e, _, _)| s <= start && end <= e)
365 .min_by_key(|(s, e, _, _)| e - s)
366 .map(|(_, _, id, _)| id.clone())?;
367 Some(sinter_core::Embed {
368 owner,
369 type_name: type_name.clone(),
370 })
371 })
372 .collect();
373 let mut raw_locals = collected
374 .locals
375 .into_iter()
376 .filter(|l| !alias_spans.contains(&(l.start, l.end)))
377 .collect::<Vec<_>>();
378 raw_locals.sort_by_key(|l| (l.start, l.end, l.name.clone(), l.type_name.is_none()));
381 raw_locals.dedup_by(|b, a| a.start == b.start && a.end == b.end && a.name == b.name);
382 let locals = raw_locals
383 .into_iter()
384 .map(|l| {
385 let scope_end = def_spans
386 .iter()
387 .filter(|(s, e, _, _)| *s <= l.start && l.end <= *e)
388 .min_by_key(|(s, e, _, _)| e - s)
389 .map_or(source.len() as u64, |(_, e, _, _)| *e as u64);
390 sinter_core::LocalBinding {
391 file: file.to_string(),
392 name: l.name,
393 span: Span {
394 start: l.start as u64,
395 end: l.end as u64,
396 },
397 scope_end,
398 type_name: l.type_name,
399 }
400 })
401 .collect();
402
403 let fields = collected
404 .fields
405 .iter()
406 .filter_map(|(start, end, name, type_name)| {
407 let owner = def_spans
408 .iter()
409 .filter(|(s, e, _, kind)| {
410 *s <= *start
411 && *end <= *e
412 && matches!(
413 kind,
414 SymbolKind::Struct
415 | SymbolKind::Class
416 | SymbolKind::Trait
417 | SymbolKind::Interface
418 )
419 })
420 .min_by_key(|(s, e, _, _)| e - s)
421 .map(|(_, _, id, _)| id.clone())?;
422 Some(FieldBinding {
423 owner,
424 name: name.clone(),
425 type_name: type_name.clone(),
426 })
427 })
428 .collect();
429
430 let trait_impls = collected
431 .trait_impls
432 .iter()
433 .map(|(start, end, trait_name)| sinter_core::TraitImpl {
434 file: file.to_string(),
435 trait_name: trait_name.clone(),
436 span: Span {
437 start: *start as u64,
438 end: *end as u64,
439 },
440 })
441 .collect();
442 let scopes = crate::scope::node_scopes(self.spec.name, source, &nodes);
443 let body_terms = crate::body_terms::body_terms(source, &nodes, &scopes);
444 Ok(FileFacts {
445 file: file.to_string(),
446 content_hash: blake3::hash(source.as_bytes()).to_hex().to_string(),
447 has_syntax_errors: root.has_error(),
448 nodes,
449 contains,
450 references,
451 locals,
452 fields,
453 embeds,
454 trait_impls,
455 scopes,
456 body_terms,
457 })
458 }
459}
460
461const RUST_PRELUDE: &[&str] = &[
464 "Ok", "Err", "Some", "None", "Vec", "String", "Box", "Option", "Result", "Self", "bool",
465 "char", "str", "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128",
466 "isize", "f32", "f64",
467];
468
469fn container_ranges(root: TsNode<'_>, kinds: &[&str]) -> Vec<tree_sitter::Range> {
473 let mut out = Vec::new();
474 let mut stack = vec![root];
475 while let Some(node) = stack.pop() {
476 if kinds.contains(&node.kind()) {
477 out.push(node.range());
478 } else {
479 for i in (0..node.child_count()).rev() {
480 stack.extend(node.child(i));
481 }
482 }
483 }
484 out.sort_by_key(|r| r.start_byte);
485 out
486}
487
488fn collect(
491 query: &Query,
492 spec: &LanguageSpec,
493 root: TsNode<'_>,
494 source: &str,
495 entries: &mut Vec<RawEntry>,
496 out: &mut Collected,
497) {
498 {
499 let mut cursor = QueryCursor::new();
500 let mut matches = cursor.matches(query, root, source.as_bytes());
501 while let Some(m) = matches.next() {
502 let mut def: Option<(TsNode, SymbolKind)> = None;
503 let mut scope: Option<TsNode> = None;
504 let mut name: Option<TsNode> = None;
505 let mut qualifier: Option<TsNode> = None;
506 let mut reference: Option<(TsNode, Relation)> = None;
507 let mut refpath: Option<TsNode> = None;
508 let mut import_path: Option<TsNode> = None;
509 let mut import_module: Option<TsNode> = None;
510 let mut import_name: Option<TsNode> = None;
511 let mut import_alias: Option<TsNode> = None;
512 let mut import_star = false;
513 let mut match_locals: Vec<TsNode> = Vec::new();
514 let mut local_type: Option<TsNode> = None;
515 let mut field_name: Option<TsNode> = None;
516 let mut field_type: Option<TsNode> = None;
517 let mut trait_name: Option<TsNode> = None;
518 let mut trait_impl: Option<TsNode> = None;
519 for cap in m.captures {
520 let cap_name = &query.capture_names()[cap.index as usize];
521 if let Some(kind_str) = cap_name.strip_prefix("def.") {
522 if let Some(kind) = SymbolKind::from_str_opt(kind_str) {
523 def = Some((cap.node, kind));
524 }
525 } else if let Some(rel) = cap_name.strip_prefix("ref.") {
526 let relation = match rel {
527 "use" => Relation::Uses,
528 "read" => Relation::Reads,
529 "write" => Relation::Writes,
530 "create" => Relation::Creates,
531 "alter" => Relation::Alters,
532 "drop" => Relation::Drops,
533 _ => Relation::Calls,
534 };
535 reference = Some((cap.node, relation));
536 } else {
537 match *cap_name {
538 "scope" => scope = Some(cap.node),
539 "name" => name = Some(cap.node),
540 "qualifier" => qualifier = Some(cap.node),
541 "refpath" => refpath = Some(cap.node),
542 "import" => import_path = Some(cap.node),
543 "import.module" => import_module = Some(cap.node),
544 "import.name" => import_name = Some(cap.node),
545 "import.alias" => import_alias = Some(cap.node),
546 "import.star" => import_star = true,
547 "local" => match_locals.push(cap.node),
548 "local.type" => local_type = Some(cap.node),
549 "field.name" => field_name = Some(cap.node),
550 "field.type" => field_type = Some(cap.node),
551 "trait" => trait_name = Some(cap.node),
552 "trait.impl" => trait_impl = Some(cap.node),
553 "doc" => out.docs.push((
554 cap.node.start_byte(),
555 cap.node.end_byte(),
556 text(cap.node, source).to_string(),
557 )),
558 "embed" => out.embeds.push((
559 cap.node.start_byte(),
560 cap.node.end_byte(),
561 text(cap.node, source).to_string(),
562 )),
563 _ => {}
564 }
565 }
566 }
567 let sep = spec.path_separators.first().copied().unwrap_or(".");
568 if let (Some(t), Some(block)) = (trait_name, trait_impl) {
569 out.trait_impls.push((
570 block.start_byte(),
571 block.end_byte(),
572 text(t, source).to_string(),
573 ));
574 }
575 if let (Some(name), Some(ty)) = (field_name, field_type) {
576 out.fields.push((
577 name.start_byte(),
578 ty.end_byte(),
579 text(name, source).to_string(),
580 text(ty, source).to_string(),
581 ));
582 }
583 if let Some(a) = import_alias {
584 out.alias_spans.push((a.start_byte(), a.end_byte()));
585 }
586 let alias = import_alias.map(|a| text(a, source).to_string());
587 for l in &match_locals {
588 out.locals.push(RawLocal {
589 start: l.start_byte(),
590 end: l.end_byte(),
591 name: text(*l, source).to_string(),
592 type_name: local_type.map(|t| text(t, source).to_string()),
593 });
594 }
595 if let Some(path_node) = import_path {
596 out.refs.push(RawRef {
600 start: path_node.start_byte(),
601 end: path_node.end_byte(),
602 name: text(path_node, source)
603 .trim_matches(['"', '\'', '`'])
604 .to_string(),
605 path: None,
606 alias: alias.or_else(|| import_star.then(|| "*".to_string())),
607 relation: Relation::Imports,
608 });
609 } else if let (Some(module), Some(item)) = (import_module, import_name) {
610 let module_text = text(module, source).trim_matches(['"', '\'', '`']);
613 out.refs.push(RawRef {
614 start: module.start_byte().min(item.start_byte()),
615 end: item.end_byte().max(module.end_byte()),
616 name: format!("{module_text}{sep}{}", text(item, source)),
617 path: None,
618 alias,
619 relation: Relation::Imports,
620 });
621 } else if let (Some(module), true) = (import_module, import_star) {
622 let module_text = text(module, source).trim_matches(['"', '\'', '`']);
624 out.refs.push(RawRef {
625 start: module.start_byte(),
626 end: module.end_byte(),
627 name: format!("{module_text}{sep}*"),
628 path: None,
629 alias: Some("*".to_string()),
630 relation: Relation::Imports,
631 });
632 }
633 if let Some((node, relation)) = reference {
634 out.refs.push(RawRef {
635 start: node.start_byte(),
636 end: node.end_byte(),
637 name: text(node, source).to_string(),
638 path: refpath.map(|p| qualified_path(p, node, source)),
639 alias: None,
640 relation,
641 });
642 }
643 let container = def.map(|(n, _)| n).or(scope);
644 if let (Some(container), Some(name_node)) = (container, name) {
645 if def.is_some() {
646 out.def_name_spans
647 .push((name_node.start_byte(), name_node.end_byte()));
648 }
649 entries.push(RawEntry {
650 start: container.start_byte(),
651 end: container.end_byte(),
652 name: text(name_node, source).to_string(),
653 kind: def.map(|(_, k)| k),
654 qualifier: qualifier.map(|q| text(q, source).to_string()),
655 signature: signature(container, source),
656 doc: doc_comment(container, source, spec.comment_kinds, spec.doc_skip_kinds),
657 });
658 }
659 }
660 }
661}
662
663fn qualified_path(refpath: TsNode<'_>, name: TsNode<'_>, source: &str) -> String {
669 let receiver = refpath
670 .named_child(0)
671 .filter(|c| refpath.id() != name.id() && c.end_byte() <= name.start_byte());
672 let Some(receiver) = receiver else {
673 return text(refpath, source).to_string();
674 };
675 let sep = source[receiver.end_byte()..name.start_byte()].trim();
676 format!("{}{sep}{}", text(receiver, source), text(name, source))
677}
678
679fn text<'a>(node: TsNode<'_>, source: &'a str) -> &'a str {
680 &source[node.start_byte()..node.end_byte()]
681}
682
683fn signature(node: TsNode<'_>, source: &str) -> String {
686 let t = text(node, source);
687 let first_line = t.lines().next().unwrap_or(t);
688 let head = if first_line.trim_end().ends_with(':') || first_line.contains('{') {
689 first_line.split('{').next().unwrap_or(first_line)
690 } else {
691 let up_to_brace = t.split('{').next().unwrap_or(t);
692 if up_to_brace.len() == t.len() {
693 first_line
694 } else {
695 up_to_brace
696 }
697 };
698 head.split_whitespace().collect::<Vec<_>>().join(" ")
699}
700
701fn doc_comment(
705 node: TsNode<'_>,
706 source: &str,
707 comment_kinds: &[&str],
708 skip_kinds: &[&str],
709) -> Option<String> {
710 let comments = preceding_comments(node, comment_kinds, skip_kinds).or_else(|| {
711 node.parent()
712 .and_then(|p| preceding_comments(p, comment_kinds, skip_kinds))
713 })?;
714 let mut lines = Vec::new();
715 for c in comments {
716 for line in text(c, source).lines() {
717 let mut l = line.trim();
718 for marker in ["///", "//!", "//", "/**", "/*", "*/", "--"] {
719 if let Some(stripped) = l.strip_prefix(marker) {
720 l = stripped;
721 break;
722 }
723 }
724 if let Some(rest) = l.strip_prefix('*')
727 && !l.starts_with("**")
728 {
729 l = rest;
730 }
731 l = l.strip_suffix("*/").unwrap_or(l);
732 lines.push(l.trim());
733 }
734 }
735 while lines.first().is_some_and(|l| l.is_empty()) {
736 lines.remove(0);
737 }
738 while lines.last().is_some_and(|l| l.is_empty()) {
739 lines.pop();
740 }
741 if lines.is_empty() {
742 None
743 } else {
744 Some(lines.join("\n"))
745 }
746}
747
748fn preceding_comments<'t>(
749 node: TsNode<'t>,
750 comment_kinds: &[&str],
751 skip_kinds: &[&str],
752) -> Option<Vec<TsNode<'t>>> {
753 let mut comments = Vec::new();
754 let mut cur = node.prev_named_sibling();
755 let mut skips = 0;
756 while let Some(sib) = cur {
757 if !comment_kinds.contains(&sib.kind()) {
758 if skips < 2 && comments.is_empty() && skip_kinds.contains(&sib.kind()) {
761 skips += 1;
762 cur = sib.prev_named_sibling();
763 continue;
764 }
765 break;
766 }
767 comments.push(sib);
768 cur = sib.prev_named_sibling();
769 }
770 comments.reverse();
771 if comments.is_empty() {
772 None
773 } else {
774 Some(comments)
775 }
776}