Skip to main content

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}