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 extra_sites: Vec::new(),
360 sites_total: 0,
361 });
362 def_spans.push((entry.start, entry.end, id.clone(), kind));
363 Some(id)
364 } else {
365 None
366 };
367 stack.push((entry.end, scope_segment, id));
368 }
369
370 let mut refs = collected.refs;
373 refs.retain(|r| {
374 r.relation == Relation::Creates || !collected.def_name_spans.contains(&(r.start, r.end))
375 });
376 if self.spec.name == "rust" {
379 refs.retain(|r| {
380 r.path.is_some()
381 || !RUST_PRELUDE.contains(&r.name.as_str())
382 || entries.iter().any(|e| e.name == r.name)
383 });
384 }
385 let relation_priority = |relation: Relation| match relation {
390 Relation::Writes => 0,
391 Relation::Reads => 1,
392 _ => 2,
393 };
394 refs.sort_by_key(|r| {
395 (
396 r.start,
397 r.end,
398 r.path.is_none(),
399 relation_priority(r.relation),
400 )
401 });
402 refs.dedup_by_key(|r| (r.start, r.end));
403 let references = refs
404 .into_iter()
405 .map(|r| {
406 let enclosing = (r.relation != Relation::Creates)
410 .then(|| {
411 def_spans
412 .iter()
413 .filter(|(s, e, _, _)| *s <= r.start && r.end <= *e)
414 .min_by_key(|(s, e, _, _)| e - s)
415 .map(|(_, _, id, _)| id.clone())
416 })
417 .flatten();
418 Reference {
419 file: file.to_string(),
420 name: r.name,
421 path: r.path,
422 relation: r.relation,
423 span: Span {
424 start: r.start as u64,
425 end: r.end as u64,
426 },
427 enclosing,
428 alias: r.alias,
429 }
430 })
431 .collect();
432
433 let alias_spans = collected.alias_spans;
437 let embeds = collected
438 .embeds
439 .iter()
440 .filter_map(|(start, end, type_name)| {
441 let owner = def_spans
442 .iter()
443 .filter(|(s, e, _, _)| s <= start && end <= e)
444 .min_by_key(|(s, e, _, _)| e - s)
445 .map(|(_, _, id, _)| id.clone())?;
446 Some(sinter_core::Embed {
447 owner,
448 type_name: type_name.clone(),
449 })
450 })
451 .collect();
452 let mut raw_locals = collected
453 .locals
454 .into_iter()
455 .filter(|l| !alias_spans.contains(&(l.start, l.end)))
456 .collect::<Vec<_>>();
457 raw_locals.sort_by_key(|l| (l.start, l.end, l.name.clone(), l.type_name.is_none()));
460 raw_locals.dedup_by(|b, a| a.start == b.start && a.end == b.end && a.name == b.name);
461 let locals = raw_locals
462 .into_iter()
463 .map(|l| {
464 let scope_end = def_spans
465 .iter()
466 .filter(|(s, e, _, _)| *s <= l.start && l.end <= *e)
467 .min_by_key(|(s, e, _, _)| e - s)
468 .map_or(source.len() as u64, |(_, e, _, _)| *e as u64);
469 sinter_core::LocalBinding {
470 file: file.to_string(),
471 name: l.name,
472 span: Span {
473 start: l.start as u64,
474 end: l.end as u64,
475 },
476 scope_end,
477 type_name: l.type_name,
478 }
479 })
480 .collect();
481
482 let fields = collected
483 .fields
484 .iter()
485 .filter_map(|(start, end, name, type_name)| {
486 let owner = def_spans
487 .iter()
488 .filter(|(s, e, _, kind)| {
489 *s <= *start
490 && *end <= *e
491 && matches!(
492 kind,
493 SymbolKind::Struct
494 | SymbolKind::Class
495 | SymbolKind::Trait
496 | SymbolKind::Interface
497 )
498 })
499 .min_by_key(|(s, e, _, _)| e - s)
500 .map(|(_, _, id, _)| id.clone())?;
501 Some(FieldBinding {
502 owner,
503 name: name.clone(),
504 type_name: type_name.clone(),
505 })
506 })
507 .collect();
508
509 let trait_impls = collected
510 .trait_impls
511 .iter()
512 .map(|(start, end, trait_name)| sinter_core::TraitImpl {
513 file: file.to_string(),
514 trait_name: trait_name.clone(),
515 span: Span {
516 start: *start as u64,
517 end: *end as u64,
518 },
519 })
520 .collect();
521 let scopes = crate::scope::node_scopes(self.spec.name, source, &nodes);
522 let body_terms = crate::body_terms::body_terms(source, &nodes, &scopes);
523 Ok(FileFacts {
524 file: file.to_string(),
525 content_hash: blake3::hash(source.as_bytes()).to_hex().to_string(),
526 has_syntax_errors: root.has_error(),
527 nodes,
528 contains,
529 references,
530 locals,
531 fields,
532 embeds,
533 trait_impls,
534 scopes,
535 body_terms,
536 })
537 }
538}
539
540fn sql_language_spec() -> &'static LanguageSpec {
544 crate::language::LANGUAGES
545 .iter()
546 .find(|s| s.name == "sql")
547 .expect("sql language registered in LANGUAGES")
548}
549
550const SQL_STATEMENT_KEYWORDS: &[&str] = &[
554 "SELECT", "INSERT", "UPDATE", "DELETE", "WITH", "CREATE", "ALTER", "DROP", "TRUNCATE", "MERGE",
555 "REPLACE",
556];
557
558const RUST_PRELUDE: &[&str] = &[
561 "Ok", "Err", "Some", "None", "Vec", "String", "Box", "Option", "Result", "Self", "bool",
562 "char", "str", "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128",
563 "isize", "f32", "f64",
564];
565
566fn container_ranges(root: TsNode<'_>, kinds: &[&str]) -> Vec<tree_sitter::Range> {
570 let mut out = Vec::new();
571 let mut stack = vec![root];
572 while let Some(node) = stack.pop() {
573 if kinds.contains(&node.kind()) {
574 out.push(node.range());
575 } else {
576 for i in (0..node.child_count()).rev() {
577 stack.extend(node.child(i));
578 }
579 }
580 }
581 out.sort_by_key(|r| r.start_byte);
582 out
583}
584
585fn collect(
588 query: &Query,
589 spec: &LanguageSpec,
590 root: TsNode<'_>,
591 source: &str,
592 entries: &mut Vec<RawEntry>,
593 out: &mut Collected,
594) {
595 {
596 let mut cursor = QueryCursor::new();
597 let mut matches = cursor.matches(query, root, source.as_bytes());
598 while let Some(m) = matches.next() {
599 let mut def: Option<(TsNode, SymbolKind)> = None;
600 let mut scope: Option<TsNode> = None;
601 let mut name: Option<TsNode> = None;
602 let mut qualifier: Option<TsNode> = None;
603 let mut reference: Option<(TsNode, Relation)> = None;
604 let mut refpath: Option<TsNode> = None;
605 let mut import_path: Option<TsNode> = None;
606 let mut import_module: Option<TsNode> = None;
607 let mut import_name: Option<TsNode> = None;
608 let mut import_alias: Option<TsNode> = None;
609 let mut import_star = false;
610 let mut match_locals: Vec<TsNode> = Vec::new();
611 let mut local_type: Option<TsNode> = None;
612 let mut field_name: Option<TsNode> = None;
613 let mut field_type: Option<TsNode> = None;
614 let mut trait_name: Option<TsNode> = None;
615 let mut trait_impl: Option<TsNode> = None;
616 for cap in m.captures {
617 let cap_name = &query.capture_names()[cap.index as usize];
618 if let Some(kind_str) = cap_name.strip_prefix("def.") {
619 if let Some(kind) = SymbolKind::from_str_opt(kind_str) {
620 def = Some((cap.node, kind));
621 }
622 } else if let Some(rel) = cap_name.strip_prefix("ref.") {
623 let relation = match rel {
624 "use" => Relation::Uses,
625 "read" => Relation::Reads,
626 "write" => Relation::Writes,
627 "create" => Relation::Creates,
628 "alter" => Relation::Alters,
629 "drop" => Relation::Drops,
630 _ => Relation::Calls,
631 };
632 reference = Some((cap.node, relation));
633 } else {
634 match *cap_name {
635 "scope" => scope = Some(cap.node),
636 "name" => name = Some(cap.node),
637 "qualifier" => qualifier = Some(cap.node),
638 "refpath" => refpath = Some(cap.node),
639 "import" => import_path = Some(cap.node),
640 "import.module" => import_module = Some(cap.node),
641 "import.name" => import_name = Some(cap.node),
642 "import.alias" => import_alias = Some(cap.node),
643 "import.star" => import_star = true,
644 "local" => match_locals.push(cap.node),
645 "local.type" => local_type = Some(cap.node),
646 "field.name" => field_name = Some(cap.node),
647 "field.type" => field_type = Some(cap.node),
648 "trait" => trait_name = Some(cap.node),
649 "trait.impl" => trait_impl = Some(cap.node),
650 "doc" => out.docs.push((
651 cap.node.start_byte(),
652 cap.node.end_byte(),
653 text(cap.node, source).to_string(),
654 )),
655 "sql" => out.sql_ranges.push(cap.node.range()),
656 "embed" => out.embeds.push((
657 cap.node.start_byte(),
658 cap.node.end_byte(),
659 text(cap.node, source).to_string(),
660 )),
661 _ => {}
662 }
663 }
664 }
665 let sep = spec.path_separators.first().copied().unwrap_or(".");
666 if let (Some(t), Some(block)) = (trait_name, trait_impl) {
667 out.trait_impls.push((
668 block.start_byte(),
669 block.end_byte(),
670 text(t, source).to_string(),
671 ));
672 }
673 if let (Some(name), Some(ty)) = (field_name, field_type) {
674 out.fields.push((
675 name.start_byte(),
676 ty.end_byte(),
677 text(name, source).to_string(),
678 text(ty, source).to_string(),
679 ));
680 }
681 if let Some(a) = import_alias {
682 out.alias_spans.push((a.start_byte(), a.end_byte()));
683 }
684 let alias = import_alias.map(|a| text(a, source).to_string());
685 for l in &match_locals {
686 out.locals.push(RawLocal {
687 start: l.start_byte(),
688 end: l.end_byte(),
689 name: text(*l, source).to_string(),
690 type_name: local_type.map(|t| text(t, source).to_string()),
691 });
692 }
693 if let Some(path_node) = import_path {
694 out.refs.push(RawRef {
698 start: path_node.start_byte(),
699 end: path_node.end_byte(),
700 name: text(path_node, source)
701 .trim_matches(['"', '\'', '`'])
702 .to_string(),
703 path: None,
704 alias: alias.or_else(|| import_star.then(|| "*".to_string())),
705 relation: Relation::Imports,
706 });
707 } else if let (Some(module), Some(item)) = (import_module, import_name) {
708 let module_text = text(module, source).trim_matches(['"', '\'', '`']);
711 out.refs.push(RawRef {
712 start: module.start_byte().min(item.start_byte()),
713 end: item.end_byte().max(module.end_byte()),
714 name: format!("{module_text}{sep}{}", text(item, source)),
715 path: None,
716 alias,
717 relation: Relation::Imports,
718 });
719 } else if let (Some(module), true) = (import_module, import_star) {
720 let module_text = text(module, source).trim_matches(['"', '\'', '`']);
722 out.refs.push(RawRef {
723 start: module.start_byte(),
724 end: module.end_byte(),
725 name: format!("{module_text}{sep}*"),
726 path: None,
727 alias: Some("*".to_string()),
728 relation: Relation::Imports,
729 });
730 }
731 if let Some((node, relation)) = reference {
732 out.refs.push(RawRef {
733 start: node.start_byte(),
734 end: node.end_byte(),
735 name: text(node, source).to_string(),
736 path: refpath.map(|p| qualified_path(p, node, source)),
737 alias: None,
738 relation,
739 });
740 }
741 let container = def.map(|(n, _)| n).or(scope);
742 if let (Some(container), Some(name_node)) = (container, name) {
743 if def.is_some() {
744 out.def_name_spans
745 .push((name_node.start_byte(), name_node.end_byte()));
746 }
747 entries.push(RawEntry {
748 start: container.start_byte(),
749 end: container.end_byte(),
750 name: text(name_node, source).to_string(),
751 kind: def.map(|(_, k)| k),
752 qualifier: qualifier.map(|q| text(q, source).to_string()),
753 signature: signature(container, source),
754 doc: doc_comment(container, source, spec.comment_kinds, spec.doc_skip_kinds),
755 });
756 }
757 }
758 }
759}
760
761fn qualified_path(refpath: TsNode<'_>, name: TsNode<'_>, source: &str) -> String {
767 let receiver = refpath
768 .named_child(0)
769 .filter(|c| refpath.id() != name.id() && c.end_byte() <= name.start_byte());
770 let Some(receiver) = receiver else {
771 return text(refpath, source).to_string();
772 };
773 let sep = source[receiver.end_byte()..name.start_byte()].trim();
774 format!("{}{sep}{}", text(receiver, source), text(name, source))
775}
776
777fn text<'a>(node: TsNode<'_>, source: &'a str) -> &'a str {
778 &source[node.start_byte()..node.end_byte()]
779}
780
781fn signature(node: TsNode<'_>, source: &str) -> String {
784 let t = text(node, source);
785 let first_line = t.lines().next().unwrap_or(t);
786 let head = if first_line.trim_end().ends_with(':') || first_line.contains('{') {
787 first_line.split('{').next().unwrap_or(first_line)
788 } else {
789 let up_to_brace = t.split('{').next().unwrap_or(t);
790 if up_to_brace.len() == t.len() {
791 first_line
792 } else {
793 up_to_brace
794 }
795 };
796 head.split_whitespace().collect::<Vec<_>>().join(" ")
797}
798
799fn doc_comment(
803 node: TsNode<'_>,
804 source: &str,
805 comment_kinds: &[&str],
806 skip_kinds: &[&str],
807) -> Option<String> {
808 let comments = preceding_comments(node, comment_kinds, skip_kinds).or_else(|| {
809 node.parent()
810 .and_then(|p| preceding_comments(p, comment_kinds, skip_kinds))
811 })?;
812 let mut lines = Vec::new();
813 for c in comments {
814 for line in text(c, source).lines() {
815 let mut l = line.trim();
816 for marker in ["///", "//!", "//", "/**", "/*", "*/", "--"] {
817 if let Some(stripped) = l.strip_prefix(marker) {
818 l = stripped;
819 break;
820 }
821 }
822 if let Some(rest) = l.strip_prefix('*')
825 && !l.starts_with("**")
826 {
827 l = rest;
828 }
829 l = l.strip_suffix("*/").unwrap_or(l);
830 lines.push(l.trim());
831 }
832 }
833 while lines.first().is_some_and(|l| l.is_empty()) {
834 lines.remove(0);
835 }
836 while lines.last().is_some_and(|l| l.is_empty()) {
837 lines.pop();
838 }
839 if lines.is_empty() {
840 None
841 } else {
842 Some(lines.join("\n"))
843 }
844}
845
846fn preceding_comments<'t>(
847 node: TsNode<'t>,
848 comment_kinds: &[&str],
849 skip_kinds: &[&str],
850) -> Option<Vec<TsNode<'t>>> {
851 let mut comments = Vec::new();
852 let mut cur = node.prev_named_sibling();
853 let mut skips = 0;
854 while let Some(sib) = cur {
855 if !comment_kinds.contains(&sib.kind()) {
856 if skips < 2 && comments.is_empty() && skip_kinds.contains(&sib.kind()) {
859 skips += 1;
860 cur = sib.prev_named_sibling();
861 continue;
862 }
863 break;
864 }
865 comments.push(sib);
866 cur = sib.prev_named_sibling();
867 }
868 comments.reverse();
869 if comments.is_empty() {
870 None
871 } else {
872 Some(comments)
873 }
874}