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 Other,
57}
58
59impl CompileErrorKind {
60 const fn describe(self) -> &'static str {
63 match self {
64 Self::Syntax => "the query is not valid s-expression syntax",
65 Self::UnknownNodeKind => "no such node kind in this grammar",
66 Self::UnknownField => "no such field in this grammar",
67 Self::UnknownCapture => "the query refers to a capture it never binds",
68 Self::ImpossiblePattern => "this pattern can never match",
69 Self::NoCaptures => "the query binds no captures",
70 Self::Other => "the grammar rejected this query",
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Error)]
81pub struct CompileError {
82 pub kind: CompileErrorKind,
84 pub language: LanguageId,
86 pub position: Position,
88 pub offset: usize,
90 pub detail: String,
92 pub line: String,
94}
95
96impl fmt::Display for CompileError {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 writeln!(f, "query error: {}", self.kind.describe())?;
99
100 if !self.detail.is_empty() {
101 match self.kind {
102 CompileErrorKind::UnknownNodeKind => writeln!(
103 f,
104 " the {} grammar has no node kind `{}`",
105 self.language, self.detail
106 )?,
107 CompileErrorKind::UnknownField => writeln!(
108 f,
109 " the {} grammar has no field `{}`",
110 self.language, self.detail
111 )?,
112 _ => writeln!(f, " {}", self.detail)?,
113 }
114 }
115
116 if !self.line.is_empty() {
117 let gutter = format!("{}", self.position.line);
118 let pad = " ".repeat(gutter.len());
119 writeln!(
120 f,
121 "{pad} --> query:{}:{}",
122 self.position.line, self.position.column
123 )?;
124 writeln!(f, "{pad} |")?;
125 writeln!(f, "{gutter} | {}", self.line)?;
126 let caret_pad = " ".repeat(self.position.column.saturating_sub(1) as usize);
129 writeln!(f, "{pad} | {caret_pad}^")?;
130 }
131
132 Ok(())
133 }
134}
135
136#[derive(Debug)]
138pub struct CompiledQuery {
139 query: Query,
140 language: LanguageId,
141 capture_names: Vec<String>,
142}
143
144impl CompiledQuery {
145 pub fn compile(language: &dyn Language, source: &str) -> Result<Self, CompileError> {
152 let grammar = language.grammar();
153 let id = language.id();
154
155 let query = Query::new(&grammar, source).map_err(|err| {
156 let kind = match err.kind {
157 QueryErrorKind::Syntax => CompileErrorKind::Syntax,
158 QueryErrorKind::NodeType => CompileErrorKind::UnknownNodeKind,
159 QueryErrorKind::Field => CompileErrorKind::UnknownField,
160 QueryErrorKind::Capture => CompileErrorKind::UnknownCapture,
161 QueryErrorKind::Structure => CompileErrorKind::ImpossiblePattern,
162 _ => CompileErrorKind::Other,
163 };
164
165 let detail = match kind {
169 CompileErrorKind::Syntax => String::new(),
170 _ => err.message.trim_matches('"').to_owned(),
171 };
172
173 CompileError {
174 kind,
175 language: id,
176 position: Position::new(
177 u32::try_from(err.row).unwrap_or(u32::MAX).saturating_add(1),
178 u32::try_from(err.column)
179 .unwrap_or(u32::MAX)
180 .saturating_add(1),
181 ),
182 offset: err.offset,
183 detail,
184 line: source.lines().nth(err.row).unwrap_or_default().to_owned(),
185 }
186 })?;
187
188 let capture_names: Vec<String> = query
189 .capture_names()
190 .iter()
191 .map(|name| (*name).to_owned())
192 .collect();
193
194 if capture_names.is_empty() {
198 return Err(CompileError {
199 kind: CompileErrorKind::NoCaptures,
200 language: id,
201 position: Position::START,
202 offset: 0,
203 detail: "add a capture such as `@match` so the handler can reference the node"
204 .to_owned(),
205 line: source.lines().next().unwrap_or_default().to_owned(),
206 });
207 }
208
209 Ok(Self {
210 query,
211 language: id,
212 capture_names,
213 })
214 }
215
216 #[must_use]
218 pub const fn language(&self) -> LanguageId {
219 self.language
220 }
221
222 #[must_use]
224 pub fn capture_names(&self) -> &[String] {
225 &self.capture_names
226 }
227
228 #[must_use]
230 pub fn pattern_count(&self) -> usize {
231 self.query.pattern_count()
232 }
233
234 pub fn for_each_match<'tree>(
244 &self,
245 tree: &'tree Tree,
246 source: &[u8],
247 visit: impl FnMut(QueryMatch<'_, 'tree>),
248 ) {
249 self.for_each_match_in(tree.root_node(), source, visit);
250 }
251
252 pub fn for_each_match_in<'tree>(
258 &self,
259 node: Node<'tree>,
260 source: &[u8],
261 mut visit: impl FnMut(QueryMatch<'_, 'tree>),
262 ) {
263 let mut cursor = QueryCursor::new();
264 let mut matches = cursor.matches(&self.query, node, source);
265
266 while let Some(m) = matches.next() {
267 let captures = m
268 .captures
269 .iter()
270 .map(|capture| {
271 let name = self
272 .capture_names
273 .get(capture.index as usize)
274 .map_or("", String::as_str);
275 (name, capture.node)
276 })
277 .collect();
278
279 visit(QueryMatch {
280 pattern_index: m.pattern_index,
281 captures,
282 });
283 }
284 }
285}
286
287#[derive(Debug, Clone)]
289pub struct QueryMatch<'q, 'tree> {
290 pub pattern_index: usize,
292 pub captures: Vec<(&'q str, Node<'tree>)>,
294}
295
296impl<'tree> QueryMatch<'_, 'tree> {
297 #[must_use]
302 pub fn get(&self, name: &str) -> Option<Node<'tree>> {
303 self.captures
304 .iter()
305 .find(|(n, _)| *n == name)
306 .map(|(_, node)| *node)
307 }
308
309 pub fn get_all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = Node<'tree>> + 'a {
311 self.captures
312 .iter()
313 .filter(move |(n, _)| *n == name)
314 .map(|(_, node)| *node)
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use lanekeep_lang_js::{JavaScript, Tsx, TypeScript};
321
322 use super::*;
323
324 fn parse(language: &dyn Language, source: &str) -> Tree {
325 let mut parser = tree_sitter::Parser::new();
326 parser
327 .set_language(&language.grammar())
328 .expect("grammar loads");
329 parser.parse(source, None).expect("parser returns a tree")
330 }
331
332 fn compile(source: &str) -> CompiledQuery {
333 CompiledQuery::compile(&TypeScript, source).expect("query compiles")
334 }
335
336 fn compile_err(source: &str) -> CompileError {
337 CompiledQuery::compile(&TypeScript, source).expect_err("query should not compile")
338 }
339
340 fn run(query: &CompiledQuery, source: &str) -> Vec<Vec<String>> {
342 let tree = parse(&TypeScript, source);
343 let mut out = Vec::new();
344 query.for_each_match(&tree, source.as_bytes(), |m| {
345 out.push(
346 m.captures
347 .iter()
348 .map(|(name, node)| {
349 let text = node.utf8_text(source.as_bytes()).unwrap_or("<invalid>");
350 format!("{name}={text}")
351 })
352 .collect(),
353 );
354 });
355 out
356 }
357
358 #[test]
359 fn compiles_a_simple_query() {
360 let query = compile("(identifier) @id");
361 assert_eq!(query.capture_names(), ["id"]);
362 assert_eq!(query.pattern_count(), 1);
363 assert_eq!(query.language().as_str(), "typescript");
364 }
365
366 #[test]
367 fn reports_capture_names_in_index_order() {
368 let query =
369 compile("(pair key: (property_identifier) @prop value: (number) @value) @match");
370 assert_eq!(query.capture_names(), ["prop", "value", "match"]);
371 }
372
373 #[test]
374 fn matches_expose_captures_by_name() {
375 let query =
376 compile("(pair key: (property_identifier) @prop value: (number) @value) @match");
377 let matches = run(&query, "const s = { padding: 12, margin: 4 };");
378
379 assert_eq!(matches.len(), 2);
380 assert!(matches[0].contains(&"prop=padding".to_owned()));
381 assert!(matches[0].contains(&"value=12".to_owned()));
382 assert!(matches[1].contains(&"prop=margin".to_owned()));
383 assert!(matches[1].contains(&"value=4".to_owned()));
384 }
385
386 #[test]
387 fn get_returns_the_node_for_a_capture() {
388 let query = compile("(pair key: (property_identifier) @prop) @match");
389 let source = "const s = { padding: 12 };";
390 let tree = parse(&TypeScript, source);
391
392 let mut seen = Vec::new();
393 query.for_each_match(&tree, source.as_bytes(), |m| {
394 let prop = m.get("prop").expect("prop is bound");
395 seen.push(
396 prop.utf8_text(source.as_bytes())
397 .unwrap_or_default()
398 .to_owned(),
399 );
400 assert!(m.get("nope").is_none(), "unbound capture must be None");
401 });
402
403 assert_eq!(seen, ["padding"]);
404 }
405
406 #[test]
407 fn match_order_is_deterministic() {
408 let query = compile("(identifier) @id");
413 let source = "const alpha = 1; const beta = 2; function gamma() { return delta }";
414
415 let first = run(&query, source);
416 for _ in 0..25 {
417 assert_eq!(
418 run(&query, source),
419 first,
420 "match order varied between runs"
421 );
422 }
423 assert!(
424 first.len() >= 4,
425 "expected several matches, got {}",
426 first.len()
427 );
428 }
429
430 #[test]
431 fn a_query_matching_nothing_yields_no_matches() {
432 let query = compile("(class_declaration) @c");
433 assert!(run(&query, "const x = 1;").is_empty());
434 }
435
436 #[test]
437 fn handles_an_empty_source_file() {
438 let query = compile("(identifier) @id");
439 assert!(run(&query, "").is_empty());
440 }
441
442 #[test]
443 fn rejects_a_query_with_no_captures() {
444 let err = compile_err("(identifier)");
448 assert_eq!(err.kind, CompileErrorKind::NoCaptures);
449 assert!(
450 err.to_string().contains("@match"),
451 "should suggest adding a capture"
452 );
453 }
454
455 #[test]
456 fn rejects_an_unknown_node_kind_with_a_useful_message() {
457 let err = compile_err("(nonexistent_node) @x");
459 assert_eq!(err.kind, CompileErrorKind::UnknownNodeKind);
460 assert_eq!(err.detail, "nonexistent_node");
461
462 let rendered = err.to_string();
463 assert!(
464 rendered.contains("typescript"),
465 "should name the grammar: {rendered}"
466 );
467 assert!(
468 rendered.contains("nonexistent_node"),
469 "should name the node: {rendered}"
470 );
471 assert!(
472 rendered.contains("-->"),
473 "should point at a position: {rendered}"
474 );
475 assert!(rendered.contains('^'), "should carry a caret: {rendered}");
476 }
477
478 #[test]
479 fn rejects_an_unknown_field() {
480 let err = compile_err("(pair nonexistent_field: (number) @n) @m");
481 assert_eq!(err.kind, CompileErrorKind::UnknownField);
482 assert_eq!(err.detail, "nonexistent_field");
483 assert!(err.to_string().contains("no field"), "{err}");
484 }
485
486 #[test]
487 fn rejects_malformed_syntax() {
488 let err = compile_err("(pair key: (property_identifier) @a");
489 assert_eq!(err.kind, CompileErrorKind::Syntax);
490 }
491
492 #[test]
493 fn points_at_the_right_line_of_a_multiline_query() {
494 let err = CompiledQuery::compile(
497 &TypeScript,
498 "(pair\n key: (property_identifier) @prop\n value: (nonexistent_node) @v) @m",
499 )
500 .expect_err("should not compile");
501
502 assert_eq!(err.position.line, 3, "should point at the third line");
503 assert!(
504 err.line.contains("nonexistent_node"),
505 "excerpt should be that line: {err:?}"
506 );
507
508 let rendered = err.to_string();
509 assert!(rendered.contains("query:3:"), "{rendered}");
510 let caret_line = rendered.lines().last().unwrap_or_default();
512 let caret_col = caret_line.find('^').unwrap_or(0);
513 assert!(
514 caret_col > 4,
515 "caret should be indented to the token: {rendered}"
516 );
517 }
518
519 #[test]
520 fn compiles_against_each_language() {
521 let jsx = "(jsx_element) @el";
524 assert!(
525 CompiledQuery::compile(&Tsx, jsx).is_ok(),
526 "TSX should know jsx_element"
527 );
528 assert!(
529 CompiledQuery::compile(&JavaScript, jsx).is_ok(),
530 "JS should know jsx_element"
531 );
532
533 let err = CompiledQuery::compile(&TypeScript, jsx)
534 .expect_err("plain TypeScript has no JSX nodes");
535 assert_eq!(err.kind, CompileErrorKind::UnknownNodeKind);
536 assert_eq!(err.language.as_str(), "typescript");
537
538 let types = "(type_annotation) @t";
539 assert!(CompiledQuery::compile(&TypeScript, types).is_ok());
540 assert!(
541 CompiledQuery::compile(&JavaScript, types).is_err(),
542 "JavaScript has no type annotations"
543 );
544 }
545
546 #[test]
547 fn supports_alternations_and_multiple_patterns() {
548 let query = compile("[(number) (string)] @literal");
549 let matches = run(&query, "const a = 1; const b = 'two';");
550 assert_eq!(matches.len(), 2);
551
552 let two = compile("(number) @n\n(string) @s");
553 assert_eq!(two.pattern_count(), 2);
554 assert_eq!(two.capture_names(), ["n", "s"]);
555 }
556
557 #[test]
558 fn get_all_returns_every_binding_of_a_repeated_capture() {
559 let query = compile("(object (pair) @entry) @obj");
562 let source = "const s = { a: 1, b: 2, c: 3 };";
563 let tree = parse(&TypeScript, source);
564
565 let mut counts = Vec::new();
566 query.for_each_match(&tree, source.as_bytes(), |m| {
567 counts.push(m.get_all("entry").count());
568 assert!(m.get("entry").is_some());
569 });
570
571 assert!(!counts.is_empty(), "expected at least one match");
572 }
573
574 #[test]
575 fn nodes_carry_positions_usable_for_reporting() {
576 let query = compile("(number) @n");
577 let source = "const a = 1;\nconst b = 22;";
578 let tree = parse(&TypeScript, source);
579
580 let mut positions = Vec::new();
581 query.for_each_match(&tree, source.as_bytes(), |m| {
582 let node = m.get("n").expect("bound");
583 let start = node.start_position();
584 positions.push((start.row + 1, start.column + 1));
585 });
586
587 assert_eq!(positions, [(1, 11), (2, 11)]);
588 }
589}