Skip to main content

meta_ast/language/
mod.rs

1//! Language system: compile-time enum dispatch over 8 language packs.
2//!
3//! `LangId` enum selects a `LanguageSpec` via exhaustive `match`.
4//! Each `LanguageSpec` bundles a grammar constructor, tree-sitter query
5//! constructors (symbols, imports, references), and language-specific
6//! extraction heuristics.
7
8pub(crate) mod c;
9pub(crate) mod common;
10pub(crate) mod cpp;
11pub(crate) mod go;
12pub mod import_resolver;
13pub(crate) mod javascript;
14pub(crate) mod python;
15pub(crate) mod rust;
16pub(crate) mod tsx;
17pub(crate) mod typescript;
18
19use serde::Serialize;
20use tree_sitter::Query;
21
22use crate::model::Visibility;
23
24/// Configuration for extracting doc comments from source code.
25///
26/// Used for languages where doc comments are tree-sitter extras
27/// (siblings of declarations, not children) and cannot be captured
28/// inline in queries.
29#[derive(Debug, Clone)]
30pub struct DocCommentConfig {
31    /// Line comment prefixes that indicate doc comments (e.g., `["///", "//!"]` for Rust).
32    pub line_prefixes: &'static [&'static str],
33    /// Block comment opening (e.g., `Some("/**")` for doxygen/JSDoc).
34    pub block_open: Option<&'static str>,
35    /// Block comment closing (e.g., `"*/"`).
36    pub block_close: &'static str,
37    /// Whether to strip leading `*` continuation markers in block comments.
38    pub strip_continuation_marker: bool,
39}
40
41/// Default visibility assumed when a symbol declares no explicit modifier.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum DefaultVisibility {
45    /// When visibility is None, treat the symbol as public (Python, C functions).
46    PublicByDefault,
47    /// When visibility is None, treat the symbol as private (Rust, JS, TS, Go, C++).
48    PrivateByDefault,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, strum::Display, strum::AsRefStr)]
52#[non_exhaustive]
53#[serde(rename_all = "snake_case")]
54#[strum(serialize_all = "snake_case")]
55#[repr(usize)]
56pub enum LangId {
57    Python,
58    JavaScript,
59    TypeScript,
60    Tsx,
61    C,
62    Cpp,
63    Rust,
64    Go,
65}
66
67impl LangId {
68    pub const COUNT: usize = 8;
69
70    pub fn all() -> [LangId; Self::COUNT] {
71        [
72            LangId::Python,
73            LangId::JavaScript,
74            LangId::TypeScript,
75            LangId::Tsx,
76            LangId::C,
77            LangId::Cpp,
78            LangId::Rust,
79            LangId::Go,
80        ]
81    }
82
83    pub fn spec(self) -> &'static LanguageSpec {
84        spec_for(self)
85    }
86
87    #[cfg(feature = "metacall-deploy")]
88    pub fn metacall_tag(self) -> &'static str {
89        crate::deploy::tags::metacall_tag(self)
90    }
91}
92
93#[derive(Debug, Clone, Serialize)]
94pub struct RawSymbol<'a> {
95    pub name: std::borrow::Cow<'a, str>,
96    pub kind: crate::model::SymbolKind,
97    pub source_range: crate::model::SourceRange,
98    pub visibility: Option<crate::model::Visibility>,
99    pub signature: Option<std::borrow::Cow<'a, str>>,
100    pub docstring: Option<std::borrow::Cow<'a, str>>,
101    pub is_async: bool,
102}
103
104pub struct LanguageSpec {
105    pub extensions: &'static [&'static str],
106    pub grammar_fn: fn() -> tree_sitter::Language,
107    pub query_fn: fn() -> &'static Query,
108    pub import_path_resolver: fn(
109        raw: &str,
110        source_dir: &std::path::Path,
111        project_root: &std::path::Path,
112    ) -> Option<std::path::PathBuf>,
113    pub import_ref_query_fn: fn() -> &'static Query,
114    pub class_like_parents: &'static [&'static str],
115    pub ancestor_visibility_rules: &'static [(&'static str, Visibility)],
116    pub visibility_from_name: Option<fn(&str) -> Option<Visibility>>,
117    pub import_statement_kinds: &'static [&'static str],
118    pub default_visibility: DefaultVisibility,
119    pub doc_comment_config: Option<DocCommentConfig>,
120}
121
122pub fn spec_for(id: LangId) -> &'static LanguageSpec {
123    match id {
124        LangId::Python => &python::PYTHON_SPEC,
125        LangId::JavaScript => &javascript::JS_SPEC,
126        LangId::TypeScript => &typescript::TS_SPEC,
127        LangId::Tsx => &tsx::TSX_SPEC,
128        LangId::C => &c::C_SPEC,
129        LangId::Cpp => &cpp::CPP_SPEC,
130        LangId::Rust => &rust::RUST_SPEC,
131        LangId::Go => &go::GO_SPEC,
132    }
133}
134
135pub fn grammar_for(id: LangId) -> tree_sitter::Language {
136    (spec_for(id).grammar_fn)()
137}
138
139/// Eagerly initialize all language query statics.
140/// Call at startup to fail fast on query compilation bugs.
141pub fn validate_queries() {
142    for id in LangId::all() {
143        let _ = (spec_for(id).query_fn)();
144        let _ = (spec_for(id).import_ref_query_fn)();
145    }
146}
147
148pub fn extract_symbols_for<'a>(
149    id: LangId,
150    tree: &'a tree_sitter::Tree,
151    source: &'a [u8],
152) -> Vec<RawSymbol<'a>> {
153    common::extract_with_spec(tree, source, spec_for(id))
154}
155
156pub fn extract_imports_and_references_for<'a>(
157    id: LangId,
158    tree: &'a tree_sitter::Tree,
159    source: &'a [u8],
160    file_path: &std::path::Path,
161) -> (
162    Vec<crate::model::UnresolvedImport>,
163    Vec<crate::model::UnresolvedReference>,
164) {
165    common::extract_imports_and_references_with_spec(tree, source, spec_for(id), file_path)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn lang_id_all_variants_exist() {
174        let variants = LangId::all();
175        for i in 0..variants.len() {
176            for j in (i + 1)..variants.len() {
177                assert_ne!(variants[i], variants[j]);
178            }
179        }
180    }
181
182    #[test]
183    fn lang_id_display() {
184        assert_eq!(format!("{}", LangId::Python), "python");
185    }
186
187    #[test]
188    fn lang_id_serde_snake_case() {
189        let json = serde_json::to_string(&LangId::Python).unwrap();
190        assert_eq!(json, "\"python\"");
191    }
192
193    #[test]
194    fn grammar_for_all_variants() {
195        for id in LangId::all() {
196            let _lang = grammar_for(id);
197        }
198    }
199
200    #[test]
201    fn extract_symbols_for_python() {
202        let mut parser = tree_sitter::Parser::new();
203        parser.set_language(&grammar_for(LangId::Python)).unwrap();
204        let tree = parser.parse(b"def hello(): pass", None).unwrap();
205        let symbols = extract_symbols_for(LangId::Python, &tree, b"def hello(): pass");
206        assert!(!symbols.is_empty());
207    }
208
209    #[test]
210    fn spec_for_returns_spec_with_matching_extensions() {
211        let python_spec = spec_for(LangId::Python);
212        assert!(python_spec.extensions.contains(&"py"));
213        assert!(python_spec.extensions.contains(&"pyi"));
214
215        let js_spec = spec_for(LangId::JavaScript);
216        assert!(js_spec.extensions.contains(&"js"));
217    }
218
219    #[test]
220    fn lang_id_count_matches_variant_count() {
221        assert_eq!(LangId::COUNT, 8);
222        assert_eq!(LangId::all().len(), LangId::COUNT);
223    }
224
225    #[test]
226    fn all_specs_have_non_empty_extensions() {
227        for id in LangId::all() {
228            let spec = spec_for(id);
229            assert!(
230                !spec.extensions.is_empty(),
231                "{id:?} spec has empty extensions"
232            );
233        }
234    }
235
236    #[test]
237    fn no_duplicate_extensions_across_specs() {
238        use std::collections::HashSet;
239        let mut seen: HashSet<&str> = HashSet::new();
240        for id in LangId::all() {
241            let spec = spec_for(id);
242            for &ext in spec.extensions {
243                assert!(
244                    seen.insert(ext),
245                    "extension {ext:?} appears in more than one language spec"
246                );
247            }
248        }
249    }
250
251    #[test]
252    fn grammar_fn_smoke_test_all_variants() {
253        for id in LangId::all() {
254            let spec = spec_for(id);
255            let grammar = (spec.grammar_fn)();
256            let mut parser = tree_sitter::Parser::new();
257            assert!(
258                parser.set_language(&grammar).is_ok(),
259                "grammar_fn failed for {id:?}"
260            );
261        }
262    }
263
264    #[test]
265    fn query_fn_smoke_test_all_variants() {
266        for id in LangId::all() {
267            let spec = spec_for(id);
268            let _query = (spec.query_fn)();
269        }
270    }
271
272    #[test]
273    #[cfg(feature = "metacall-deploy")]
274    fn test_lang_id_metacall_tag() {
275        assert_eq!(LangId::Python.metacall_tag(), "py");
276        assert_eq!(LangId::JavaScript.metacall_tag(), "node");
277        assert_eq!(LangId::TypeScript.metacall_tag(), "ts");
278        assert_eq!(LangId::Tsx.metacall_tag(), "ts");
279        assert_eq!(LangId::C.metacall_tag(), "c");
280        assert_eq!(LangId::Cpp.metacall_tag(), "cpp");
281        assert_eq!(LangId::Rust.metacall_tag(), "rs");
282        assert_eq!(LangId::Go.metacall_tag(), "go");
283    }
284}