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