1pub(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 ruby;
18pub(crate) mod rust;
19pub(crate) mod tsx;
20pub(crate) mod typescript;
21
22use serde::{Deserialize, Serialize};
23use tree_sitter::Query;
24
25use crate::model::Visibility;
26
27#[derive(Debug, Clone)]
33pub struct DocCommentConfig {
34 pub line_prefixes: &'static [&'static str],
36 pub block_open: Option<&'static str>,
38 pub block_close: &'static str,
40 pub strip_continuation_marker: bool,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum DefaultVisibility {
48 PublicByDefault,
50 PrivateByDefault,
52}
53
54pub const C_LIKE_DOC_COMMENT: DocCommentConfig = DocCommentConfig {
58 line_prefixes: &["//"],
59 block_open: Some("/**"),
60 block_close: "*/",
61 strip_continuation_marker: true,
62};
63
64#[derive(
65 Debug,
66 Clone,
67 Copy,
68 PartialEq,
69 Eq,
70 Hash,
71 Serialize,
72 Deserialize,
73 strum::Display,
74 strum::AsRefStr,
75 strum::EnumString,
76)]
77#[non_exhaustive]
78#[serde(rename_all = "snake_case")]
79#[strum(serialize_all = "snake_case")]
80#[repr(usize)]
81pub enum LangId {
82 Python,
83 #[strum(serialize = "javascript")]
84 #[serde(rename = "javascript")]
85 JavaScript,
86 #[strum(serialize = "typescript")]
87 #[serde(rename = "typescript")]
88 TypeScript,
89 Tsx,
90 C,
91 Cpp,
92 Rust,
93 Go,
94 Ruby,
95}
96
97impl LangId {
98 pub const COUNT: usize = 9;
99
100 pub fn all() -> [LangId; Self::COUNT] {
101 [
102 LangId::Python,
103 LangId::JavaScript,
104 LangId::TypeScript,
105 LangId::Tsx,
106 LangId::C,
107 LangId::Cpp,
108 LangId::Rust,
109 LangId::Go,
110 LangId::Ruby,
111 ]
112 }
113
114 pub fn spec(self) -> &'static LanguageSpec {
115 spec_for(self)
116 }
117
118 #[cfg(feature = "metacall-deploy")]
119 pub fn metacall_tag(self) -> &'static str {
120 crate::deploy::tags::metacall_tag(self)
121 }
122}
123
124#[derive(Debug, Clone, Serialize)]
125pub struct RawSymbol<'a> {
126 pub name: std::borrow::Cow<'a, str>,
127 pub kind: crate::model::SymbolKind,
128 pub source_range: crate::model::SourceRange,
129 pub visibility: Option<crate::model::Visibility>,
130 pub signature: Option<std::borrow::Cow<'a, str>>,
131 pub docstring: Option<std::borrow::Cow<'a, str>>,
132 pub is_async: bool,
133}
134
135pub struct LanguageSpec {
136 pub extensions: &'static [&'static str],
137 pub grammar_fn: fn() -> tree_sitter::Language,
138 pub query_fn: fn() -> &'static Query,
139 pub import_path_resolver: fn(
140 raw: &str,
141 source_dir: &std::path::Path,
142 project_root: &std::path::Path,
143 ) -> Option<std::path::PathBuf>,
144 pub import_ref_query_fn: fn() -> &'static Query,
145 pub class_like_parents: &'static [&'static str],
146 pub ancestor_visibility_rules: &'static [(&'static str, Visibility)],
147 pub visibility_from_name: Option<fn(&str) -> Option<Visibility>>,
148 pub import_statement_kinds: &'static [&'static str],
149 pub default_visibility: DefaultVisibility,
150 pub doc_comment_config: Option<DocCommentConfig>,
151}
152
153pub fn spec_for(id: LangId) -> &'static LanguageSpec {
154 match id {
155 LangId::Python => &python::PYTHON_SPEC,
156 LangId::JavaScript => &javascript::JS_SPEC,
157 LangId::TypeScript => &typescript::TS_SPEC,
158 LangId::Tsx => &tsx::TSX_SPEC,
159 LangId::C => &c::C_SPEC,
160 LangId::Cpp => &cpp::CPP_SPEC,
161 LangId::Rust => &rust::RUST_SPEC,
162 LangId::Go => &go::GO_SPEC,
163 LangId::Ruby => &ruby::RUBY_SPEC,
164 }
165}
166
167pub fn grammar_for(id: LangId) -> tree_sitter::Language {
168 (spec_for(id).grammar_fn)()
169}
170
171pub fn validate_queries() {
174 for id in LangId::all() {
175 let _ = (spec_for(id).query_fn)();
176 let _ = (spec_for(id).import_ref_query_fn)();
177 }
178}
179
180pub fn extract_symbols_for<'a>(
181 id: LangId,
182 tree: &'a tree_sitter::Tree,
183 source: &'a [u8],
184) -> Vec<RawSymbol<'a>> {
185 common::extract_with_spec(tree, source, spec_for(id))
186}
187
188pub fn extract_imports_and_references_for<'a>(
189 id: LangId,
190 tree: &'a tree_sitter::Tree,
191 source: &'a [u8],
192 file_path: &std::path::Path,
193) -> (
194 Vec<crate::model::UnresolvedImport>,
195 Vec<crate::model::UnresolvedReference>,
196) {
197 common::extract_imports_and_references_with_spec(tree, source, spec_for(id), file_path)
198}
199
200#[cfg(test)]
201mod tests {
202 use std::str::FromStr;
203
204 use super::*;
205
206 #[test]
207 fn lang_id_all_variants_exist() {
208 let variants = LangId::all();
209 for i in 0..variants.len() {
210 for j in (i + 1)..variants.len() {
211 assert_ne!(variants[i], variants[j]);
212 }
213 }
214 }
215
216 #[test]
217 fn lang_id_display() {
218 assert_eq!(format!("{}", LangId::Python), "python");
219 assert_eq!(format!("{}", LangId::Ruby), "ruby");
220 assert_eq!(format!("{}", LangId::JavaScript), "javascript");
221 assert_eq!(format!("{}", LangId::TypeScript), "typescript");
222 }
223
224 #[test]
225 fn lang_id_vocabulary_is_canonical() {
226 let expected = [
227 "python",
228 "javascript",
229 "typescript",
230 "tsx",
231 "c",
232 "cpp",
233 "rust",
234 "go",
235 "ruby",
236 ];
237 let all = LangId::all();
238 let actual: Vec<&str> = all.iter().map(|l| l.as_ref()).collect();
239 assert_eq!(actual, expected);
240 }
241
242 #[test]
243 fn lang_id_from_str_round_trip() {
244 for id in LangId::all() {
245 let name: &str = id.as_ref();
246 let parsed = LangId::from_str(name)
247 .unwrap_or_else(|e| panic!("from_str failed for documented name {name:?}: {e}"));
248 assert_eq!(parsed, id);
249 }
250 assert_eq!(LangId::from_str("typescript"), Ok(LangId::TypeScript));
251 assert_eq!(LangId::from_str("javascript"), Ok(LangId::JavaScript));
252 }
253
254 #[test]
255 fn lang_id_serde_snake_case() {
256 let json = serde_json::to_string(&LangId::Python).unwrap();
257 assert_eq!(json, "\"python\"");
258
259 let json = serde_json::to_string(&LangId::Ruby).unwrap();
260 assert_eq!(json, "\"ruby\"");
261
262 let json = serde_json::to_string(&LangId::JavaScript).unwrap();
263 assert_eq!(json, "\"javascript\"");
264
265 let json = serde_json::to_string(&LangId::TypeScript).unwrap();
266 assert_eq!(json, "\"typescript\"");
267 }
268
269 #[test]
270 fn grammar_for_all_variants() {
271 for id in LangId::all() {
272 let _lang = grammar_for(id);
273 }
274 }
275
276 #[test]
277 fn extract_symbols_for_python() {
278 let mut parser = tree_sitter::Parser::new();
279 parser.set_language(&grammar_for(LangId::Python)).unwrap();
280 let tree = parser.parse(b"def hello(): pass", None).unwrap();
281 let symbols = extract_symbols_for(LangId::Python, &tree, b"def hello(): pass");
282 assert!(!symbols.is_empty());
283 }
284
285 #[test]
286 fn spec_for_returns_spec_with_matching_extensions() {
287 let python_spec = spec_for(LangId::Python);
288 assert!(python_spec.extensions.contains(&"py"));
289 assert!(python_spec.extensions.contains(&"pyi"));
290
291 let js_spec = spec_for(LangId::JavaScript);
292 assert!(js_spec.extensions.contains(&"js"));
293 }
294
295 #[test]
296 fn lang_id_count_matches_variant_count() {
297 assert_eq!(LangId::COUNT, 9);
298 assert_eq!(LangId::all().len(), LangId::COUNT);
299 }
300
301 #[test]
302 fn all_specs_have_non_empty_extensions() {
303 for id in LangId::all() {
304 let spec = spec_for(id);
305 assert!(
306 !spec.extensions.is_empty(),
307 "{id:?} spec has empty extensions"
308 );
309 }
310 }
311
312 #[test]
313 fn no_duplicate_extensions_across_specs() {
314 use std::collections::HashSet;
315 let mut seen: HashSet<&str> = HashSet::new();
316 for id in LangId::all() {
317 let spec = spec_for(id);
318 for &ext in spec.extensions {
319 assert!(
320 seen.insert(ext),
321 "extension {ext:?} appears in more than one language spec"
322 );
323 }
324 }
325 }
326
327 #[test]
328 fn grammar_fn_smoke_test_all_variants() {
329 for id in LangId::all() {
330 let spec = spec_for(id);
331 let grammar = (spec.grammar_fn)();
332 let mut parser = tree_sitter::Parser::new();
333 assert!(
334 parser.set_language(&grammar).is_ok(),
335 "grammar_fn failed for {id:?}"
336 );
337 }
338 }
339
340 #[test]
341 fn query_fn_smoke_test_all_variants() {
342 for id in LangId::all() {
343 let spec = spec_for(id);
344 let _query = (spec.query_fn)();
345 }
346 }
347
348 #[test]
349 #[cfg(feature = "metacall-deploy")]
350 fn test_lang_id_metacall_tag() {
351 assert_eq!(LangId::Python.metacall_tag(), "py");
352 assert_eq!(LangId::JavaScript.metacall_tag(), "node");
353 assert_eq!(LangId::TypeScript.metacall_tag(), "ts");
354 assert_eq!(LangId::Tsx.metacall_tag(), "ts");
355 assert_eq!(LangId::C.metacall_tag(), "c");
356 assert_eq!(LangId::Cpp.metacall_tag(), "cpp");
357 assert_eq!(LangId::Rust.metacall_tag(), "rs");
358 assert_eq!(LangId::Go.metacall_tag(), "go");
359 assert_eq!(LangId::Ruby.metacall_tag(), "rb");
360 }
361}