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/// Information on an `AST` node.
146///
147/// Serialized as a flat object with `snake_case` keys: `type`, `value`,
148/// `span`, `field_name`, `children`.
149#[derive(Debug, Serialize)]
150#[serde(rename_all = "snake_case")]
151pub struct AstNode {
152    /// The type of node
153    pub r#type: &'static str,
154    /// The code associated to a node
155    pub value: String,
156    /// The start and end positions of a node in a code
157    pub span: Option<Span>,
158    /// Tree-sitter grammar field name through which the parent reaches
159    /// this node (e.g. `left`, `right`, `name`, `body`).
160    ///
161    /// `None` for the root node, anonymous tokens (punctuation, keywords),
162    /// and any child that does not occupy a named grammar field. Consumers
163    /// of the JSON output rely on this to distinguish structurally
164    /// equivalent children without grammar-specific positional knowledge.
165    pub field_name: Option<&'static str>,
166    /// The children of a node
167    pub children: Vec<AstNode>,
168}
169
170impl AstNode {
171    /// Builds an `AstNode` with the supplied type, value, span, and
172    /// children. The `field_name` is set to `None`; use
173    /// [`AstNode::with_field_name`] to record the tree-sitter grammar
174    /// field through which the parent reaches this node.
175    #[must_use]
176    pub fn new(
177        r#type: &'static str,
178        value: String,
179        span: Option<Span>,
180        children: Vec<AstNode>,
181    ) -> Self {
182        Self::with_field_name(r#type, value, span, None, children)
183    }
184
185    /// Builds an `AstNode` carrying the tree-sitter grammar field name
186    /// (`left`, `right`, `name`, `body`, ...) through which the parent
187    /// reaches this node.
188    #[must_use]
189    pub fn with_field_name(
190        r#type: &'static str,
191        value: String,
192        span: Option<Span>,
193        field_name: Option<&'static str>,
194        children: Vec<AstNode>,
195    ) -> Self {
196        Self {
197            r#type,
198            value,
199            span,
200            field_name,
201            children,
202        }
203    }
204}
205
206fn build<T: ParserTrait>(parser: &T, span: bool, comment: bool) -> Option<AstNode> {
207    // Iterative depth-first walk that materializes `AstNode`s bottom-up.
208    // Each frame holds the pending parent node, the grammar field name
209    // through which its own parent reached it (None for the root), the
210    // already-materialized child `AstNode`s, and the next child index to
211    // descend into. The parent's `field_name_for_child(idx)` lookup is
212    // O(1) and avoids the parallel cursor walk that was required when
213    // field names had to be captured via `TreeCursor::field_name()`.
214    struct Frame<'a> {
215        node: crate::Node<'a>,
216        field: Option<&'static str>,
217        children: Vec<AstNode>,
218        next_child_index: usize,
219    }
220
221    let code = parser.code();
222    let root = parser.root();
223    let mut stack: Vec<Frame<'_>> = vec![Frame {
224        node: root,
225        field: None,
226        children: Vec::with_capacity(root.child_count()),
227        next_child_index: 0,
228    }];
229
230    loop {
231        let frame = stack
232            .last_mut()
233            .expect("stack invariant: loop only runs while stack is non-empty");
234        let child_count = frame.node.child_count();
235        if frame.next_child_index < child_count {
236            let idx = frame.next_child_index;
237            frame.next_child_index += 1;
238            // `Node::child` is O(1) (direct tree-sitter pointer
239            // arithmetic); `field_name_for_child` returns the static
240            // grammar field for that child position. Tree-sitter caps
241            // child indices at u32, so the cast is safe by invariant.
242            let child = frame
243                .node
244                .child(idx)
245                .expect("stack invariant: idx < child_count so the child exists");
246            let field = frame.node.field_name_for_child(
247                u32::try_from(idx).expect("invariant: tree-sitter caps child indices at u32::MAX"),
248            );
249            stack.push(Frame {
250                node: child,
251                field,
252                children: Vec::with_capacity(child.child_count()),
253                next_child_index: 0,
254            });
255        } else {
256            let frame = stack
257                .pop()
258                .expect("stack invariant: just observed non-empty via last_mut()");
259            let node = T::Checker::get_ast_node(
260                &frame.node,
261                code,
262                span,
263                comment,
264                frame.field,
265                frame.children,
266            );
267            match (node, stack.last_mut()) {
268                (Some(ast), Some(parent)) => parent.children.push(ast),
269                (Some(ast), None) => return Some(ast),
270                (None, None) => return None,
271                (None, Some(_)) => {}
272            }
273        }
274    }
275}
276
277/// Configuration options for retrieving the nodes of an `AST`.
278#[derive(Debug)]
279pub struct AstCfg {
280    /// The id associated to a request for an `AST`
281    pub id: String,
282    /// The resolved source-language slug to echo in the response
283    /// envelope (#654). The #540 canonical lowercase slug.
284    pub language: String,
285    /// If `true`, nodes representing comments are ignored
286    pub comment: bool,
287    /// If `true`, the start and end positions of a node in a code
288    /// are considered
289    pub span: bool,
290}
291
292/// Build the AST dump for `parser` under `cfg`. Backs [`crate::Ast::dump`];
293/// the AST-extraction analogue of [`crate::spaces::metrics_inner`] /
294/// [`crate::ops::ops_inner`].
295pub(crate) fn dump_inner<T: ParserTrait>(parser: &T, cfg: AstCfg) -> AstResponse {
296    AstResponse {
297        id: cfg.id,
298        language: cfg.language,
299        root: build(parser, cfg.span, cfg.comment),
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use std::path::PathBuf;
306
307    use super::*;
308
309    fn build_ast<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
310        let path = PathBuf::from(filename);
311        let parser = P::new(code.to_vec(), &path, None);
312        let cfg = AstCfg {
313            id: String::new(),
314            language: String::new(),
315            comment: false,
316            span: false,
317        };
318        dump_inner(&parser, cfg)
319            .root
320            .expect("parser should produce a root AST node")
321    }
322
323    fn build_ast_with_span<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
324        let path = PathBuf::from(filename);
325        let parser = P::new(code.to_vec(), &path, None);
326        let cfg = AstCfg {
327            id: String::new(),
328            language: String::new(),
329            comment: false,
330            span: true,
331        };
332        dump_inner(&parser, cfg)
333            .root
334            .expect("parser should produce a root AST node")
335    }
336
337    fn find_first<'a>(node: &'a AstNode, kind: &str) -> Option<&'a AstNode> {
338        if node.r#type == kind {
339            return Some(node);
340        }
341        node.children.iter().find_map(|c| find_first(c, kind))
342    }
343
344    fn find_child<'a>(parent: &'a AstNode, field: &str) -> Option<&'a AstNode> {
345        parent.children.iter().find(|c| c.field_name == Some(field))
346    }
347
348    #[test]
349    fn root_has_no_field_name() {
350        let root = build_ast::<crate::RustParser>(b"fn main() {}", "test.rs");
351        assert_eq!(root.field_name, None);
352    }
353
354    #[test]
355    fn rust_assignment_carries_left_and_right_field_names() {
356        // `assignment_expression` in the Rust grammar names its operands
357        // `left` and `right`. Without `FieldName` exposed in the JSON,
358        // downstream consumers cannot distinguish the two `identifier`
359        // children. This is the canonical example from issue #244.
360        let root =
361            build_ast::<crate::RustParser>(b"fn f() { let mut a = 0; a = a + 1; }", "test.rs");
362        let assign = find_first(&root, "assignment_expression")
363            .expect("expected an assignment_expression node");
364        let left = find_child(assign, "left").expect("expected a `left` child");
365        let right = find_child(assign, "right").expect("expected a `right` child");
366        assert_eq!(left.field_name, Some("left"));
367        assert_eq!(right.field_name, Some("right"));
368        // Anonymous `=` token is a child too, with no field name.
369        assert!(
370            assign
371                .children
372                .iter()
373                .any(|c| c.r#type == "=" && c.field_name.is_none()),
374            "expected the `=` token child to carry no field name; got {:?}",
375            assign
376                .children
377                .iter()
378                .map(|c| (c.r#type, c.field_name))
379                .collect::<Vec<_>>(),
380        );
381    }
382
383    #[test]
384    fn rust_function_carries_name_and_body_field_names() {
385        // `function_item` names children `name`, `parameters`, `body`.
386        // Assert the field name directly on the AstNode so a bug that
387        // misnames a field (e.g. always emits "body") fails even if
388        // the target node kinds coincidentally line up.
389        let root =
390            build_ast::<crate::RustParser>(b"fn greet(name: &str) -> &str { name }", "test.rs");
391        let func = find_first(&root, "function_item").expect("expected a function_item node");
392        let name_child = find_child(func, "name").expect("function_item should have a name child");
393        assert_eq!(name_child.field_name, Some("name"));
394        assert_eq!(name_child.r#type, "identifier");
395        let params_child =
396            find_child(func, "parameters").expect("function_item should have a parameters child");
397        assert_eq!(params_child.field_name, Some("parameters"));
398        assert_eq!(params_child.r#type, "parameters");
399        let body_child = find_child(func, "body").expect("function_item should have a body child");
400        assert_eq!(body_child.field_name, Some("body"));
401        assert_eq!(body_child.r#type, "block");
402    }
403
404    #[test]
405    fn cpp_assignment_carries_left_and_right_field_names() {
406        // Cross-language confirmation: the C/C++ grammar uses the same
407        // `left`/`right` field names for `assignment_expression`.
408        let root =
409            build_ast::<crate::CppParser>(b"int main(){ int x = 0; x = x + 1; }", "test.cpp");
410        let assign = find_first(&root, "assignment_expression")
411            .expect("expected an assignment_expression node");
412        assert_eq!(
413            find_child(assign, "left").map(|n| n.r#type),
414            Some("identifier")
415        );
416        assert_eq!(
417            find_child(assign, "right").map(|n| n.r#type),
418            Some("binary_expression")
419        );
420    }
421
422    #[test]
423    fn serialized_json_includes_field_name_key() {
424        // Regression for the Serialize derive: every node must serialize
425        // a `field_name` key (null or string). Verifying via JSON
426        // string-match catches accidental removal of the field from
427        // the serializer.
428        let root = build_ast::<crate::RustParser>(b"fn f(){ let a = 1; }", "test.rs");
429        let json = serde_json::to_string(&root).expect("serialize");
430        assert!(
431            json.contains("\"field_name\""),
432            "field_name missing from JSON: {json}"
433        );
434        // The let binding's `pattern` and `value` fields should both
435        // appear as string values in the JSON.
436        assert!(
437            json.contains("\"field_name\":\"pattern\""),
438            "expected pattern field name; got {json}"
439        );
440        assert!(
441            json.contains("\"field_name\":\"value\""),
442            "expected value field name; got {json}"
443        );
444    }
445
446    #[test]
447    fn serialized_json_uses_snake_case_keys() {
448        // The serialized AST shape uses snake_case keys (#535). This
449        // anchors the key scheme against accidental reversion to the
450        // former PascalCase `Type`/`TextValue`/`Span`/`Children`.
451        let root = build_ast_with_span::<crate::RustParser>(b"fn f(){}", "test.rs");
452        let json = serde_json::to_string(&root).expect("serialize");
453        for key in [
454            "\"type\":",
455            "\"value\":",
456            "\"span\":",
457            "\"field_name\":",
458            "\"children\":",
459        ] {
460            assert!(json.contains(key), "expected key {key}; got {json}");
461        }
462        for legacy in ["\"Type\"", "\"TextValue\"", "\"Span\"", "\"Children\""] {
463            assert!(
464                !json.contains(legacy),
465                "unexpected PascalCase key {legacy}; got {json}"
466            );
467        }
468    }
469
470    #[test]
471    fn span_serializes_as_named_object() {
472        // The span is a flat named object preserving the 1-based
473        // tree-sitter line/column values in the original tuple order
474        // (start_line, start_col, end_line, end_col). The `*_line`
475        // vocabulary matches /function and /metrics (#638).
476        // `fn f(){}` is 8 bytes on one line, so the root spans the whole
477        // half-open byte range [0, 8) (#727).
478        let root = build_ast_with_span::<crate::RustParser>(b"fn f(){}", "test.rs");
479        let span = root
480            .span
481            .expect("root span present when span tracking is on");
482        assert_eq!(span, Span::new(1, 1, 1, 9, 0, 8));
483        let json = serde_json::to_string(&root.span).expect("serialize span");
484        assert!(
485            json.contains("\"start_line\":1")
486                && json.contains("\"start_col\":1")
487                && json.contains("\"end_line\":1")
488                && json.contains("\"end_col\":9")
489                && json.contains("\"start_byte\":0")
490                && json.contains("\"end_byte\":8"),
491            "expected named span object with byte offsets; got {json}"
492        );
493        // The pre-2.0 `*_row` keys must be gone (#638).
494        assert!(
495            !json.contains("start_row") && !json.contains("end_row"),
496            "unexpected pre-2.0 *_row span keys; got {json}"
497        );
498    }
499
500    #[test]
501    fn span_round_trips_through_serde() {
502        // Span derives Deserialize for wire round-trip parity.
503        let span = Span::new(2, 3, 4, 5, 11, 42);
504        let json = serde_json::to_string(&span).expect("serialize");
505        let back: Span = serde_json::from_str(&json).expect("deserialize");
506        assert_eq!(span, back);
507    }
508
509    #[test]
510    fn span_deserializes_pre_byte_offsets_with_default() {
511        // The byte fields carry `#[serde(default)]`, so a span object
512        // serialized before they existed (line/col only) still
513        // deserializes, with the offsets defaulting to 0 (#727).
514        let legacy = r#"{"start_line":2,"start_col":3,"end_line":4,"end_col":5}"#;
515        let span: Span = serde_json::from_str(legacy).expect("deserialize legacy span");
516        assert_eq!(span, Span::new(2, 3, 4, 5, 0, 0));
517    }
518}