module

Noir::TreeSitter

Thin high-level facade. Keeps tree lifetime tied to an object so callers don't have to think about ts_tree_delete.

Constants

ESCAPE_SEQUENCE_RE = /\\(?:u\{([0-9A-Fa-f]{1,6})\}|u([0-9A-Fa-f]{4})|x([0-9A-Fa-f]{2})|(.))/m
MAX_AST_DEPTH = 1024

Recursion guard for AST walkers. Crystal's default fiber stack is generous (~8 MB) but a malicious source file with deeply nested syntax — (((((((...))))))) chains, deeply nested object literals, recursive template expressions — could cascade through a custom walker until the stack runs out. Real production code rarely nests beyond ~100 levels, so 1024 is comfortably above legitimate input and well below the stack ceiling.

NAMED_CHILD_CURSOR_THRESHOLD = 8

Above this many named children, switch from indexed access to a tree cursor. ts_node_named_child(node, i) is O(i) (it re-walks the sibling list each call), so the indexed loop is O(n^2) in the child count; a cursor walks each child in O(1) amortised but costs one allocation to set up. Small nodes stay on the allocation-free indexed path; wide nodes (large class bodies, program roots on big files) take the cursor and avoid the quadratic. Both paths yield the exact same named children in the same order.

PARSE_FAILURE_MAX_ENTRIES = 4096

Sources that already failed to parse, keyed by content fingerprint and grammar.

parse_timeout_micros bounds ONE ts_parser_parse_string call, but a file is offered to every analyzer of its language, and each one parses it independently — nine Rust analyzers each burn the full ceiling on the same unparsable .rs file, so a 10 s bound costs 90 s. The verdict is a pure function of (content, grammar), so remembering it turns that back into one ceiling per file. A timeout is wall-clock and so not strictly deterministic; that is the point — re-running a parse we already know takes longer than the ceiling cannot succeed in less time on the second try, it can only cost the ceiling again.

Keyed on content rather than path because parse never sees a path, and content is the better key anyway: the CodeLocator cache hands the same string to sibling analyzers, and two paths holding identical bytes parse identically.

Class methods

call_name_row(call : LibTreeSitter::TSNode) : Int32

Row of a call's method name. A chained call node starts at the chain head, so app\n .get(...)\n .post(...) would put every link on the app line. JS member_expression.property, Java method_invocation.name and Kotlin's trailing navigation_suffix name the link (Kotlin wraps .post("/x") { } in a second call for the trailing lambda); any other shape falls back to the call's own row.

Source
decode_string_literal(node : LibTreeSitter::TSNode, source : String) : String

A string literal's string_fragment and decoded escape_sequence text, or its raw text minus the surrounding double quotes when the grammar exposes no fragments.

Source
descend

Runs the block one level deeper on the same budget as each_named_child, or returns nil without running it once that budget is spent. For recursion that descends through field(...) instead — the callee extractors' a.b.c receiver builders recurse once per chain link, and a generated x.b.b.b…(1) chain of 100k links overflowed the stack and aborted the whole scan.

Source
each_named_arg(node : LibTreeSitter::TSNode, &)

each_named_child minus comments. Grammars attach comments as named extra nodes anywhere, so f("/a", // note + newline + g) has three named arguments; readers that index or count arguments must skip them.

Source
each_named_child(node : LibTreeSitter::TSNode, &)

Iterates named children without allocating an array.

Yields nothing once the walk has descended MAX_AST_DEPTH levels. Practically every extractor in the tree descends by recursing inside this block, so bounding it here bounds all of them at once: the alternative is threading a depth parameter through ~300 hand-rolled walkers and remembering to do it in the next one. A source file with thousands of nested syntactic constructs (((((...)))), chained builders, generated code) otherwise recurses until the fiber stack runs out, and a stack overflow is a hard abort — it kills the whole scan, not just the file, and no rescue in parallel_analyze can catch it. Walkers that thread their own depth against MAX_AST_DEPTH still cut earlier and more precisely; this is the backstop for the ones that don't.

The cursor path lives in a separate @[NoInline] method on purpose: these extractors recurse through each_named_child's block, so any local this method reserves (notably the 32-byte TSTreeCursor) is paid at every recursion level. Keeping the cursor out of this frame preserves the small original frame for the common narrow-node path, so legitimate deep input stays well inside the stack (guarded by spec/unit_test/miniparser/extractor_recursion_depth_spec).

Source
field(node : LibTreeSitter::TSNode, name : String) : LibTreeSitter::TSNode | Nil
Source
first_named_arg(node : LibTreeSitter::TSNode) : LibTreeSitter::TSNode | Nil

First argument with comments skipped, see each_named_arg.

Source
first_named_child(node : LibTreeSitter::TSNode) : LibTreeSitter::TSNode | Nil
Source
js_family_language(path : String) : LibTreeSitter::TSLanguage
Source
kotlin_string_content(content : LibTreeSitter::TSNode, literal : LibTreeSitter::TSNode, source : String) : String

A Kotlin string_content child of literal, decoded. The grammar keeps escapes inline in the content; a raw """ string has none to decode.

Source
name_row(node : LibTreeSitter::TSNode) : Int32 | Nil

0-based row of a declaration's name field: the signature line, which sits below however many annotation lines the declaration starts with.

Source
named_arg_count(node : LibTreeSitter::TSNode) : Int32

Argument count with comments skipped, see each_named_arg.

Source
node_end_row(node : LibTreeSitter::TSNode) : Int32
Source
node_has_error?(node : LibTreeSitter::TSNode) : Bool
Source
node_start_row(node : LibTreeSitter::TSNode) : Int32
Source
node_text(node : LibTreeSitter::TSNode, source : String) : String
Source
node_type(node : LibTreeSitter::TSNode) : String
Source
parent(node : LibTreeSitter::TSNode) : LibTreeSitter::TSNode | Nil
Source
parse(source : String, language : LibTreeSitter::TSLanguage, &)

Parses source with the given language and yields the root LibTreeSitter::TSNode. The parser is checked out from a per-language pool and returned when the block exits; the tree is freed in the same ensure.

Source
parse_go(source : String, &)

Parses source with the Go grammar and yields the root node.

Source
parse_java(source : String, &)

Parses source with the Java grammar and yields the root node.

Source
parse_javascript(source : String, &)

Parses source with the JavaScript grammar and yields the root node. Covers .js / .mjs / .cjs files; the JSX superset is recognised too. Extractors that may see TypeScript call parse_js_family, which picks the grammar by extension.

Source
parse_js_family(source : String, path : String, &)

Parses JS-family source with the grammar path's extension calls for: TypeScript for .ts / .mts / .cts, TSX for .tsx, and JavaScript (JSX included) for everything else, "" among it.

Source
parse_kotlin(source : String, &)

Parses source with the Kotlin grammar and yields the root node.

Source
parse_python(source : String, &)

Parses source with the Python grammar and yields the root node.

Source
parse_ruby(source : String, &)

Parses source with the Ruby grammar and yields the root node. Covers .rb files.

Source
parse_rust(source : String, &)

Parses source with the Rust grammar and yields the root node. Covers .rs files. Used by the Rust framework analyzers (axum, actix-web, rocket, …) and the Rust callee extractor.

Source
parse_timeout_micros

Wall-clock ceiling for a single ts_parser_parse_string call.

tree-sitter's error recovery is quadratic on badly-malformed input: a 200 KB single line of unterminated string literals in a .go file spends minutes inside ts_parser__recover before returning. Nothing in noir can interrupt that — the parse is one blocking C call, so the scan simply stops, with no output and no way to tell which file did it. Source files are capped at MediaFilter::MAX_FILE_SIZE (10 MB) — the larger MAX_SPEC_FILE_SIZE covers only specification documents, which no vendored grammar parses — and a well-formed file that size parses in well under a second, so ten seconds is ~100x headroom over any legitimate input while still bounding the pathological case.

On expiry the parse returns null and parse raises, which the per-file rescue in parallel_analyze / scan_files logs at debug — one file dropped instead of the whole run. Writable so a spec can force the expiry path deterministically — a sub-millisecond ceiling times out on any non-trivial source, where reproducing a real 10 s timeout would need a pathological fixture and ten seconds of suite time. Nothing in a scan writes it: the value is read once per parse and comes from NOIR_PARSE_TIMEOUT_MS.

Source
parse_timeout_micros=(parse_timeout_micros : UInt64)

Wall-clock ceiling for a single ts_parser_parse_string call.

tree-sitter's error recovery is quadratic on badly-malformed input: a 200 KB single line of unterminated string literals in a .go file spends minutes inside ts_parser__recover before returning. Nothing in noir can interrupt that — the parse is one blocking C call, so the scan simply stops, with no output and no way to tell which file did it. Source files are capped at MediaFilter::MAX_FILE_SIZE (10 MB) — the larger MAX_SPEC_FILE_SIZE covers only specification documents, which no vendored grammar parses — and a well-formed file that size parses in well under a second, so ten seconds is ~100x headroom over any legitimate input while still bounding the pathological case.

On expiry the parse returns null and parse raises, which the per-file rescue in parallel_analyze / scan_files logs at debug — one file dropped instead of the whole run. Writable so a spec can force the expiry path deterministically — a sub-millisecond ceiling times out on any non-trivial source, where reproducing a real 10 s timeout would need a pathological fixture and ten seconds of suite time. Nothing in a scan writes it: the value is read once per parse and comes from NOIR_PARSE_TIMEOUT_MS.

Source
parser_pool_size(language : LibTreeSitter::TSLanguage) : Int32

Idle parser count for a given language. Exposed for tests and diagnostics; not part of the public API contract.

Source
timeout_micros_from_env(raw : String | Nil) : UInt64

Milliseconds to microseconds, clamped so an absurd value means "effectively no limit" instead of an OverflowError that aborts every command at startup.

Source
unescape(text : String) : String

The value of the backslash escapes in text: an escape_sequence node (Java/JS), or a Kotlin string_content, which keeps its escapes inline. \\ → \, \" → ", \n → newline, é → é; any other \c is c, as in JavaScript.

Source
unwrap_expression(node : LibTreeSitter::TSNode) : LibTreeSitter::TSNode

(x), x as T, x satisfies T, x! and <T>x all evaluate to x. Readers of literals, arrays, objects and handlers look through them, so '/a' as const reads as '/a' and (async () => {}) satisfies H as the arrow. Bounded: pathological ((((x)))) nesting stops at 32 layers.

Source
walk(node : LibTreeSitter::TSNode, &block : LibTreeSitter::TSNode -> )

Pre-order walk: yields node, then every named descendant. Depth is bounded by each_named_child.

Source