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
47/// Unified metrics extracted in a single AST traversal.
48#[derive(Debug, Clone, Copy, PartialEq)]
49pub struct TreeMetrics {
50    pub error_ratio: f64,
51    pub node_count: usize,
52}
53
54/// Calculate parse metrics (error ratio and named node count) in a single AST pass.
55pub fn tree_metrics(tree: &tree_sitter::Tree, source: &[u8]) -> TreeMetrics {
56    if source.is_empty() {
57        return TreeMetrics {
58            error_ratio: 0.0,
59            node_count: 0,
60        };
61    }
62    let mut cursor = tree.walk();
63    let mut total = 0u32;
64    let mut errors = 0u32;
65    let mut named = 0u32;
66    let mut reached_root = false;
67
68    while !reached_root {
69        let n = cursor.node();
70        total += 1;
71        if n.is_named() {
72            named += 1;
73        }
74        if n.is_error() || n.is_missing() {
75            errors += 1;
76        }
77        if cursor.goto_first_child() {
78            continue;
79        }
80        if cursor.goto_next_sibling() {
81            continue;
82        }
83        let mut retracing = true;
84        while retracing {
85            if !cursor.goto_parent() {
86                reached_root = true;
87                break;
88            }
89            if cursor.goto_next_sibling() {
90                retracing = false;
91            }
92        }
93    }
94
95    let error_ratio = if total == 0 {
96        0.0
97    } else {
98        errors as f64 / total as f64
99    };
100
101    TreeMetrics {
102        error_ratio,
103        node_count: named as usize,
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::language::LangId;
111
112    #[test]
113    fn parse_tree_valid_python() {
114        let tree = parse_tree(LangId::Python, b"def hello(): pass").unwrap();
115        assert!(!tree.root_node().has_error());
116        assert_eq!(tree.root_node().kind(), "module");
117    }
118
119    #[test]
120    fn parse_tree_switches_languages() {
121        let python = parse_tree(LangId::Python, b"def hello(): pass").unwrap();
122        assert_eq!(python.root_node().kind(), "module");
123
124        let javascript = parse_tree(LangId::JavaScript, b"function hello() {}").unwrap();
125        assert_eq!(javascript.root_node().kind(), "program");
126    }
127
128    #[test]
129    fn tree_metrics_valid_source() {
130        let tree = parse_tree(LangId::Python, b"def hello(): pass").unwrap();
131        let metrics = tree_metrics(&tree, b"def hello(): pass");
132        assert!(metrics.error_ratio < 0.1);
133        assert!(metrics.node_count > 0);
134    }
135
136    #[test]
137    fn tree_metrics_malformed() {
138        let tree = parse_tree(LangId::Python, b"def broken(").unwrap();
139        let metrics = tree_metrics(&tree, b"def broken(");
140        assert!(metrics.error_ratio > 0.0);
141    }
142
143    #[test]
144    fn tree_metrics_empty_source() {
145        let tree = parse_tree(LangId::Python, b"").unwrap();
146        let metrics = tree_metrics(&tree, b"");
147        assert_eq!(metrics.error_ratio, 0.0);
148        assert_eq!(metrics.node_count, 0);
149    }
150}