module
Noir::TreeSitter
Overview
Thin high-level facade. Keeps tree lifetime tied to an object so callers
don't have to think about ts_tree_delete.
Defined in:
ext/tree_sitter/tree_sitter.crConstant Summary
-
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,programroots 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_microsbounds ONEts_parser_parse_stringcall, 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.rsfile, 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
.parsenever sees a path, and content is the better key anyway: theCodeLocatorcache hands the same string to sibling analyzers, and two paths holding identical bytes parse identically.
Class Method Summary
-
.each_named_child(node : LibTreeSitter::TSNode, &)
Iterates named children without allocating an array.
- .field(node : LibTreeSitter::TSNode, name : String) : LibTreeSitter::TSNode | Nil
- .node_end_row(node : LibTreeSitter::TSNode) : Int32
- .node_start_row(node : LibTreeSitter::TSNode) : Int32
- .node_text(node : LibTreeSitter::TSNode, source : String) : String
- .node_type(node : LibTreeSitter::TSNode) : String
-
.parse(source : String, language : LibTreeSitter::TSLanguage, &)
Parses
sourcewith the givenlanguageand yields the rootLibTreeSitter::TSNode. -
.parse_failure_count : Int32
Number of remembered parse failures.
-
.parse_go(source : String, &)
Parses
sourcewith the Go grammar and yields the root node. -
.parse_java(source : String, &)
Parses
sourcewith the Java grammar and yields the root node. -
.parse_javascript(source : String, &)
Parses
sourcewith the JavaScript grammar and yields the root node. -
.parse_kotlin(source : String, &)
Parses
sourcewith the Kotlin grammar and yields the root node. -
.parse_python(source : String, &)
Parses
sourcewith the Python grammar and yields the root node. -
.parse_rust(source : String, &)
Parses
sourcewith the Rust grammar and yields the root node. -
.parse_timeout_micros : UInt64
Wall-clock ceiling for a single
ts_parser_parse_stringcall. -
.parse_timeout_micros=(parse_timeout_micros : UInt64)
Wall-clock ceiling for a single
ts_parser_parse_stringcall. -
.parser_pool_size(language : LibTreeSitter::TSLanguage) : Int32
Idle parser count for a given language.
-
.query_error_name(error_type : Int32) : String
Names the
error_typethatts_query_newwrites back on failure. -
.walk_depth : Int32
Descent depth of the AST walk currently running on this thread.
Class Method Detail
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).
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.
Number of remembered parse failures. Exposed for specs; not part of the public API contract.
Parses source with the Go grammar and yields the root node.
Parses source with the Java grammar and yields the root node.
Parses source with the JavaScript grammar and yields the
root node. Covers .js / .mjs / .cjs files; the JSX
superset is recognised too — JSX-bearing TypeScript needs
parse_typescript (not yet vendored).
Parses source with the Kotlin grammar and yields the root node.
Parses source with the Python grammar and yields the root node.
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.
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. Files are capped at MediaFilter::MAX_FILE_SIZE (10 MB) 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.
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. Files are capped at MediaFilter::MAX_FILE_SIZE (10 MB) 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.
Idle parser count for a given language. Exposed for tests and diagnostics; not part of the public API contract.
Names the error_type that ts_query_new writes back on failure.
LibTreeSitter's TS_QUERY_ERROR_* constants existed for exactly this
and were never used, so a broken query reported a bare code=2 and the
reader had to go find TSQueryError in tree-sitter's api.h to learn
that meant an unknown node type.
Descent depth of the AST walk currently running on this thread. Exposed for specs; not part of the public API contract.