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 pack;
17pub(crate) mod python;
18pub(crate) mod ruby;
19pub(crate) mod rust;
20pub(crate) mod tsx;
21pub(crate) mod typescript;
22
23use serde::{Deserialize, Serialize};
24use tree_sitter::Query;
25
26use crate::model::Visibility;
27
28#[derive(Debug, Clone)]
34pub struct DocCommentConfig {
35 pub line_prefixes: &'static [&'static str],
37 pub block_open: Option<&'static str>,
39 pub block_close: &'static str,
41 pub strip_continuation_marker: bool,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum DefaultVisibility {
49 PublicByDefault,
51 PrivateByDefault,
53}
54
55pub const C_LIKE_DOC_COMMENT: DocCommentConfig = DocCommentConfig {
59 line_prefixes: &["//"],
60 block_open: Some("/**"),
61 block_close: "*/",
62 strip_continuation_marker: true,
63};
64
65#[derive(
66 Debug,
67 Clone,
68 Copy,
69 PartialEq,
70 Eq,
71 Hash,
72 Serialize,
73 Deserialize,
74 strum::Display,
75 strum::AsRefStr,
76 strum::EnumString,
77)]
78#[non_exhaustive]
79#[serde(rename_all = "snake_case")]
80#[strum(serialize_all = "snake_case")]
81#[repr(usize)]
82pub enum LangId {
83 Python,
84 #[strum(serialize = "javascript")]
85 #[serde(rename = "javascript")]
86 JavaScript,
87 #[strum(serialize = "typescript")]
88 #[serde(rename = "typescript")]
89 TypeScript,
90 Tsx,
91 C,
92 Cpp,
93 Rust,
94 Go,
95 Ruby,
96}
97
98impl LangId {
99 pub const COUNT: usize = 9;
100
101 pub fn all() -> [LangId; Self::COUNT] {
102 [
103 LangId::Python,
104 LangId::JavaScript,
105 LangId::TypeScript,
106 LangId::Tsx,
107 LangId::C,
108 LangId::Cpp,
109 LangId::Rust,
110 LangId::Go,
111 LangId::Ruby,
112 ]
113 }
114
115 pub fn spec(self) -> &'static LanguageSpec {
116 spec_for(self)
117 }
118
119 #[cfg(feature = "metacall-deploy")]
120 pub fn metacall_tag(self) -> &'static str {
121 crate::deploy::tags::metacall_tag(self)
122 }
123}
124
125#[derive(Debug, Clone, Serialize)]
126pub struct RawSymbol<'a> {
127 pub name: std::borrow::Cow<'a, str>,
128 pub kind: crate::model::SymbolKind,
129 pub source_range: crate::model::SourceRange,
130 pub name_range: Option<crate::model::SourceRange>,
132 pub visibility: Option<crate::model::Visibility>,
133 pub signature: Option<std::borrow::Cow<'a, str>>,
134 pub docstring: Option<std::borrow::Cow<'a, str>>,
135 pub is_async: bool,
136}
137
138pub struct LanguageSpec {
139 pub extensions: &'static [&'static str],
140 pub grammar_fn: fn() -> tree_sitter::Language,
141 pub query_fn: fn() -> Result<&'static Query, crate::error::Error>,
142 pub import_path_resolver: fn(
143 raw: &str,
144 source_dir: &std::path::Path,
145 project_root: &std::path::Path,
146 ) -> Option<std::path::PathBuf>,
147 pub import_ref_query_fn: fn() -> Result<&'static Query, crate::error::Error>,
148 pub class_like_parents: &'static [&'static str],
149 pub ancestor_visibility_rules: &'static [(&'static str, Visibility)],
150 pub visibility_from_name: Option<fn(&str) -> Option<Visibility>>,
151 pub import_statement_kinds: &'static [&'static str],
152 pub default_visibility: DefaultVisibility,
153 pub doc_comment_config: Option<DocCommentConfig>,
154}
155
156pub fn spec_for(id: LangId) -> &'static LanguageSpec {
157 match id {
158 LangId::Python => &python::PYTHON_SPEC,
159 LangId::JavaScript => &javascript::JS_SPEC,
160 LangId::TypeScript => &typescript::TS_SPEC,
161 LangId::Tsx => &tsx::TSX_SPEC,
162 LangId::C => &c::C_SPEC,
163 LangId::Cpp => &cpp::CPP_SPEC,
164 LangId::Rust => &rust::RUST_SPEC,
165 LangId::Go => &go::GO_SPEC,
166 LangId::Ruby => &ruby::RUBY_SPEC,
167 }
168}
169
170pub fn grammar_for(id: LangId) -> tree_sitter::Language {
171 (spec_for(id).grammar_fn)()
172}
173
174pub fn validate_queries() {
179 for id in LangId::all() {
180 let spec = spec_for(id);
181 for (label, outcome) in [
182 ("symbols", (spec.query_fn)()),
183 ("imports and references", (spec.import_ref_query_fn)()),
184 ] {
185 if let Err(error) = outcome {
186 tracing::error!(language = ?id, query = label, %error, "query failed to compile");
187 }
188 }
189 }
190}
191
192pub fn extract_symbols_for<'a>(
198 id: LangId,
199 tree: &'a tree_sitter::Tree,
200 source: &'a [u8],
201) -> Vec<RawSymbol<'a>> {
202 match extract_symbols_for_checked(id, tree, source) {
203 Ok(symbols) => symbols,
204 Err(error) => {
205 tracing::error!(language = ?id, %error, "symbol extraction skipped");
206 Vec::new()
207 }
208 }
209}
210
211pub fn extract_symbols_for_checked<'a>(
212 id: LangId,
213 tree: &'a tree_sitter::Tree,
214 source: &'a [u8],
215) -> Result<Vec<RawSymbol<'a>>, crate::error::Error> {
216 common::extract_with_spec(tree, source, spec_for(id))
217}
218
219pub fn extract_imports_and_references_for<'a>(
221 id: LangId,
222 tree: &'a tree_sitter::Tree,
223 source: &'a [u8],
224 file_path: &std::path::Path,
225) -> (
226 Vec<crate::model::UnresolvedImport>,
227 Vec<crate::model::UnresolvedReference>,
228 Vec<crate::error::Diagnostic>,
229) {
230 match extract_imports_and_references_for_checked(id, tree, source, file_path) {
231 Ok(extracted) => extracted,
232 Err(error) => {
233 tracing::error!(language = ?id, %error, "import and reference extraction skipped");
234 (Vec::new(), Vec::new(), Vec::new())
235 }
236 }
237}
238
239pub fn extract_imports_and_references_for_checked<'a>(
240 id: LangId,
241 tree: &'a tree_sitter::Tree,
242 source: &'a [u8],
243 file_path: &std::path::Path,
244) -> Result<common::ImportExtraction, crate::error::Error> {
245 common::extract_imports_and_references_with_spec(tree, source, spec_for(id), file_path)
246}
247
248#[cfg(test)]
249mod tests {
250 use std::str::FromStr;
251
252 use super::*;
253
254 #[test]
255 fn lang_id_all_variants_exist() {
256 let variants = LangId::all();
257 for i in 0..variants.len() {
258 for j in (i + 1)..variants.len() {
259 assert_ne!(variants[i], variants[j]);
260 }
261 }
262 }
263
264 #[test]
265 fn lang_id_display() {
266 assert_eq!(format!("{}", LangId::Python), "python");
267 assert_eq!(format!("{}", LangId::Ruby), "ruby");
268 assert_eq!(format!("{}", LangId::JavaScript), "javascript");
269 assert_eq!(format!("{}", LangId::TypeScript), "typescript");
270 }
271
272 #[test]
273 fn lang_id_vocabulary_is_canonical() {
274 let expected = [
275 "python",
276 "javascript",
277 "typescript",
278 "tsx",
279 "c",
280 "cpp",
281 "rust",
282 "go",
283 "ruby",
284 ];
285 let all = LangId::all();
286 let actual: Vec<&str> = all.iter().map(|l| l.as_ref()).collect();
287 assert_eq!(actual, expected);
288 }
289
290 #[test]
291 fn lang_id_from_str_round_trip() {
292 for id in LangId::all() {
293 let name: &str = id.as_ref();
294 let parsed = LangId::from_str(name)
295 .unwrap_or_else(|e| panic!("from_str failed for documented name {name:?}: {e}"));
296 assert_eq!(parsed, id);
297 }
298 assert_eq!(LangId::from_str("typescript"), Ok(LangId::TypeScript));
299 assert_eq!(LangId::from_str("javascript"), Ok(LangId::JavaScript));
300 }
301
302 #[test]
303 fn lang_id_serde_snake_case() {
304 let json = serde_json::to_string(&LangId::Python).unwrap();
305 assert_eq!(json, "\"python\"");
306
307 let json = serde_json::to_string(&LangId::Ruby).unwrap();
308 assert_eq!(json, "\"ruby\"");
309
310 let json = serde_json::to_string(&LangId::JavaScript).unwrap();
311 assert_eq!(json, "\"javascript\"");
312
313 let json = serde_json::to_string(&LangId::TypeScript).unwrap();
314 assert_eq!(json, "\"typescript\"");
315 }
316
317 #[test]
318 fn grammar_for_all_variants() {
319 for id in LangId::all() {
320 let _lang = grammar_for(id);
321 }
322 }
323
324 #[test]
325 fn extract_symbols_for_python() {
326 let mut parser = tree_sitter::Parser::new();
327 parser.set_language(&grammar_for(LangId::Python)).unwrap();
328 let tree = parser.parse(b"def hello(): pass", None).unwrap();
329 let symbols = extract_symbols_for(LangId::Python, &tree, b"def hello(): pass");
330 assert!(!symbols.is_empty());
331 }
332
333 #[test]
334 fn spec_for_returns_spec_with_matching_extensions() {
335 let python_spec = spec_for(LangId::Python);
336 assert!(python_spec.extensions.contains(&"py"));
337 assert!(python_spec.extensions.contains(&"pyi"));
338
339 let js_spec = spec_for(LangId::JavaScript);
340 assert!(js_spec.extensions.contains(&"js"));
341 }
342
343 #[test]
344 fn lang_id_count_matches_variant_count() {
345 assert_eq!(LangId::COUNT, 9);
346 assert_eq!(LangId::all().len(), LangId::COUNT);
347 }
348
349 #[test]
350 fn all_specs_have_non_empty_extensions() {
351 for id in LangId::all() {
352 let spec = spec_for(id);
353 assert!(
354 !spec.extensions.is_empty(),
355 "{id:?} spec has empty extensions"
356 );
357 }
358 }
359
360 #[test]
361 fn no_duplicate_extensions_across_specs() {
362 use std::collections::HashSet;
363 let mut seen: HashSet<&str> = HashSet::new();
364 for id in LangId::all() {
365 let spec = spec_for(id);
366 for &ext in spec.extensions {
367 assert!(
368 seen.insert(ext),
369 "extension {ext:?} appears in more than one language spec"
370 );
371 }
372 }
373 }
374
375 #[test]
376 fn grammar_fn_smoke_test_all_variants() {
377 for id in LangId::all() {
378 let spec = spec_for(id);
379 let grammar = (spec.grammar_fn)();
380 let mut parser = tree_sitter::Parser::new();
381 assert!(
382 parser.set_language(&grammar).is_ok(),
383 "grammar_fn failed for {id:?}"
384 );
385 }
386 }
387
388 #[test]
389 fn query_fn_smoke_test_all_variants() {
390 for id in LangId::all() {
391 let spec = spec_for(id);
392 let _query = (spec.query_fn)();
393 }
394 }
395
396 #[test]
397 #[cfg(feature = "metacall-deploy")]
398 fn test_lang_id_metacall_tag() {
399 assert_eq!(LangId::Python.metacall_tag(), "py");
400 assert_eq!(LangId::JavaScript.metacall_tag(), "node");
401 assert_eq!(LangId::TypeScript.metacall_tag(), "ts");
402 assert_eq!(LangId::Tsx.metacall_tag(), "ts");
403 assert_eq!(LangId::C.metacall_tag(), "c");
404 assert_eq!(LangId::Cpp.metacall_tag(), "c");
405 assert_eq!(LangId::Rust.metacall_tag(), "rs");
406 assert_eq!(LangId::Go.metacall_tag(), "go");
407 assert_eq!(LangId::Ruby.metacall_tag(), "rb");
408 }
409
410 #[test]
414 fn specs_keep_their_documented_surface() {
415 struct Expected {
416 extensions: &'static [&'static str],
417 default_visibility: DefaultVisibility,
418 doc_prefixes: Option<&'static [&'static str]>,
419 doc_block_open: Option<&'static str>,
420 class_like_parents: &'static [&'static str],
421 ancestor_rules: usize,
422 import_kinds: &'static [&'static str],
423 visibility_from_name: bool,
424 }
425
426 let public = DefaultVisibility::PublicByDefault;
427 let private = DefaultVisibility::PrivateByDefault;
428 let cases: [(LangId, Expected); 9] = [
429 (
430 LangId::Python,
431 Expected {
432 extensions: &["py", "pyi"],
433 default_visibility: public,
434 doc_prefixes: None,
435 doc_block_open: None,
436 class_like_parents: &["class_definition"],
437 ancestor_rules: 0,
438 import_kinds: &["import_statement", "import_from_statement"],
439 visibility_from_name: false,
440 },
441 ),
442 (
443 LangId::JavaScript,
444 Expected {
445 extensions: &["js", "mjs", "cjs"],
446 default_visibility: private,
447 doc_prefixes: Some(&["//"]),
448 doc_block_open: Some("/**"),
449 class_like_parents: &["class_declaration", "class"],
450 ancestor_rules: 1,
451 import_kinds: &["import_statement"],
452 visibility_from_name: false,
453 },
454 ),
455 (
456 LangId::TypeScript,
457 Expected {
458 extensions: &["ts", "cts", "mts"],
459 default_visibility: private,
460 doc_prefixes: Some(&["//"]),
461 doc_block_open: Some("/**"),
462 class_like_parents: &["class_declaration", "class"],
463 ancestor_rules: 1,
464 import_kinds: &["import_statement"],
465 visibility_from_name: false,
466 },
467 ),
468 (
469 LangId::Tsx,
470 Expected {
471 extensions: &["tsx"],
472 default_visibility: private,
473 doc_prefixes: Some(&["//"]),
474 doc_block_open: Some("/**"),
475 class_like_parents: &["class_declaration", "class"],
476 ancestor_rules: 1,
477 import_kinds: &["import_statement"],
478 visibility_from_name: false,
479 },
480 ),
481 (
482 LangId::C,
483 Expected {
484 extensions: &["c", "h"],
485 default_visibility: public,
486 doc_prefixes: Some(&["//"]),
487 doc_block_open: Some("/**"),
488 class_like_parents: &[],
489 ancestor_rules: 0,
490 import_kinds: &["preproc_include"],
491 visibility_from_name: false,
492 },
493 ),
494 (
495 LangId::Cpp,
496 Expected {
497 extensions: &["cc", "cpp", "cxx", "hpp"],
498 default_visibility: private,
499 doc_prefixes: Some(&["//"]),
500 doc_block_open: Some("/**"),
501 class_like_parents: &["class_specifier", "struct_specifier"],
502 ancestor_rules: 0,
503 import_kinds: &["preproc_include"],
504 visibility_from_name: false,
505 },
506 ),
507 (
508 LangId::Rust,
509 Expected {
510 extensions: &["rs"],
511 default_visibility: private,
512 doc_prefixes: Some(&["///", "//!"]),
513 doc_block_open: Some("/**"),
514 class_like_parents: &["impl_item"],
515 ancestor_rules: 0,
516 import_kinds: &["use_declaration"],
517 visibility_from_name: false,
518 },
519 ),
520 (
521 LangId::Go,
522 Expected {
523 extensions: &["go"],
524 default_visibility: private,
525 doc_prefixes: Some(&["//"]),
526 doc_block_open: None,
527 class_like_parents: &[],
528 ancestor_rules: 0,
529 import_kinds: &["import_declaration"],
530 visibility_from_name: true,
531 },
532 ),
533 (
534 LangId::Ruby,
535 Expected {
536 extensions: &["rb", "gemspec"],
537 default_visibility: public,
538 doc_prefixes: Some(&["#"]),
539 doc_block_open: None,
540 class_like_parents: &["class", "module"],
541 ancestor_rules: 0,
542 import_kinds: &[],
543 visibility_from_name: false,
544 },
545 ),
546 ];
547
548 for (lang, want) in cases {
549 let spec = spec_for(lang);
550 assert_eq!(spec.extensions, want.extensions, "{lang:?} extensions");
551 assert_eq!(
552 spec.default_visibility, want.default_visibility,
553 "{lang:?} default visibility"
554 );
555 assert_eq!(
556 spec.class_like_parents, want.class_like_parents,
557 "{lang:?} class-like parents"
558 );
559 assert_eq!(
560 spec.ancestor_visibility_rules.len(),
561 want.ancestor_rules,
562 "{lang:?} ancestor visibility rules"
563 );
564 assert_eq!(
565 spec.import_statement_kinds, want.import_kinds,
566 "{lang:?} import statement kinds"
567 );
568 assert_eq!(
569 spec.visibility_from_name.is_some(),
570 want.visibility_from_name,
571 "{lang:?} name-based visibility"
572 );
573 match (spec.doc_comment_config.as_ref(), want.doc_prefixes) {
574 (None, None) => {}
575 (Some(config), Some(prefixes)) => {
576 assert_eq!(config.line_prefixes, prefixes, "{lang:?} doc prefixes");
577 assert_eq!(
578 config.block_open, want.doc_block_open,
579 "{lang:?} doc block opener"
580 );
581 }
582 (config, prefixes) => panic!(
583 "{lang:?} doc comment configuration mismatch: {config:?} against {prefixes:?}"
584 ),
585 }
586 }
587
588 let rule = spec_for(LangId::Go).visibility_from_name;
589 assert!(rule.is_some(), "Go derives visibility from the name");
590 let rule = rule.unwrap();
591 assert_eq!(rule("Exported"), Some(Visibility::Public));
592 assert_eq!(rule("hidden"), None);
593 }
594}