Skip to main content

meta_ast/parser/
mod.rs

1//! Tree-sitter parser lifecycle and parse quality metrics.
2//!
3//! Maintains a thread-local pool of `Parser` instances (one per
4//! language) to avoid re-initializing grammars. Provides `parse_tree`
5//! for single-file parsing and `error_ratio` for parse quality estimation.
6
7use std::cell::RefCell;
8
9use tree_sitter::Parser;
10
11use crate::error::Error;
12use crate::language::LangId;
13
14thread_local! {
15    static PARSERS: RefCell<[Option<Parser>; LangId::COUNT]> = const { RefCell::new([const { None }; LangId::COUNT]) };
16}
17
18fn get_or_init_parser(
19    parsers: &mut [Option<Parser>; LangId::COUNT],
20    lang: LangId,
21) -> Result<&mut Parser, Error> {
22    let idx = lang as usize;
23    if parsers[idx].is_none() {
24        let mut parser = Parser::new();
25        let grammar = crate::language::grammar_for(lang);
26        parser
27            .set_language(&grammar)
28            .map_err(|e| Error::Config(format!("failed to set language: {e}")))?;
29        parsers[idx] = Some(parser);
30    }
31    parsers[idx]
32        .as_mut()
33        .ok_or_else(|| Error::Config("parser slot was not initialized".into()))
34}
35
36pub(crate) fn parse_tree(lang: LangId, source: &[u8]) -> Result<tree_sitter::Tree, Error> {
37    PARSERS.with(|cache| {
38        let parsers = &mut *cache.borrow_mut();
39        let parser = get_or_init_parser(parsers, lang)?;
40        parser.parse(source, None).ok_or_else(|| Error::Parse {
41            path: Default::default(),
42            message: "parser returned no tree".into(),
43        })
44    })
45}
46
47pub(crate) fn error_ratio(tree: &tree_sitter::Tree, source: &[u8]) -> f64 {
48    if source.is_empty() {
49        return 0.0;
50    }
51    let root = tree.root_node();
52    if !root.has_error() {
53        return 0.0;
54    }
55    let mut total = 0u32;
56    let mut errors = 0u32;
57    count_nodes(&root, &mut total, &mut errors);
58    if total == 0 {
59        return 0.0;
60    }
61    errors as f64 / total as f64
62}
63
64/// Count total tree-sitter nodes (named + anonymous) in a parse tree.
65///
66/// Walks the tree once, same O(n) pass as `error_ratio` but without the
67/// error-tracking overhead. Useful as a proxy for computational surface
68/// area in deployment metrics.
69///
70/// Iterates with a single reusable cursor instead of allocating a fresh
71/// `TreeCursor` per node, which the recursive `node.children(&mut node.walk())`
72/// form does for every node in the tree.
73pub fn ast_node_count(tree: &tree_sitter::Tree) -> usize {
74    let mut cursor = tree.walk();
75    let mut total = 0u32;
76    let mut reached_root = false;
77    while !reached_root {
78        if cursor.node().is_named() {
79            total += 1;
80        }
81        if cursor.goto_first_child() {
82            continue;
83        }
84        if cursor.goto_next_sibling() {
85            continue;
86        }
87        let mut retracing = true;
88        while retracing {
89            if !cursor.goto_parent() {
90                reached_root = true;
91                break;
92            }
93            if cursor.goto_next_sibling() {
94                retracing = false;
95            }
96        }
97    }
98    total as usize
99}
100
101fn count_nodes(node: &tree_sitter::Node, total: &mut u32, errors: &mut u32) {
102    let mut cursor = node.walk();
103    let mut reached_root = false;
104    while !reached_root {
105        let n = cursor.node();
106        *total += 1;
107        if n.is_error() || n.is_missing() {
108            *errors += 1;
109        }
110        if cursor.goto_first_child() {
111            continue;
112        }
113        if cursor.goto_next_sibling() {
114            continue;
115        }
116        let mut retracing = true;
117        while retracing {
118            if !cursor.goto_parent() {
119                reached_root = true;
120                break;
121            }
122            if cursor.goto_next_sibling() {
123                retracing = false;
124            }
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::language::LangId;
133
134    #[test]
135    fn parse_tree_valid_python() {
136        let tree = parse_tree(LangId::Python, b"def hello(): pass").unwrap();
137        assert!(!tree.root_node().has_error());
138        assert_eq!(tree.root_node().kind(), "module");
139    }
140
141    #[test]
142    fn parse_tree_switches_languages() {
143        let python = parse_tree(LangId::Python, b"def hello(): pass").unwrap();
144        assert_eq!(python.root_node().kind(), "module");
145
146        let javascript = parse_tree(LangId::JavaScript, b"function hello() {}").unwrap();
147        assert_eq!(javascript.root_node().kind(), "program");
148    }
149
150    #[test]
151    fn error_ratio_valid_source() {
152        let tree = parse_tree(LangId::Python, b"def hello(): pass").unwrap();
153        let ratio = error_ratio(&tree, b"def hello(): pass");
154        assert!(ratio < 0.1);
155    }
156
157    #[test]
158    fn error_ratio_malformed() {
159        let tree = parse_tree(LangId::Python, b"def broken(").unwrap();
160        let ratio = error_ratio(&tree, b"def broken(");
161        assert!(ratio > 0.0);
162    }
163
164    #[test]
165    fn error_ratio_empty_source() {
166        let tree = parse_tree(LangId::Python, b"").unwrap();
167        let ratio = error_ratio(&tree, b"");
168        assert_eq!(ratio, 0.0);
169    }
170}