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}