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