big_code_analysis/ast.rs
1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::enum_glob_use, clippy::if_not_else, clippy::wildcard_imports)]
8
9use serde::{Deserialize, Serialize};
10
11use crate::*;
12
13/// Start and end positions of a node in a code in terms of lines, columns,
14/// and byte offsets.
15///
16/// Serialized as a flat object
17/// `{start_line, start_col, end_line, end_col, start_byte, end_byte}`. The
18/// line/column pairs are 1-based; the byte offsets are 0-based half-open
19/// (`[start_byte, end_byte)`) indices into the parsed source bytes
20/// ([`Ast::source`](crate::Ast::source)). The `*_line` vocabulary aligns the
21/// `/ast` span field names with the `/function` and `/metrics` endpoints
22/// (`start_line` / `end_line`), so a client correlating spans across
23/// endpoints no longer special-cases `*_row` vs `*_line` per endpoint
24/// (#638). The former `start_row` / `end_row` keys were renamed as a
25/// `2.0`-line break.
26///
27/// The byte offsets let structural consumers slice the original source for a
28/// node — including internal nodes, whose `value` text the dump omits — so a
29/// caller can recover any subtree's exact bytes without re-deriving offsets
30/// from lines and columns (#727). They mirror tree-sitter's own
31/// `Node::start_byte` / `Node::end_byte`.
32///
33/// The struct is `#[non_exhaustive]`: construct it through [`Span::new`] and
34/// read its public fields, but do not rely on struct-literal construction or
35/// exhaustive destructuring from outside the crate. This is the last planned
36/// shape break to the type. The two byte fields carry `#[serde(default)]` so
37/// span objects serialized before they existed still deserialize (the
38/// offsets default to `0`).
39///
40/// A node's span is `None` for the root and any node when span tracking is
41/// disabled; in that case the wrapping `Option<Span>` serializes as `null`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
43#[non_exhaustive]
44pub struct Span {
45 /// Line of the start position (1-based).
46 pub start_line: usize,
47 /// Column of the start position (1-based).
48 pub start_col: usize,
49 /// Line of the end position (1-based).
50 pub end_line: usize,
51 /// Column of the end position (1-based).
52 pub end_col: usize,
53 /// Byte offset of the node's first byte in the source (0-based).
54 #[serde(default)]
55 pub start_byte: usize,
56 /// Byte offset one past the node's last byte in the source (0-based,
57 /// exclusive).
58 #[serde(default)]
59 pub end_byte: usize,
60}
61
62impl Span {
63 /// Builds a [`Span`] from 1-based line/column pairs and 0-based,
64 /// half-open byte offsets (`[start_byte, end_byte)`).
65 ///
66 /// This is the supported construction path now that the struct is
67 /// `#[non_exhaustive]`.
68 #[must_use]
69 pub fn new(
70 start_line: usize,
71 start_col: usize,
72 end_line: usize,
73 end_col: usize,
74 start_byte: usize,
75 end_byte: usize,
76 ) -> Self {
77 Self {
78 start_line,
79 start_col,
80 end_line,
81 end_col,
82 start_byte,
83 end_byte,
84 }
85 }
86}
87
88/// The payload of an `Ast` request.
89///
90/// Unknown fields are rejected with a deserialization error naming the
91/// offending key, so a typo'd field cannot silently change request
92/// semantics (#633). The web boundary renders that as a `400` carrying the
93/// `unknown_field` `error_kind` token.
94#[derive(Debug, Deserialize, Serialize)]
95#[serde(deny_unknown_fields)]
96pub struct AstPayload {
97 /// The id associated to a request for an `AST`.
98 ///
99 /// Optional on the wire (#645): an omitted `id` deserializes to the
100 /// empty string, which every downstream surface already treats as
101 /// "no correlation id". Defaulting it stops clients eating a `400`
102 /// for a field whose absence has an obvious meaning.
103 #[serde(default)]
104 pub id: String,
105 /// The filename associated to a source code file
106 pub file_name: String,
107 /// The code to be represented as an `AST`
108 pub code: String,
109 /// If `true`, nodes representing comments are ignored. Optional on
110 /// the wire (#645): omitting it defaults to `false`, matching the
111 /// `bool` default and the most common request shape.
112 #[serde(default)]
113 pub comment: bool,
114 /// If `true`, the start and end positions of a node in a code
115 /// are considered. Optional on the wire (#645): omitting it defaults
116 /// to `false`.
117 #[serde(default)]
118 pub span: bool,
119}
120
121/// The response of an `AST` request.
122///
123/// The envelope echoes the resolved `language` slug alongside `id` and
124/// `root`, matching the `/function`, `/comment`, and `/metrics` analysis
125/// endpoints (#654). AST node kinds are grammar-specific, so an `/ast`
126/// consumer most needs to confirm which grammar actually parsed the
127/// source. `language` is the #540 canonical lowercase slug (the same value
128/// the sibling endpoints emit). The added field is a `2.0`-line shape
129/// change to this published library type (STABILITY.md).
130#[derive(Debug, Serialize)]
131pub struct AstResponse {
132 /// The id associated to a request for an `AST`
133 pub id: String,
134 /// The resolved source-language slug that produced this tree (#654).
135 ///
136 /// The #540 canonical lowercase slug (e.g. `cpp`, `python`), matching
137 /// the other analysis endpoints' `language` echo.
138 pub language: String,
139 /// The root node of an `AST`
140 ///
141 /// If `None`, an error has occurred
142 pub root: Option<AstNode>,
143}
144
145/// Greatest AST depth [`AstNode`] will serialize.
146///
147/// An `AstNode` tree is as deep as the parsed expression nesting, which a
148/// caller controls directly — 100 000 levels fit in a 200 KB payload, well
149/// inside `bca-web`'s body cap. `serde` cannot emit a tree without one
150/// native stack frame per level, and overflowing that stack aborts the
151/// process rather than raising a catchable panic (#1056), so the depth is
152/// bounded: a deeper tree fails serialization with an ordinary serializer
153/// error naming the limit.
154///
155/// The bound is set well clear of real source: the deepest AST across the
156/// ~8 000-file corpus under `tests/repositories` (TensorFlow, serde,
157/// DeepSpeech, …) is 188 levels. It is also set well clear of the stack:
158/// the earliest measured overflow of any emitted format was 2 000 levels
159/// on a debug build's default 2 MiB thread.
160pub const MAX_AST_SERIALIZE_DEPTH: usize = 512;
161
162/// Serializes an [`AstNode`]'s children one level deeper, refusing to
163/// descend past [`MAX_AST_SERIALIZE_DEPTH`].
164fn serialize_children<S: serde::Serializer>(
165 children: &[AstNode],
166 serializer: S,
167) -> Result<S::Ok, S::Error> {
168 crate::recursion::serialize_bounded(children, MAX_AST_SERIALIZE_DEPTH, "AstNode", serializer)
169}
170
171/// Information on an `AST` node.
172///
173/// Serialized as a flat object with `snake_case` keys: `type`, `value`,
174/// `span`, `field_name`, `children`.
175#[derive(Debug, Serialize)]
176#[serde(rename_all = "snake_case")]
177pub struct AstNode {
178 /// The type of node
179 pub r#type: &'static str,
180 /// The code associated to a node
181 pub value: String,
182 /// The start and end positions of a node in a code
183 pub span: Option<Span>,
184 /// Tree-sitter grammar field name through which the parent reaches
185 /// this node (e.g. `left`, `right`, `name`, `body`).
186 ///
187 /// `None` for the root node, anonymous tokens (punctuation, keywords),
188 /// and any child that does not occupy a named grammar field. Consumers
189 /// of the JSON output rely on this to distinguish structurally
190 /// equivalent children without grammar-specific positional knowledge.
191 pub field_name: Option<&'static str>,
192 /// The children of a node
193 #[serde(serialize_with = "serialize_children")]
194 pub children: Vec<AstNode>,
195}
196
197// AST depth is caller-controlled — 100 000 levels of nested parentheses
198// fit in a 200 KB payload — so the compiler-generated `Drop` glue would
199// recurse once per level and abort the process (#1056). See
200// [`crate::recursion`].
201crate::recursion::impl_iterative_drop!(AstNode, children);
202
203impl AstNode {
204 /// Builds an `AstNode` with the supplied type, value, span, and
205 /// children. The `field_name` is set to `None`; use
206 /// [`AstNode::with_field_name`] to record the tree-sitter grammar
207 /// field through which the parent reaches this node.
208 #[must_use]
209 pub fn new(
210 r#type: &'static str,
211 value: String,
212 span: Option<Span>,
213 children: Vec<AstNode>,
214 ) -> Self {
215 Self::with_field_name(r#type, value, span, None, children)
216 }
217
218 /// Builds an `AstNode` carrying the tree-sitter grammar field name
219 /// (`left`, `right`, `name`, `body`, ...) through which the parent
220 /// reaches this node.
221 #[must_use]
222 pub fn with_field_name(
223 r#type: &'static str,
224 value: String,
225 span: Option<Span>,
226 field_name: Option<&'static str>,
227 children: Vec<AstNode>,
228 ) -> Self {
229 Self {
230 r#type,
231 value,
232 span,
233 field_name,
234 children,
235 }
236 }
237}
238
239fn build<T: ParserTrait>(parser: &T, span: bool, comment: bool) -> Option<AstNode> {
240 // Iterative depth-first walk that materializes `AstNode`s bottom-up.
241 // Each frame holds the pending parent node, the grammar field name
242 // through which its own parent reached it (None for the root), the
243 // already-materialized child `AstNode`s, and the next child index to
244 // descend into. The parent's `field_name_for_child(idx)` lookup is
245 // O(1) and avoids the parallel cursor walk that was required when
246 // field names had to be captured via `TreeCursor::field_name()`.
247 struct Frame<'a> {
248 node: crate::Node<'a>,
249 field: Option<&'static str>,
250 children: Vec<AstNode>,
251 next_child_index: usize,
252 }
253
254 let code = parser.code();
255 let root = parser.root();
256 let mut stack: Vec<Frame<'_>> = vec![Frame {
257 node: root,
258 field: None,
259 children: Vec::with_capacity(root.child_count()),
260 next_child_index: 0,
261 }];
262
263 loop {
264 let frame = stack
265 .last_mut()
266 .expect("stack invariant: loop only runs while stack is non-empty");
267 let child_count = frame.node.child_count();
268 if frame.next_child_index < child_count {
269 let idx = frame.next_child_index;
270 frame.next_child_index += 1;
271 // `Node::child` is O(1) (direct tree-sitter pointer
272 // arithmetic); `field_name_for_child` returns the static
273 // grammar field for that child position. Tree-sitter caps
274 // child indices at u32, so the cast is safe by invariant.
275 let child = frame
276 .node
277 .child(idx)
278 .expect("stack invariant: idx < child_count so the child exists");
279 let field = frame.node.field_name_for_child(
280 u32::try_from(idx).expect("invariant: tree-sitter caps child indices at u32::MAX"),
281 );
282 stack.push(Frame {
283 node: child,
284 field,
285 children: Vec::with_capacity(child.child_count()),
286 next_child_index: 0,
287 });
288 } else {
289 let frame = stack
290 .pop()
291 .expect("stack invariant: just observed non-empty via last_mut()");
292 let node = T::Checker::get_ast_node(
293 &frame.node,
294 code,
295 span,
296 comment,
297 frame.field,
298 frame.children,
299 );
300 match (node, stack.last_mut()) {
301 (Some(ast), Some(parent)) => parent.children.push(ast),
302 (Some(ast), None) => return Some(ast),
303 (None, None) => return None,
304 (None, Some(_)) => {}
305 }
306 }
307 }
308}
309
310/// Configuration options for retrieving the nodes of an `AST`.
311#[derive(Debug)]
312pub struct AstCfg {
313 /// The id associated to a request for an `AST`
314 pub id: String,
315 /// The resolved source-language slug to echo in the response
316 /// envelope (#654). The #540 canonical lowercase slug.
317 pub language: String,
318 /// If `true`, nodes representing comments are ignored
319 pub comment: bool,
320 /// If `true`, the start and end positions of a node in a code
321 /// are considered
322 pub span: bool,
323}
324
325/// Build the AST dump for `parser` under `cfg`. Backs [`crate::Ast::dump`];
326/// the AST-extraction analogue of [`crate::spaces::metrics_inner`] /
327/// [`crate::ops::ops_inner`].
328pub(crate) fn dump_inner<T: ParserTrait>(parser: &T, cfg: AstCfg) -> AstResponse {
329 AstResponse {
330 id: cfg.id,
331 language: cfg.language,
332 root: build(parser, cfg.span, cfg.comment),
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use std::path::PathBuf;
339
340 use super::*;
341
342 fn build_ast<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
343 let path = PathBuf::from(filename);
344 let parser = P::new(code.to_vec(), &path, None);
345 let cfg = AstCfg {
346 id: String::new(),
347 language: String::new(),
348 comment: false,
349 span: false,
350 };
351 dump_inner(&parser, cfg)
352 .root
353 .expect("parser should produce a root AST node")
354 }
355
356 fn build_ast_with_span<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
357 let path = PathBuf::from(filename);
358 let parser = P::new(code.to_vec(), &path, None);
359 let cfg = AstCfg {
360 id: String::new(),
361 language: String::new(),
362 comment: false,
363 span: true,
364 };
365 dump_inner(&parser, cfg)
366 .root
367 .expect("parser should produce a root AST node")
368 }
369
370 fn find_first<'a>(node: &'a AstNode, kind: &str) -> Option<&'a AstNode> {
371 if node.r#type == kind {
372 return Some(node);
373 }
374 node.children.iter().find_map(|c| find_first(c, kind))
375 }
376
377 fn find_child<'a>(parent: &'a AstNode, field: &str) -> Option<&'a AstNode> {
378 parent.children.iter().find(|c| c.field_name == Some(field))
379 }
380
381 #[test]
382 fn root_has_no_field_name() {
383 let root = build_ast::<crate::RustParser>(b"fn main() {}", "test.rs");
384 assert_eq!(root.field_name, None);
385 }
386
387 #[test]
388 fn rust_assignment_carries_left_and_right_field_names() {
389 // `assignment_expression` in the Rust grammar names its operands
390 // `left` and `right`. Without `FieldName` exposed in the JSON,
391 // downstream consumers cannot distinguish the two `identifier`
392 // children. This is the canonical example from issue #244.
393 let root =
394 build_ast::<crate::RustParser>(b"fn f() { let mut a = 0; a = a + 1; }", "test.rs");
395 let assign = find_first(&root, "assignment_expression")
396 .expect("expected an assignment_expression node");
397 let left = find_child(assign, "left").expect("expected a `left` child");
398 let right = find_child(assign, "right").expect("expected a `right` child");
399 assert_eq!(left.field_name, Some("left"));
400 assert_eq!(right.field_name, Some("right"));
401 // Anonymous `=` token is a child too, with no field name.
402 assert!(
403 assign
404 .children
405 .iter()
406 .any(|c| c.r#type == "=" && c.field_name.is_none()),
407 "expected the `=` token child to carry no field name; got {:?}",
408 assign
409 .children
410 .iter()
411 .map(|c| (c.r#type, c.field_name))
412 .collect::<Vec<_>>(),
413 );
414 }
415
416 #[test]
417 fn rust_function_carries_name_and_body_field_names() {
418 // `function_item` names children `name`, `parameters`, `body`.
419 // Assert the field name directly on the AstNode so a bug that
420 // misnames a field (e.g. always emits "body") fails even if
421 // the target node kinds coincidentally line up.
422 let root =
423 build_ast::<crate::RustParser>(b"fn greet(name: &str) -> &str { name }", "test.rs");
424 let func = find_first(&root, "function_item").expect("expected a function_item node");
425 let name_child = find_child(func, "name").expect("function_item should have a name child");
426 assert_eq!(name_child.field_name, Some("name"));
427 assert_eq!(name_child.r#type, "identifier");
428 let params_child =
429 find_child(func, "parameters").expect("function_item should have a parameters child");
430 assert_eq!(params_child.field_name, Some("parameters"));
431 assert_eq!(params_child.r#type, "parameters");
432 let body_child = find_child(func, "body").expect("function_item should have a body child");
433 assert_eq!(body_child.field_name, Some("body"));
434 assert_eq!(body_child.r#type, "block");
435 }
436
437 #[test]
438 fn cpp_assignment_carries_left_and_right_field_names() {
439 // Cross-language confirmation: the C/C++ grammar uses the same
440 // `left`/`right` field names for `assignment_expression`.
441 let root =
442 build_ast::<crate::CppParser>(b"int main(){ int x = 0; x = x + 1; }", "test.cpp");
443 let assign = find_first(&root, "assignment_expression")
444 .expect("expected an assignment_expression node");
445 assert_eq!(
446 find_child(assign, "left").map(|n| n.r#type),
447 Some("identifier")
448 );
449 assert_eq!(
450 find_child(assign, "right").map(|n| n.r#type),
451 Some("binary_expression")
452 );
453 }
454
455 #[test]
456 fn serialized_json_includes_field_name_key() {
457 // Regression for the Serialize derive: every node must serialize
458 // a `field_name` key (null or string). Verifying via JSON
459 // string-match catches accidental removal of the field from
460 // the serializer.
461 let root = build_ast::<crate::RustParser>(b"fn f(){ let a = 1; }", "test.rs");
462 let json = serde_json::to_string(&root).expect("serialize");
463 assert!(
464 json.contains("\"field_name\""),
465 "field_name missing from JSON: {json}"
466 );
467 // The let binding's `pattern` and `value` fields should both
468 // appear as string values in the JSON.
469 assert!(
470 json.contains("\"field_name\":\"pattern\""),
471 "expected pattern field name; got {json}"
472 );
473 assert!(
474 json.contains("\"field_name\":\"value\""),
475 "expected value field name; got {json}"
476 );
477 }
478
479 #[test]
480 fn serialized_json_uses_snake_case_keys() {
481 // The serialized AST shape uses snake_case keys (#535). This
482 // anchors the key scheme against accidental reversion to the
483 // former PascalCase `Type`/`TextValue`/`Span`/`Children`.
484 let root = build_ast_with_span::<crate::RustParser>(b"fn f(){}", "test.rs");
485 let json = serde_json::to_string(&root).expect("serialize");
486 for key in [
487 "\"type\":",
488 "\"value\":",
489 "\"span\":",
490 "\"field_name\":",
491 "\"children\":",
492 ] {
493 assert!(json.contains(key), "expected key {key}; got {json}");
494 }
495 for legacy in ["\"Type\"", "\"TextValue\"", "\"Span\"", "\"Children\""] {
496 assert!(
497 !json.contains(legacy),
498 "unexpected PascalCase key {legacy}; got {json}"
499 );
500 }
501 }
502
503 #[test]
504 fn span_serializes_as_named_object() {
505 // The span is a flat named object preserving the 1-based
506 // tree-sitter line/column values in the original tuple order
507 // (start_line, start_col, end_line, end_col). The `*_line`
508 // vocabulary matches /function and /metrics (#638).
509 // `fn f(){}` is 8 bytes on one line, so the root spans the whole
510 // half-open byte range [0, 8) (#727).
511 let root = build_ast_with_span::<crate::RustParser>(b"fn f(){}", "test.rs");
512 let span = root
513 .span
514 .expect("root span present when span tracking is on");
515 assert_eq!(span, Span::new(1, 1, 1, 9, 0, 8));
516 let json = serde_json::to_string(&root.span).expect("serialize span");
517 assert!(
518 json.contains("\"start_line\":1")
519 && json.contains("\"start_col\":1")
520 && json.contains("\"end_line\":1")
521 && json.contains("\"end_col\":9")
522 && json.contains("\"start_byte\":0")
523 && json.contains("\"end_byte\":8"),
524 "expected named span object with byte offsets; got {json}"
525 );
526 // The pre-2.0 `*_row` keys must be gone (#638).
527 assert!(
528 !json.contains("start_row") && !json.contains("end_row"),
529 "unexpected pre-2.0 *_row span keys; got {json}"
530 );
531 }
532
533 #[test]
534 fn span_round_trips_through_serde() {
535 // Span derives Deserialize for wire round-trip parity.
536 let span = Span::new(2, 3, 4, 5, 11, 42);
537 let json = serde_json::to_string(&span).expect("serialize");
538 let back: Span = serde_json::from_str(&json).expect("deserialize");
539 assert_eq!(span, back);
540 }
541
542 #[test]
543 fn span_deserializes_pre_byte_offsets_with_default() {
544 // The byte fields carry `#[serde(default)]`, so a span object
545 // serialized before they existed (line/col only) still
546 // deserializes, with the offsets defaulting to 0 (#727).
547 let legacy = r#"{"start_line":2,"start_col":3,"end_line":4,"end_col":5}"#;
548 let span: Span = serde_json::from_str(legacy).expect("deserialize legacy span");
549 assert_eq!(span, Span::new(2, 3, 4, 5, 0, 0));
550 }
551
552 // -----------------------------------------------------------------
553 // Stack-depth regression tests (#1056)
554 // -----------------------------------------------------------------
555
556 /// A chain of `depth` nested [`AstNode`]s below the root, built
557 /// directly so the depth is exact rather than a function of how many
558 /// AST levels a grammar spends per source construct.
559 fn ast_chain(depth: usize) -> AstNode {
560 let mut node = AstNode::new("leaf", String::new(), None, Vec::new());
561 for _ in 0..depth {
562 node = AstNode::new("branch", String::new(), None, vec![node]);
563 }
564 node
565 }
566
567 #[test]
568 fn ast_depth_at_the_serialize_limit_is_accepted_and_one_deeper_is_not() {
569 let at_limit = ast_chain(MAX_AST_SERIALIZE_DEPTH);
570 serde_json::to_string(&at_limit).expect("the limit itself must serialize");
571
572 let past_limit = ast_chain(MAX_AST_SERIALIZE_DEPTH + 1);
573 let err = serde_json::to_string(&past_limit).expect_err("one deeper must be refused");
574 assert!(
575 err.to_string().contains(&format!(
576 "AstNode nesting is deeper than the serialization limit of \
577 {MAX_AST_SERIALIZE_DEPTH} levels"
578 )),
579 "the error must name the type and the limit, got: {err}"
580 );
581 }
582
583 #[test]
584 fn a_pathologically_deep_ast_errors_and_tears_down_without_overflowing() {
585 // The chain is built directly rather than parsed, but this depth
586 // is reachable through `bca-web`'s `/ast`: 50 000 levels of
587 // nested parentheses fit in 100 KB, an order of magnitude inside
588 // the 4 MiB body cap. Both the recursive `Serialize` and the
589 // compiler-generated `Drop` glue used to abort the process here —
590 // `SIGABRT`, which `spawn_blocking` cannot contain (#1056).
591 const DEPTH: usize = 50_000;
592 // The stack a `bca` consumer thread and a `tokio` blocking thread
593 // get; the serialization limit is dimensioned against it.
594 const PRODUCTION_STACK: usize = 2 * 1024 * 1024;
595 let message = std::thread::Builder::new()
596 .stack_size(PRODUCTION_STACK)
597 .spawn(|| {
598 let root = ast_chain(DEPTH);
599 let message = serde_json::to_string(&root)
600 .expect_err("depth past the limit must fail, not serialize")
601 .to_string();
602 // `root` drops here: the iterative `Drop` must unwind
603 // 50 000 levels without touching the stack.
604 message
605 })
606 .expect("spawn bounded-stack thread")
607 .join()
608 .expect("bounded-stack thread must not overflow");
609 assert!(
610 message.contains("AstNode nesting is deeper than the serialization limit"),
611 "the error must explain the refusal, got: {message}"
612 );
613 }
614}