1use std::collections::BTreeMap;
16
17use harn_hostlib::ast::{api, Language};
18use regex::Regex;
19use streaming_iterator::StreamingIterator;
20use tree_sitter::{Node, Query, QueryCursor};
21
22use crate::engine::{Binding, Span};
23use crate::error::RulesError;
24use crate::model::{AtomicMatcher, RuleNode, StopBy, StopKeyword};
25use crate::pattern::{compile_pattern, ROOT_CAPTURE};
26
27type Bindings = BTreeMap<String, Binding>;
29
30pub struct EvalMatch {
32 pub span: Span,
34 pub text: String,
36 pub bindings: Bindings,
38}
39
40pub struct CompiledRuleTree {
43 top: CompiledNode,
44 utils: BTreeMap<String, CompiledNode>,
45}
46
47struct CompiledNode {
48 atomic: Option<CompiledAtomic>,
49 inside: Option<Box<CompiledRel>>,
50 has: Option<Box<CompiledRel>>,
51 follows: Option<Box<CompiledRel>>,
52 precedes: Option<Box<CompiledRel>>,
53 all: Vec<CompiledNode>,
54 any: Vec<CompiledNode>,
55 not: Option<Box<CompiledNode>>,
56 matches: Option<String>,
57}
58
59struct CompiledRel {
60 node: CompiledNode,
61 stop_by: CompiledStopBy,
62 field: Option<String>,
63}
64
65enum CompiledStopBy {
66 Neighbor,
67 End,
68 Rule(Box<CompiledNode>),
69}
70
71enum CompiledAtomic {
72 Query { query: Query, metavars: Vec<String> },
73 Kind(String),
74 Regex(Regex),
75}
76
77impl CompiledRuleTree {
78 pub fn compile(
81 rule_id: &str,
82 language: Language,
83 top: &RuleNode,
84 utils: &BTreeMap<String, RuleNode>,
85 ) -> Result<Self, RulesError> {
86 if top.is_empty() {
87 return Err(RulesError::PatternCompile {
88 rule: rule_id.to_string(),
89 message: "rule node is empty (no atomic / relational / composite key)".into(),
90 });
91 }
92 let compiled_utils = utils
93 .iter()
94 .map(|(id, node)| Ok((id.clone(), compile_node(rule_id, language, node)?)))
95 .collect::<Result<BTreeMap<_, _>, RulesError>>()?;
96 Ok(CompiledRuleTree {
97 top: compile_node(rule_id, language, top)?,
98 utils: compiled_utils,
99 })
100 }
101
102 pub fn find(
104 &self,
105 rule_id: &str,
106 language: Language,
107 source: &str,
108 ) -> Result<Vec<EvalMatch>, RulesError> {
109 let tree = api::parse_tree(source, language).map_err(|err| RulesError::SourceParse {
110 rule: rule_id.to_string(),
111 message: err.to_string(),
112 })?;
113 let ctx = Ctx {
114 source,
115 utils: &self.utils,
116 };
117 let root = tree.root_node();
118
119 let mut seen: BTreeMap<(usize, usize), EvalMatch> = BTreeMap::new();
120 for node in seed_candidates(&self.top, &ctx, root) {
121 if let Some(bindings) = node_satisfies(&self.top, node, &ctx) {
122 let key = (node.start_byte(), node.end_byte());
123 seen.entry(key).or_insert_with(|| EvalMatch {
124 span: Span::of(node),
125 text: ctx.text(node),
126 bindings,
127 });
128 }
129 }
130 Ok(seen.into_values().collect())
131 }
132}
133
134struct Ctx<'a> {
136 source: &'a str,
137 utils: &'a BTreeMap<String, CompiledNode>,
138}
139
140impl Ctx<'_> {
141 #[expect(
142 clippy::string_slice,
143 reason = "tree-sitter node ranges are char-aligned byte offsets into the parsed source"
144 )]
145 fn text(&self, node: Node<'_>) -> String {
146 self.source[node.start_byte()..node.end_byte()].to_string()
147 }
148}
149
150fn compile_node(
155 rule_id: &str,
156 language: Language,
157 node: &RuleNode,
158) -> Result<CompiledNode, RulesError> {
159 let mkerr = |message: String| RulesError::PatternCompile {
160 rule: rule_id.to_string(),
161 message,
162 };
163
164 let atomic = match node.atomic().map_err(mkerr)? {
165 None => None,
166 Some(AtomicMatcher::Pattern(snippet)) => {
167 let ts_language = language
168 .ts_language()
169 .ok_or_else(|| mkerr(format!("grammar for `{}` unavailable", language.name())))?;
170 let compiled =
171 compile_pattern(&snippet, language).map_err(|m| mkerr(format!("pattern: {m}")))?;
172 let query = Query::new(&ts_language, &compiled.query).map_err(|e| {
173 RulesError::QueryRejected {
174 rule: rule_id.to_string(),
175 message: e.to_string(),
176 query: compiled.query.clone(),
177 }
178 })?;
179 Some(CompiledAtomic::Query {
180 query,
181 metavars: compiled.metavars,
182 })
183 }
184 Some(AtomicMatcher::Kind(kind)) => Some(CompiledAtomic::Kind(kind)),
185 Some(AtomicMatcher::Regex(re)) => Some(CompiledAtomic::Regex(
186 Regex::new(&re).map_err(|e| mkerr(format!("regex `{re}` invalid: {e}")))?,
187 )),
188 Some(AtomicMatcher::RawQuery(raw)) => {
189 let ts_language = language
190 .ts_language()
191 .ok_or_else(|| mkerr(format!("grammar for `{}` unavailable", language.name())))?;
192 let query = Query::new(&ts_language, &raw).map_err(|e| RulesError::QueryRejected {
193 rule: rule_id.to_string(),
194 message: e.to_string(),
195 query: raw.clone(),
196 })?;
197 if !query.capture_names().contains(&ROOT_CAPTURE) {
203 return Err(mkerr(format!(
204 "raw `query` must bind the matched node to `@{ROOT_CAPTURE}`"
205 )));
206 }
207 let metavars = query
208 .capture_names()
209 .iter()
210 .filter(|name| **name != ROOT_CAPTURE)
211 .map(|name| name.to_string())
212 .collect();
213 Some(CompiledAtomic::Query { query, metavars })
214 }
215 };
216
217 let rel = |sub: &Option<Box<RuleNode>>| -> Result<Option<Box<CompiledRel>>, RulesError> {
218 match sub {
219 None => Ok(None),
220 Some(n) => Ok(Some(Box::new(compile_rel(rule_id, language, n)?))),
221 }
222 };
223
224 let compile_list = |list: &Option<Vec<RuleNode>>| -> Result<Vec<CompiledNode>, RulesError> {
225 list.iter()
226 .flatten()
227 .map(|n| compile_node(rule_id, language, n))
228 .collect()
229 };
230
231 Ok(CompiledNode {
232 atomic,
233 inside: rel(&node.inside)?,
234 has: rel(&node.has)?,
235 follows: rel(&node.follows)?,
236 precedes: rel(&node.precedes)?,
237 all: compile_list(&node.all)?,
238 any: compile_list(&node.any)?,
239 not: match &node.not {
240 None => None,
241 Some(n) => Some(Box::new(compile_node(rule_id, language, n)?)),
242 },
243 matches: node.matches.clone(),
244 })
245}
246
247fn compile_rel(
248 rule_id: &str,
249 language: Language,
250 node: &RuleNode,
251) -> Result<CompiledRel, RulesError> {
252 let stop_by = match &node.stop_by {
253 None | Some(StopBy::Keyword(StopKeyword::Neighbor)) => CompiledStopBy::Neighbor,
254 Some(StopBy::Keyword(StopKeyword::End)) => CompiledStopBy::End,
255 Some(StopBy::Rule(r)) => {
256 CompiledStopBy::Rule(Box::new(compile_node(rule_id, language, r)?))
257 }
258 };
259 Ok(CompiledRel {
260 node: compile_node(rule_id, language, node)?,
261 stop_by,
262 field: node.field.clone(),
263 })
264}
265
266fn seed_candidates<'t>(top: &CompiledNode, ctx: &Ctx<'_>, root: Node<'t>) -> Vec<Node<'t>> {
273 match &top.atomic {
274 Some(CompiledAtomic::Query { query, .. }) => {
275 let mut out = Vec::new();
276 let mut seen = std::collections::HashSet::new();
277 let root_index = root_capture_index(query);
278 let mut cursor = QueryCursor::new();
279 let mut it = cursor.matches(query, root, ctx.source.as_bytes());
280 while let Some(m) = it.next() {
281 for cap in m.captures {
282 if Some(cap.index) == root_index && seen.insert(cap.node.id()) {
283 out.push(cap.node);
284 }
285 }
286 }
287 out
288 }
289 Some(CompiledAtomic::Kind(kind)) => {
290 let mut out = Vec::new();
291 for_each_named_descendant(root, &mut |n| {
292 if n.kind() == kind {
293 out.push(n);
294 }
295 });
296 out
297 }
298 Some(CompiledAtomic::Regex(_)) | None => {
299 let mut out = Vec::new();
300 for_each_named_descendant(root, &mut |n| out.push(n));
301 out
302 }
303 }
304}
305
306fn node_satisfies(cnode: &CompiledNode, node: Node<'_>, ctx: &Ctx<'_>) -> Option<Bindings> {
308 let mut bindings = Bindings::new();
309
310 if let Some(atomic) = &cnode.atomic {
311 merge(&mut bindings, atomic_match(atomic, node, ctx)?);
312 }
313 if let Some(rel) = &cnode.inside {
314 merge(&mut bindings, eval_inside(rel, node, ctx)?);
315 }
316 if let Some(rel) = &cnode.has {
317 merge(&mut bindings, eval_has(rel, node, ctx)?);
318 }
319 if let Some(rel) = &cnode.follows {
320 merge(&mut bindings, eval_sibling(rel, node, ctx, Dir::Before)?);
321 }
322 if let Some(rel) = &cnode.precedes {
323 merge(&mut bindings, eval_sibling(rel, node, ctx, Dir::After)?);
324 }
325 for sub in &cnode.all {
326 merge(&mut bindings, node_satisfies(sub, node, ctx)?);
327 }
328 if !cnode.any.is_empty() {
329 let matched = cnode
330 .any
331 .iter()
332 .find_map(|sub| node_satisfies(sub, node, ctx));
333 merge(&mut bindings, matched?);
334 }
335 if let Some(not) = &cnode.not {
336 if node_satisfies(not, node, ctx).is_some() {
337 return None;
338 }
339 }
340 if let Some(id) = &cnode.matches {
341 let util = ctx.utils.get(id)?;
342 merge(&mut bindings, node_satisfies(util, node, ctx)?);
343 }
344
345 Some(bindings)
346}
347
348fn atomic_match(atomic: &CompiledAtomic, node: Node<'_>, ctx: &Ctx<'_>) -> Option<Bindings> {
349 match atomic {
350 CompiledAtomic::Kind(kind) => (node.kind() == kind).then(Bindings::new),
351 CompiledAtomic::Regex(re) => re.is_match(&ctx.text(node)).then(Bindings::new),
352 CompiledAtomic::Query { query, metavars } => {
353 let root_index = root_capture_index(query);
354 let names: Vec<&str> = query.capture_names().to_vec();
355 let mut cursor = QueryCursor::new();
356 let mut it = cursor.matches(query, node, ctx.source.as_bytes());
357 while let Some(m) = it.next() {
358 let roots_here = m
360 .captures
361 .iter()
362 .any(|c| Some(c.index) == root_index && c.node.id() == node.id());
363 if !roots_here {
364 continue;
365 }
366 let mut bindings = Bindings::new();
367 for cap in m.captures {
368 let name = names[cap.index as usize];
369 if metavars.iter().any(|mv| mv == name) {
370 bindings.entry(name.to_string()).or_insert_with(|| {
371 Binding::new(ctx.text(cap.node), Span::of(cap.node))
372 });
373 }
374 }
375 return Some(bindings);
376 }
377 None
378 }
379 }
380}
381
382fn eval_inside(rel: &CompiledRel, node: Node<'_>, ctx: &Ctx<'_>) -> Option<Bindings> {
383 let mut current = node.parent();
384 let mut child = node;
385 while let Some(ancestor) = current {
386 if let CompiledStopBy::Rule(stop) = &rel.stop_by {
387 if node_satisfies(stop, ancestor, ctx).is_some()
388 && node_satisfies(&rel.node, ancestor, ctx).is_none()
389 {
390 return None;
392 }
393 }
394 if let Some(b) = node_satisfies(&rel.node, ancestor, ctx) {
395 if field_ok(rel.field.as_deref(), ancestor, child) {
396 return Some(b);
397 }
398 }
399 if matches!(rel.stop_by, CompiledStopBy::Neighbor) {
400 return None;
401 }
402 child = ancestor;
403 current = ancestor.parent();
404 }
405 None
406}
407
408fn eval_has(rel: &CompiledRel, node: Node<'_>, ctx: &Ctx<'_>) -> Option<Bindings> {
409 let neighbor = matches!(rel.stop_by, CompiledStopBy::Neighbor);
410 let mut found: Option<Bindings> = None;
411 let mut cursor = node.walk();
412 let children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
413 for child in children {
414 if let Some(b) = node_satisfies(&rel.node, child, ctx) {
415 if field_ok(rel.field.as_deref(), node, child) {
416 found = Some(b);
417 break;
418 }
419 }
420 if !neighbor {
421 if let Some(b) = eval_has(rel, child, ctx) {
422 found = Some(b);
423 break;
424 }
425 }
426 }
427 found
428}
429
430enum Dir {
431 Before,
432 After,
433}
434
435fn eval_sibling(rel: &CompiledRel, node: Node<'_>, ctx: &Ctx<'_>, dir: Dir) -> Option<Bindings> {
436 let neighbor = matches!(rel.stop_by, CompiledStopBy::Neighbor);
437 let mut sib = match dir {
438 Dir::Before => node.prev_named_sibling(),
439 Dir::After => node.next_named_sibling(),
440 };
441 while let Some(s) = sib {
442 if let Some(b) = node_satisfies(&rel.node, s, ctx) {
443 return Some(b);
444 }
445 if neighbor {
446 return None;
447 }
448 sib = match dir {
449 Dir::Before => s.prev_named_sibling(),
450 Dir::After => s.next_named_sibling(),
451 };
452 }
453 None
454}
455
456fn field_ok(field: Option<&str>, parent: Node<'_>, child: Node<'_>) -> bool {
460 match field {
461 None => true,
462 Some(name) => parent
463 .child_by_field_name(name)
464 .is_some_and(|f| f.id() == child.id()),
465 }
466}
467
468fn merge(into: &mut Bindings, from: Bindings) {
469 for (k, v) in from {
470 into.entry(k).or_insert(v);
471 }
472}
473
474fn root_capture_index(query: &Query) -> Option<u32> {
475 query
476 .capture_names()
477 .iter()
478 .position(|n| *n == ROOT_CAPTURE)
479 .map(|i| i as u32)
480}
481
482fn for_each_named_descendant<'t>(node: Node<'t>, f: &mut impl FnMut(Node<'t>)) {
483 let mut cursor = node.walk();
484 for child in node.named_children(&mut cursor) {
485 f(child);
486 for_each_named_descendant(child, f);
487 }
488}