1use std::fmt;
33
34mod capture_sites;
35pub use capture_sites::{CaptureSite, capture_sites};
36
37use lanekeep_core::Position;
38use lanekeep_lang::{Language, LanguageId};
39use streaming_iterator::StreamingIterator;
40use thiserror::Error;
41use tree_sitter::{Node, Query, QueryCursor, QueryErrorKind, Tree};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum CompileErrorKind {
46 Syntax,
48 UnknownNodeKind,
50 UnknownField,
52 UnknownCapture,
54 ImpossiblePattern,
56 NoCaptures,
58 UnsupportedPredicate,
60 Other,
62}
63
64impl CompileErrorKind {
65 const fn describe(self) -> &'static str {
68 match self {
69 Self::Syntax => "the query is not valid s-expression syntax",
70 Self::UnknownNodeKind => "no such node kind in this grammar",
71 Self::UnknownField => "no such field in this grammar",
72 Self::UnknownCapture => "the query refers to a capture it never binds",
73 Self::ImpossiblePattern => "this pattern can never match",
74 Self::NoCaptures => "the query binds no captures",
75 Self::UnsupportedPredicate => "the query uses a predicate that is never applied",
76 Self::Other => "the grammar rejected this query",
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Error)]
87pub struct CompileError {
88 pub kind: CompileErrorKind,
90 pub language: LanguageId,
92 pub position: Position,
94 pub offset: usize,
96 pub detail: String,
98 pub line: String,
100}
101
102impl fmt::Display for CompileError {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 writeln!(f, "query error: {}", self.kind.describe())?;
105
106 if !self.detail.is_empty() {
107 match self.kind {
108 CompileErrorKind::UnknownNodeKind => writeln!(
109 f,
110 " the {} grammar has no node kind `{}`",
111 self.language, self.detail
112 )?,
113 CompileErrorKind::UnknownField => writeln!(
114 f,
115 " the {} grammar has no field `{}`",
116 self.language, self.detail
117 )?,
118 CompileErrorKind::UnsupportedPredicate => writeln!(
119 f,
120 " the predicate `{}` is parsed but never applied; remove it",
121 self.detail
122 )?,
123 _ => writeln!(f, " {}", self.detail)?,
124 }
125 }
126
127 if !self.line.is_empty() {
128 let gutter = format!("{}", self.position.line);
129 let pad = " ".repeat(gutter.len());
130 writeln!(
131 f,
132 "{pad} --> query:{}:{}",
133 self.position.line, self.position.column
134 )?;
135 writeln!(f, "{pad} |")?;
136 writeln!(f, "{gutter} | {}", self.line)?;
137 let caret_pad = " ".repeat(self.position.column.saturating_sub(1) as usize);
140 writeln!(f, "{pad} | {caret_pad}^")?;
141 }
142
143 Ok(())
144 }
145}
146
147#[derive(Debug)]
149pub struct CompiledQuery {
150 query: Query,
151 language: LanguageId,
152 capture_names: Vec<String>,
153}
154
155impl CompiledQuery {
156 pub fn compile(language: &dyn Language, source: &str) -> Result<Self, CompileError> {
165 let grammar = language.grammar();
166 let id = language.id();
167
168 let query = Query::new(&grammar, source).map_err(|err| {
169 let kind = match err.kind {
170 QueryErrorKind::Syntax => CompileErrorKind::Syntax,
171 QueryErrorKind::NodeType => CompileErrorKind::UnknownNodeKind,
172 QueryErrorKind::Field => CompileErrorKind::UnknownField,
173 QueryErrorKind::Capture => CompileErrorKind::UnknownCapture,
174 QueryErrorKind::Structure => CompileErrorKind::ImpossiblePattern,
175 _ => CompileErrorKind::Other,
176 };
177
178 let detail = match kind {
182 CompileErrorKind::Syntax => String::new(),
183 _ => err.message.trim_matches('"').to_owned(),
184 };
185
186 CompileError {
187 kind,
188 language: id,
189 position: Position::new(
190 u32::try_from(err.row).unwrap_or(u32::MAX).saturating_add(1),
191 u32::try_from(err.column)
192 .unwrap_or(u32::MAX)
193 .saturating_add(1),
194 ),
195 offset: err.offset,
196 detail,
197 line: source.lines().nth(err.row).unwrap_or_default().to_owned(),
198 }
199 })?;
200
201 for pattern in 0..query.pattern_count() {
208 let operator = query
209 .general_predicates(pattern)
210 .first()
211 .map(|predicate| predicate.operator.to_string())
212 .or_else(|| {
213 query
214 .property_predicates(pattern)
215 .first()
216 .map(|(_, is_positive)| {
217 if *is_positive { "is?" } else { "is-not?" }.to_owned()
218 })
219 })
220 .or_else(|| {
221 query
222 .property_settings(pattern)
223 .first()
224 .map(|_| "set!".to_owned())
225 });
226
227 if let Some(operator) = operator {
228 let start = query.start_byte_for_pattern(pattern);
229 let end = query.end_byte_for_pattern(pattern);
230 let needle = format!("#{operator}");
231 let offset = source[start..end]
232 .find(&needle)
233 .map_or(start, |rel| start + rel);
234 let (position, line) = position_at(source, offset);
235 return Err(CompileError {
236 kind: CompileErrorKind::UnsupportedPredicate,
237 language: id,
238 position,
239 offset,
240 detail: needle,
241 line: line.to_owned(),
242 });
243 }
244 }
245
246 let capture_names: Vec<String> = query
247 .capture_names()
248 .iter()
249 .map(|name| (*name).to_owned())
250 .collect();
251
252 if capture_names.is_empty() {
256 return Err(CompileError {
257 kind: CompileErrorKind::NoCaptures,
258 language: id,
259 position: Position::START,
260 offset: 0,
261 detail: "add a capture such as `@match` so the handler can reference the node"
262 .to_owned(),
263 line: source.lines().next().unwrap_or_default().to_owned(),
264 });
265 }
266
267 Ok(Self {
268 query,
269 language: id,
270 capture_names,
271 })
272 }
273
274 #[must_use]
276 pub const fn language(&self) -> LanguageId {
277 self.language
278 }
279
280 #[must_use]
282 pub fn capture_names(&self) -> &[String] {
283 &self.capture_names
284 }
285
286 #[must_use]
288 pub fn pattern_count(&self) -> usize {
289 self.query.pattern_count()
290 }
291
292 pub fn for_each_match<'tree>(
302 &self,
303 tree: &'tree Tree,
304 source: &[u8],
305 visit: impl FnMut(QueryMatch<'_, 'tree>),
306 ) {
307 self.for_each_match_in(tree.root_node(), source, visit);
308 }
309
310 pub fn for_each_match_in<'tree>(
316 &self,
317 node: Node<'tree>,
318 source: &[u8],
319 mut visit: impl FnMut(QueryMatch<'_, 'tree>),
320 ) {
321 let mut cursor = QueryCursor::new();
322 let mut matches = cursor.matches(&self.query, node, source);
323
324 while let Some(m) = matches.next() {
325 let captures = m
326 .captures()
327 .iter()
328 .map(|capture| {
329 let name = self
330 .capture_names
331 .get(capture.index as usize)
332 .map_or("", String::as_str);
333 (name, capture.node)
334 })
335 .collect();
336
337 visit(QueryMatch {
338 pattern_index: m.pattern_index,
339 captures,
340 });
341 }
342 }
343}
344
345#[derive(Debug, Clone)]
347pub struct QueryMatch<'q, 'tree> {
348 pub pattern_index: usize,
350 pub captures: Vec<(&'q str, Node<'tree>)>,
352}
353
354impl<'tree> QueryMatch<'_, 'tree> {
355 #[must_use]
360 pub fn get(&self, name: &str) -> Option<Node<'tree>> {
361 self.captures
362 .iter()
363 .find(|(n, _)| *n == name)
364 .map(|(_, node)| *node)
365 }
366
367 pub fn get_all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = Node<'tree>> + 'a {
369 self.captures
370 .iter()
371 .filter(move |(n, _)| *n == name)
372 .map(|(_, node)| *node)
373 }
374}
375
376fn position_at(source: &str, offset: usize) -> (Position, &str) {
381 let before = &source[..offset.min(source.len())];
382 let line = u32::try_from(before.bytes().filter(|&b| b == b'\n').count())
383 .unwrap_or(u32::MAX)
384 .saturating_add(1);
385 let last_nl = before.rfind('\n').map_or(0, |i| i + 1);
386 let column = u32::try_from(before[last_nl..].chars().count())
387 .unwrap_or(u32::MAX)
388 .saturating_add(1);
389 let line_text = source.lines().nth((line - 1) as usize).unwrap_or_default();
390 (Position::new(line, column), line_text)
391}
392
393#[cfg(test)]
394mod tests {
395 use lanekeep_lang_js::{JavaScript, Tsx, TypeScript};
396
397 use super::*;
398
399 fn parse(language: &dyn Language, source: &str) -> Tree {
400 let mut parser = tree_sitter::Parser::new();
401 parser
402 .set_language(&language.grammar())
403 .expect("grammar loads");
404 parser.parse(source, None).expect("parser returns a tree")
405 }
406
407 fn compile(source: &str) -> CompiledQuery {
408 CompiledQuery::compile(&TypeScript, source).expect("query compiles")
409 }
410
411 fn compile_err(source: &str) -> CompileError {
412 CompiledQuery::compile(&TypeScript, source).expect_err("query should not compile")
413 }
414
415 fn run(query: &CompiledQuery, source: &str) -> Vec<Vec<String>> {
417 let tree = parse(&TypeScript, source);
418 let mut out = Vec::new();
419 query.for_each_match(&tree, source.as_bytes(), |m| {
420 out.push(
421 m.captures
422 .iter()
423 .map(|(name, node)| {
424 let text = node.utf8_text(source.as_bytes()).unwrap_or("<invalid>");
425 format!("{name}={text}")
426 })
427 .collect(),
428 );
429 });
430 out
431 }
432
433 #[test]
434 fn compiles_a_simple_query() {
435 let query = compile("(identifier) @id");
436 assert_eq!(query.capture_names(), ["id"]);
437 assert_eq!(query.pattern_count(), 1);
438 assert_eq!(query.language().as_str(), "typescript");
439 }
440
441 #[test]
442 fn reports_capture_names_in_index_order() {
443 let query =
444 compile("(pair key: (property_identifier) @prop value: (number) @value) @match");
445 assert_eq!(query.capture_names(), ["prop", "value", "match"]);
446 }
447
448 #[test]
449 fn matches_expose_captures_by_name() {
450 let query =
451 compile("(pair key: (property_identifier) @prop value: (number) @value) @match");
452 let matches = run(&query, "const s = { padding: 12, margin: 4 };");
453
454 assert_eq!(matches.len(), 2);
455 assert!(matches[0].contains(&"prop=padding".to_owned()));
456 assert!(matches[0].contains(&"value=12".to_owned()));
457 assert!(matches[1].contains(&"prop=margin".to_owned()));
458 assert!(matches[1].contains(&"value=4".to_owned()));
459 }
460
461 #[test]
462 fn get_returns_the_node_for_a_capture() {
463 let query = compile("(pair key: (property_identifier) @prop) @match");
464 let source = "const s = { padding: 12 };";
465 let tree = parse(&TypeScript, source);
466
467 let mut seen = Vec::new();
468 query.for_each_match(&tree, source.as_bytes(), |m| {
469 let prop = m.get("prop").expect("prop is bound");
470 seen.push(
471 prop.utf8_text(source.as_bytes())
472 .unwrap_or_default()
473 .to_owned(),
474 );
475 assert!(m.get("nope").is_none(), "unbound capture must be None");
476 });
477
478 assert_eq!(seen, ["padding"]);
479 }
480
481 #[test]
482 fn match_order_is_deterministic() {
483 let query = compile("(identifier) @id");
488 let source = "const alpha = 1; const beta = 2; function gamma() { return delta }";
489
490 let first = run(&query, source);
491 for _ in 0..25 {
492 assert_eq!(
493 run(&query, source),
494 first,
495 "match order varied between runs"
496 );
497 }
498 assert!(
499 first.len() >= 4,
500 "expected several matches, got {}",
501 first.len()
502 );
503 }
504
505 #[test]
506 fn a_query_matching_nothing_yields_no_matches() {
507 let query = compile("(class_declaration) @c");
508 assert!(run(&query, "const x = 1;").is_empty());
509 }
510
511 #[test]
512 fn handles_an_empty_source_file() {
513 let query = compile("(identifier) @id");
514 assert!(run(&query, "").is_empty());
515 }
516
517 #[test]
518 fn rejects_a_query_with_no_captures() {
519 let err = compile_err("(identifier)");
523 assert_eq!(err.kind, CompileErrorKind::NoCaptures);
524 assert!(
525 err.to_string().contains("@match"),
526 "should suggest adding a capture"
527 );
528 }
529
530 #[test]
531 fn rejects_an_unknown_node_kind_with_a_useful_message() {
532 let err = compile_err("(nonexistent_node) @x");
534 assert_eq!(err.kind, CompileErrorKind::UnknownNodeKind);
535 assert_eq!(err.detail, "nonexistent_node");
536
537 let rendered = err.to_string();
538 assert!(
539 rendered.contains("typescript"),
540 "should name the grammar: {rendered}"
541 );
542 assert!(
543 rendered.contains("nonexistent_node"),
544 "should name the node: {rendered}"
545 );
546 assert!(
547 rendered.contains("-->"),
548 "should point at a position: {rendered}"
549 );
550 assert!(rendered.contains('^'), "should carry a caret: {rendered}");
551 }
552
553 #[test]
554 fn rejects_an_unknown_field() {
555 let err = compile_err("(pair nonexistent_field: (number) @n) @m");
556 assert_eq!(err.kind, CompileErrorKind::UnknownField);
557 assert_eq!(err.detail, "nonexistent_field");
558 assert!(err.to_string().contains("no field"), "{err}");
559 }
560
561 #[test]
562 fn rejects_malformed_syntax() {
563 let err = compile_err("(pair key: (property_identifier) @a");
564 assert_eq!(err.kind, CompileErrorKind::Syntax);
565 }
566
567 #[test]
568 fn points_at_the_right_line_of_a_multiline_query() {
569 let err = CompiledQuery::compile(
572 &TypeScript,
573 "(pair\n key: (property_identifier) @prop\n value: (nonexistent_node) @v) @m",
574 )
575 .expect_err("should not compile");
576
577 assert_eq!(err.position.line, 3, "should point at the third line");
578 assert!(
579 err.line.contains("nonexistent_node"),
580 "excerpt should be that line: {err:?}"
581 );
582
583 let rendered = err.to_string();
584 assert!(rendered.contains("query:3:"), "{rendered}");
585 let caret_line = rendered.lines().last().unwrap_or_default();
587 let caret_col = caret_line.find('^').unwrap_or(0);
588 assert!(
589 caret_col > 4,
590 "caret should be indented to the token: {rendered}"
591 );
592 }
593
594 #[test]
595 fn compiles_against_each_language() {
596 let jsx = "(jsx_element) @el";
599 assert!(
600 CompiledQuery::compile(&Tsx, jsx).is_ok(),
601 "TSX should know jsx_element"
602 );
603 assert!(
604 CompiledQuery::compile(&JavaScript, jsx).is_ok(),
605 "JS should know jsx_element"
606 );
607
608 let err = CompiledQuery::compile(&TypeScript, jsx)
609 .expect_err("plain TypeScript has no JSX nodes");
610 assert_eq!(err.kind, CompileErrorKind::UnknownNodeKind);
611 assert_eq!(err.language.as_str(), "typescript");
612
613 let types = "(type_annotation) @t";
614 assert!(CompiledQuery::compile(&TypeScript, types).is_ok());
615 assert!(
616 CompiledQuery::compile(&JavaScript, types).is_err(),
617 "JavaScript has no type annotations"
618 );
619 }
620
621 #[test]
622 fn supports_alternations_and_multiple_patterns() {
623 let query = compile("[(number) (string)] @literal");
624 let matches = run(&query, "const a = 1; const b = 'two';");
625 assert_eq!(matches.len(), 2);
626
627 let two = compile("(number) @n\n(string) @s");
628 assert_eq!(two.pattern_count(), 2);
629 assert_eq!(two.capture_names(), ["n", "s"]);
630 }
631
632 #[test]
633 fn get_all_returns_every_binding_of_a_repeated_capture() {
634 let query = compile("(object (pair) @entry) @obj");
637 let source = "const s = { a: 1, b: 2, c: 3 };";
638 let tree = parse(&TypeScript, source);
639
640 let mut counts = Vec::new();
641 query.for_each_match(&tree, source.as_bytes(), |m| {
642 counts.push(m.get_all("entry").count());
643 assert!(m.get("entry").is_some());
644 });
645
646 assert!(!counts.is_empty(), "expected at least one match");
647 }
648
649 #[test]
650 fn nodes_carry_positions_usable_for_reporting() {
651 let query = compile("(number) @n");
652 let source = "const a = 1;\nconst b = 22;";
653 let tree = parse(&TypeScript, source);
654
655 let mut positions = Vec::new();
656 query.for_each_match(&tree, source.as_bytes(), |m| {
657 let node = m.get("n").expect("bound");
658 let start = node.start_position();
659 positions.push((start.row + 1, start.column + 1));
660 });
661
662 assert_eq!(positions, [(1, 11), (2, 11)]);
663 }
664
665 #[test]
666 fn text_predicates_filter_matches() {
667 let source = "const alpha = 1; const beta = 2;";
674
675 let query = compile("((identifier) @id (#eq? @id \"alpha\"))");
676 assert_eq!(run(&query, source), vec![vec!["id=alpha".to_owned()]]);
677
678 let query = compile("((identifier) @id (#not-eq? @id \"alpha\"))");
679 assert_eq!(run(&query, source), vec![vec!["id=beta".to_owned()]]);
680
681 let query = compile("((identifier) @id (#match? @id \"^a\"))");
682 assert_eq!(run(&query, source), vec![vec!["id=alpha".to_owned()]]);
683
684 let query = compile("((identifier) @id (#not-match? @id \"^a\"))");
685 assert_eq!(run(&query, source), vec![vec!["id=beta".to_owned()]]);
686
687 let source = "const alpha = 1; const beta = 2; const gamma = 3;";
688 let query = compile("((identifier) @id (#any-of? @id \"alpha\" \"gamma\"))");
689 assert_eq!(
690 run(&query, source),
691 vec![vec!["id=alpha".to_owned()], vec!["id=gamma".to_owned()]]
692 );
693
694 let query = compile("((identifier) @id (#not-any-of? @id \"alpha\" \"gamma\"))");
695 assert_eq!(run(&query, source), vec![vec!["id=beta".to_owned()]]);
696 }
697
698 #[test]
699 fn rejects_a_general_predicate_naming_the_operator() {
700 let err = compile_err("((identifier) @id (#is? @id \"x\"))");
704 assert_eq!(err.kind, CompileErrorKind::UnsupportedPredicate);
705 let rendered = err.to_string();
706 assert!(
707 rendered.contains("#is?"),
708 "should name the operator: {rendered}"
709 );
710 assert!(
711 rendered.contains("-->"),
712 "should point at a position: {rendered}"
713 );
714 assert!(rendered.contains('^'), "should carry a caret: {rendered}");
715
716 let err = compile_err("((identifier) @id (#set! @id \"x\"))");
717 assert_eq!(err.kind, CompileErrorKind::UnsupportedPredicate);
718 assert!(err.to_string().contains("#set!"), "{}", err);
719
720 let err = compile_err("((identifier) @id (#is-not? @id \"x\"))");
722 assert_eq!(err.kind, CompileErrorKind::UnsupportedPredicate);
723 assert!(err.to_string().contains("#is-not?"), "{}", err);
724
725 let err = compile_err("((identifier) @id (#foo? @id \"x\"))");
726 assert_eq!(err.kind, CompileErrorKind::UnsupportedPredicate);
727 assert!(err.to_string().contains("#foo?"), "{}", err);
728 }
729
730 #[test]
731 fn general_predicate_error_points_at_the_operator() {
732 let err = CompiledQuery::compile(
733 &TypeScript,
734 "((pair\n key: (property_identifier) @prop\n value: (number) @v) @m\n (#is? @prop \"x\"))",
735 )
736 .expect_err("should not compile");
737 assert_eq!(err.kind, CompileErrorKind::UnsupportedPredicate);
738 assert_eq!(err.position.line, 4, "should point at the fourth line");
739 assert!(
740 err.line.contains("#is?"),
741 "excerpt should be that line: {err:?}"
742 );
743 let rendered = err.to_string();
744 assert!(rendered.contains("query:4:"), "{rendered}");
745 let caret_line = rendered.lines().last().unwrap_or_default();
747 let caret_col = caret_line.find('^').unwrap_or(0);
748 assert!(
749 caret_col > 2,
750 "caret should be indented to the token: {rendered}"
751 );
752 }
753}