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