Skip to main content

normalize_languages/
grammar_loader.rs

1//! Dynamic grammar loading for tree-sitter.
2//!
3//! Loads tree-sitter grammars from shared libraries (.so/.dylib/.dll).
4//! Also loads highlight queries (.scm files) for syntax highlighting.
5//! Grammars are compiled from arborium sources via `cargo xtask build-grammars`.
6//!
7//! # ABI Compatibility
8//!
9//! Tree-sitter grammars have an ABI version embedded at compile time. The tree-sitter
10//! library only loads grammars within its supported version range:
11//! - tree-sitter 0.24: ABI 13-14
12//! - tree-sitter 0.25+: ABI 13-15
13//!
14//! Arborium grammar crates embed the ABI version in their parser.c source. When arborium
15//! updates to use newer tree-sitter, grammars must be recompiled. Stale grammars in
16//! `~/.config/normalize/grammars/` may cause `LanguageError { version: N }` if incompatible.
17//!
18//! # Lifetime Requirements
19//!
20//! **IMPORTANT**: The `GrammarLoader` must outlive any `Language` or `Tree` obtained from it.
21//! The loader holds the shared library (`Library`) that contains the grammar's code. If the
22//! loader is dropped, the library is unloaded, and any `Language`/`Tree` references become
23//! dangling pointers (use-after-free, likely segfault).
24//!
25//! Safe patterns:
26//! - Use the global singleton (see [`crate::parsers::grammar_loader()`])
27//! - Keep the loader in scope for the duration of tree usage
28//! - Return `(Tree, GrammarLoader)` tuples from helper functions
29//!
30//! Unsafe pattern (causes segfault):
31//! ```ignore
32//! fn parse(code: &str) -> Tree {
33//!     let loader = GrammarLoader::new();  // Created here
34//!     let lang = loader.get("python").ok().unwrap();
35//!     let mut parser = Parser::new();
36//!     parser.set_language(&lang).unwrap();
37//!     parser.parse(code, None).unwrap()   // Tree returned
38//! }  // loader dropped here - library unloaded!
39//! // Tree now has dangling pointers -> segfault on use
40//! ```
41
42use libloading::{Library, Symbol};
43use std::collections::HashMap;
44use std::path::{Path, PathBuf};
45use std::sync::{Arc, RwLock};
46use tree_sitter::Language;
47use tree_sitter_language::LanguageFn;
48
49/// Error returned by [`GrammarLoader::get`].
50#[derive(Debug, thiserror::Error)]
51pub enum GrammarLoadError {
52    /// No `.so`/`.dylib` file for this grammar exists in any search path.
53    #[error("grammar '{0}' not found in search paths")]
54    NotFound(String),
55    /// The shared library was found but could not be loaded (e.g., missing
56    /// symbols, OS-level `dlopen` failure).
57    #[error("failed to load grammar '{grammar}': {detail}")]
58    LoadFailed {
59        /// Grammar name (e.g. `"python"`).
60        grammar: String,
61        /// Underlying error message from libloading.
62        detail: String,
63    },
64}
65
66/// Loaded grammar with its backing library.
67///
68/// The `_library` field keeps the shared library loaded in memory. The `language`
69/// field contains pointers into this library's memory. Dropping the library while
70/// the language is in use causes undefined behavior (typically segfault).
71struct LoadedGrammar {
72    /// Backing shared library - must outlive any use of `language`.
73    _library: Library,
74    /// Tree-sitter Language (contains pointers into `_library`).
75    language: Language,
76}
77
78/// Dynamic grammar loader with caching.
79pub struct GrammarLoader {
80    /// Search paths for grammar libraries.
81    search_paths: Vec<PathBuf>,
82    /// Cached loaded grammars.
83    cache: RwLock<HashMap<String, Arc<LoadedGrammar>>>,
84    /// Cached highlight queries.
85    highlight_cache: RwLock<HashMap<String, Arc<String>>>,
86    /// Cached injection queries.
87    injection_cache: RwLock<HashMap<String, Arc<String>>>,
88    /// Cached locals queries.
89    locals_cache: RwLock<HashMap<String, Arc<String>>>,
90    /// Cached complexity queries.
91    complexity_cache: RwLock<HashMap<String, Arc<String>>>,
92    /// Cached calls queries.
93    calls_cache: RwLock<HashMap<String, Arc<String>>>,
94    /// Cached type queries.
95    types_cache: RwLock<HashMap<String, Arc<String>>>,
96    /// Cached tags queries.
97    tags_cache: RwLock<HashMap<String, Arc<String>>>,
98    /// Cached imports queries.
99    imports_cache: RwLock<HashMap<String, Arc<String>>>,
100    /// Cached decorations queries.
101    decorations_cache: RwLock<HashMap<String, Arc<String>>>,
102    /// Cached test-regions queries.
103    test_regions_cache: RwLock<HashMap<String, Arc<String>>>,
104    /// Cached compiled tree-sitter queries (keyed by "grammar:query_type").
105    compiled_query_cache: RwLock<HashMap<String, Arc<tree_sitter::Query>>>,
106}
107
108impl GrammarLoader {
109    /// Create a new grammar loader with default search paths.
110    ///
111    /// Search order:
112    /// 1. `NORMALIZE_GRAMMAR_PATH` environment variable (colon-separated)
113    /// 2. `~/.config/normalize/grammars/`
114    pub fn new() -> Self {
115        let mut paths = Vec::new();
116
117        // Environment variable takes priority
118        if let Ok(env_path) = std::env::var("NORMALIZE_GRAMMAR_PATH") {
119            for p in env_path.split(':') {
120                if !p.is_empty() {
121                    paths.push(PathBuf::from(p));
122                }
123            }
124        }
125
126        // User config directory
127        if let Some(config) = dirs::config_dir() {
128            paths.push(config.join("normalize/grammars"));
129        }
130
131        Self {
132            search_paths: paths,
133            cache: RwLock::new(HashMap::new()),
134            highlight_cache: RwLock::new(HashMap::new()),
135            injection_cache: RwLock::new(HashMap::new()),
136            locals_cache: RwLock::new(HashMap::new()),
137            complexity_cache: RwLock::new(HashMap::new()),
138            calls_cache: RwLock::new(HashMap::new()),
139            types_cache: RwLock::new(HashMap::new()),
140            tags_cache: RwLock::new(HashMap::new()),
141            imports_cache: RwLock::new(HashMap::new()),
142            decorations_cache: RwLock::new(HashMap::new()),
143            test_regions_cache: RwLock::new(HashMap::new()),
144            compiled_query_cache: RwLock::new(HashMap::new()),
145        }
146    }
147
148    /// Create a loader with custom search paths.
149    pub fn with_paths(paths: Vec<PathBuf>) -> Self {
150        Self {
151            search_paths: paths,
152            cache: RwLock::new(HashMap::new()),
153            highlight_cache: RwLock::new(HashMap::new()),
154            injection_cache: RwLock::new(HashMap::new()),
155            locals_cache: RwLock::new(HashMap::new()),
156            complexity_cache: RwLock::new(HashMap::new()),
157            calls_cache: RwLock::new(HashMap::new()),
158            types_cache: RwLock::new(HashMap::new()),
159            tags_cache: RwLock::new(HashMap::new()),
160            imports_cache: RwLock::new(HashMap::new()),
161            decorations_cache: RwLock::new(HashMap::new()),
162            test_regions_cache: RwLock::new(HashMap::new()),
163            compiled_query_cache: RwLock::new(HashMap::new()),
164        }
165    }
166
167    /// Add a search path.
168    pub fn add_path(&mut self, path: PathBuf) {
169        self.search_paths.push(path);
170    }
171
172    /// Get a grammar by name.
173    ///
174    /// Returns `Ok(lang)` if found and loaded successfully,
175    /// `Err(GrammarLoadError::NotFound)` if no `.so`/`.dylib` exists in any
176    /// search path, and other `Err` variants for load or ABI failures.
177    pub fn get(&self, name: &str) -> Result<Language, GrammarLoadError> {
178        // Check cache first
179        if let Some(loaded) = self
180            .cache
181            .read()
182            .unwrap_or_else(|e| e.into_inner())
183            .get(name)
184        {
185            return Ok(loaded.language.clone());
186        }
187
188        self.load_external(name)
189    }
190
191    /// Get the highlight query for a grammar.
192    ///
193    /// Returns None if no highlight query found for the grammar.
194    /// Query files are {name}.highlights.scm in the grammar search paths.
195    pub fn get_highlights(&self, name: &str) -> Option<Arc<String>> {
196        // Check cache first
197        if let Some(query) = self
198            .highlight_cache
199            .read()
200            .unwrap_or_else(|e| e.into_inner())
201            .get(name)
202        {
203            return Some(Arc::clone(query));
204        }
205
206        self.load_query(name, "highlights", &self.highlight_cache)
207    }
208
209    /// Get the injection query for a grammar.
210    ///
211    /// Returns None if no injection query found for the grammar.
212    /// Query files are {name}.injections.scm in the grammar search paths.
213    pub fn get_injections(&self, name: &str) -> Option<Arc<String>> {
214        // Check cache first
215        if let Some(query) = self
216            .injection_cache
217            .read()
218            .unwrap_or_else(|e| e.into_inner())
219            .get(name)
220        {
221            return Some(Arc::clone(query));
222        }
223
224        self.load_query(name, "injections", &self.injection_cache)
225    }
226
227    /// Get the locals query for a grammar.
228    ///
229    /// Returns None if no locals query found for the grammar.
230    /// Query files are {name}.locals.scm in the grammar search paths.
231    pub fn get_locals(&self, name: &str) -> Option<Arc<String>> {
232        // Check cache first
233        if let Some(query) = self
234            .locals_cache
235            .read()
236            .unwrap_or_else(|e| e.into_inner())
237            .get(name)
238        {
239            return Some(Arc::clone(query));
240        }
241
242        self.load_query(name, "locals", &self.locals_cache)
243    }
244
245    /// Get the complexity query for a grammar.
246    ///
247    /// Returns None if no complexity query found for the grammar.
248    /// Query files are {name}.complexity.scm in the grammar search paths.
249    /// Uses `@complexity` captures for nodes that increase cyclomatic complexity,
250    /// and `@nesting` captures for nodes that increase nesting depth.
251    pub fn get_complexity(&self, name: &str) -> Option<Arc<String>> {
252        // Check cache first
253        if let Some(query) = self
254            .complexity_cache
255            .read()
256            .unwrap_or_else(|e| e.into_inner())
257            .get(name)
258        {
259            return Some(Arc::clone(query));
260        }
261
262        // Try external files, then fall back to bundled queries
263        self.load_query(name, "complexity", &self.complexity_cache)
264            .or_else(|| {
265                let content = bundled_complexity_query(name)?;
266                let query = Arc::new(content.to_string());
267                self.complexity_cache
268                    .write()
269                    .unwrap_or_else(|e| e.into_inner())
270                    .insert(name.to_string(), Arc::clone(&query));
271                Some(query)
272            })
273    }
274
275    /// Get the calls query for a grammar.
276    ///
277    /// Returns None if no calls query found for the grammar.
278    /// Query files are {name}.calls.scm in the grammar search paths.
279    /// Uses `@call` captures for call expressions and `@call.qualifier` for
280    /// method call receivers (e.g. `foo` in `foo.bar()`).
281    pub fn get_calls(&self, name: &str) -> Option<Arc<String>> {
282        // Check cache first
283        if let Some(query) = self
284            .calls_cache
285            .read()
286            .unwrap_or_else(|e| e.into_inner())
287            .get(name)
288        {
289            return Some(Arc::clone(query));
290        }
291
292        // Try external files, then fall back to bundled queries
293        self.load_query(name, "calls", &self.calls_cache)
294            .or_else(|| {
295                let content = bundled_calls_query(name)?;
296                let query = Arc::new(content.to_string());
297                self.calls_cache
298                    .write()
299                    .unwrap_or_else(|e| e.into_inner())
300                    .insert(name.to_string(), Arc::clone(&query));
301                Some(query)
302            })
303    }
304
305    /// Get the types query for a grammar.
306    ///
307    /// Returns the bundled query for supported languages, or an external file if one
308    /// exists at `{name}.types.scm` in the grammar search paths (external wins).
309    pub fn get_types(&self, name: &str) -> Option<Arc<String>> {
310        // Check cache first
311        if let Some(query) = self
312            .types_cache
313            .read()
314            .unwrap_or_else(|e| e.into_inner())
315            .get(name)
316        {
317            return Some(Arc::clone(query));
318        }
319
320        // External file takes priority over bundled
321        if let Some(q) = self.load_query(name, "types", &self.types_cache) {
322            return Some(q);
323        }
324
325        // Fall back to bundled query
326        let bundled = bundled_types_query(name)?;
327        let query = Arc::new(bundled.to_string());
328        self.types_cache
329            .write()
330            .unwrap_or_else(|e| e.into_inner())
331            .insert(name.to_string(), Arc::clone(&query));
332        Some(query)
333    }
334
335    /// Get the tags query for a grammar.
336    ///
337    /// Tags queries use the tree-sitter tags format with `@name.definition.*` and
338    /// `@name.reference.*` captures for symbol navigation (used by GitHub Linguist,
339    /// nvim-treesitter, etc.).
340    ///
341    /// Returns the bundled query for supported languages, or an external file if one
342    /// exists at `{name}.tags.scm` in the grammar search paths (external wins).
343    pub fn get_tags(&self, name: &str) -> Option<Arc<String>> {
344        // Check cache first
345        if let Some(query) = self
346            .tags_cache
347            .read()
348            .unwrap_or_else(|e| e.into_inner())
349            .get(name)
350        {
351            return Some(Arc::clone(query));
352        }
353
354        // External file takes priority over bundled
355        if let Some(q) = self.load_query(name, "tags", &self.tags_cache) {
356            return Some(q);
357        }
358
359        // Fall back to bundled query
360        let bundled = bundled_tags_query(name)?;
361        let query = Arc::new(bundled.to_string());
362        self.tags_cache
363            .write()
364            .unwrap_or_else(|e| e.into_inner())
365            .insert(name.to_string(), Arc::clone(&query));
366        Some(query)
367    }
368
369    /// Get the imports query for a grammar.
370    ///
371    /// Returns the bundled query for supported languages, or an external file if one
372    /// exists at `{name}.imports.scm` in the grammar search paths (external wins).
373    pub fn get_imports(&self, name: &str) -> Option<Arc<String>> {
374        // Check cache first
375        if let Some(query) = self
376            .imports_cache
377            .read()
378            .unwrap_or_else(|e| e.into_inner())
379            .get(name)
380        {
381            return Some(Arc::clone(query));
382        }
383
384        // External file takes priority over bundled
385        if let Some(q) = self.load_query(name, "imports", &self.imports_cache) {
386            return Some(q);
387        }
388
389        // Fall back to bundled query
390        let bundled = bundled_imports_query(name)?;
391        let query = Arc::new(bundled.to_string());
392        self.imports_cache
393            .write()
394            .unwrap_or_else(|e| e.into_inner())
395            .insert(name.to_string(), Arc::clone(&query));
396        Some(query)
397    }
398
399    /// Get the decorations query for a grammar.
400    ///
401    /// Returns the bundled query for supported languages, or an external file if one
402    /// exists at `{name}.decorations.scm` in the grammar search paths (external wins).
403    /// Uses `@decoration` captures for doc comments, attributes, decorators, and
404    /// annotations that immediately precede a definition.
405    pub fn get_decorations(&self, name: &str) -> Option<Arc<String>> {
406        // Check cache first
407        if let Some(query) = self
408            .decorations_cache
409            .read()
410            .unwrap_or_else(|e| e.into_inner())
411            .get(name)
412        {
413            return Some(Arc::clone(query));
414        }
415
416        // External file takes priority over bundled
417        if let Some(q) = self.load_query(name, "decorations", &self.decorations_cache) {
418            return Some(q);
419        }
420
421        // Fall back to bundled query
422        let bundled = bundled_decorations_query(name)?;
423        let query = Arc::new(bundled.to_string());
424        self.decorations_cache
425            .write()
426            .unwrap_or_else(|e| e.into_inner())
427            .insert(name.to_string(), Arc::clone(&query));
428        Some(query)
429    }
430
431    /// Get the test-regions query for a grammar.
432    ///
433    /// Returns None if no test-regions query exists for the grammar.
434    /// Query files are `{name}.test_regions.scm` in the grammar search paths.
435    /// Uses `@test_region` captures for byte ranges of test-only code that
436    /// rules may opt to skip (via `applies_in_tests = false`, the default).
437    ///
438    /// Languages without a `.test_regions.scm` simply have no AST-based test
439    /// detection — path-based excludes (e.g. `**/tests/**` or `*_test.go`)
440    /// remain the way to scope rules in those cases.
441    pub fn get_test_regions(&self, name: &str) -> Option<Arc<String>> {
442        // Check cache first
443        if let Some(query) = self
444            .test_regions_cache
445            .read()
446            .unwrap_or_else(|e| e.into_inner())
447            .get(name)
448        {
449            return Some(Arc::clone(query));
450        }
451
452        // External file takes priority over bundled
453        if let Some(q) = self.load_query(name, "test_regions", &self.test_regions_cache) {
454            return Some(q);
455        }
456
457        // Fall back to bundled query
458        let bundled = bundled_test_regions_query(name)?;
459        let query = Arc::new(bundled.to_string());
460        self.test_regions_cache
461            .write()
462            .unwrap_or_else(|e| e.into_inner())
463            .insert(name.to_string(), Arc::clone(&query));
464        Some(query)
465    }
466
467    /// Load a query file (.scm) from external file.
468    fn load_query(
469        &self,
470        name: &str,
471        query_type: &str,
472        cache: &RwLock<HashMap<String, Arc<String>>>,
473    ) -> Option<Arc<String>> {
474        let scm_name = format!("{name}.{query_type}.scm");
475
476        for search_path in &self.search_paths {
477            let scm_path = search_path.join(&scm_name);
478            if scm_path.exists()
479                && let Ok(content) = std::fs::read_to_string(&scm_path)
480            {
481                let query = Arc::new(content);
482
483                // Cache it
484                cache
485                    .write()
486                    .unwrap_or_else(|e| e.into_inner())
487                    .insert(name.to_string(), Arc::clone(&query));
488
489                return Some(query);
490            }
491        }
492
493        None
494    }
495
496    /// Get a compiled tree-sitter query, using the cache to avoid recompilation.
497    ///
498    /// `grammar_name` is the grammar name (e.g. "rust", "python").
499    /// `query_type` is the query category (e.g. "tags", "complexity", "calls").
500    /// `query_str` is the raw .scm query string.
501    ///
502    /// Returns the compiled query or None if compilation fails.
503    pub fn get_compiled_query(
504        &self,
505        grammar_name: &str,
506        query_type: &str,
507        query_str: &str,
508    ) -> Option<Arc<tree_sitter::Query>> {
509        let key = format!("{grammar_name}:{query_type}");
510
511        // Check cache
512        {
513            let cache = self
514                .compiled_query_cache
515                .read()
516                .unwrap_or_else(|e| e.into_inner());
517            if let Some(q) = cache.get(&key) {
518                return Some(Arc::clone(q));
519            }
520        }
521
522        // Compile and cache
523        let grammar = self.get(grammar_name).ok()?;
524        let compiled = tree_sitter::Query::new(&grammar, query_str).ok()?;
525        let arc = Arc::new(compiled);
526
527        self.compiled_query_cache
528            .write()
529            .unwrap_or_else(|e| e.into_inner())
530            .insert(key, Arc::clone(&arc));
531
532        Some(arc)
533    }
534
535    /// Load a grammar from external .so file.
536    fn load_external(&self, name: &str) -> Result<Language, GrammarLoadError> {
537        let lib_name = grammar_lib_name(name);
538
539        for search_path in &self.search_paths {
540            let lib_path = search_path.join(&lib_name);
541            if lib_path.exists() {
542                return self.load_from_path(name, &lib_path);
543            }
544        }
545
546        Err(GrammarLoadError::NotFound(name.to_string()))
547    }
548
549    /// Load grammar from a specific path.
550    fn load_from_path(&self, name: &str, path: &Path) -> Result<Language, GrammarLoadError> {
551        // SAFETY: Loading shared libraries is inherently unsafe. We accept this risk because:
552        // 1. Grammars come from arborium (bundled) or user-configured search paths
553        // 2. The alternative (no dynamic loading) would require compiling all grammars statically
554        // 3. Tree-sitter grammars are widely used and well-tested
555        let library = unsafe {
556            Library::new(path).map_err(|e| {
557                log::debug!("Failed to load grammar at {}: {}", path.display(), e);
558                GrammarLoadError::LoadFailed {
559                    grammar: name.to_string(),
560                    detail: e.to_string(),
561                }
562            })?
563        };
564
565        let symbol_name = grammar_symbol_name(name);
566        // SAFETY: We call the tree-sitter grammar function which returns a Language pointer.
567        // The function signature is defined by tree-sitter's C ABI. We trust that:
568        // 1. The symbol exists (checked by library.get)
569        // 2. The function conforms to tree-sitter's expected signature
570        // 3. The returned Language is valid for the lifetime of the library
571        let language = unsafe {
572            let func: Result<Symbol<unsafe extern "C" fn() -> *const ()>, _> =
573                library.get(symbol_name.as_bytes());
574            match func {
575                Ok(f) => {
576                    let lang_fn = LanguageFn::from_raw(*f);
577                    Language::new(lang_fn)
578                }
579                Err(e) => {
580                    log::debug!(
581                        "Grammar '{}' at {} missing symbol '{}': {}",
582                        name,
583                        path.display(),
584                        symbol_name,
585                        e
586                    );
587                    return Err(GrammarLoadError::LoadFailed {
588                        grammar: name.to_string(),
589                        detail: format!("symbol '{}' not found: {}", symbol_name, e),
590                    });
591                }
592            }
593        };
594
595        // Cache the loaded grammar
596        let loaded = Arc::new(LoadedGrammar {
597            _library: library,
598            language: language.clone(),
599        });
600
601        self.cache
602            .write()
603            .unwrap_or_else(|e| e.into_inner())
604            .insert(name.to_string(), loaded);
605
606        Ok(language)
607    }
608
609    /// List available grammars in search paths.
610    pub fn available_external(&self) -> Vec<String> {
611        let mut grammars = Vec::new();
612        let ext = grammar_extension();
613
614        for path in &self.search_paths {
615            if let Ok(entries) = std::fs::read_dir(path) {
616                for entry in entries.flatten() {
617                    let name = entry.file_name();
618                    let name_str = name.to_string_lossy();
619                    if name_str.ends_with(ext) {
620                        let grammar_name = name_str.trim_end_matches(ext);
621                        if !grammars.contains(&grammar_name.to_string()) {
622                            grammars.push(grammar_name.to_string());
623                        }
624                    }
625                }
626            }
627        }
628
629        grammars.sort();
630        grammars
631    }
632
633    /// List available grammars in search paths, with their file paths.
634    pub fn available_external_with_paths(&self) -> Vec<(String, std::path::PathBuf)> {
635        let mut grammars: Vec<(String, std::path::PathBuf)> = Vec::new();
636        let ext = grammar_extension();
637
638        for dir in &self.search_paths {
639            if let Ok(entries) = std::fs::read_dir(dir) {
640                for entry in entries.flatten() {
641                    let name = entry.file_name();
642                    let name_str = name.to_string_lossy();
643                    if name_str.ends_with(ext) {
644                        let grammar_name = name_str.trim_end_matches(ext).to_string();
645                        if !grammars.iter().any(|(n, _)| n == &grammar_name) {
646                            grammars.push((grammar_name, entry.path()));
647                        }
648                    }
649                }
650            }
651        }
652
653        grammars.sort_by(|a, b| a.0.cmp(&b.0));
654        grammars
655    }
656}
657
658impl Default for GrammarLoader {
659    fn default() -> Self {
660        Self::new()
661    }
662}
663
664/// Get the library file name for a grammar.
665fn grammar_lib_name(name: &str) -> String {
666    let ext = grammar_extension();
667    format!("{name}{ext}")
668}
669
670/// Get the expected symbol name for a grammar.
671fn grammar_symbol_name(name: &str) -> String {
672    // Special cases for arborium grammars with non-standard symbol names
673    match name {
674        "rust" => return "tree_sitter_rust_orchard".to_string(),
675        "vb" => return "tree_sitter_vb_dotnet".to_string(),
676        _ => {}
677    }
678    // Most grammars use tree_sitter_{name} with hyphens replaced by underscores
679    let normalized = name.replace('-', "_");
680    format!("tree_sitter_{normalized}")
681}
682
683/// Return a bundled types query for languages with built-in support.
684/// Returns None for languages without a bundled query.
685fn bundled_types_query(name: &str) -> Option<&'static str> {
686    match name {
687        "rust" => Some(include_str!("queries/rust.types.scm")),
688        "typescript" => Some(include_str!("queries/typescript.types.scm")),
689        "tsx" => Some(include_str!("queries/tsx.types.scm")),
690        "python" => Some(include_str!("queries/python.types.scm")),
691        "java" => Some(include_str!("queries/java.types.scm")),
692        "go" => Some(include_str!("queries/go.types.scm")),
693        "c" => Some(include_str!("queries/c.types.scm")),
694        "cpp" => Some(include_str!("queries/cpp.types.scm")),
695        "kotlin" => Some(include_str!("queries/kotlin.types.scm")),
696        "swift" => Some(include_str!("queries/swift.types.scm")),
697        "c-sharp" => Some(include_str!("queries/c-sharp.types.scm")),
698        "scala" => Some(include_str!("queries/scala.types.scm")),
699        "haskell" => Some(include_str!("queries/haskell.types.scm")),
700        "ruby" => Some(include_str!("queries/ruby.types.scm")),
701        "dart" => Some(include_str!("queries/dart.types.scm")),
702        "elixir" => Some(include_str!("queries/elixir.types.scm")),
703        "ocaml" => Some(include_str!("queries/ocaml.types.scm")),
704        "erlang" => Some(include_str!("queries/erlang.types.scm")),
705        "zig" => Some(include_str!("queries/zig.types.scm")),
706        "fsharp" => Some(include_str!("queries/fsharp.types.scm")),
707        "gleam" => Some(include_str!("queries/gleam.types.scm")),
708        "julia" => Some(include_str!("queries/julia.types.scm")),
709        "r" => Some(include_str!("queries/r.types.scm")),
710        "d" => Some(include_str!("queries/d.types.scm")),
711        "objc" => Some(include_str!("queries/objc.types.scm")),
712        "vb" => Some(include_str!("queries/vb.types.scm")),
713        "groovy" => Some(include_str!("queries/groovy.types.scm")),
714        "ada" => Some(include_str!("queries/ada.types.scm")),
715        "agda" => Some(include_str!("queries/agda.types.scm")),
716        "elm" => Some(include_str!("queries/elm.types.scm")),
717        "idris" => Some(include_str!("queries/idris.types.scm")),
718        "lean" => Some(include_str!("queries/lean.types.scm")),
719        "php" => Some(include_str!("queries/php.types.scm")),
720        "powershell" => Some(include_str!("queries/powershell.types.scm")),
721        "rescript" => Some(include_str!("queries/rescript.types.scm")),
722        "verilog" => Some(include_str!("queries/verilog.types.scm")),
723        "vhdl" => Some(include_str!("queries/vhdl.types.scm")),
724        "sql" => Some(include_str!("queries/sql.types.scm")),
725        "hcl" => Some(include_str!("queries/hcl.types.scm")),
726        "glsl" => Some(include_str!("queries/glsl.types.scm")),
727        "hlsl" => Some(include_str!("queries/hlsl.types.scm")),
728        "clojure" => Some(include_str!("queries/clojure.types.scm")),
729        "commonlisp" => Some(include_str!("queries/commonlisp.types.scm")),
730        "elisp" => Some(include_str!("queries/elisp.types.scm")),
731        "javascript" => Some(include_str!("queries/javascript.types.scm")),
732        "lua" => Some(include_str!("queries/lua.types.scm")),
733        "scheme" => Some(include_str!("queries/scheme.types.scm")),
734        "graphql" => Some(include_str!("queries/graphql.types.scm")),
735        "nix" => Some(include_str!("queries/nix.types.scm")),
736        "starlark" => Some(include_str!("queries/starlark.types.scm")),
737        "matlab" => Some(include_str!("queries/matlab.types.scm")),
738        "tlaplus" => Some(include_str!("queries/tlaplus.types.scm")),
739        "typst" => Some(include_str!("queries/typst.types.scm")),
740        _ => None,
741    }
742}
743
744/// Return a bundled tags query for languages with built-in support.
745///
746/// Tags queries use the tree-sitter tags format (`@name.definition.*` and
747/// `@name.reference.*` captures) for symbol navigation. Sources are vendored from
748/// official tree-sitter grammar repositories (MIT licensed).
749fn bundled_tags_query(name: &str) -> Option<&'static str> {
750    match name {
751        "rust" => Some(include_str!("queries/rust.tags.scm")),
752        "python" => Some(include_str!("queries/python.tags.scm")),
753        "javascript" => Some(include_str!("queries/javascript.tags.scm")),
754        "typescript" => Some(include_str!("queries/typescript.tags.scm")),
755        "tsx" => Some(include_str!("queries/tsx.tags.scm")),
756        "go" => Some(include_str!("queries/go.tags.scm")),
757        "java" => Some(include_str!("queries/java.tags.scm")),
758        "c" => Some(include_str!("queries/c.tags.scm")),
759        "cpp" => Some(include_str!("queries/cpp.tags.scm")),
760        "ruby" => Some(include_str!("queries/ruby.tags.scm")),
761        "kotlin" => Some(include_str!("queries/kotlin.tags.scm")),
762        "scala" => Some(include_str!("queries/scala.tags.scm")),
763        "elixir" => Some(include_str!("queries/elixir.tags.scm")),
764        "swift" => Some(include_str!("queries/swift.tags.scm")),
765        "haskell" => Some(include_str!("queries/haskell.tags.scm")),
766        "dart" => Some(include_str!("queries/dart.tags.scm")),
767        "ocaml" => Some(include_str!("queries/ocaml.tags.scm")),
768        "fsharp" => Some(include_str!("queries/fsharp.tags.scm")),
769        "gleam" => Some(include_str!("queries/gleam.tags.scm")),
770        "zig" => Some(include_str!("queries/zig.tags.scm")),
771        "julia" => Some(include_str!("queries/julia.tags.scm")),
772        "erlang" => Some(include_str!("queries/erlang.tags.scm")),
773        "lua" => Some(include_str!("queries/lua.tags.scm")),
774        "php" => Some(include_str!("queries/php.tags.scm")),
775        "perl" => Some(include_str!("queries/perl.tags.scm")),
776        "r" => Some(include_str!("queries/r.tags.scm")),
777        "groovy" => Some(include_str!("queries/groovy.tags.scm")),
778        "c-sharp" => Some(include_str!("queries/c-sharp.tags.scm")),
779        "d" => Some(include_str!("queries/d.tags.scm")),
780        "graphql" => Some(include_str!("queries/graphql.tags.scm")),
781        "objc" => Some(include_str!("queries/objc.tags.scm")),
782        "vb" => Some(include_str!("queries/vb.tags.scm")),
783        "powershell" => Some(include_str!("queries/powershell.tags.scm")),
784        "clojure" => Some(include_str!("queries/clojure.tags.scm")),
785        "commonlisp" => Some(include_str!("queries/commonlisp.tags.scm")),
786        "scheme" => Some(include_str!("queries/scheme.tags.scm")),
787        "elisp" => Some(include_str!("queries/elisp.tags.scm")),
788        "bash" => Some(include_str!("queries/bash.tags.scm")),
789        "fish" => Some(include_str!("queries/fish.tags.scm")),
790        "zsh" => Some(include_str!("queries/zsh.tags.scm")),
791        "ada" => Some(include_str!("queries/ada.tags.scm")),
792        "idris" => Some(include_str!("queries/idris.tags.scm")),
793        "lean" => Some(include_str!("queries/lean.tags.scm")),
794        "rescript" => Some(include_str!("queries/rescript.tags.scm")),
795        "elm" => Some(include_str!("queries/elm.tags.scm")),
796        "markdown" => Some(include_str!("queries/markdown.tags.scm")),
797        "nix" => Some(include_str!("queries/nix.tags.scm")),
798        "prolog" => Some(include_str!("queries/prolog.tags.scm")),
799        "agda" => Some(include_str!("queries/agda.tags.scm")),
800        "awk" => Some(include_str!("queries/awk.tags.scm")),
801        "cmake" => Some(include_str!("queries/cmake.tags.scm")),
802        "glsl" => Some(include_str!("queries/glsl.tags.scm")),
803        "hcl" => Some(include_str!("queries/hcl.tags.scm")),
804        "hlsl" => Some(include_str!("queries/hlsl.tags.scm")),
805        "jq" => Some(include_str!("queries/jq.tags.scm")),
806        "matlab" => Some(include_str!("queries/matlab.tags.scm")),
807        "meson" => Some(include_str!("queries/meson.tags.scm")),
808        "nginx" => Some(include_str!("queries/nginx.tags.scm")),
809        "scss" => Some(include_str!("queries/scss.tags.scm")),
810        "sql" => Some(include_str!("queries/sql.tags.scm")),
811        "starlark" => Some(include_str!("queries/starlark.tags.scm")),
812        "svelte" => Some(include_str!("queries/svelte.tags.scm")),
813        "tlaplus" => Some(include_str!("queries/tlaplus.tags.scm")),
814        "typst" => Some(include_str!("queries/typst.tags.scm")),
815        "verilog" => Some(include_str!("queries/verilog.tags.scm")),
816        "vhdl" => Some(include_str!("queries/vhdl.tags.scm")),
817        "vim" => Some(include_str!("queries/vim.tags.scm")),
818        "vue" => Some(include_str!("queries/vue.tags.scm")),
819        "jinja2" => Some(include_str!("queries/jinja2.tags.scm")),
820        "json" => Some(include_str!("queries/json.tags.scm")),
821        "toml" => Some(include_str!("queries/toml.tags.scm")),
822        "yaml" => Some(include_str!("queries/yaml.tags.scm")),
823        "css" => Some(include_str!("queries/css.tags.scm")),
824        "html" => Some(include_str!("queries/html.tags.scm")),
825        "xml" => Some(include_str!("queries/xml.tags.scm")),
826        "thrift" => Some(include_str!("queries/thrift.tags.scm")),
827        "dockerfile" => Some(include_str!("queries/dockerfile.tags.scm")),
828        "caddy" => Some(include_str!("queries/caddy.tags.scm")),
829        _ => None,
830    }
831}
832
833/// Get the shared library extension for the current platform.
834fn grammar_extension() -> &'static str {
835    if cfg!(target_os = "macos") {
836        ".dylib"
837    } else if cfg!(target_os = "windows") {
838        ".dll"
839    } else {
840        ".so"
841    }
842}
843
844/// Return a bundled complexity query for a grammar, if available.
845///
846/// These are compiled into the binary so they work without external .scm files.
847/// External files in search paths take priority (for user customization).
848fn bundled_complexity_query(name: &str) -> Option<&'static str> {
849    match name {
850        "rust" => Some(include_str!("queries/rust.complexity.scm")),
851        "python" => Some(include_str!("queries/python.complexity.scm")),
852        "go" => Some(include_str!("queries/go.complexity.scm")),
853        "javascript" => Some(include_str!("queries/javascript.complexity.scm")),
854        "typescript" => Some(include_str!("queries/typescript.complexity.scm")),
855        "tsx" => Some(include_str!("queries/tsx.complexity.scm")),
856        "java" => Some(include_str!("queries/java.complexity.scm")),
857        "c" => Some(include_str!("queries/c.complexity.scm")),
858        "cpp" => Some(include_str!("queries/cpp.complexity.scm")),
859        "ruby" => Some(include_str!("queries/ruby.complexity.scm")),
860        "kotlin" => Some(include_str!("queries/kotlin.complexity.scm")),
861        "swift" => Some(include_str!("queries/swift.complexity.scm")),
862        "c-sharp" => Some(include_str!("queries/c-sharp.complexity.scm")),
863        "bash" => Some(include_str!("queries/bash.complexity.scm")),
864        "lua" => Some(include_str!("queries/lua.complexity.scm")),
865        "elixir" => Some(include_str!("queries/elixir.complexity.scm")),
866        "scala" => Some(include_str!("queries/scala.complexity.scm")),
867        "dart" => Some(include_str!("queries/dart.complexity.scm")),
868        "zig" => Some(include_str!("queries/zig.complexity.scm")),
869        "ocaml" => Some(include_str!("queries/ocaml.complexity.scm")),
870        "erlang" => Some(include_str!("queries/erlang.complexity.scm")),
871        "php" => Some(include_str!("queries/php.complexity.scm")),
872        "haskell" => Some(include_str!("queries/haskell.complexity.scm")),
873        "r" => Some(include_str!("queries/r.complexity.scm")),
874        "julia" => Some(include_str!("queries/julia.complexity.scm")),
875        "perl" => Some(include_str!("queries/perl.complexity.scm")),
876        "groovy" => Some(include_str!("queries/groovy.complexity.scm")),
877        "elm" => Some(include_str!("queries/elm.complexity.scm")),
878        "powershell" => Some(include_str!("queries/powershell.complexity.scm")),
879        "fish" => Some(include_str!("queries/fish.complexity.scm")),
880        "fsharp" => Some(include_str!("queries/fsharp.complexity.scm")),
881        "gleam" => Some(include_str!("queries/gleam.complexity.scm")),
882        "clojure" => Some(include_str!("queries/clojure.complexity.scm")),
883        "commonlisp" => Some(include_str!("queries/commonlisp.complexity.scm")),
884        "scheme" => Some(include_str!("queries/scheme.complexity.scm")),
885        "d" => Some(include_str!("queries/d.complexity.scm")),
886        "objc" => Some(include_str!("queries/objc.complexity.scm")),
887        "vb" => Some(include_str!("queries/vb.complexity.scm")),
888        "elisp" => Some(include_str!("queries/elisp.complexity.scm")),
889        "hcl" => Some(include_str!("queries/hcl.complexity.scm")),
890        "matlab" => Some(include_str!("queries/matlab.complexity.scm")),
891        "nix" => Some(include_str!("queries/nix.complexity.scm")),
892        "sql" => Some(include_str!("queries/sql.complexity.scm")),
893        "starlark" => Some(include_str!("queries/starlark.complexity.scm")),
894        "vim" => Some(include_str!("queries/vim.complexity.scm")),
895        "zsh" => Some(include_str!("queries/zsh.complexity.scm")),
896        "rescript" => Some(include_str!("queries/rescript.complexity.scm")),
897        "idris" => Some(include_str!("queries/idris.complexity.scm")),
898        "lean" => Some(include_str!("queries/lean.complexity.scm")),
899        "ada" => Some(include_str!("queries/ada.complexity.scm")),
900        "agda" => Some(include_str!("queries/agda.complexity.scm")),
901        "awk" => Some(include_str!("queries/awk.complexity.scm")),
902        "cmake" => Some(include_str!("queries/cmake.complexity.scm")),
903        "glsl" => Some(include_str!("queries/glsl.complexity.scm")),
904        "graphql" => Some(include_str!("queries/graphql.complexity.scm")),
905        "hlsl" => Some(include_str!("queries/hlsl.complexity.scm")),
906        "jq" => Some(include_str!("queries/jq.complexity.scm")),
907        "meson" => Some(include_str!("queries/meson.complexity.scm")),
908        "nginx" => Some(include_str!("queries/nginx.complexity.scm")),
909        "prolog" => Some(include_str!("queries/prolog.complexity.scm")),
910        "scss" => Some(include_str!("queries/scss.complexity.scm")),
911        "svelte" => Some(include_str!("queries/svelte.complexity.scm")),
912        "tlaplus" => Some(include_str!("queries/tlaplus.complexity.scm")),
913        "typst" => Some(include_str!("queries/typst.complexity.scm")),
914        "verilog" => Some(include_str!("queries/verilog.complexity.scm")),
915        "vhdl" => Some(include_str!("queries/vhdl.complexity.scm")),
916        "vue" => Some(include_str!("queries/vue.complexity.scm")),
917        "batch" => Some(include_str!("queries/batch.complexity.scm")),
918        "thrift" => Some(include_str!("queries/thrift.complexity.scm")),
919        "jinja2" => Some(include_str!("queries/jinja2.complexity.scm")),
920        _ => None,
921    }
922}
923
924/// Return a bundled calls query for a grammar, if available.
925fn bundled_calls_query(name: &str) -> Option<&'static str> {
926    match name {
927        "python" => Some(include_str!("queries/python.calls.scm")),
928        "rust" => Some(include_str!("queries/rust.calls.scm")),
929        "typescript" => Some(include_str!("queries/typescript.calls.scm")),
930        "tsx" => Some(include_str!("queries/tsx.calls.scm")),
931        "javascript" => Some(include_str!("queries/javascript.calls.scm")),
932        "java" => Some(include_str!("queries/java.calls.scm")),
933        "go" => Some(include_str!("queries/go.calls.scm")),
934        "c" => Some(include_str!("queries/c.calls.scm")),
935        "cpp" => Some(include_str!("queries/cpp.calls.scm")),
936        "ruby" => Some(include_str!("queries/ruby.calls.scm")),
937        "kotlin" => Some(include_str!("queries/kotlin.calls.scm")),
938        "swift" => Some(include_str!("queries/swift.calls.scm")),
939        "c-sharp" => Some(include_str!("queries/c-sharp.calls.scm")),
940        "bash" => Some(include_str!("queries/bash.calls.scm")),
941        "scala" => Some(include_str!("queries/scala.calls.scm")),
942        "elixir" => Some(include_str!("queries/elixir.calls.scm")),
943        "lua" => Some(include_str!("queries/lua.calls.scm")),
944        "dart" => Some(include_str!("queries/dart.calls.scm")),
945        "graphql" => Some(include_str!("queries/graphql.calls.scm")),
946        "ocaml" => Some(include_str!("queries/ocaml.calls.scm")),
947        "erlang" => Some(include_str!("queries/erlang.calls.scm")),
948        "zig" => Some(include_str!("queries/zig.calls.scm")),
949        "julia" => Some(include_str!("queries/julia.calls.scm")),
950        "r" => Some(include_str!("queries/r.calls.scm")),
951        "haskell" => Some(include_str!("queries/haskell.calls.scm")),
952        "php" => Some(include_str!("queries/php.calls.scm")),
953        "perl" => Some(include_str!("queries/perl.calls.scm")),
954        "fsharp" => Some(include_str!("queries/fsharp.calls.scm")),
955        "gleam" => Some(include_str!("queries/gleam.calls.scm")),
956        "groovy" => Some(include_str!("queries/groovy.calls.scm")),
957        "clojure" => Some(include_str!("queries/clojure.calls.scm")),
958        "d" => Some(include_str!("queries/d.calls.scm")),
959        "objc" => Some(include_str!("queries/objc.calls.scm")),
960        "elisp" => Some(include_str!("queries/elisp.calls.scm")),
961        "hcl" => Some(include_str!("queries/hcl.calls.scm")),
962        "matlab" => Some(include_str!("queries/matlab.calls.scm")),
963        "nix" => Some(include_str!("queries/nix.calls.scm")),
964        "starlark" => Some(include_str!("queries/starlark.calls.scm")),
965        "vim" => Some(include_str!("queries/vim.calls.scm")),
966        "zsh" => Some(include_str!("queries/zsh.calls.scm")),
967        "rescript" => Some(include_str!("queries/rescript.calls.scm")),
968        "prolog" => Some(include_str!("queries/prolog.calls.scm")),
969        "sql" => Some(include_str!("queries/sql.calls.scm")),
970        "ada" => Some(include_str!("queries/ada.calls.scm")),
971        "agda" => Some(include_str!("queries/agda.calls.scm")),
972        "awk" => Some(include_str!("queries/awk.calls.scm")),
973        "batch" => Some(include_str!("queries/batch.calls.scm")),
974        "cmake" => Some(include_str!("queries/cmake.calls.scm")),
975        "elm" => Some(include_str!("queries/elm.calls.scm")),
976        "fish" => Some(include_str!("queries/fish.calls.scm")),
977        "idris" => Some(include_str!("queries/idris.calls.scm")),
978        "lean" => Some(include_str!("queries/lean.calls.scm")),
979        "meson" => Some(include_str!("queries/meson.calls.scm")),
980        "powershell" => Some(include_str!("queries/powershell.calls.scm")),
981        "scheme" => Some(include_str!("queries/scheme.calls.scm")),
982        "thrift" => Some(include_str!("queries/thrift.calls.scm")),
983        "tlaplus" => Some(include_str!("queries/tlaplus.calls.scm")),
984        "verilog" => Some(include_str!("queries/verilog.calls.scm")),
985        "vhdl" => Some(include_str!("queries/vhdl.calls.scm")),
986        "vb" => Some(include_str!("queries/vb.calls.scm")),
987        "commonlisp" => Some(include_str!("queries/commonlisp.calls.scm")),
988        "scss" => Some(include_str!("queries/scss.calls.scm")),
989        "glsl" => Some(include_str!("queries/glsl.calls.scm")),
990        "hlsl" => Some(include_str!("queries/hlsl.calls.scm")),
991        "typst" => Some(include_str!("queries/typst.calls.scm")),
992        "svelte" => Some(include_str!("queries/svelte.calls.scm")),
993        "vue" => Some(include_str!("queries/vue.calls.scm")),
994        "jq" => Some(include_str!("queries/jq.calls.scm")),
995        "jinja2" => Some(include_str!("queries/jinja2.calls.scm")),
996        "nginx" => Some(include_str!("queries/nginx.calls.scm")),
997        _ => None,
998    }
999}
1000
1001/// Return a bundled imports query for a grammar, if available.
1002fn bundled_imports_query(name: &str) -> Option<&'static str> {
1003    match name {
1004        "python" => Some(include_str!("queries/python.imports.scm")),
1005        "javascript" => Some(include_str!("queries/javascript.imports.scm")),
1006        "go" => Some(include_str!("queries/go.imports.scm")),
1007        "lua" => Some(include_str!("queries/lua.imports.scm")),
1008        "rust" => Some(include_str!("queries/rust.imports.scm")),
1009        "typescript" => Some(include_str!("queries/typescript.imports.scm")),
1010        "tsx" => Some(include_str!("queries/tsx.imports.scm")),
1011        "java" => Some(include_str!("queries/java.imports.scm")),
1012        "kotlin" => Some(include_str!("queries/kotlin.imports.scm")),
1013        "c-sharp" => Some(include_str!("queries/c-sharp.imports.scm")),
1014        "ruby" => Some(include_str!("queries/ruby.imports.scm")),
1015        "swift" => Some(include_str!("queries/swift.imports.scm")),
1016        "scala" => Some(include_str!("queries/scala.imports.scm")),
1017        "elixir" => Some(include_str!("queries/elixir.imports.scm")),
1018        "dart" => Some(include_str!("queries/dart.imports.scm")),
1019        "php" => Some(include_str!("queries/php.imports.scm")),
1020        "c" => Some(include_str!("queries/c.imports.scm")),
1021        "cpp" => Some(include_str!("queries/cpp.imports.scm")),
1022        "bash" => Some(include_str!("queries/bash.imports.scm")),
1023        "zsh" => Some(include_str!("queries/zsh.imports.scm")),
1024        "fish" => Some(include_str!("queries/fish.imports.scm")),
1025        "perl" => Some(include_str!("queries/perl.imports.scm")),
1026        "r" => Some(include_str!("queries/r.imports.scm")),
1027        "haskell" => Some(include_str!("queries/haskell.imports.scm")),
1028        "ocaml" => Some(include_str!("queries/ocaml.imports.scm")),
1029        "fsharp" => Some(include_str!("queries/fsharp.imports.scm")),
1030        "erlang" => Some(include_str!("queries/erlang.imports.scm")),
1031        "gleam" => Some(include_str!("queries/gleam.imports.scm")),
1032        "zig" => Some(include_str!("queries/zig.imports.scm")),
1033        "julia" => Some(include_str!("queries/julia.imports.scm")),
1034        "groovy" => Some(include_str!("queries/groovy.imports.scm")),
1035        "clojure" => Some(include_str!("queries/clojure.imports.scm")),
1036        "commonlisp" => Some(include_str!("queries/commonlisp.imports.scm")),
1037        "scheme" => Some(include_str!("queries/scheme.imports.scm")),
1038        "elisp" => Some(include_str!("queries/elisp.imports.scm")),
1039        "d" => Some(include_str!("queries/d.imports.scm")),
1040        "objc" => Some(include_str!("queries/objc.imports.scm")),
1041        "vb" => Some(include_str!("queries/vb.imports.scm")),
1042        "powershell" => Some(include_str!("queries/powershell.imports.scm")),
1043        "vim" => Some(include_str!("queries/vim.imports.scm")),
1044        "matlab" => Some(include_str!("queries/matlab.imports.scm")),
1045        "nix" => Some(include_str!("queries/nix.imports.scm")),
1046        "starlark" => Some(include_str!("queries/starlark.imports.scm")),
1047        "rescript" => Some(include_str!("queries/rescript.imports.scm")),
1048        "idris" => Some(include_str!("queries/idris.imports.scm")),
1049        "ada" => Some(include_str!("queries/ada.imports.scm")),
1050        "agda" => Some(include_str!("queries/agda.imports.scm")),
1051        "asciidoc" => Some(include_str!("queries/asciidoc.imports.scm")),
1052        "caddy" => Some(include_str!("queries/caddy.imports.scm")),
1053        "capnp" => Some(include_str!("queries/capnp.imports.scm")),
1054        "cmake" => Some(include_str!("queries/cmake.imports.scm")),
1055        "devicetree" => Some(include_str!("queries/devicetree.imports.scm")),
1056        "dockerfile" => Some(include_str!("queries/dockerfile.imports.scm")),
1057        "elm" => Some(include_str!("queries/elm.imports.scm")),
1058        "hcl" => Some(include_str!("queries/hcl.imports.scm")),
1059        "hlsl" => Some(include_str!("queries/hlsl.imports.scm")),
1060        "jq" => Some(include_str!("queries/jq.imports.scm")),
1061        "lean" => Some(include_str!("queries/lean.imports.scm")),
1062        "meson" => Some(include_str!("queries/meson.imports.scm")),
1063        "nginx" => Some(include_str!("queries/nginx.imports.scm")),
1064        "ninja" => Some(include_str!("queries/ninja.imports.scm")),
1065        "prolog" => Some(include_str!("queries/prolog.imports.scm")),
1066        "awk" => Some(include_str!("queries/awk.imports.scm")),
1067        "css" => Some(include_str!("queries/css.imports.scm")),
1068        "glsl" => Some(include_str!("queries/glsl.imports.scm")),
1069        "html" => Some(include_str!("queries/html.imports.scm")),
1070        "jinja2" => Some(include_str!("queries/jinja2.imports.scm")),
1071        "scss" => Some(include_str!("queries/scss.imports.scm")),
1072        "thrift" => Some(include_str!("queries/thrift.imports.scm")),
1073        "tlaplus" => Some(include_str!("queries/tlaplus.imports.scm")),
1074        "typst" => Some(include_str!("queries/typst.imports.scm")),
1075        "verilog" => Some(include_str!("queries/verilog.imports.scm")),
1076        "vhdl" => Some(include_str!("queries/vhdl.imports.scm")),
1077        "wit" => Some(include_str!("queries/wit.imports.scm")),
1078        _ => None,
1079    }
1080}
1081
1082/// Return a bundled decorations query for a grammar, if available.
1083///
1084/// Uses `@decoration` captures for doc comments, attributes, decorators,
1085/// and annotations that immediately precede a definition.
1086fn bundled_decorations_query(name: &str) -> Option<&'static str> {
1087    match name {
1088        "rust" => Some(include_str!("queries/rust.decorations.scm")),
1089        "python" => Some(include_str!("queries/python.decorations.scm")),
1090        "javascript" => Some(include_str!("queries/javascript.decorations.scm")),
1091        "typescript" => Some(include_str!("queries/typescript.decorations.scm")),
1092        "tsx" => Some(include_str!("queries/tsx.decorations.scm")),
1093        "java" => Some(include_str!("queries/java.decorations.scm")),
1094        "kotlin" => Some(include_str!("queries/kotlin.decorations.scm")),
1095        "scala" => Some(include_str!("queries/scala.decorations.scm")),
1096        "c-sharp" => Some(include_str!("queries/c-sharp.decorations.scm")),
1097        "php" => Some(include_str!("queries/php.decorations.scm")),
1098        "swift" => Some(include_str!("queries/swift.decorations.scm")),
1099        "dart" => Some(include_str!("queries/dart.decorations.scm")),
1100        "ocaml" => Some(include_str!("queries/ocaml.decorations.scm")),
1101        "rescript" => Some(include_str!("queries/rescript.decorations.scm")),
1102        "fsharp" => Some(include_str!("queries/fsharp.decorations.scm")),
1103        "elixir" => Some(include_str!("queries/elixir.decorations.scm")),
1104        "erlang" => Some(include_str!("queries/erlang.decorations.scm")),
1105        "gleam" => Some(include_str!("queries/gleam.decorations.scm")),
1106        "lean" => Some(include_str!("queries/lean.decorations.scm")),
1107        "groovy" => Some(include_str!("queries/groovy.decorations.scm")),
1108        "vb" => Some(include_str!("queries/vb.decorations.scm")),
1109        "haskell" => Some(include_str!("queries/haskell.decorations.scm")),
1110        "go" => Some(include_str!("queries/go.decorations.scm")),
1111        "c" => Some(include_str!("queries/c.decorations.scm")),
1112        "cpp" => Some(include_str!("queries/cpp.decorations.scm")),
1113        "objc" => Some(include_str!("queries/objc.decorations.scm")),
1114        "ruby" => Some(include_str!("queries/ruby.decorations.scm")),
1115        "r" => Some(include_str!("queries/r.decorations.scm")),
1116        "lua" => Some(include_str!("queries/lua.decorations.scm")),
1117        "zig" => Some(include_str!("queries/zig.decorations.scm")),
1118        "idris" => Some(include_str!("queries/idris.decorations.scm")),
1119        "agda" => Some(include_str!("queries/agda.decorations.scm")),
1120        "elm" => Some(include_str!("queries/elm.decorations.scm")),
1121        "julia" => Some(include_str!("queries/julia.decorations.scm")),
1122        "perl" => Some(include_str!("queries/perl.decorations.scm")),
1123        "verilog" => Some(include_str!("queries/verilog.decorations.scm")),
1124        "vhdl" => Some(include_str!("queries/vhdl.decorations.scm")),
1125        "ada" => Some(include_str!("queries/ada.decorations.scm")),
1126        "capnp" => Some(include_str!("queries/capnp.decorations.scm")),
1127        "thrift" => Some(include_str!("queries/thrift.decorations.scm")),
1128        "graphql" => Some(include_str!("queries/graphql.decorations.scm")),
1129        "wit" => Some(include_str!("queries/wit.decorations.scm")),
1130        "clojure" => Some(include_str!("queries/clojure.decorations.scm")),
1131        "scheme" => Some(include_str!("queries/scheme.decorations.scm")),
1132        "prolog" => Some(include_str!("queries/prolog.decorations.scm")),
1133        _ => None,
1134    }
1135}
1136
1137/// Return a bundled test-regions query for a grammar, if available.
1138///
1139/// Captures `@test_region` for byte ranges of test-only source regions
1140/// (e.g. inline `#[cfg(test)] mod ...` blocks in Rust). Languages whose
1141/// test conventions are filename-based (e.g. Go's `*_test.go`) or where
1142/// no AST-level distinction exists return None and rely on path-based
1143/// excludes instead.
1144fn bundled_test_regions_query(name: &str) -> Option<&'static str> {
1145    match name {
1146        "rust" => Some(include_str!("queries/rust.test_regions.scm")),
1147        _ => None,
1148    }
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153    use super::*;
1154
1155    #[test]
1156    fn test_grammar_lib_name() {
1157        let name = grammar_lib_name("python");
1158        assert!(name.starts_with("python."));
1159    }
1160
1161    #[test]
1162    fn test_grammar_symbol_name() {
1163        assert_eq!(grammar_symbol_name("python"), "tree_sitter_python");
1164        assert_eq!(grammar_symbol_name("rust"), "tree_sitter_rust_orchard");
1165        assert_eq!(grammar_symbol_name("ssh-config"), "tree_sitter_ssh_config");
1166        assert_eq!(grammar_symbol_name("vb"), "tree_sitter_vb_dotnet");
1167    }
1168
1169    #[test]
1170    fn test_bundled_tags_queries() {
1171        for lang in &[
1172            "rust",
1173            "python",
1174            "javascript",
1175            "typescript",
1176            "tsx",
1177            "go",
1178            "java",
1179            "c",
1180            "cpp",
1181            "ruby",
1182            "kotlin",
1183            "scala",
1184            "elixir",
1185            "swift",
1186            "haskell",
1187            "dart",
1188            "ocaml",
1189            "fsharp",
1190            "gleam",
1191            "zig",
1192            "julia",
1193            "erlang",
1194            "lua",
1195            "php",
1196            "perl",
1197            "r",
1198            "groovy",
1199            "d",
1200            "objc",
1201            "vb",
1202            "powershell",
1203            "clojure",
1204            "commonlisp",
1205            "scheme",
1206            "elisp",
1207            "bash",
1208            "fish",
1209            "zsh",
1210            "ada",
1211            "idris",
1212            "lean",
1213            "rescript",
1214            "elm",
1215        ] {
1216            let query = bundled_tags_query(lang);
1217            assert!(query.is_some(), "Missing bundled tags query for {lang}");
1218            assert!(
1219                !query.unwrap().is_empty(),
1220                "Empty bundled tags query for {lang}"
1221            );
1222        }
1223    }
1224
1225    #[test]
1226    fn test_bundled_types_queries() {
1227        for lang in &[
1228            "rust",
1229            "python",
1230            "typescript",
1231            "tsx",
1232            "java",
1233            "go",
1234            "c",
1235            "cpp",
1236            "kotlin",
1237            "swift",
1238            "c-sharp",
1239            "scala",
1240            "haskell",
1241            "ruby",
1242            "dart",
1243            "elixir",
1244            "ocaml",
1245            "erlang",
1246            "zig",
1247            "fsharp",
1248            "gleam",
1249            "julia",
1250            "r",
1251            "d",
1252            "objc",
1253            "vb",
1254            "groovy",
1255            "ada",
1256            "agda",
1257            "elm",
1258            "idris",
1259            "lean",
1260            "php",
1261            "powershell",
1262            "rescript",
1263            "verilog",
1264            "vhdl",
1265            "sql",
1266            "hcl",
1267            "glsl",
1268            "hlsl",
1269            "clojure",
1270            "commonlisp",
1271            "elisp",
1272            "javascript",
1273            "lua",
1274            "scheme",
1275            "graphql",
1276            "nix",
1277            "starlark",
1278            "matlab",
1279            "tlaplus",
1280            "typst",
1281        ] {
1282            let query = bundled_types_query(lang);
1283            assert!(query.is_some(), "Missing bundled types query for {lang}");
1284            assert!(
1285                !query.unwrap().is_empty(),
1286                "Empty bundled types query for {lang}"
1287            );
1288        }
1289    }
1290
1291    #[test]
1292    fn test_bundled_complexity_queries() {
1293        for lang in &[
1294            "rust",
1295            "python",
1296            "go",
1297            "javascript",
1298            "typescript",
1299            "tsx",
1300            "java",
1301            "c",
1302            "cpp",
1303            "ruby",
1304            "kotlin",
1305            "swift",
1306            "c-sharp",
1307            "bash",
1308            "lua",
1309            "elixir",
1310            "scala",
1311            "dart",
1312            "zig",
1313            "ocaml",
1314            "erlang",
1315            "php",
1316            "haskell",
1317            "r",
1318            "julia",
1319            "perl",
1320            "groovy",
1321            "elm",
1322            "powershell",
1323            "fish",
1324            "fsharp",
1325            "gleam",
1326            "clojure",
1327            "commonlisp",
1328            "scheme",
1329            "d",
1330            "objc",
1331            "vb",
1332            "elisp",
1333            "hcl",
1334            "matlab",
1335            "nix",
1336            "sql",
1337            "starlark",
1338            "vim",
1339            "zsh",
1340            "rescript",
1341            "idris",
1342            "lean",
1343        ] {
1344            let query = bundled_complexity_query(lang);
1345            assert!(
1346                query.is_some(),
1347                "Missing bundled complexity query for {lang}"
1348            );
1349            assert!(
1350                !query.unwrap().is_empty(),
1351                "Empty bundled complexity query for {lang}"
1352            );
1353        }
1354    }
1355
1356    #[test]
1357    fn test_bundled_calls_queries() {
1358        for lang in &[
1359            "python",
1360            "rust",
1361            "typescript",
1362            "tsx",
1363            "javascript",
1364            "java",
1365            "go",
1366            "c",
1367            "cpp",
1368            "ruby",
1369            "kotlin",
1370            "swift",
1371            "c-sharp",
1372            "bash",
1373            "scala",
1374            "elixir",
1375            "lua",
1376            "dart",
1377            "ocaml",
1378            "erlang",
1379            "zig",
1380            "julia",
1381            "r",
1382            "haskell",
1383            "php",
1384            "perl",
1385            "fsharp",
1386            "gleam",
1387            "groovy",
1388            "clojure",
1389            "d",
1390            "objc",
1391            "elisp",
1392            "hcl",
1393            "matlab",
1394            "nix",
1395            "starlark",
1396            "vim",
1397            "zsh",
1398            "rescript",
1399            "prolog",
1400            "sql",
1401            "ada",
1402            "agda",
1403            "awk",
1404            "batch",
1405            "cmake",
1406            "elm",
1407            "fish",
1408            "idris",
1409            "lean",
1410            "meson",
1411            "powershell",
1412            "scheme",
1413            "thrift",
1414            "tlaplus",
1415            "verilog",
1416            "vhdl",
1417            "vb",
1418            "commonlisp",
1419            "scss",
1420            "jinja2",
1421            "nginx",
1422        ] {
1423            let query = bundled_calls_query(lang);
1424            assert!(query.is_some(), "Missing bundled calls query for {lang}");
1425            assert!(
1426                !query.unwrap().is_empty(),
1427                "Empty bundled calls query for {lang}"
1428            );
1429        }
1430    }
1431
1432    #[test]
1433    fn test_bundled_imports_queries() {
1434        for lang in &[
1435            "awk",
1436            "python",
1437            "javascript",
1438            "go",
1439            "lua",
1440            "rust",
1441            "typescript",
1442            "tsx",
1443            "java",
1444            "kotlin",
1445            "c-sharp",
1446            "ruby",
1447            "swift",
1448            "scala",
1449            "elixir",
1450            "dart",
1451            "php",
1452            "c",
1453            "cpp",
1454            "bash",
1455            "zsh",
1456            "fish",
1457            "perl",
1458            "r",
1459            "haskell",
1460            "ocaml",
1461            "fsharp",
1462            "erlang",
1463            "gleam",
1464            "zig",
1465            "julia",
1466            "groovy",
1467            "clojure",
1468            "commonlisp",
1469            "scheme",
1470            "elisp",
1471            "d",
1472            "objc",
1473            "vb",
1474            "powershell",
1475            "vim",
1476            "matlab",
1477            "nix",
1478            "starlark",
1479            "rescript",
1480            "idris",
1481            "ada",
1482            "agda",
1483            "asciidoc",
1484            "caddy",
1485            "capnp",
1486            "cmake",
1487            "devicetree",
1488            "dockerfile",
1489            "elm",
1490            "hcl",
1491            "hlsl",
1492            "jq",
1493            "lean",
1494            "meson",
1495            "nginx",
1496            "ninja",
1497            "prolog",
1498            "css",
1499            "glsl",
1500            "html",
1501            "jinja2",
1502            "scss",
1503            "thrift",
1504            "tlaplus",
1505            "typst",
1506            "verilog",
1507            "vhdl",
1508            "wit",
1509        ] {
1510            let query = bundled_imports_query(lang);
1511            assert!(query.is_some(), "Missing bundled imports query for {lang}");
1512            assert!(
1513                !query.unwrap().is_empty(),
1514                "Empty bundled imports query for {lang}"
1515            );
1516        }
1517    }
1518
1519    #[test]
1520    fn test_get_imports_returns_bundled() {
1521        let loader = GrammarLoader::with_paths(vec![]);
1522        assert!(loader.get_imports("awk").is_some());
1523        assert!(loader.get_imports("python").is_some());
1524        assert!(loader.get_imports("javascript").is_some());
1525        assert!(loader.get_imports("go").is_some());
1526        assert!(loader.get_imports("lua").is_some());
1527        assert!(loader.get_imports("rust").is_some());
1528        assert!(loader.get_imports("typescript").is_some());
1529        assert!(loader.get_imports("tsx").is_some());
1530        assert!(loader.get_imports("java").is_some());
1531        assert!(loader.get_imports("kotlin").is_some());
1532        assert!(loader.get_imports("c-sharp").is_some());
1533        assert!(loader.get_imports("ruby").is_some());
1534        assert!(loader.get_imports("swift").is_some());
1535        assert!(loader.get_imports("scala").is_some());
1536        assert!(loader.get_imports("elixir").is_some());
1537        assert!(loader.get_imports("dart").is_some());
1538        assert!(loader.get_imports("php").is_some());
1539        assert!(loader.get_imports("c").is_some());
1540        assert!(loader.get_imports("cpp").is_some());
1541        assert!(loader.get_imports("bash").is_some());
1542        assert!(loader.get_imports("zsh").is_some());
1543        assert!(loader.get_imports("fish").is_some());
1544        assert!(loader.get_imports("perl").is_some());
1545        assert!(loader.get_imports("r").is_some());
1546        assert!(loader.get_imports("haskell").is_some());
1547        assert!(loader.get_imports("ocaml").is_some());
1548        assert!(loader.get_imports("fsharp").is_some());
1549        assert!(loader.get_imports("erlang").is_some());
1550        assert!(loader.get_imports("gleam").is_some());
1551        assert!(loader.get_imports("zig").is_some());
1552        assert!(loader.get_imports("julia").is_some());
1553        assert!(loader.get_imports("groovy").is_some());
1554        assert!(loader.get_imports("clojure").is_some());
1555        assert!(loader.get_imports("commonlisp").is_some());
1556        assert!(loader.get_imports("scheme").is_some());
1557        assert!(loader.get_imports("elisp").is_some());
1558        assert!(loader.get_imports("d").is_some());
1559        assert!(loader.get_imports("objc").is_some());
1560        assert!(loader.get_imports("vb").is_some());
1561        assert!(loader.get_imports("powershell").is_some());
1562        assert!(loader.get_imports("vim").is_some());
1563        assert!(loader.get_imports("matlab").is_some());
1564        assert!(loader.get_imports("nix").is_some());
1565        assert!(loader.get_imports("starlark").is_some());
1566        assert!(loader.get_imports("rescript").is_some());
1567        assert!(loader.get_imports("idris").is_some());
1568        assert!(loader.get_imports("ada").is_some());
1569        assert!(loader.get_imports("agda").is_some());
1570        assert!(loader.get_imports("asciidoc").is_some());
1571        assert!(loader.get_imports("caddy").is_some());
1572        assert!(loader.get_imports("capnp").is_some());
1573        assert!(loader.get_imports("cmake").is_some());
1574        assert!(loader.get_imports("devicetree").is_some());
1575        assert!(loader.get_imports("dockerfile").is_some());
1576        assert!(loader.get_imports("elm").is_some());
1577        assert!(loader.get_imports("hcl").is_some());
1578        assert!(loader.get_imports("hlsl").is_some());
1579        assert!(loader.get_imports("jq").is_some());
1580        assert!(loader.get_imports("lean").is_some());
1581        assert!(loader.get_imports("meson").is_some());
1582        assert!(loader.get_imports("nginx").is_some());
1583        assert!(loader.get_imports("ninja").is_some());
1584        assert!(loader.get_imports("prolog").is_some());
1585        assert!(loader.get_imports("css").is_some());
1586        assert!(loader.get_imports("glsl").is_some());
1587        assert!(loader.get_imports("html").is_some());
1588        assert!(loader.get_imports("jinja2").is_some());
1589        assert!(loader.get_imports("scss").is_some());
1590        assert!(loader.get_imports("thrift").is_some());
1591        assert!(loader.get_imports("tlaplus").is_some());
1592        assert!(loader.get_imports("typst").is_some());
1593        assert!(loader.get_imports("verilog").is_some());
1594        assert!(loader.get_imports("vhdl").is_some());
1595        assert!(loader.get_imports("wit").is_some());
1596        assert!(loader.get_imports("unknown-lang-xyz").is_none());
1597    }
1598
1599    #[test]
1600    fn test_get_tags_returns_bundled() {
1601        let loader = GrammarLoader::with_paths(vec![]);
1602        assert!(loader.get_tags("rust").is_some());
1603        assert!(loader.get_tags("python").is_some());
1604        assert!(loader.get_tags("go").is_some());
1605        assert!(loader.get_tags("unknown-lang-xyz").is_none());
1606    }
1607
1608    #[test]
1609    fn test_tags_queries_compile() {
1610        let loader = GrammarLoader::new();
1611        let langs = [
1612            "zig",
1613            "clojure",
1614            "scheme",
1615            "nix",
1616            "prolog",
1617            "toml",
1618            "json",
1619            "yaml",
1620            "css",
1621            "html",
1622            "xml",
1623            "thrift",
1624            "dockerfile",
1625            "caddy",
1626        ];
1627        for lang in langs {
1628            let tags = loader.get_tags(lang);
1629            assert!(
1630                tags.is_some(),
1631                "{lang}: no tags query found (missing from bundled_tags_query)"
1632            );
1633            let tags_str = tags.unwrap();
1634            let grammar = loader.get(lang).ok();
1635            if grammar.is_none() {
1636                eprintln!("{lang}: grammar .so not found, skipping compilation check");
1637                continue;
1638            }
1639            let result = tree_sitter::Query::new(&grammar.unwrap(), &tags_str);
1640            assert!(
1641                result.is_ok(),
1642                "{lang}: tags query compilation failed: {:?}",
1643                result.err()
1644            );
1645        }
1646    }
1647
1648    #[test]
1649    fn test_scheme_node_kinds() {
1650        let loader = GrammarLoader::new();
1651        let grammar = loader.get("scheme").ok();
1652        if grammar.is_none() {
1653            eprintln!("scheme: grammar .so not found or load failed");
1654            return;
1655        }
1656        eprintln!("scheme: grammar loaded ok");
1657        let g = grammar.unwrap();
1658        // Test which node names compile in queries against the installed grammar
1659        let test_cases = [
1660            ("list", "(list) @x"),
1661            ("symbol", "(symbol) @x"),
1662            ("named_node", "(named_node) @x"),
1663            ("identifier", "(identifier) @x"),
1664        ];
1665        for (name, query_str) in test_cases {
1666            match tree_sitter::Query::new(&g, query_str) {
1667                Ok(_) => eprintln!("scheme node '{name}': valid"),
1668                Err(e) => eprintln!("scheme node '{name}': INVALID - {e:?}"),
1669            }
1670        }
1671        // Now test parsing the fixture
1672        use tree_sitter::Parser;
1673        let src = "(define (add a b) (+ a b))\n(define (multiply a b) (* a b))\n";
1674        let mut parser = Parser::new();
1675        parser.set_language(&g).unwrap();
1676        let tree = parser.parse(src, None).unwrap();
1677        eprintln!("scheme sexp: {}", tree.root_node().to_sexp());
1678        // Walk the tree and print node kinds
1679        fn walk(node: tree_sitter::Node, depth: usize) {
1680            let prefix = "  ".repeat(depth);
1681            eprintln!(
1682                "{prefix}kind={} named={} text_len={}",
1683                node.kind(),
1684                node.is_named(),
1685                node.byte_range().len()
1686            );
1687            let mut cursor = node.walk();
1688            for child in node.children(&mut cursor) {
1689                walk(child, depth + 1);
1690            }
1691        }
1692        walk(tree.root_node(), 0);
1693    }
1694
1695    #[test]
1696    fn test_get_types_returns_bundled() {
1697        let loader = GrammarLoader::with_paths(vec![]);
1698        assert!(loader.get_types("rust").is_some());
1699        assert!(loader.get_types("python").is_some());
1700        assert!(loader.get_types("java").is_some());
1701        assert!(loader.get_types("go").is_some());
1702        assert!(loader.get_types("c").is_some());
1703        assert!(loader.get_types("cpp").is_some());
1704        assert!(loader.get_types("kotlin").is_some());
1705        assert!(loader.get_types("swift").is_some());
1706        assert!(loader.get_types("c-sharp").is_some());
1707        assert!(loader.get_types("unknown-lang-xyz").is_none());
1708    }
1709
1710    #[test]
1711    fn test_get_calls_returns_bundled() {
1712        let loader = GrammarLoader::with_paths(vec![]);
1713        assert!(loader.get_calls("rust").is_some());
1714        assert!(loader.get_calls("python").is_some());
1715        assert!(loader.get_calls("go").is_some());
1716        assert!(loader.get_calls("c").is_some());
1717        assert!(loader.get_calls("cpp").is_some());
1718        assert!(loader.get_calls("ruby").is_some());
1719        assert!(loader.get_calls("kotlin").is_some());
1720        assert!(loader.get_calls("swift").is_some());
1721        assert!(loader.get_calls("c-sharp").is_some());
1722        assert!(loader.get_calls("bash").is_some());
1723        assert!(loader.get_calls("unknown-lang-xyz").is_none());
1724    }
1725
1726    #[test]
1727    fn test_load_from_env() {
1728        // Set up env var pointing to target/grammars
1729        let grammar_path = std::env::current_dir().unwrap().join("target/grammars");
1730
1731        if !grammar_path.exists() {
1732            eprintln!("Skipping: run `cargo xtask build-grammars` first");
1733            return;
1734        }
1735
1736        // SAFETY: This is a test that runs single-threaded
1737        unsafe {
1738            std::env::set_var("NORMALIZE_GRAMMAR_PATH", grammar_path.to_str().unwrap());
1739        }
1740
1741        let loader = GrammarLoader::new();
1742
1743        // Should load python from .so
1744        let ext = grammar_extension();
1745        if grammar_path.join(format!("python{ext}")).exists() {
1746            let lang = loader.get("python").ok();
1747            assert!(lang.is_some(), "Failed to load python grammar");
1748        }
1749
1750        // Clean up
1751        // SAFETY: This is a test that runs single-threaded
1752        unsafe {
1753            std::env::remove_var("NORMALIZE_GRAMMAR_PATH");
1754        }
1755    }
1756}