1pub(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#[derive(Debug, Clone)]
30pub struct DocCommentConfig {
31 pub line_prefixes: &'static [&'static str],
33 pub block_open: Option<&'static str>,
35 pub block_close: &'static str,
37 pub strip_continuation_marker: bool,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum DefaultVisibility {
45 PublicByDefault,
47 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
139pub 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}