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 embedded_sql: Option<(Parser, Query)>,
39}
40
41struct RawEntry {
43 start: usize,
44 end: usize,
45 name: String,
46 kind: Option<SymbolKind>,
48 qualifier: Option<String>,
50 signature: String,
51 doc: Option<String>,
52}
53
54struct RawRef {
56 start: usize,
57 end: usize,
58 name: String,
59 path: Option<String>,
60 alias: Option<String>,
61 relation: Relation,
62}
63
64struct RawLocal {
66 start: usize,
67 end: usize,
68 name: String,
69 type_name: Option<String>,
70}
71
72#[derive(Default)]
74struct Collected {
75 refs: Vec<RawRef>,
76 locals: Vec<RawLocal>,
77 fields: Vec<(usize, usize, String, String)>,
79 embeds: Vec<(usize, usize, String)>,
81 trait_impls: Vec<(usize, usize, String)>,
83 alias_spans: Vec<(usize, usize)>,
86 def_name_spans: Vec<(usize, usize)>,
89 sql_ranges: Vec<tree_sitter::Range>,
92 docs: Vec<(usize, usize, String)>,
96}
97
98impl Extractor {
99 pub fn new(spec: &'static LanguageSpec) -> Result<Self, ExtractError> {
100 let language = (spec.grammar)();
101 let mut parser = Parser::new();
102 parser
103 .set_language(&language)
104 .map_err(|e| ExtractError::Query {
105 language: spec.name,
106 message: e.to_string(),
107 })?;
108 let query = Query::new(&language, spec.query_source).map_err(|e| ExtractError::Query {
109 language: spec.name,
110 message: e.to_string(),
111 })?;
112 let inline = spec
113 .inline
114 .map(|i| {
115 let language = (i.grammar)();
116 let mut parser = Parser::new();
117 parser
118 .set_language(&language)
119 .map_err(|e| (spec.name, e.to_string()))?;
120 let query = Query::new(&language, i.query_source)
121 .map_err(|e| (spec.name, e.to_string()))?;
122 Ok((parser, query))
123 })
124 .transpose()
125 .map_err(
126 |(language, message): (&'static str, String)| ExtractError::Query {
127 language,
128 message,
129 },
130 )?;
131 Ok(Self {
132 spec,
133 parser,
134 query,
135 inline,
136 embedded_sql: None,
137 })
138 }
139
140 pub fn extract(&mut self, file: &str, source: &str) -> Result<FileFacts, ExtractError> {
142 let tree = self
143 .parser
144 .parse(source, None)
145 .ok_or_else(|| ExtractError::Parse(file.to_string()))?;
146 let root = tree.root_node();
147
148 let mut entries = Vec::new();
149 let mut collected = Collected::default();
150 collect(
151 &self.query,
152 self.spec,
153 root,
154 source,
155 &mut entries,
156 &mut collected,
157 );
158 if let (Some((parser, query)), Some(ispec)) = (&mut self.inline, self.spec.inline) {
162 let ranges = container_ranges(root, ispec.container_kinds);
163 if !ranges.is_empty() {
164 parser.set_included_ranges(&ranges).map_err(|e| {
170 ExtractError::Parse(format!("{} (inline ranges: {e})", self.spec.name))
171 })?;
172 let inline_tree = parser
173 .parse(source, None)
174 .ok_or_else(|| ExtractError::Parse(format!("{} (inline)", self.spec.name)))?;
175 collect(
176 query,
177 self.spec,
178 inline_tree.root_node(),
179 source,
180 &mut entries,
181 &mut collected,
182 );
183 }
184 }
185 if !collected.sql_ranges.is_empty() {
192 if self.embedded_sql.is_none() {
193 let sql_spec = sql_language_spec();
194 let language = (sql_spec.grammar)();
195 let mut parser = Parser::new();
196 parser
197 .set_language(&language)
198 .map_err(|e| ExtractError::Query {
199 language: sql_spec.name,
200 message: e.to_string(),
201 })?;
202 let query = Query::new(&language, sql_spec.query_source).map_err(|e| {
203 ExtractError::Query {
204 language: sql_spec.name,
205 message: e.to_string(),
206 }
207 })?;
208 self.embedded_sql = Some((parser, query));
209 }
210 let sql_spec = sql_language_spec();
211 let (parser, query) = self.embedded_sql.as_mut().expect("built above");
212 for range in std::mem::take(&mut collected.sql_ranges) {
213 if parser.set_included_ranges(&[range]).is_err() {
216 continue;
217 }
218 let Some(tree) = parser.parse(source, None) else {
219 continue;
220 };
221 let refs_before = collected.refs.len();
222 collect(
223 query,
224 sql_spec,
225 tree.root_node(),
226 source,
227 &mut entries,
228 &mut collected,
229 );
230 if collected.refs.len() == refs_before && tree.root_node().has_error() {
236 let text = &source[range.start_byte..range.end_byte];
237 let head = text.split_whitespace().next().unwrap_or("");
238 if SQL_STATEMENT_KEYWORDS
239 .iter()
240 .any(|k| head.eq_ignore_ascii_case(k))
241 {
242 let collapsed = text.split_whitespace().collect::<Vec<_>>().join(" ");
243 collected.refs.push(RawRef {
244 start: range.start_byte,
245 end: range.end_byte,
246 name: collapsed.chars().take(60).collect(),
247 path: None,
248 alias: None,
249 relation: Relation::Uses,
250 });
251 }
252 }
253 }
254 }
255 entries.sort_by_key(|e| (e.start, usize::MAX - e.end));
256 let mut doc_set = vec![false; entries.len()];
261 for (d_start, d_end, text) in &collected.docs {
262 let owner = entries
263 .iter_mut()
264 .enumerate()
265 .filter(|(_, e)| e.kind.is_some() && e.start <= *d_start && *d_end <= e.end)
266 .min_by_key(|(_, e)| e.end - e.start);
267 if let Some((i, entry)) = owner {
268 let cleaned: Vec<&str> = text.lines().map(str::trim).collect();
269 let trimmed = cleaned.join("\n");
270 let trimmed = trimmed.trim_matches('\n');
271 if trimmed.is_empty() {
272 continue;
273 }
274 entry.doc = match entry.doc.take().filter(|_| doc_set[i]) {
275 Some(prev) if prev.len() >= DOC_CAP_BYTES => Some(prev),
278 Some(prev) => Some(format!("{prev}\n\n{trimmed}")),
279 None => Some(trimmed.to_string()),
280 };
281 doc_set[i] = true;
282 }
283 }
284 entries.dedup_by(|b, a| {
288 let same = a.start == b.start && a.end == b.end && a.name == b.name;
289 if same && a.kind == Some(SymbolKind::Variable) && b.kind.is_some() {
290 a.kind = b.kind;
291 }
292 same
293 });
294
295 let file_id = NodeId::new(file);
296 let mut nodes = vec![Node {
297 id: file_id.clone(),
298 kind: SymbolKind::File,
299 name: file.to_string(),
300 file: file.to_string(),
301 span: Span {
302 start: 0,
303 end: source.len().max(1) as u64,
304 },
305 signature: String::new(),
306 doc: None,
307 }];
308 let mut contains = Vec::new();
309
310 let mut stack: Vec<(usize, String, Option<NodeId>)> = Vec::new();
312 let mut def_spans: Vec<(usize, usize, NodeId, SymbolKind)> = Vec::new();
315
316 for entry in &entries {
317 while stack.last().is_some_and(|(end, _, _)| *end <= entry.start) {
318 stack.pop();
319 }
320 let mut path: Vec<&str> = stack.iter().map(|(_, name, _)| name.as_str()).collect();
321 if let Some(q) = &entry.qualifier {
322 path.push(q);
323 }
324 path.push(&entry.name);
325 let qualified = path.join("::");
326 let scope_segment = entry
328 .qualifier
329 .as_ref()
330 .map_or(entry.name.clone(), |q| format!("{q}::{}", entry.name));
331
332 let id = if let Some(kind) = entry.kind {
333 let id = NodeId::new(format!("{file}#{qualified}@{}", entry.start));
334 let parent = stack
335 .iter()
336 .rev()
337 .find_map(|(_, _, id)| id.clone())
338 .unwrap_or_else(|| file_id.clone());
339 nodes.push(Node {
340 id: id.clone(),
341 kind,
342 name: entry.name.clone(),
343 file: file.to_string(),
344 span: Span {
345 start: entry.start as u64,
346 end: entry.end as u64,
347 },
348 signature: entry.signature.clone(),
349 doc: entry.doc.clone(),
350 });
351 contains.push(Edge {
352 src: parent,
353 dst: id.clone(),
354 relation: Relation::Contains,
355 evidence: Evidence::Structural,
356 confidence: Evidence::Structural.confidence(),
357 site: None,
359 });
360 def_spans.push((entry.start, entry.end, id.clone(), kind));
361 Some(id)
362 } else {
363 None
364 };
365 stack.push((entry.end, scope_segment, id));
366 }
367
368 let mut refs = collected.refs;
371 refs.retain(|r| {
372 r.relation == Relation::Creates || !collected.def_name_spans.contains(&(r.start, r.end))
373 });
374 if self.spec.name == "rust" {
377 refs.retain(|r| {
378 r.path.is_some()
379 || !RUST_PRELUDE.contains(&r.name.as_str())
380 || entries.iter().any(|e| e.name == r.name)
381 });
382 }
383 let relation_priority = |relation: Relation| match relation {
388 Relation::Writes => 0,
389 Relation::Reads => 1,
390 _ => 2,
391 };
392 refs.sort_by_key(|r| {
393 (
394 r.start,
395 r.end,
396 r.path.is_none(),
397 relation_priority(r.relation),
398 )
399 });
400 refs.dedup_by_key(|r| (r.start, r.end));
401 let references = refs
402 .into_iter()
403 .map(|r| {
404 let enclosing = (r.relation != Relation::Creates)
408 .then(|| {
409 def_spans
410 .iter()
411 .filter(|(s, e, _, _)| *s <= r.start && r.end <= *e)
412 .min_by_key(|(s, e, _, _)| e - s)
413 .map(|(_, _, id, _)| id.clone())
414 })
415 .flatten();
416 Reference {
417 file: file.to_string(),
418 name: r.name,
419 path: r.path,
420 relation: r.relation,
421 span: Span {
422 start: r.start as u64,
423 end: r.end as u64,
424 },
425 enclosing,
426 alias: r.alias,
427 }
428 })
429 .collect();
430
431 let alias_spans = collected.alias_spans;
435 let embeds = collected
436 .embeds
437 .iter()
438 .filter_map(|(start, end, type_name)| {
439 let owner = def_spans
440 .iter()
441 .filter(|(s, e, _, _)| s <= start && end <= e)
442 .min_by_key(|(s, e, _, _)| e - s)
443 .map(|(_, _, id, _)| id.clone())?;
444 Some(sinter_core::Embed {
445 owner,
446 type_name: type_name.clone(),
447 })
448 })
449 .collect();
450 let mut raw_locals = collected
451 .locals
452 .into_iter()
453 .filter(|l| !alias_spans.contains(&(l.start, l.end)))
454 .collect::<Vec<_>>();
455 raw_locals.sort_by_key(|l| (l.start, l.end, l.name.clone(), l.type_name.is_none()));
458 raw_locals.dedup_by(|b, a| a.start == b.start && a.end == b.end && a.name == b.name);
459 let locals = raw_locals
460 .into_iter()
461 .map(|l| {
462 let scope_end = def_spans
463 .iter()
464 .filter(|(s, e, _, _)| *s <= l.start && l.end <= *e)
465 .min_by_key(|(s, e, _, _)| e - s)
466 .map_or(source.len() as u64, |(_, e, _, _)| *e as u64);
467 sinter_core::LocalBinding {
468 file: file.to_string(),
469 name: l.name,
470 span: Span {
471 start: l.start as u64,
472 end: l.end as u64,
473 },
474 scope_end,
475 type_name: l.type_name,
476 }
477 })
478 .collect();
479
480 let fields = collected
481 .fields
482 .iter()
483 .filter_map(|(start, end, name, type_name)| {
484 let owner = def_spans
485 .iter()
486 .filter(|(s, e, _, kind)| {
487 *s <= *start
488 && *end <= *e
489 && matches!(
490 kind,
491 SymbolKind::Struct
492 | SymbolKind::Class
493 | SymbolKind::Trait
494 | SymbolKind::Interface
495 )
496 })
497 .min_by_key(|(s, e, _, _)| e - s)
498 .map(|(_, _, id, _)| id.clone())?;
499 Some(FieldBinding {
500 owner,
501 name: name.clone(),
502 type_name: type_name.clone(),
503 })
504 })
505 .collect();
506
507 let trait_impls = collected
508 .trait_impls
509 .iter()
510 .map(|(start, end, trait_name)| sinter_core::TraitImpl {
511 file: file.to_string(),
512 trait_name: trait_name.clone(),
513 span: Span {
514 start: *start as u64,
515 end: *end as u64,
516 },
517 })
518 .collect();
519 let scopes = crate::scope::node_scopes(self.spec.name, source, &nodes);
520 let body_terms = crate::body_terms::body_terms(source, &nodes, &scopes);
521 Ok(FileFacts {
522 file: file.to_string(),
523 content_hash: blake3::hash(source.as_bytes()).to_hex().to_string(),
524 has_syntax_errors: root.has_error(),
525 nodes,
526 contains,
527 references,
528 locals,
529 fields,
530 embeds,
531 trait_impls,
532 scopes,
533 body_terms,
534 })
535 }
536}
537
538fn sql_language_spec() -> &'static LanguageSpec {
542 crate::language::LANGUAGES
543 .iter()
544 .find(|s| s.name == "sql")
545 .expect("sql language registered in LANGUAGES")
546}
547
548const SQL_STATEMENT_KEYWORDS: &[&str] = &[
552 "SELECT", "INSERT", "UPDATE", "DELETE", "WITH", "CREATE", "ALTER", "DROP", "TRUNCATE", "MERGE",
553 "REPLACE",
554];
555
556const RUST_PRELUDE: &[&str] = &[
559 "Ok", "Err", "Some", "None", "Vec", "String", "Box", "Option", "Result", "Self", "bool",
560 "char", "str", "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128",
561 "isize", "f32", "f64",
562];
563
564fn container_ranges(root: TsNode<'_>, kinds: &[&str]) -> Vec<tree_sitter::Range> {
568 let mut out = Vec::new();
569 let mut stack = vec![root];
570 while let Some(node) = stack.pop() {
571 if kinds.contains(&node.kind()) {
572 out.push(node.range());
573 } else {
574 for i in (0..node.child_count()).rev() {
575 stack.extend(node.child(i));
576 }
577 }
578 }
579 out.sort_by_key(|r| r.start_byte);
580 out
581}
582
583fn collect(
586 query: &Query,
587 spec: &LanguageSpec,
588 root: TsNode<'_>,
589 source: &str,
590 entries: &mut Vec<RawEntry>,
591 out: &mut Collected,
592) {
593 {
594 let mut cursor = QueryCursor::new();
595 let mut matches = cursor.matches(query, root, source.as_bytes());
596 while let Some(m) = matches.next() {
597 let mut def: Option<(TsNode, SymbolKind)> = None;
598 let mut scope: Option<TsNode> = None;
599 let mut name: Option<TsNode> = None;
600 let mut qualifier: Option<TsNode> = None;
601 let mut reference: Option<(TsNode, Relation)> = None;
602 let mut refpath: Option<TsNode> = None;
603 let mut import_path: Option<TsNode> = None;
604 let mut import_module: Option<TsNode> = None;
605 let mut import_name: Option<TsNode> = None;
606 let mut import_alias: Option<TsNode> = None;
607 let mut import_star = false;
608 let mut match_locals: Vec<TsNode> = Vec::new();
609 let mut local_type: Option<TsNode> = None;
610 let mut field_name: Option<TsNode> = None;
611 let mut field_type: Option<TsNode> = None;
612 let mut trait_name: Option<TsNode> = None;
613 let mut trait_impl: Option<TsNode> = None;
614 for cap in m.captures {
615 let cap_name = &query.capture_names()[cap.index as usize];
616 if let Some(kind_str) = cap_name.strip_prefix("def.") {
617 if let Some(kind) = SymbolKind::from_str_opt(kind_str) {
618 def = Some((cap.node, kind));
619 }
620 } else if let Some(rel) = cap_name.strip_prefix("ref.") {
621 let relation = match rel {
622 "use" => Relation::Uses,
623 "read" => Relation::Reads,
624 "write" => Relation::Writes,
625 "create" => Relation::Creates,
626 "alter" => Relation::Alters,
627 "drop" => Relation::Drops,
628 _ => Relation::Calls,
629 };
630 reference = Some((cap.node, relation));
631 } else {
632 match *cap_name {
633 "scope" => scope = Some(cap.node),
634 "name" => name = Some(cap.node),
635 "qualifier" => qualifier = Some(cap.node),
636 "refpath" => refpath = Some(cap.node),
637 "import" => import_path = Some(cap.node),
638 "import.module" => import_module = Some(cap.node),
639 "import.name" => import_name = Some(cap.node),
640 "import.alias" => import_alias = Some(cap.node),
641 "import.star" => import_star = true,
642 "local" => match_locals.push(cap.node),
643 "local.type" => local_type = Some(cap.node),
644 "field.name" => field_name = Some(cap.node),
645 "field.type" => field_type = Some(cap.node),
646 "trait" => trait_name = Some(cap.node),
647 "trait.impl" => trait_impl = Some(cap.node),
648 "doc" => out.docs.push((
649 cap.node.start_byte(),
650 cap.node.end_byte(),
651 text(cap.node, source).to_string(),
652 )),
653 "sql" => out.sql_ranges.push(cap.node.range()),
654 "embed" => out.embeds.push((
655 cap.node.start_byte(),
656 cap.node.end_byte(),
657 text(cap.node, source).to_string(),
658 )),
659 _ => {}
660 }
661 }
662 }
663 let sep = spec.path_separators.first().copied().unwrap_or(".");
664 if let (Some(t), Some(block)) = (trait_name, trait_impl) {
665 out.trait_impls.push((
666 block.start_byte(),
667 block.end_byte(),
668 text(t, source).to_string(),
669 ));
670 }
671 if let (Some(name), Some(ty)) = (field_name, field_type) {
672 out.fields.push((
673 name.start_byte(),
674 ty.end_byte(),
675 text(name, source).to_string(),
676 text(ty, source).to_string(),
677 ));
678 }
679 if let Some(a) = import_alias {
680 out.alias_spans.push((a.start_byte(), a.end_byte()));
681 }
682 let alias = import_alias.map(|a| text(a, source).to_string());
683 for l in &match_locals {
684 out.locals.push(RawLocal {
685 start: l.start_byte(),
686 end: l.end_byte(),
687 name: text(*l, source).to_string(),
688 type_name: local_type.map(|t| text(t, source).to_string()),
689 });
690 }
691 if let Some(path_node) = import_path {
692 out.refs.push(RawRef {
696 start: path_node.start_byte(),
697 end: path_node.end_byte(),
698 name: text(path_node, source)
699 .trim_matches(['"', '\'', '`'])
700 .to_string(),
701 path: None,
702 alias: alias.or_else(|| import_star.then(|| "*".to_string())),
703 relation: Relation::Imports,
704 });
705 } else if let (Some(module), Some(item)) = (import_module, import_name) {
706 let module_text = text(module, source).trim_matches(['"', '\'', '`']);
709 out.refs.push(RawRef {
710 start: module.start_byte().min(item.start_byte()),
711 end: item.end_byte().max(module.end_byte()),
712 name: format!("{module_text}{sep}{}", text(item, source)),
713 path: None,
714 alias,
715 relation: Relation::Imports,
716 });
717 } else if let (Some(module), true) = (import_module, import_star) {
718 let module_text = text(module, source).trim_matches(['"', '\'', '`']);
720 out.refs.push(RawRef {
721 start: module.start_byte(),
722 end: module.end_byte(),
723 name: format!("{module_text}{sep}*"),
724 path: None,
725 alias: Some("*".to_string()),
726 relation: Relation::Imports,
727 });
728 }
729 if let Some((node, relation)) = reference {
730 out.refs.push(RawRef {
731 start: node.start_byte(),
732 end: node.end_byte(),
733 name: text(node, source).to_string(),
734 path: refpath.map(|p| qualified_path(p, node, source)),
735 alias: None,
736 relation,
737 });
738 }
739 let container = def.map(|(n, _)| n).or(scope);
740 if let (Some(container), Some(name_node)) = (container, name) {
741 if def.is_some() {
742 out.def_name_spans
743 .push((name_node.start_byte(), name_node.end_byte()));
744 }
745 entries.push(RawEntry {
746 start: container.start_byte(),
747 end: container.end_byte(),
748 name: text(name_node, source).to_string(),
749 kind: def.map(|(_, k)| k),
750 qualifier: qualifier.map(|q| text(q, source).to_string()),
751 signature: signature(container, source),
752 doc: doc_comment(container, source, spec.comment_kinds, spec.doc_skip_kinds),
753 });
754 }
755 }
756 }
757}
758
759fn qualified_path(refpath: TsNode<'_>, name: TsNode<'_>, source: &str) -> String {
765 let receiver = refpath
766 .named_child(0)
767 .filter(|c| refpath.id() != name.id() && c.end_byte() <= name.start_byte());
768 let Some(receiver) = receiver else {
769 return text(refpath, source).to_string();
770 };
771 let sep = source[receiver.end_byte()..name.start_byte()].trim();
772 format!("{}{sep}{}", text(receiver, source), text(name, source))
773}
774
775fn text<'a>(node: TsNode<'_>, source: &'a str) -> &'a str {
776 &source[node.start_byte()..node.end_byte()]
777}
778
779fn signature(node: TsNode<'_>, source: &str) -> String {
782 let t = text(node, source);
783 let first_line = t.lines().next().unwrap_or(t);
784 let head = if first_line.trim_end().ends_with(':') || first_line.contains('{') {
785 first_line.split('{').next().unwrap_or(first_line)
786 } else {
787 let up_to_brace = t.split('{').next().unwrap_or(t);
788 if up_to_brace.len() == t.len() {
789 first_line
790 } else {
791 up_to_brace
792 }
793 };
794 head.split_whitespace().collect::<Vec<_>>().join(" ")
795}
796
797fn doc_comment(
801 node: TsNode<'_>,
802 source: &str,
803 comment_kinds: &[&str],
804 skip_kinds: &[&str],
805) -> Option<String> {
806 let comments = preceding_comments(node, comment_kinds, skip_kinds).or_else(|| {
807 node.parent()
808 .and_then(|p| preceding_comments(p, comment_kinds, skip_kinds))
809 })?;
810 let mut lines = Vec::new();
811 for c in comments {
812 for line in text(c, source).lines() {
813 let mut l = line.trim();
814 for marker in ["///", "//!", "//", "/**", "/*", "*/", "--"] {
815 if let Some(stripped) = l.strip_prefix(marker) {
816 l = stripped;
817 break;
818 }
819 }
820 if let Some(rest) = l.strip_prefix('*')
823 && !l.starts_with("**")
824 {
825 l = rest;
826 }
827 l = l.strip_suffix("*/").unwrap_or(l);
828 lines.push(l.trim());
829 }
830 }
831 while lines.first().is_some_and(|l| l.is_empty()) {
832 lines.remove(0);
833 }
834 while lines.last().is_some_and(|l| l.is_empty()) {
835 lines.pop();
836 }
837 if lines.is_empty() {
838 None
839 } else {
840 Some(lines.join("\n"))
841 }
842}
843
844fn preceding_comments<'t>(
845 node: TsNode<'t>,
846 comment_kinds: &[&str],
847 skip_kinds: &[&str],
848) -> Option<Vec<TsNode<'t>>> {
849 let mut comments = Vec::new();
850 let mut cur = node.prev_named_sibling();
851 let mut skips = 0;
852 while let Some(sib) = cur {
853 if !comment_kinds.contains(&sib.kind()) {
854 if skips < 2 && comments.is_empty() && skip_kinds.contains(&sib.kind()) {
857 skips += 1;
858 cur = sib.prev_named_sibling();
859 continue;
860 }
861 break;
862 }
863 comments.push(sib);
864 cur = sib.prev_named_sibling();
865 }
866 comments.reverse();
867 if comments.is_empty() {
868 None
869 } else {
870 Some(comments)
871 }
872}