Skip to main content

tree_sitter_loader/
loader.rs

1#![cfg_attr(not(any(test, doctest)), doc = include_str!("../README.md"))]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4#[cfg(unix)]
5use std::fmt::Write as _;
6#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
7use std::ops::Range;
8use std::{
9    collections::HashMap,
10    env, fs,
11    hash::{Hash as _, Hasher as _},
12    io::{BufRead, BufReader},
13    marker::PhantomData,
14    mem,
15    path::{Path, PathBuf},
16    process::Command,
17    sync::Mutex,
18    time::{Duration, Instant, SystemTime},
19};
20
21use etcetera::BaseStrategy as _;
22use libloading::{Library, Symbol};
23use log::{error, info, warn};
24use once_cell::sync::OnceCell;
25use regex::{Regex, RegexBuilder};
26use semver::Version;
27use serde::{Deserialize, Deserializer, Serialize};
28use thiserror::Error;
29use tree_sitter::Language;
30#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
31use tree_sitter::QueryError;
32#[cfg(feature = "tree-sitter-highlight")]
33use tree_sitter::QueryErrorKind;
34#[cfg(feature = "wasm")]
35use tree_sitter::WasmError;
36#[cfg(feature = "tree-sitter-highlight")]
37use tree_sitter_highlight::HighlightConfiguration;
38#[cfg(feature = "tree-sitter-tags")]
39use tree_sitter_tags::{Error as TagsError, TagsConfiguration};
40
41static WASM_TOOL_LOCK: Mutex<()> = Mutex::new(());
42
43const WASI_SDK_VERSION: &str = include_str!("../wasi-sdk-version").trim_ascii();
44const BINARYEN_VERSION: &str = include_str!("../binaryen-version").trim_ascii();
45
46#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
47const ARCH_OS: Result<&str, LoaderError> = Ok("arm64-macos");
48#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
49const ARCH_OS: Result<&str, LoaderError> = Ok("x86_64-macos");
50#[cfg(all(
51    target_os = "macos",
52    not(any(target_arch = "aarch64", target_arch = "x86_64"))
53))]
54const ARCH_OS: Result<&str, LoaderError> = Err(LoaderError::WasiSDKPlatform);
55
56#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
57const ARCH_OS: Result<&str, LoaderError> = Ok("arm64-windows");
58#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
59const ARCH_OS: Result<&str, LoaderError> = Ok("x86_64-windows");
60#[cfg(all(
61    target_os = "windows",
62    not(any(target_arch = "aarch64", target_arch = "x86_64"))
63))]
64const ARCH_OS: Result<&str, LoaderError> = Err(LoaderError::WasiSDKPlatform);
65
66#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
67const ARCH_OS: Result<&str, LoaderError> = Ok("arm64-linux");
68#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
69const ARCH_OS: Result<&str, LoaderError> = Ok("x86_64-linux");
70#[cfg(all(
71    target_os = "linux",
72    not(any(target_arch = "aarch64", target_arch = "x86_64"))
73))]
74const ARCH_OS: Result<&str, LoaderError> = Err(LoaderError::WasiSDKPlatform);
75
76#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
77const ARCH_OS: Result<&str, LoaderError> = Err(LoaderError::WasiSDKPlatform);
78
79pub type LoaderResult<T> = Result<T, LoaderError>;
80
81#[derive(Debug, Error)]
82pub enum LoaderError {
83    #[error(transparent)]
84    Compiler(CompilerError),
85    #[error("Parser compilation failed.\nStdout: {0}\nStderr: {1}")]
86    Compilation(String, String),
87    #[error(
88        "Lock file '{0}' appears stale\nIf there isn't another concurrent tree-sitter instance, remove it"
89    )]
90    LockFileTimeout(PathBuf),
91    #[error("Failed to execute curl for {0} -- {1}")]
92    Curl(String, std::io::Error),
93    #[error("Failed to load language in current directory:\n{0}")]
94    CurrentDirectoryLoad(Box<Self>),
95    #[error("External file path {0} is outside of parser directory {1}")]
96    ExternalFile(String, String),
97    #[error("Failed to extract archive {0} to {1}")]
98    Extraction(String, String),
99    #[error("Failed to load language for file name {0}:\n{1}")]
100    FileNameLoad(String, Box<Self>),
101    #[error("Failed to parse the language name from grammar.json at {0}")]
102    GrammarJSON(String),
103    #[error(transparent)]
104    HomeDir(#[from] etcetera::HomeDirError),
105    #[error(transparent)]
106    IO(IoError),
107    #[error(transparent)]
108    Library(LibraryError),
109    #[error("Failed to compare binary and source timestamps:\n{0}")]
110    ModifiedTime(Box<Self>),
111    #[error("No language found")]
112    NoLanguage,
113    #[error(transparent)]
114    Query(LoaderQueryError),
115    #[error("Failed to load language for scope '{0}':\n{1}")]
116    ScopeLoad(String, Box<Self>),
117    #[error(transparent)]
118    Serialization(#[from] serde_json::Error),
119    #[error(transparent)]
120    Symbol(SymbolError),
121    #[error(transparent)]
122    Tags(#[from] TagsError),
123    #[error("Failed to execute tar for {0} -- {1}")]
124    Tar(String, std::io::Error),
125    #[error("Unknown scope '{0}'")]
126    UnknownScope(String),
127    #[error("Failed to download {tool} from {url}")]
128    WasmToolDownload { tool: &'static str, url: String },
129    #[error(transparent)]
130    WasmTool(#[from] WasmToolError),
131    #[error("Unsupported platform for wasi-sdk")]
132    WasiSDKPlatform,
133    #[cfg(feature = "wasm")]
134    #[error(transparent)]
135    Wasm(#[from] WasmError),
136    #[error("Failed to run wasi-sdk clang -- {0}")]
137    WasmCompiler(std::io::Error),
138    #[error("Failed to run wasm-opt -- {0}")]
139    WasmOptimizer(std::io::Error),
140    #[error("wasi-sdk clang command failed: {0}")]
141    WasmCompilation(String),
142    #[error("wasm-opt command failed: {0}")]
143    WasmOptimization(String),
144}
145
146#[derive(Debug, Error)]
147pub struct CompilerError {
148    pub error: std::io::Error,
149    pub command: Box<Command>,
150}
151
152impl std::fmt::Display for CompilerError {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        write!(
155            f,
156            "Failed to execute the C compiler with the following command:\n{:?}\nError: {}",
157            *self.command, self.error
158        )?;
159        Ok(())
160    }
161}
162
163#[derive(Debug, Error)]
164pub struct IoError {
165    pub error: std::io::Error,
166    pub path: Option<PathBuf>,
167}
168
169impl IoError {
170    fn new(error: std::io::Error, path: Option<&Path>) -> Self {
171        Self {
172            error,
173            path: path.map(Path::to_path_buf),
174        }
175    }
176}
177
178impl std::fmt::Display for IoError {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        write!(f, "{}", self.error)?;
181        if let Some(ref path) = self.path {
182            write!(f, " ({})", path.display())?;
183        }
184        Ok(())
185    }
186}
187
188#[derive(Debug, Error)]
189pub struct LibraryError {
190    pub error: libloading::Error,
191    pub path: String,
192}
193
194impl std::fmt::Display for LibraryError {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        write!(
197            f,
198            "Error opening dynamic library {} -- {}",
199            self.path, self.error
200        )?;
201        Ok(())
202    }
203}
204
205#[derive(Debug, Error)]
206pub struct LoaderQueryError {
207    pub error: QueryError,
208    pub file: Option<String>,
209}
210
211impl std::fmt::Display for LoaderQueryError {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        if let Some(ref path) = self.file {
214            writeln!(f, "Error in query file {path}:")?;
215        }
216        write!(f, "{}", self.error)?;
217        Ok(())
218    }
219}
220
221#[derive(Debug, Error)]
222pub struct SymbolError {
223    pub error: libloading::Error,
224    pub symbol_name: String,
225    pub path: String,
226}
227
228impl std::fmt::Display for SymbolError {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        write!(
231            f,
232            "Failed to load symbol {} from {} -- {}",
233            self.symbol_name, self.path, self.error
234        )?;
235        Ok(())
236    }
237}
238
239#[derive(Debug, Error)]
240pub struct WasmToolError {
241    pub exe: &'static str,
242    pub toolchain: &'static str,
243    pub tool_dir: String,
244    pub possible_executables: Vec<&'static str>,
245    pub download: bool,
246}
247
248impl std::fmt::Display for WasmToolError {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        if self.download {
251            write!(
252                f,
253                "Failed to find {} executable in downloaded {} at '{}'.",
254                self.exe, self.toolchain, self.tool_dir
255            )?;
256        } else {
257            let toolchain_upper = self.toolchain.replace('-', "_").to_ascii_uppercase();
258            write!(
259                f,
260                "TREE_SITTER_{toolchain_upper}_PATH is set to '{}', but no clang executable found in 'bin/' directory.",
261                self.tool_dir
262            )?;
263        }
264
265        let possible_exes = self.possible_executables.join(", ");
266        write!(f, " Looked for: {possible_exes}.")?;
267
268        Ok(())
269    }
270}
271
272pub const DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME: &str = "highlights.scm";
273
274pub const DEFAULT_INJECTIONS_QUERY_FILE_NAME: &str = "injections.scm";
275
276pub const DEFAULT_LOCALS_QUERY_FILE_NAME: &str = "locals.scm";
277
278pub const DEFAULT_TAGS_QUERY_FILE_NAME: &str = "tags.scm";
279
280#[derive(Default, Deserialize, Serialize)]
281pub struct Config {
282    #[serde(default)]
283    #[serde(
284        rename = "parser-directories",
285        deserialize_with = "deserialize_parser_directories"
286    )]
287    pub parser_directories: Vec<PathBuf>,
288}
289
290#[derive(Serialize, Deserialize, Clone, Default)]
291#[serde(untagged)]
292pub enum PathsJSON {
293    #[default]
294    Empty,
295    Single(PathBuf),
296    Multiple(Vec<PathBuf>),
297}
298
299impl PathsJSON {
300    fn into_vec(self) -> Option<Vec<PathBuf>> {
301        match self {
302            Self::Empty => None,
303            Self::Single(s) => Some(vec![s]),
304            Self::Multiple(s) => Some(s),
305        }
306    }
307
308    const fn is_empty(&self) -> bool {
309        matches!(self, Self::Empty)
310    }
311
312    /// Represent this set of paths as a string that can be included in templates
313    #[must_use]
314    pub fn to_variable_value<'a>(&'a self, default: &'a PathBuf) -> &'a str {
315        match self {
316            Self::Empty => Some(default),
317            Self::Single(path_buf) => Some(path_buf),
318            Self::Multiple(paths) => paths.first(),
319        }
320        .map_or("", |path| path.as_os_str().to_str().unwrap_or(""))
321    }
322}
323
324#[derive(Serialize, Deserialize, Clone)]
325#[serde(untagged)]
326pub enum PackageJSONAuthor {
327    String(String),
328    Object {
329        name: String,
330        email: Option<String>,
331        url: Option<String>,
332    },
333}
334
335#[derive(Serialize, Deserialize, Clone)]
336#[serde(untagged)]
337pub enum PackageJSONRepository {
338    String(String),
339    Object { url: String },
340}
341
342#[derive(Serialize, Deserialize)]
343pub struct PackageJSON {
344    pub name: String,
345    pub version: Version,
346    pub description: Option<String>,
347    pub author: Option<PackageJSONAuthor>,
348    pub maintainers: Option<Vec<PackageJSONAuthor>>,
349    pub license: Option<String>,
350    pub repository: Option<PackageJSONRepository>,
351    #[serde(default)]
352    #[serde(rename = "tree-sitter", skip_serializing_if = "Option::is_none")]
353    pub tree_sitter: Option<Vec<LanguageConfigurationJSON>>,
354}
355
356fn default_path() -> PathBuf {
357    PathBuf::from(".")
358}
359
360#[derive(Serialize, Deserialize, Clone)]
361#[serde(rename_all = "kebab-case")]
362pub struct LanguageConfigurationJSON {
363    #[serde(default = "default_path")]
364    pub path: PathBuf,
365    pub scope: Option<String>,
366    pub file_types: Option<Vec<String>>,
367    pub content_regex: Option<String>,
368    pub first_line_regex: Option<String>,
369    pub injection_regex: Option<String>,
370    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
371    pub highlights: PathsJSON,
372    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
373    pub injections: PathsJSON,
374    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
375    pub locals: PathsJSON,
376    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
377    pub tags: PathsJSON,
378    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
379    pub external_files: PathsJSON,
380}
381
382#[derive(Serialize, Deserialize)]
383#[serde(rename_all = "kebab-case")]
384pub struct TreeSitterJSON {
385    #[serde(rename = "$schema")]
386    pub schema: Option<String>,
387    pub grammars: Vec<Grammar>,
388    pub metadata: Metadata,
389    #[serde(default)]
390    pub bindings: Bindings,
391}
392
393impl TreeSitterJSON {
394    pub fn from_file(path: &Path) -> LoaderResult<Self> {
395        let path = path.join("tree-sitter.json");
396        Ok(serde_json::from_str(&fs::read_to_string(&path).map_err(
397            |e| LoaderError::IO(IoError::new(e, Some(path.as_path()))),
398        )?)?)
399    }
400
401    #[must_use]
402    pub const fn has_multiple_language_configs(&self) -> bool {
403        self.grammars.len() > 1
404    }
405}
406
407#[derive(Serialize, Deserialize)]
408#[serde(rename_all = "kebab-case")]
409pub struct Grammar {
410    pub name: String,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub camelcase: Option<String>,
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub title: Option<String>,
415    pub scope: String,
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub path: Option<PathBuf>,
418    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
419    pub external_files: PathsJSON,
420    pub file_types: Option<Vec<String>>,
421    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
422    pub highlights: PathsJSON,
423    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
424    pub injections: PathsJSON,
425    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
426    pub locals: PathsJSON,
427    #[serde(default, skip_serializing_if = "PathsJSON::is_empty")]
428    pub tags: PathsJSON,
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub injection_regex: Option<String>,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub first_line_regex: Option<String>,
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub content_regex: Option<String>,
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub class_name: Option<String>,
437}
438
439#[derive(Serialize, Deserialize)]
440pub struct Metadata {
441    pub version: Version,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub license: Option<String>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub description: Option<String>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub authors: Option<Vec<Author>>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub links: Option<Links>,
450    #[serde(skip)]
451    pub namespace: Option<String>,
452}
453
454#[derive(Serialize, Deserialize)]
455pub struct Author {
456    pub name: String,
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub email: Option<String>,
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub url: Option<String>,
461}
462
463#[derive(Serialize, Deserialize)]
464pub struct Links {
465    pub repository: String,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub funding: Option<String>,
468}
469
470#[derive(Serialize, Deserialize, Clone)]
471#[serde(default)]
472pub struct Bindings {
473    pub c: bool,
474    pub go: bool,
475    pub java: bool,
476    #[serde(skip)]
477    pub kotlin: bool,
478    pub node: bool,
479    pub python: bool,
480    pub rust: bool,
481    pub swift: bool,
482    pub zig: bool,
483}
484
485impl Bindings {
486    /// return available languages and its default enabled state.
487    #[must_use]
488    pub const fn languages(&self) -> [(&'static str, bool); 8] {
489        [
490            ("c", true),
491            ("go", true),
492            ("java", false),
493            // Comment out Kotlin until the bindings are actually available.
494            // ("kotlin", false),
495            ("node", true),
496            ("python", true),
497            ("rust", true),
498            ("swift", true),
499            ("zig", false),
500        ]
501    }
502
503    /// construct Bindings from a language list. If a language isn't supported, its name will be put on the error part.
504    pub fn with_enabled_languages<'a, I>(languages: I) -> Result<Self, &'a str>
505    where
506        I: Iterator<Item = &'a str>,
507    {
508        let mut out = Self {
509            c: false,
510            go: false,
511            java: false,
512            kotlin: false,
513            node: false,
514            python: false,
515            rust: false,
516            swift: false,
517            zig: false,
518        };
519
520        for v in languages {
521            match v {
522                "c" => out.c = true,
523                "go" => out.go = true,
524                "java" => out.java = true,
525                // Comment out Kotlin until the bindings are actually available.
526                // "kotlin" => out.kotlin = true,
527                "node" => out.node = true,
528                "python" => out.python = true,
529                "rust" => out.rust = true,
530                "swift" => out.swift = true,
531                "zig" => out.zig = true,
532                unsupported => return Err(unsupported),
533            }
534        }
535
536        Ok(out)
537    }
538}
539
540impl Default for Bindings {
541    fn default() -> Self {
542        Self {
543            c: true,
544            go: true,
545            java: false,
546            kotlin: false,
547            node: true,
548            python: true,
549            rust: true,
550            swift: true,
551            zig: false,
552        }
553    }
554}
555
556// Replace `~` or `$HOME` with home path string.
557// (While paths like "~/.tree-sitter/config.json" can be deserialized,
558// they're not valid path for I/O modules.)
559fn deserialize_parser_directories<'de, D>(deserializer: D) -> Result<Vec<PathBuf>, D::Error>
560where
561    D: Deserializer<'de>,
562{
563    let paths = Vec::<PathBuf>::deserialize(deserializer)?;
564    let Ok(home) = etcetera::home_dir() else {
565        return Ok(paths);
566    };
567    let standardized = paths
568        .into_iter()
569        .map(|path| standardize_path(path, &home))
570        .collect();
571    Ok(standardized)
572}
573
574fn standardize_path(path: PathBuf, home: &Path) -> PathBuf {
575    if let Ok(p) = path.strip_prefix("~") {
576        return home.join(p);
577    }
578    if let Ok(p) = path.strip_prefix("$HOME") {
579        return home.join(p);
580    }
581    path
582}
583
584fn display_build_cmd(cmd: &Command) {
585    let mut env_vars = String::new();
586    for (key, val) in cmd.get_envs() {
587        env_vars.push_str(&key.to_string_lossy());
588        if let Some(v) = val {
589            env_vars.push('=');
590            env_vars.push_str(&v.to_string_lossy());
591        }
592        env_vars.push('\n');
593    }
594    if !env_vars.is_empty() {
595        env_vars.pop(); // remove last '\n'
596    }
597    info!(
598        "[{}] {} {}\n",
599        cmd.get_current_dir()
600            .unwrap_or_else(|| Path::new(""))
601            .display(),
602        cmd.get_program().to_string_lossy(),
603        cmd.get_args()
604            .map(|s| s.to_string_lossy())
605            .collect::<Vec<_>>()
606            .join(" "),
607    );
608    for (key, val) in cmd.get_envs() {
609        let mut env_str = key.to_string_lossy().to_string();
610        if let Some(v) = val {
611            env_str.push('=');
612            env_str.push_str(&v.to_string_lossy());
613        }
614        info!("{env_str}");
615    }
616}
617
618impl Config {
619    #[must_use]
620    pub fn initial() -> Self {
621        let home_dir = etcetera::home_dir().expect("Cannot determine home directory");
622        Self {
623            parser_directories: vec![
624                home_dir.join("github"),
625                home_dir.join("src"),
626                home_dir.join("source"),
627                home_dir.join("projects"),
628                home_dir.join("dev"),
629                home_dir.join("git"),
630            ],
631        }
632    }
633}
634
635const BUILD_TARGET: &str = env!("BUILD_TARGET");
636
637pub struct LanguageConfiguration<'a> {
638    pub scope: Option<String>,
639    pub content_regex: Option<Regex>,
640    pub first_line_regex: Option<Regex>,
641    pub injection_regex: Option<Regex>,
642    pub file_types: Vec<String>,
643    pub root_path: PathBuf,
644    pub highlights_filenames: Option<Vec<PathBuf>>,
645    pub injections_filenames: Option<Vec<PathBuf>>,
646    pub locals_filenames: Option<Vec<PathBuf>>,
647    pub tags_filenames: Option<Vec<PathBuf>>,
648    pub language_name: String,
649    language_id: usize,
650    #[cfg(feature = "tree-sitter-highlight")]
651    highlight_config: OnceCell<Option<HighlightConfiguration>>,
652    #[cfg(feature = "tree-sitter-tags")]
653    tags_config: OnceCell<Option<TagsConfiguration>>,
654    #[cfg(feature = "tree-sitter-highlight")]
655    highlight_names: &'a Mutex<Vec<String>>,
656    #[cfg(feature = "tree-sitter-highlight")]
657    use_all_highlight_names: bool,
658    _phantom: PhantomData<&'a ()>,
659}
660
661pub struct Loader {
662    pub parser_lib_path: PathBuf,
663    languages_by_id: Vec<(PathBuf, OnceCell<Language>, Option<Vec<PathBuf>>)>,
664    language_configurations: Vec<LanguageConfiguration<'static>>,
665    language_configuration_ids_by_file_type: HashMap<String, Vec<usize>>,
666    language_configuration_in_current_path: Option<usize>,
667    language_configuration_ids_by_first_line_regex: HashMap<String, Vec<usize>>,
668    #[cfg(feature = "tree-sitter-highlight")]
669    highlight_names: Box<Mutex<Vec<String>>>,
670    #[cfg(feature = "tree-sitter-highlight")]
671    use_all_highlight_names: bool,
672    debug_build: bool,
673    sanitize_build: bool,
674    force_rebuild: bool,
675    verbose: bool,
676
677    #[cfg(feature = "wasm")]
678    wasm_store: Mutex<Option<tree_sitter::WasmStore>>,
679}
680
681pub struct CompileConfig<'a> {
682    pub src_path: &'a Path,
683    pub header_paths: Vec<&'a Path>,
684    pub parser_path: PathBuf,
685    pub scanner_path: Option<PathBuf>,
686    pub external_files: Option<&'a [PathBuf]>,
687    pub output_path: Option<PathBuf>,
688    pub flags: &'a [&'a str],
689    pub sanitize: bool,
690    pub name: String,
691}
692
693impl<'a> CompileConfig<'a> {
694    #[must_use]
695    pub fn new(
696        src_path: &'a Path,
697        externals: Option<&'a [PathBuf]>,
698        output_path: Option<PathBuf>,
699    ) -> Self {
700        Self {
701            src_path,
702            header_paths: vec![src_path],
703            parser_path: src_path.join("parser.c"),
704            scanner_path: None,
705            external_files: externals,
706            output_path,
707            flags: &[],
708            sanitize: false,
709            name: String::new(),
710        }
711    }
712}
713
714/// Generate a temporary file path for atomic writes. The temp file is a hidden
715/// dotfile alongside the target, tagged with the current process and thread id
716/// for debuggability (e.g. `.javascript.so.12345.ThreadId(3)`).
717fn temp_path(path: &Path) -> PathBuf {
718    let filename = path
719        .file_name()
720        .expect("output_path must have a filename")
721        .to_string_lossy();
722    path.with_file_name(format!(
723        ".{filename}.{}.{:?}",
724        std::process::id(),
725        std::thread::current().id()
726    ))
727}
728
729/// RAII lock file guard. The lock file is created atomically via
730/// [`create_new`](`fs::OpenOptions::create_new`) and removed on drop.
731/// and removed on drop.
732struct LockFile {
733    path: PathBuf,
734}
735
736impl LockFile {
737    /// Attempt to atomically create the lock file.
738    ///
739    /// Returns `Ok(Some)` if we won the race, `Ok(None)` if we lost,
740    /// or the underlying IO error otherwise.
741    fn create(path: &Path) -> LoaderResult<Option<Self>> {
742        match fs::OpenOptions::new()
743            .create_new(true)
744            .write(true)
745            .open(path)
746        {
747            Ok(_) => Ok(Some(Self {
748                path: path.to_path_buf(),
749            })),
750            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(None),
751            Err(e) => Err(LoaderError::IO(IoError::new(e, Some(path)))),
752        }
753    }
754
755    /// Wait for an existing lock file to be removed by whoever created it.
756    /// If the lock file persists beyond `timeout`, return [`LoaderError::LockFileTimeout`]
757    fn wait_for_removal(path: &Path, timeout: Duration) -> LoaderResult<()> {
758        let mut sleep_ms = 100;
759        let deadline = Instant::now() + timeout;
760        while path.exists() {
761            if Instant::now() > deadline {
762                return Err(LoaderError::LockFileTimeout(path.to_path_buf()));
763            }
764            std::thread::sleep(Duration::from_millis(sleep_ms));
765            sleep_ms = (sleep_ms * 2).min(1000);
766        }
767
768        Ok(())
769    }
770}
771
772impl Drop for LockFile {
773    fn drop(&mut self) {
774        match fs::remove_file(&self.path) {
775            Ok(()) => {}
776            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
777            Err(e) => warn!("Failed to remove lock file '{}': {e}.", self.path.display()),
778        }
779    }
780}
781
782impl Loader {
783    pub fn new() -> LoaderResult<Self> {
784        let parser_lib_path = if let Ok(path) = env::var("TREE_SITTER_LIBDIR") {
785            PathBuf::from(path)
786        } else {
787            if cfg!(target_os = "macos") {
788                let legacy_apple_path = etcetera::base_strategy::Apple::new()?
789                    .cache_dir() // `$HOME/Library/Caches/`
790                    .join("tree-sitter");
791                if legacy_apple_path.exists() && legacy_apple_path.is_dir() {
792                    std::fs::remove_dir_all(&legacy_apple_path).map_err(|e| {
793                        LoaderError::IO(IoError::new(e, Some(legacy_apple_path.as_path())))
794                    })?;
795                }
796            }
797
798            etcetera::choose_base_strategy()?
799                .cache_dir()
800                .join("tree-sitter")
801                .join("lib")
802        };
803        Ok(Self::with_parser_lib_path(parser_lib_path))
804    }
805
806    #[must_use]
807    pub fn with_parser_lib_path(parser_lib_path: PathBuf) -> Self {
808        Self {
809            parser_lib_path,
810            languages_by_id: Vec::new(),
811            language_configurations: Vec::new(),
812            language_configuration_ids_by_file_type: HashMap::new(),
813            language_configuration_in_current_path: None,
814            language_configuration_ids_by_first_line_regex: HashMap::new(),
815            #[cfg(feature = "tree-sitter-highlight")]
816            highlight_names: Box::new(Mutex::new(Vec::new())),
817            #[cfg(feature = "tree-sitter-highlight")]
818            use_all_highlight_names: true,
819            debug_build: false,
820            sanitize_build: false,
821            force_rebuild: false,
822            verbose: false,
823
824            #[cfg(feature = "wasm")]
825            wasm_store: Mutex::default(),
826        }
827    }
828
829    #[cfg(feature = "tree-sitter-highlight")]
830    #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))]
831    pub fn configure_highlights(&mut self, names: &[String]) {
832        self.use_all_highlight_names = false;
833        let mut highlights = self.highlight_names.lock().unwrap();
834        highlights.clear();
835        highlights.extend(names.iter().cloned());
836    }
837
838    #[must_use]
839    #[cfg(feature = "tree-sitter-highlight")]
840    #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))]
841    pub fn highlight_names(&self) -> Vec<String> {
842        self.highlight_names.lock().unwrap().clone()
843    }
844
845    pub fn find_all_languages(&mut self, config: &Config) -> LoaderResult<()> {
846        if config.parser_directories.is_empty() {
847            warn!(concat!(
848                "You have not configured any parser directories!\n",
849                "Please run `tree-sitter init-config` and edit the resulting\n",
850                "configuration file to indicate where we should look for\n",
851                "language grammars.\n"
852            ));
853        }
854        for parser_container_dir in &config.parser_directories {
855            if let Ok(entries) = fs::read_dir(parser_container_dir) {
856                for entry in entries {
857                    let entry = entry.map_err(|e| LoaderError::IO(IoError::new(e, None)))?;
858                    if let Some(parser_dir_name) = entry.file_name().to_str()
859                        && parser_dir_name.starts_with("tree-sitter-")
860                    {
861                        self.find_language_configurations_at_path(
862                            &parser_container_dir.join(parser_dir_name),
863                            false,
864                        )
865                        .ok();
866                    }
867                }
868            }
869        }
870        Ok(())
871    }
872
873    pub fn languages_at_path(&mut self, path: &Path) -> LoaderResult<Vec<(Language, String)>> {
874        if let Ok(configurations) = self.find_language_configurations_at_path(path, true) {
875            let mut language_ids = configurations
876                .iter()
877                .map(|c| (c.language_id, c.language_name.clone()))
878                .collect::<Vec<_>>();
879            language_ids.sort_unstable();
880            language_ids.dedup();
881            language_ids
882                .into_iter()
883                .map(|(id, name)| Ok((self.language_for_id(id)?, name)))
884                .collect::<LoaderResult<Vec<_>>>()
885        } else {
886            Ok(Vec::new())
887        }
888    }
889
890    #[must_use]
891    pub fn get_all_language_configurations(&self) -> Vec<(&LanguageConfiguration<'static>, &Path)> {
892        self.language_configurations
893            .iter()
894            .map(|c| (c, self.languages_by_id[c.language_id].0.as_ref()))
895            .collect()
896    }
897
898    pub fn language_configuration_for_scope(
899        &self,
900        scope: &str,
901    ) -> LoaderResult<Option<(Language, &LanguageConfiguration<'static>)>> {
902        for configuration in &self.language_configurations {
903            if configuration.scope.as_ref().is_some_and(|s| s == scope) {
904                let language = self.language_for_id(configuration.language_id)?;
905                return Ok(Some((language, configuration)));
906            }
907        }
908        Ok(None)
909    }
910
911    pub fn language_configuration_for_first_line_regex(
912        &self,
913        path: &Path,
914    ) -> LoaderResult<Option<(Language, &LanguageConfiguration<'static>)>> {
915        self.language_configuration_ids_by_first_line_regex
916            .iter()
917            .try_fold(None, |_, (regex, ids)| {
918                if let Some(regex) = Self::regex(Some(regex)) {
919                    let file = fs::File::open(path)
920                        .map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))?;
921                    let reader = BufReader::new(file);
922                    let first_line = reader
923                        .lines()
924                        .next()
925                        .transpose()
926                        .map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))?;
927                    if let Some(first_line) = first_line
928                        && regex.is_match(&first_line)
929                        && !ids.is_empty()
930                    {
931                        let configuration = &self.language_configurations[ids[0]];
932                        let language = self.language_for_id(configuration.language_id)?;
933                        return Ok(Some((language, configuration)));
934                    }
935                }
936
937                Ok(None)
938            })
939    }
940
941    pub fn language_configuration_for_file_name(
942        &self,
943        path: &Path,
944    ) -> LoaderResult<Option<(Language, &LanguageConfiguration<'static>)>> {
945        // Find all the language configurations that match this file name
946        // or a suffix of the file name.
947        let configuration_ids = path
948            .file_name()
949            .and_then(|n| n.to_str())
950            .and_then(|file_name| self.language_configuration_ids_by_file_type.get(file_name))
951            .or_else(|| {
952                let mut path = path.to_owned();
953                let mut extensions = Vec::with_capacity(2);
954                while let Some(extension) = path.extension() {
955                    extensions.push(extension.to_str()?.to_string());
956                    path = PathBuf::from(path.file_stem()?.to_os_string());
957                }
958                extensions.reverse();
959                // Try longest extension suffixs first (e.g. "foo.bar.baz"->"bar.baz"->"baz"),
960                // stopping at the first match.
961                (0..extensions.len())
962                    .map(|i| extensions[i..].join("."))
963                    .find_map(|key| self.language_configuration_ids_by_file_type.get(&key))
964            });
965
966        if let Some(configuration_ids) = configuration_ids
967            && !configuration_ids.is_empty()
968        {
969            let configuration = if configuration_ids.len() == 1 {
970                &self.language_configurations[configuration_ids[0]]
971            }
972            // If multiple language configurations match, then determine which
973            // one to use by applying the configurations' content regexes.
974            else {
975                let file_contents =
976                    fs::read(path).map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))?;
977                let file_contents = String::from_utf8_lossy(&file_contents);
978                let mut best_score = -2isize;
979                let mut best_configuration_id = None;
980                for configuration_id in configuration_ids {
981                    let config = &self.language_configurations[*configuration_id];
982
983                    // If the language configuration has a content regex, assign
984                    // a score based on the length of the first match.
985                    let score;
986                    if let Some(content_regex) = &config.content_regex {
987                        if let Some(mat) = content_regex.find(&file_contents) {
988                            score = (mat.end() - mat.start()) as isize;
989                        }
990                        // If the content regex does not match, then *penalize* this
991                        // language configuration, so that language configurations
992                        // without content regexes are preferred over those with
993                        // non-matching content regexes.
994                        else {
995                            score = -1;
996                        }
997                    } else {
998                        score = 0;
999                    }
1000                    if score > best_score {
1001                        best_configuration_id = Some(*configuration_id);
1002                        best_score = score;
1003                    }
1004                }
1005
1006                &self.language_configurations[best_configuration_id.unwrap()]
1007            };
1008
1009            let language = self.language_for_id(configuration.language_id)?;
1010            return Ok(Some((language, configuration)));
1011        }
1012
1013        Ok(None)
1014    }
1015
1016    pub fn language_configuration_for_injection_string(
1017        &self,
1018        string: &str,
1019    ) -> LoaderResult<Option<(Language, &LanguageConfiguration<'static>)>> {
1020        let mut best_match_length = 0;
1021        let mut best_match_position = None;
1022        for (i, configuration) in self.language_configurations.iter().enumerate() {
1023            if let Some(injection_regex) = &configuration.injection_regex
1024                && let Some(mat) = injection_regex.find(string)
1025            {
1026                let length = mat.end() - mat.start();
1027                if length > best_match_length {
1028                    best_match_position = Some(i);
1029                    best_match_length = length;
1030                }
1031            }
1032        }
1033
1034        if let Some(i) = best_match_position {
1035            let configuration = &self.language_configurations[i];
1036            let language = self.language_for_id(configuration.language_id)?;
1037            Ok(Some((language, configuration)))
1038        } else {
1039            Ok(None)
1040        }
1041    }
1042
1043    pub fn language_for_configuration(
1044        &self,
1045        configuration: &LanguageConfiguration,
1046    ) -> LoaderResult<Language> {
1047        self.language_for_id(configuration.language_id)
1048    }
1049
1050    fn language_for_id(&self, id: usize) -> LoaderResult<Language> {
1051        let (path, language, externals) = &self.languages_by_id[id];
1052        language
1053            .get_or_try_init(|| {
1054                let src_path = path.join("src");
1055                self.load_language_at_path(CompileConfig::new(
1056                    &src_path,
1057                    externals.as_deref(),
1058                    None,
1059                ))
1060            })
1061            .cloned()
1062    }
1063
1064    pub fn compile_parser_at_path(
1065        &self,
1066        grammar_path: &Path,
1067        output_path: PathBuf,
1068        flags: &[&str],
1069    ) -> LoaderResult<()> {
1070        let src_path = grammar_path.join("src");
1071        let mut config = CompileConfig::new(&src_path, None, Some(output_path));
1072        config.flags = flags;
1073        self.load_language_at_path(config).map(|_| ())
1074    }
1075
1076    pub fn load_language_at_path(&self, mut config: CompileConfig) -> LoaderResult<Language> {
1077        let grammar_path = config.src_path.join("grammar.json");
1078        config.name = Self::grammar_json_name(&grammar_path)?;
1079        self.load_language_at_path_with_name(config)
1080    }
1081
1082    pub fn load_language_at_path_with_name(
1083        &self,
1084        mut config: CompileConfig,
1085    ) -> LoaderResult<Language> {
1086        let mut lib_name = config.name.clone();
1087        let language_fn_name = format!("tree_sitter_{}", config.name.replace('-', "_"));
1088        if self.debug_build {
1089            lib_name.push_str(".debug._");
1090        }
1091
1092        if self.sanitize_build {
1093            lib_name.push_str(".sanitize._");
1094            config.sanitize = true;
1095        }
1096
1097        if config.output_path.is_none() {
1098            fs::create_dir_all(&self.parser_lib_path).map_err(|e| {
1099                LoaderError::IO(IoError::new(e, Some(self.parser_lib_path.as_path())))
1100            })?;
1101        }
1102
1103        let mut recompile = self.force_rebuild || config.output_path.is_some(); // if specified, always recompile
1104
1105        let output_path = config.output_path.unwrap_or_else(|| {
1106            let mut path = self.parser_lib_path.join(lib_name);
1107            path.set_extension(env::consts::DLL_EXTENSION);
1108            #[cfg(feature = "wasm")]
1109            if self.wasm_store.lock().unwrap().is_some() {
1110                path.set_extension("wasm");
1111            }
1112            path
1113        });
1114        config.output_path = Some(output_path.clone());
1115
1116        let parser_path = config.src_path.join("parser.c");
1117        config.scanner_path = self.get_scanner_path(config.src_path);
1118
1119        let mut paths_to_check = vec![parser_path];
1120
1121        if let Some(scanner_path) = config.scanner_path.as_ref() {
1122            paths_to_check.push(scanner_path.clone());
1123        }
1124
1125        paths_to_check.extend(
1126            config
1127                .external_files
1128                .unwrap_or_default()
1129                .iter()
1130                .map(|p| config.src_path.join(p)),
1131        );
1132
1133        if !recompile {
1134            recompile = needs_recompile(&output_path, &paths_to_check)?;
1135        }
1136
1137        // Create a unique lock path based on the output path hash to prevent
1138        // interference when multiple processes build the same grammar (by name)
1139        // to different output locations
1140        let lock_hash = {
1141            let mut hasher = std::hash::DefaultHasher::new();
1142            output_path.hash(&mut hasher);
1143            format!("{:x}", hasher.finish())
1144        };
1145
1146        let mut lock_path = etcetera::choose_base_strategy()?.cache_dir();
1147        lock_path.push(format!(
1148            "tree-sitter{slash}lock{slash}{name}-{lock_hash}.lock",
1149            slash = std::path::MAIN_SEPARATOR,
1150            name = config.name
1151        ));
1152
1153        // Synchronize compilation across threads/processes using an atomic lock
1154        // file. Exactly one caller wins the race to compile. Losers wait for the
1155        // lock file to be removed, then skip compilation and proceed to loading.
1156        //
1157        // Loading only (`recompile == false`) doesn't need a lock because
1158        // `compile_parser_to_dylib` writes to a temp file and atomically renames
1159        // into place, so loaders always see a complete shared library (either the
1160        // new or previous copy).
1161        //
1162        // The `LockFile` ensures cleanup on drop, and stale locks from killed
1163        // processes are detected via a timeout in `wait_for_removal`.
1164        if recompile {
1165            let parent_path = lock_path.parent().unwrap();
1166            fs::create_dir_all(parent_path)
1167                .map_err(|e| LoaderError::IO(IoError::new(e, Some(parent_path))))?;
1168
1169            match LockFile::create(&lock_path)? {
1170                Some(_lock) => {
1171                    // We won the race, so compile with the lock.
1172                    let compile_wasm;
1173                    #[cfg(feature = "wasm")]
1174                    {
1175                        compile_wasm = self.wasm_store.lock().unwrap().is_some();
1176                    }
1177                    #[cfg(not(feature = "wasm"))]
1178                    {
1179                        compile_wasm = false;
1180                    };
1181                    #[cfg(feature = "wasm")]
1182                    if compile_wasm {
1183                        self.compile_parser_to_wasm(
1184                            &config.name,
1185                            config.src_path,
1186                            config
1187                                .scanner_path
1188                                .as_ref()
1189                                .and_then(|p| p.strip_prefix(config.src_path).ok()),
1190                            &output_path,
1191                        )?;
1192                    }
1193                    if !compile_wasm {
1194                        self.compile_parser_to_dylib(&config)?;
1195                        if config.scanner_path.is_some() {
1196                            Self::check_external_scanner(&output_path);
1197                        }
1198                    }
1199                    // _lock dropped here, removing the lock file.
1200                }
1201                // Another thread/process is compiling (or a previous run
1202                // crashed and left a stale lock). Wait for it to finish.
1203                None => LockFile::wait_for_removal(&lock_path, Duration::from_secs(30))?,
1204            }
1205        }
1206
1207        #[cfg(feature = "wasm")]
1208        if let Some(wasm_store) = self.wasm_store.lock().unwrap().as_mut() {
1209            let wasm_bytes = fs::read(&output_path)
1210                .map_err(|e| LoaderError::IO(IoError::new(e, Some(output_path.as_path()))))?;
1211            return Ok(wasm_store.load_language(&config.name, &wasm_bytes)?);
1212        }
1213
1214        Self::load_language(&output_path, &language_fn_name)
1215    }
1216
1217    pub fn load_language(path: &Path, function_name: &str) -> LoaderResult<Language> {
1218        let library = unsafe { Library::new(path) }.map_err(|e| {
1219            LoaderError::Library(LibraryError {
1220                error: e,
1221                path: path.to_string_lossy().to_string(),
1222            })
1223        })?;
1224        let language = unsafe {
1225            let language_fn = library
1226                .get::<Symbol<unsafe extern "C" fn() -> Language>>(function_name.as_bytes())
1227                .map_err(|e| {
1228                    LoaderError::Symbol(SymbolError {
1229                        error: e,
1230                        symbol_name: function_name.to_string(),
1231                        path: path.to_string_lossy().to_string(),
1232                    })
1233                })?;
1234            language_fn()
1235        };
1236        mem::forget(library);
1237        Ok(language)
1238    }
1239
1240    fn compile_parser_to_dylib(&self, config: &CompileConfig) -> LoaderResult<()> {
1241        let mut cc_config = cc::Build::new();
1242        cc_config
1243            .cargo_metadata(false)
1244            .cargo_warnings(false)
1245            .target(BUILD_TARGET)
1246            // BUILD_TARGET from the build environment becomes a runtime host for cc.
1247            // Otherwise, when cross compiled, cc will keep looking for a cross-compiler
1248            // on the target system instead of the native compiler.
1249            .host(BUILD_TARGET)
1250            .debug(self.debug_build)
1251            .file(&config.parser_path)
1252            .includes(&config.header_paths)
1253            .std("c11");
1254
1255        if let Some(scanner_path) = config.scanner_path.as_ref() {
1256            cc_config.file(scanner_path);
1257        }
1258
1259        if self.debug_build {
1260            cc_config.opt_level(0).extra_warnings(true);
1261        } else {
1262            cc_config.opt_level(2).extra_warnings(false);
1263        }
1264
1265        for flag in config.flags {
1266            cc_config.define(flag, None);
1267        }
1268
1269        let compiler = cc_config.get_compiler();
1270        let mut command = compiler.to_command();
1271
1272        let output_path = config.output_path.as_ref().unwrap();
1273
1274        // Compile to a temporary path, then atomically rename into place.
1275        // This ensures loaders never see a half-written .so file.
1276        let temp_output = temp_path(output_path);
1277
1278        let temp_dir = if compiler.is_like_msvc() {
1279            let out = format!("-out:{}", temp_output.to_str().unwrap());
1280            command.arg(if self.debug_build { "-LDd" } else { "-LD" });
1281            command.arg("-utf-8");
1282
1283            // Windows creates intermediate files when compiling (.exp, .lib, .obj), which causes
1284            // issues when multiple processes are compiling in the same directory. This creates a
1285            // temporary directory for those files to go into, which is deleted after compilation.
1286            let temp_dir = output_path.parent().unwrap().join(format!(
1287                "tmp_{}_{:?}",
1288                std::process::id(),
1289                std::thread::current().id()
1290            ));
1291            std::fs::create_dir_all(&temp_dir).unwrap();
1292
1293            command.arg(format!("/Fo{}\\", temp_dir.display()));
1294            command.args(cc_config.get_files());
1295            command.arg("-link").arg(out);
1296            command.arg(format!("/IMPLIB:{}.lib", temp_dir.join("temp").display()));
1297
1298            Some(temp_dir)
1299        } else {
1300            command.arg("-Werror=implicit-function-declaration");
1301            if cfg!(any(target_os = "macos", target_os = "ios")) {
1302                command.arg("-dynamiclib");
1303                // TODO: remove when supported
1304                command.arg("-UTREE_SITTER_REUSE_ALLOCATOR");
1305            } else {
1306                command.arg("-shared");
1307                // Sanitizer builds reference runtime symbols that `--no-undefined`
1308                // would reject at link time.
1309                let sanitizing = compiler
1310                    .args()
1311                    .iter()
1312                    .any(|a| a.to_str().is_some_and(|s| s.starts_with("-fsanitize=")));
1313                if !sanitizing {
1314                    command.arg("-Wl,--no-undefined");
1315                }
1316                #[cfg(target_os = "openbsd")]
1317                command.arg("-lc");
1318            }
1319            command.args(cc_config.get_files());
1320            command.arg("-o").arg(&temp_output);
1321
1322            None
1323        };
1324
1325        if self.verbose {
1326            display_build_cmd(&command);
1327        }
1328
1329        let output = command.output().map_err(|e| {
1330            LoaderError::Compiler(CompilerError {
1331                error: e,
1332                command: Box::new(command),
1333            })
1334        })?;
1335
1336        if self.verbose {
1337            if !output.stdout.is_empty() {
1338                info!("stdout:{}", String::from_utf8_lossy(&output.stdout));
1339            }
1340            if !output.stderr.is_empty() {
1341                info!("stderr:{}", String::from_utf8_lossy(&output.stderr));
1342            }
1343        }
1344
1345        if let Some(temp_dir) = temp_dir {
1346            let _ = fs::remove_dir_all(temp_dir);
1347        }
1348
1349        if output.status.success() {
1350            fs::rename(&temp_output, output_path).map_err(|e| {
1351                let _ = fs::remove_file(&temp_output);
1352                LoaderError::IO(IoError::new(e, Some(output_path)))
1353            })?;
1354            Ok(())
1355        } else {
1356            let _ = fs::remove_file(&temp_output);
1357            Err(LoaderError::Compilation(
1358                String::from_utf8_lossy(&output.stdout).to_string(),
1359                String::from_utf8_lossy(&output.stderr).to_string(),
1360            ))
1361        }
1362    }
1363
1364    #[cfg(unix)]
1365    fn check_external_scanner(library_path: &Path) {
1366        let section = " T ";
1367        // Older ppc toolchains incorrectly report functions in the Data section. This bug has been
1368        // fixed, but we still need to account for older systems.
1369        let old_ppc_section = if cfg!(all(target_arch = "powerpc64", target_os = "linux")) {
1370            Some(" D ")
1371        } else {
1372            None
1373        };
1374        let nm_cmd = env::var("NM").unwrap_or_else(|_| "nm".to_owned());
1375        let command = Command::new(nm_cmd)
1376            .arg("--defined-only")
1377            .arg(library_path)
1378            .output();
1379        if let Ok(output) = command
1380            && output.status.success()
1381        {
1382            let mut non_static_symbols = String::new();
1383            for line in String::from_utf8_lossy(&output.stdout).lines() {
1384                if (line.contains(section) || old_ppc_section.is_some_and(|s| line.contains(s)))
1385                    && let Some(function_name) = line.split_whitespace().collect::<Vec<_>>().get(2)
1386                    && !line.contains("tree_sitter_")
1387                {
1388                    writeln!(&mut non_static_symbols, "  `{function_name}`").unwrap();
1389                }
1390            }
1391            if !non_static_symbols.is_empty() {
1392                warn!(
1393                    "Found non-static non-tree-sitter functions in the external scanner\n{non_static_symbols}\n{}",
1394                    concat!(
1395                        "Consider making these functions static, they can cause conflicts ",
1396                        "when another tree-sitter project uses the same function name."
1397                    )
1398                );
1399            }
1400        } else {
1401            warn!(
1402                "Failed to run `nm` to verify symbols in {}",
1403                library_path.display()
1404            );
1405        }
1406    }
1407
1408    #[cfg(windows)]
1409    fn check_external_scanner(_library_path: &Path) {
1410        // TODO: there's no nm command on windows, whoever wants to implement this can and should :)
1411    }
1412
1413    pub fn compile_parser_to_wasm(
1414        &self,
1415        language_name: &str,
1416        src_path: &Path,
1417        scanner_filename: Option<&Path>,
1418        output_path: &Path,
1419    ) -> LoaderResult<()> {
1420        let tool_lock = WASM_TOOL_LOCK.lock().expect("Wasm tool mutex poisoned");
1421        let clang_exe = Self::ensure_wasi_sdk_exists()?;
1422        let wasm_opt_exe = Self::ensure_binaryen_exists()?;
1423        drop(tool_lock);
1424
1425        // Compile to a temporary path, then atomically rename into place.
1426        // This ensures loaders never see a half-written .wasm file.
1427        let temp_output = temp_path(output_path);
1428        let temp_output_str = temp_output.to_str().unwrap();
1429
1430        let mut compile_command = Command::new(&clang_exe);
1431        compile_command.current_dir(src_path).args([
1432            "--target=wasm32-wasip1",
1433            "-o",
1434            temp_output_str,
1435            "-fPIC",
1436            "-shared",
1437            "--no-wasm-opt",
1438            if self.debug_build { "-g" } else { "-Os" },
1439            format!("-Wl,--export=tree_sitter_{language_name}").as_str(),
1440            "-Wl,--allow-undefined",
1441            "-Wl,--no-entry",
1442            "-nostdlib",
1443            "-fno-exceptions",
1444            "-fvisibility=hidden",
1445            "-I",
1446            ".",
1447            "parser.c",
1448        ]);
1449
1450        if let Some(scanner_filename) = scanner_filename {
1451            compile_command.arg(scanner_filename);
1452        }
1453
1454        if self.verbose {
1455            display_build_cmd(&compile_command);
1456        }
1457
1458        let compile_output = compile_command
1459            .output()
1460            .map_err(LoaderError::WasmCompiler)?;
1461        if self.verbose {
1462            if !compile_output.stdout.is_empty() {
1463                info!("stdout:{}", String::from_utf8_lossy(&compile_output.stdout));
1464            }
1465            if !compile_output.stderr.is_empty() {
1466                info!("stderr:{}", String::from_utf8_lossy(&compile_output.stderr));
1467            }
1468        }
1469
1470        if !compile_output.status.success() {
1471            let _ = fs::remove_file(&temp_output);
1472            return Err(LoaderError::WasmCompilation(
1473                String::from_utf8_lossy(&compile_output.stderr).to_string(),
1474            ));
1475        }
1476
1477        let mut opt_command = Command::new(&wasm_opt_exe);
1478        opt_command
1479            .current_dir(src_path)
1480            .args([temp_output_str, "-Os", "-o", temp_output_str]);
1481
1482        if self.verbose {
1483            display_build_cmd(&opt_command);
1484        }
1485
1486        let opt_output = opt_command.output().map_err(LoaderError::WasmOptimizer)?;
1487        if self.verbose {
1488            if !opt_output.stdout.is_empty() {
1489                info!("stdout:{}", String::from_utf8_lossy(&opt_output.stdout));
1490            }
1491            if !opt_output.stderr.is_empty() {
1492                info!("stderr:{}", String::from_utf8_lossy(&opt_output.stderr));
1493            }
1494        }
1495
1496        if !opt_output.status.success() {
1497            let _ = fs::remove_file(&temp_output);
1498            return Err(LoaderError::WasmOptimization(
1499                String::from_utf8_lossy(&opt_output.stderr).to_string(),
1500            ));
1501        }
1502
1503        fs::rename(&temp_output, output_path).map_err(|e| {
1504            let _ = fs::remove_file(&temp_output);
1505            LoaderError::IO(IoError::new(e, Some(output_path)))
1506        })?;
1507
1508        Ok(())
1509    }
1510
1511    /// Extracts a tar.gz archive with `tar`, stripping the first path component.
1512    fn extract_tar_gz_with_strip(archive_path: &Path, destination: &Path) -> LoaderResult<()> {
1513        let status = Command::new("tar")
1514            .arg("-xzf")
1515            .arg(archive_path)
1516            .arg("--strip-components=1")
1517            .arg("-C")
1518            .arg(destination)
1519            .status()
1520            .map_err(|e| LoaderError::Tar(archive_path.to_string_lossy().to_string(), e))?;
1521
1522        if !status.success() {
1523            return Err(LoaderError::Extraction(
1524                archive_path.to_string_lossy().to_string(),
1525                destination.to_string_lossy().to_string(),
1526            ));
1527        }
1528
1529        Ok(())
1530    }
1531
1532    /// This ensures that wasi-sdk is available, downloading and extracting it if necessary,
1533    /// and returns the path to the `clang` executable.
1534    ///
1535    /// If `TREE_SITTER_WASI_SDK_PATH` is set, it will use that path to look for the clang executable.
1536    fn ensure_wasi_sdk_exists() -> LoaderResult<PathBuf> {
1537        let possible_executables = if cfg!(windows) {
1538            vec![
1539                "clang.exe",
1540                "wasm32-unknown-wasi-clang.exe",
1541                "wasm32-wasi-clang.exe",
1542            ]
1543        } else {
1544            vec!["clang", "wasm32-unknown-wasi-clang", "wasm32-wasi-clang"]
1545        };
1546
1547        if let Some(path) = Self::get_existing_tool(
1548            "clang",
1549            "wasi-sdk",
1550            WASI_SDK_VERSION,
1551            &possible_executables,
1552            "TREE_SITTER_WASI_SDK_PATH",
1553        )? {
1554            return Ok(path);
1555        }
1556
1557        let arch_os = ARCH_OS?;
1558        let sdk_filename = format!("wasi-sdk-{WASI_SDK_VERSION}-{arch_os}.tar.gz");
1559        let wasi_sdk_major_version = WASI_SDK_VERSION
1560            .trim_end_matches(char::is_numeric) // trim minor version...
1561            .trim_end_matches('.'); // ...and '.' separator
1562        let sdk_url = format!(
1563            "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-{wasi_sdk_major_version}/{sdk_filename}",
1564        );
1565        Self::download_tool(
1566            "clang",
1567            "wasi-sdk",
1568            WASI_SDK_VERSION,
1569            &sdk_filename,
1570            &sdk_url,
1571            &possible_executables,
1572        )
1573    }
1574
1575    /// This ensures that binaryen is available, downloading and extracting it if necessary,
1576    /// and returns the path to the `wasm-opt` executable.
1577    ///
1578    /// If `TREE_SITTER_BINARYEN_PATH` is set, it will use that path to look for the wasm-opt executable.
1579    fn ensure_binaryen_exists() -> LoaderResult<PathBuf> {
1580        let possible_executables = if cfg!(windows) {
1581            vec![
1582                "wasm-opt.exe",
1583                "wasm32-unknown-wasm-opt.exe",
1584                "wasm32-wasm-opt.exe",
1585            ]
1586        } else {
1587            vec!["wasm-opt", "wasm32-unknown-wasm-opt", "wasm32-wasm-opt"]
1588        };
1589        if let Some(path) = Self::get_existing_tool(
1590            "wasm-opt",
1591            "binaryen",
1592            BINARYEN_VERSION,
1593            &possible_executables,
1594            "TREE_SITTER_BINARYEN_PATH",
1595        )? {
1596            return Ok(path);
1597        }
1598
1599        let arch_os = ARCH_OS?.replace("arm64-linux", "aarch64-linux");
1600        let binaryen_filename = format!("binaryen-version_{BINARYEN_VERSION}-{arch_os}.tar.gz");
1601        let binaryen_url = format!(
1602            "https://github.com/WebAssembly/binaryen/releases/download/version_{BINARYEN_VERSION}/{binaryen_filename}"
1603        );
1604        Self::download_tool(
1605            "wasm-opt",
1606            "binaryen",
1607            BINARYEN_VERSION,
1608            &binaryen_filename,
1609            &binaryen_url,
1610            &possible_executables,
1611        )
1612    }
1613
1614    fn get_existing_tool(
1615        tool_name: &'static str,
1616        toolchain: &'static str,
1617        version: &str,
1618        possible_exes: &[&'static str],
1619        env_var: &str,
1620    ) -> LoaderResult<Option<PathBuf>> {
1621        if let Ok(tool_path) = std::env::var(env_var) {
1622            let tool_dir = PathBuf::from(tool_path).join("bin");
1623
1624            for exe in possible_exes {
1625                let tool_exe = tool_dir.join(exe);
1626                if tool_exe.exists() {
1627                    return Ok(Some(tool_exe));
1628                }
1629            }
1630
1631            Err(LoaderError::WasmTool(WasmToolError {
1632                exe: tool_name,
1633                toolchain,
1634                tool_dir: tool_dir.to_string_lossy().to_string(),
1635                possible_executables: possible_exes.to_vec(),
1636                download: false,
1637            }))?;
1638        }
1639
1640        let cache_dir = etcetera::choose_base_strategy()?
1641            .cache_dir()
1642            .join("tree-sitter");
1643        fs::create_dir_all(&cache_dir).map_err(|error| {
1644            LoaderError::IO(IoError {
1645                error,
1646                path: Some(cache_dir.clone()),
1647            })
1648        })?;
1649
1650        let toolchain_dir = cache_dir.join(toolchain);
1651        let version_file = toolchain_dir.join(".version");
1652
1653        // If a cached toolchain exists but the version doesn't match, remove it
1654        if toolchain_dir.exists() {
1655            let cached_version =
1656                fs::read_to_string(&version_file).unwrap_or_else(|_| "unknown".to_string());
1657            if cached_version.trim() != version {
1658                info!(
1659                    "Cached {toolchain} version ({}) doesn't match expected version ({version}), re-downloading",
1660                    cached_version.trim(),
1661                );
1662                fs::remove_dir_all(&toolchain_dir).ok();
1663                return Ok(None);
1664            }
1665        }
1666
1667        let tool_dir = toolchain_dir.join("bin");
1668
1669        for exe in possible_exes {
1670            let tool_exe = tool_dir.join(exe);
1671            if tool_exe.exists() {
1672                return Ok(Some(tool_exe));
1673            }
1674        }
1675
1676        Ok(None)
1677    }
1678
1679    fn download_tool(
1680        tool_name: &'static str,
1681        toolchain: &'static str,
1682        version: &str,
1683        filename: &str,
1684        url: &str,
1685        possible_exes: &[&'static str],
1686    ) -> LoaderResult<PathBuf> {
1687        let cache_dir = etcetera::choose_base_strategy()?
1688            .cache_dir()
1689            .join("tree-sitter");
1690        let tool_dir = cache_dir.join(toolchain);
1691
1692        fs::create_dir_all(&tool_dir).map_err(|error| {
1693            LoaderError::IO(IoError {
1694                error,
1695                path: Some(tool_dir.clone()),
1696            })
1697        })?;
1698
1699        info!("Downloading {tool_name} from {url}...");
1700        let temp_tar_dir = tempfile::tempdir_in(&cache_dir).map_err(|e| {
1701            LoaderError::IO(IoError {
1702                error: e,
1703                path: Some(cache_dir.clone()),
1704            })
1705        })?;
1706        let temp_tar_path = temp_tar_dir.path().join(filename);
1707
1708        let status = Command::new("curl")
1709            .arg("-f")
1710            .arg("-L")
1711            .arg("-o")
1712            .arg(&temp_tar_path)
1713            .arg(url)
1714            .status()
1715            .map_err(|e| LoaderError::Curl(url.to_string(), e))?;
1716
1717        if !status.success() {
1718            Err(LoaderError::WasmToolDownload {
1719                tool: tool_name,
1720                url: url.to_string(),
1721            })?;
1722        }
1723
1724        info!("Extracting {tool_name} to {}...", tool_dir.display());
1725        Self::extract_tar_gz_with_strip(&temp_tar_path, &tool_dir)?;
1726
1727        fs::write(tool_dir.join(".version"), version).ok();
1728
1729        for exe in possible_exes {
1730            let tool_exe = tool_dir.join("bin").join(exe);
1731            if tool_exe.exists() {
1732                return Ok(tool_exe);
1733            }
1734        }
1735
1736        Err(LoaderError::WasmTool(WasmToolError {
1737            exe: tool_name,
1738            toolchain,
1739            tool_dir: tool_dir.to_string_lossy().to_string(),
1740            possible_executables: possible_exes.to_vec(),
1741            download: true,
1742        }))?
1743    }
1744
1745    #[must_use]
1746    #[cfg(feature = "tree-sitter-highlight")]
1747    pub fn highlight_config_for_injection_string<'a>(
1748        &'a self,
1749        string: &str,
1750    ) -> Option<&'a HighlightConfiguration> {
1751        match self.language_configuration_for_injection_string(string) {
1752            Err(e) => {
1753                error!("Failed to load language for injection string '{string}': {e}");
1754                None
1755            }
1756            Ok(None) => None,
1757            Ok(Some((language, configuration))) => {
1758                match configuration.highlight_config(language, None) {
1759                    Err(e) => {
1760                        error!(
1761                            "Failed to load highlight config for injection string '{string}': {e}"
1762                        );
1763                        None
1764                    }
1765                    Ok(None) => None,
1766                    Ok(Some(config)) => Some(config),
1767                }
1768            }
1769        }
1770    }
1771
1772    #[must_use]
1773    pub fn get_language_configuration_in_current_path(
1774        &self,
1775    ) -> Option<&LanguageConfiguration<'static>> {
1776        self.language_configuration_in_current_path
1777            .map(|i| &self.language_configurations[i])
1778    }
1779
1780    pub fn find_language_configurations_at_path(
1781        &mut self,
1782        parser_path: &Path,
1783        set_current_path_config: bool,
1784    ) -> LoaderResult<&[LanguageConfiguration<'static>]> {
1785        let initial_language_configuration_count = self.language_configurations.len();
1786
1787        match TreeSitterJSON::from_file(parser_path) {
1788            Ok(config) => {
1789                let language_count = self.languages_by_id.len();
1790                for grammar in config.grammars {
1791                    // Determine the path to the parser directory. This can be specified in
1792                    // the tree-sitter.json, but defaults to the directory containing the
1793                    // tree-sitter.json.
1794                    let language_path =
1795                        parser_path.join(grammar.path.unwrap_or_else(|| PathBuf::from(".")));
1796
1797                    // Determine if a previous language configuration in this tree-sitter.json file
1798                    // already uses the same language.
1799                    let mut language_id = None;
1800                    for (id, (path, _, _)) in
1801                        self.languages_by_id.iter().enumerate().skip(language_count)
1802                    {
1803                        if language_path == *path {
1804                            language_id = Some(id);
1805                        }
1806                    }
1807
1808                    // If not, add a new language path to the list.
1809                    let language_id = if let Some(language_id) = language_id {
1810                        language_id
1811                    } else {
1812                        self.languages_by_id.push((
1813                            language_path,
1814                            OnceCell::new(),
1815                            grammar
1816                                .external_files
1817                                .clone()
1818                                .into_vec()
1819                                .map(|files| {
1820                                    files
1821                                        .into_iter()
1822                                        .map(|path| {
1823                                            let path = parser_path.join(path);
1824                                            // prevent p being above/outside of parser_path
1825                                            if path.starts_with(parser_path) {
1826                                                Ok(path)
1827                                            } else {
1828                                                Err(LoaderError::ExternalFile(
1829                                                    path.to_string_lossy().to_string(),
1830                                                    parser_path.to_string_lossy().to_string(),
1831                                                ))
1832                                            }
1833                                        })
1834                                        .collect::<LoaderResult<Vec<_>>>()
1835                                })
1836                                .transpose()?,
1837                        ));
1838                        self.languages_by_id.len() - 1
1839                    };
1840
1841                    let configuration = LanguageConfiguration {
1842                        root_path: parser_path.to_path_buf(),
1843                        language_name: grammar.name,
1844                        scope: Some(grammar.scope),
1845                        language_id,
1846                        file_types: grammar.file_types.unwrap_or_default(),
1847                        content_regex: Self::regex(grammar.content_regex.as_deref()),
1848                        first_line_regex: Self::regex(grammar.first_line_regex.as_deref()),
1849                        injection_regex: Self::regex(grammar.injection_regex.as_deref()),
1850                        injections_filenames: grammar.injections.into_vec(),
1851                        locals_filenames: grammar.locals.into_vec(),
1852                        tags_filenames: grammar.tags.into_vec(),
1853                        highlights_filenames: grammar.highlights.into_vec(),
1854                        #[cfg(feature = "tree-sitter-highlight")]
1855                        highlight_config: OnceCell::new(),
1856                        #[cfg(feature = "tree-sitter-tags")]
1857                        tags_config: OnceCell::new(),
1858                        #[cfg(feature = "tree-sitter-highlight")]
1859                        highlight_names: &self.highlight_names,
1860                        #[cfg(feature = "tree-sitter-highlight")]
1861                        use_all_highlight_names: self.use_all_highlight_names,
1862                        _phantom: PhantomData,
1863                    };
1864
1865                    for file_type in &configuration.file_types {
1866                        self.language_configuration_ids_by_file_type
1867                            .entry(file_type.clone())
1868                            .or_default()
1869                            .push(self.language_configurations.len());
1870                    }
1871                    if let Some(first_line_regex) = &configuration.first_line_regex {
1872                        self.language_configuration_ids_by_first_line_regex
1873                            .entry(first_line_regex.to_string())
1874                            .or_default()
1875                            .push(self.language_configurations.len());
1876                    }
1877
1878                    self.language_configurations.push(unsafe {
1879                        mem::transmute::<LanguageConfiguration<'_>, LanguageConfiguration<'static>>(
1880                            configuration,
1881                        )
1882                    });
1883
1884                    if set_current_path_config
1885                        && self.language_configuration_in_current_path.is_none()
1886                    {
1887                        self.language_configuration_in_current_path =
1888                            Some(self.language_configurations.len() - 1);
1889                    }
1890                }
1891            }
1892            Err(LoaderError::Serialization(e)) => {
1893                warn!(
1894                    "Failed to parse {} -- {e}",
1895                    parser_path.join("tree-sitter.json").display()
1896                );
1897            }
1898            _ => {}
1899        }
1900
1901        // If we didn't find any language configurations in the tree-sitter.json file,
1902        // but there is a grammar.json file, then use the grammar file to form a simple
1903        // language configuration.
1904        if self.language_configurations.len() == initial_language_configuration_count
1905            && parser_path.join("src").join("grammar.json").exists()
1906        {
1907            let grammar_path = parser_path.join("src").join("grammar.json");
1908            let language_name = Self::grammar_json_name(&grammar_path)?;
1909            let configuration = LanguageConfiguration {
1910                root_path: parser_path.to_owned(),
1911                language_name,
1912                language_id: self.languages_by_id.len(),
1913                file_types: Vec::new(),
1914                scope: None,
1915                content_regex: None,
1916                first_line_regex: None,
1917                injection_regex: None,
1918                injections_filenames: None,
1919                locals_filenames: None,
1920                highlights_filenames: None,
1921                tags_filenames: None,
1922                #[cfg(feature = "tree-sitter-highlight")]
1923                highlight_config: OnceCell::new(),
1924                #[cfg(feature = "tree-sitter-tags")]
1925                tags_config: OnceCell::new(),
1926                #[cfg(feature = "tree-sitter-highlight")]
1927                highlight_names: &self.highlight_names,
1928                #[cfg(feature = "tree-sitter-highlight")]
1929                use_all_highlight_names: self.use_all_highlight_names,
1930                _phantom: PhantomData,
1931            };
1932            self.language_configurations.push(unsafe {
1933                mem::transmute::<LanguageConfiguration<'_>, LanguageConfiguration<'static>>(
1934                    configuration,
1935                )
1936            });
1937            self.languages_by_id
1938                .push((parser_path.to_owned(), OnceCell::new(), None));
1939        }
1940
1941        Ok(&self.language_configurations[initial_language_configuration_count..])
1942    }
1943
1944    fn regex(pattern: Option<&str>) -> Option<Regex> {
1945        pattern.and_then(|r| RegexBuilder::new(r).multi_line(true).build().ok())
1946    }
1947
1948    // Matches "name":\s*"(.*?)", returning the capture
1949    fn grammar_name(json_text: &str) -> Option<String> {
1950        let i = json_text.find("\"name\":")? + "\"name\":".len();
1951        let rest = json_text[i..].trim_start();
1952        let rest = rest.strip_prefix('\"')?;
1953        let end = rest.find('\"')?;
1954
1955        Some(rest[..end].to_string())
1956    }
1957
1958    fn grammar_json_name(grammar_path: &Path) -> LoaderResult<String> {
1959        let file = fs::File::open(grammar_path)
1960            .map_err(|e| LoaderError::IO(IoError::new(e, Some(grammar_path))))?;
1961
1962        let first_three_lines = BufReader::new(file)
1963            .lines()
1964            .take(3)
1965            .collect::<Result<Vec<_>, std::io::Error>>()
1966            .map_err(|_| LoaderError::GrammarJSON(grammar_path.to_string_lossy().to_string()))?
1967            .join("\n");
1968
1969        let name = Self::grammar_name(&first_three_lines)
1970            .ok_or_else(|| LoaderError::GrammarJSON(grammar_path.to_string_lossy().to_string()))?;
1971
1972        Ok(name)
1973    }
1974
1975    pub fn select_language(
1976        &mut self,
1977        path: Option<&Path>,
1978        current_dir: &Path,
1979        scope: Option<&str>,
1980        // path to dynamic library, name of language
1981        lib_info: Option<&(PathBuf, &str)>,
1982    ) -> LoaderResult<Language> {
1983        if let Some((lib_path, language_name)) = lib_info {
1984            let language_fn_name = format!("tree_sitter_{}", language_name.replace('-', "_"));
1985            Self::load_language(lib_path, &language_fn_name)
1986        } else if let Some(scope) = scope {
1987            if let Some(config) = self
1988                .language_configuration_for_scope(scope)
1989                .map_err(|e| LoaderError::ScopeLoad(scope.to_string(), Box::new(e)))?
1990            {
1991                Ok(config.0)
1992            } else {
1993                Err(LoaderError::UnknownScope(scope.to_string()))
1994            }
1995        } else if let Some((lang, _)) = if let Some(path) = path {
1996            self.language_configuration_for_file_name(path)
1997                .map_err(|e| {
1998                    LoaderError::FileNameLoad(
1999                        path.file_name().unwrap().to_string_lossy().to_string(),
2000                        Box::new(e),
2001                    )
2002                })?
2003        } else {
2004            None
2005        } {
2006            Ok(lang)
2007        } else if let Some(id) = self.language_configuration_in_current_path {
2008            Ok(self.language_for_id(self.language_configurations[id].language_id)?)
2009        } else if let Some(lang) = self
2010            .languages_at_path(current_dir)
2011            .map_err(|e| LoaderError::CurrentDirectoryLoad(Box::new(e)))?
2012            .first()
2013            .cloned()
2014        {
2015            Ok(lang.0)
2016        } else if let Some(lang) = if let Some(path) = path {
2017            self.language_configuration_for_first_line_regex(path)?
2018        } else {
2019            None
2020        } {
2021            Ok(lang.0)
2022        } else {
2023            Err(LoaderError::NoLanguage)
2024        }
2025    }
2026
2027    pub const fn debug_build(&mut self, flag: bool) {
2028        self.debug_build = flag;
2029    }
2030
2031    pub const fn sanitize_build(&mut self, flag: bool) {
2032        self.sanitize_build = flag;
2033    }
2034
2035    pub const fn force_rebuild(&mut self, rebuild: bool) {
2036        self.force_rebuild = rebuild;
2037    }
2038
2039    pub const fn verbose_build(&mut self, verbose: bool) {
2040        self.verbose = verbose;
2041    }
2042
2043    #[cfg(feature = "wasm")]
2044    #[cfg_attr(docsrs, doc(cfg(feature = "wasm")))]
2045    pub fn use_wasm(&mut self, engine: &tree_sitter::wasmtime::Engine) {
2046        *self.wasm_store.lock().unwrap() = Some(tree_sitter::WasmStore::new(engine).unwrap());
2047    }
2048
2049    #[must_use]
2050    pub fn get_scanner_path(&self, src_path: &Path) -> Option<PathBuf> {
2051        let path = src_path.join("scanner.c");
2052        path.exists().then_some(path)
2053    }
2054}
2055
2056impl LanguageConfiguration<'_> {
2057    #[cfg(feature = "tree-sitter-highlight")]
2058    pub fn highlight_config(
2059        &self,
2060        language: Language,
2061        paths: Option<&[PathBuf]>,
2062    ) -> LoaderResult<Option<&HighlightConfiguration>> {
2063        let (highlights_filenames, injections_filenames, locals_filenames) = match paths {
2064            Some(paths) => (
2065                Some(
2066                    paths
2067                        .iter()
2068                        .filter(|p| p.ends_with(DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME))
2069                        .cloned()
2070                        .collect::<Vec<_>>(),
2071                ),
2072                Some(
2073                    paths
2074                        .iter()
2075                        .filter(|p| p.ends_with(DEFAULT_TAGS_QUERY_FILE_NAME))
2076                        .cloned()
2077                        .collect::<Vec<_>>(),
2078                ),
2079                Some(
2080                    paths
2081                        .iter()
2082                        .filter(|p| p.ends_with(DEFAULT_LOCALS_QUERY_FILE_NAME))
2083                        .cloned()
2084                        .collect::<Vec<_>>(),
2085                ),
2086            ),
2087            None => (None, None, None),
2088        };
2089        self.highlight_config
2090            .get_or_try_init(|| {
2091                let (highlights_query, highlight_ranges) = self.read_queries(
2092                    if highlights_filenames.is_some() {
2093                        highlights_filenames.as_deref()
2094                    } else {
2095                        self.highlights_filenames.as_deref()
2096                    },
2097                    DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME,
2098                )?;
2099                let (injections_query, injection_ranges) = self.read_queries(
2100                    if injections_filenames.is_some() {
2101                        injections_filenames.as_deref()
2102                    } else {
2103                        self.injections_filenames.as_deref()
2104                    },
2105                    DEFAULT_INJECTIONS_QUERY_FILE_NAME,
2106                )?;
2107                let (locals_query, locals_ranges) = self.read_queries(
2108                    if locals_filenames.is_some() {
2109                        locals_filenames.as_deref()
2110                    } else {
2111                        self.locals_filenames.as_deref()
2112                    },
2113                    DEFAULT_LOCALS_QUERY_FILE_NAME,
2114                )?;
2115
2116                if highlights_query.is_empty() {
2117                    Ok(None)
2118                } else {
2119                    let mut result = HighlightConfiguration::new(
2120                        language,
2121                        &self.language_name,
2122                        &highlights_query,
2123                        &injections_query,
2124                        &locals_query,
2125                    )
2126                    .map_err(|error| match error.kind {
2127                        QueryErrorKind::Language => {
2128                            LoaderError::Query(LoaderQueryError { error, file: None })
2129                        }
2130                        _ => {
2131                            if error.offset < injections_query.len() {
2132                                Self::include_path_in_query_error(
2133                                    error,
2134                                    &injection_ranges,
2135                                    &injections_query,
2136                                    0,
2137                                )
2138                            } else if error.offset < injections_query.len() + locals_query.len() {
2139                                Self::include_path_in_query_error(
2140                                    error,
2141                                    &locals_ranges,
2142                                    &locals_query,
2143                                    injections_query.len(),
2144                                )
2145                            } else {
2146                                Self::include_path_in_query_error(
2147                                    error,
2148                                    &highlight_ranges,
2149                                    &highlights_query,
2150                                    injections_query.len() + locals_query.len(),
2151                                )
2152                            }
2153                        }
2154                    })?;
2155                    let mut all_highlight_names = self.highlight_names.lock().unwrap();
2156                    if self.use_all_highlight_names {
2157                        for capture_name in result.query.capture_names() {
2158                            if !all_highlight_names.iter().any(|x| x == capture_name) {
2159                                all_highlight_names.push((*capture_name).to_string());
2160                            }
2161                        }
2162                    }
2163                    result.configure(all_highlight_names.as_slice());
2164                    drop(all_highlight_names);
2165                    Ok(Some(result))
2166                }
2167            })
2168            .map(Option::as_ref)
2169    }
2170
2171    #[cfg(feature = "tree-sitter-tags")]
2172    pub fn tags_config(&self, language: Language) -> LoaderResult<Option<&TagsConfiguration>> {
2173        self.tags_config
2174            .get_or_try_init(|| {
2175                let (tags_query, tags_ranges) = self
2176                    .read_queries(self.tags_filenames.as_deref(), DEFAULT_TAGS_QUERY_FILE_NAME)?;
2177                let (locals_query, locals_ranges) = self.read_queries(
2178                    self.locals_filenames.as_deref(),
2179                    DEFAULT_LOCALS_QUERY_FILE_NAME,
2180                )?;
2181                if tags_query.is_empty() {
2182                    Ok(None)
2183                } else {
2184                    TagsConfiguration::new(language, &tags_query, &locals_query)
2185                        .map(Some)
2186                        .map_err(|error| {
2187                            if let TagsError::Query(error) = error {
2188                                if error.offset < locals_query.len() {
2189                                    Self::include_path_in_query_error(
2190                                        error,
2191                                        &locals_ranges,
2192                                        &locals_query,
2193                                        0,
2194                                    )
2195                                } else {
2196                                    Self::include_path_in_query_error(
2197                                        error,
2198                                        &tags_ranges,
2199                                        &tags_query,
2200                                        locals_query.len(),
2201                                    )
2202                                }
2203                            } else {
2204                                error.into()
2205                            }
2206                        })
2207                }
2208            })
2209            .map(Option::as_ref)
2210    }
2211
2212    #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
2213    fn include_path_in_query_error(
2214        mut error: QueryError,
2215        ranges: &[(PathBuf, Range<usize>)],
2216        source: &str,
2217        start_offset: usize,
2218    ) -> LoaderError {
2219        let offset_within_section = error.offset - start_offset;
2220        let (path, range) = ranges
2221            .iter()
2222            .find(|(_, range)| range.contains(&offset_within_section))
2223            .unwrap_or_else(|| ranges.last().unwrap());
2224        error.offset = offset_within_section - range.start;
2225        error.row = source[range.start..offset_within_section]
2226            .matches('\n')
2227            .count();
2228        LoaderError::Query(LoaderQueryError {
2229            error,
2230            file: Some(path.to_string_lossy().to_string()),
2231        })
2232    }
2233
2234    #[expect(
2235        clippy::type_complexity,
2236        reason = "return type pairs query text with source file ranges"
2237    )]
2238    #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
2239    fn read_queries(
2240        &self,
2241        paths: Option<&[PathBuf]>,
2242        default_path: &str,
2243    ) -> LoaderResult<(String, Vec<(PathBuf, Range<usize>)>)> {
2244        let mut query = String::new();
2245        let mut path_ranges = Vec::new();
2246        if let Some(paths) = paths {
2247            for path in paths {
2248                let abs_path = self.root_path.join(path);
2249                let prev_query_len = query.len();
2250                query += &fs::read_to_string(&abs_path)
2251                    .map_err(|e| LoaderError::IO(IoError::new(e, Some(abs_path.as_path()))))?;
2252                path_ranges.push((path.clone(), prev_query_len..query.len()));
2253            }
2254        } else {
2255            // highlights.scm is needed to test highlights, and tags.scm to test tags
2256            if default_path == DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME
2257                || default_path == DEFAULT_TAGS_QUERY_FILE_NAME
2258            {
2259                warn!(
2260                    concat!(
2261                        "You should add a `{}` entry pointing to the {} path in the `tree-sitter` ",
2262                        "object in the grammar's tree-sitter.json file. See more here: ",
2263                        "https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting#query-paths"
2264                    ),
2265                    default_path.replace(".scm", ""),
2266                    default_path
2267                );
2268            }
2269            let queries_path = self.root_path.join("queries");
2270            let path = queries_path.join(default_path);
2271            if path.exists() {
2272                query = fs::read_to_string(&path)
2273                    .map_err(|e| LoaderError::IO(IoError::new(e, Some(path.as_path()))))?;
2274                path_ranges.push((PathBuf::from(default_path), 0..query.len()));
2275            }
2276        }
2277
2278        Ok((query, path_ranges))
2279    }
2280}
2281
2282fn needs_recompile(lib_path: &Path, paths_to_check: &[PathBuf]) -> LoaderResult<bool> {
2283    if !lib_path.exists() {
2284        return Ok(true);
2285    }
2286    let lib_mtime = mtime(lib_path).map_err(|e| LoaderError::ModifiedTime(Box::new(e)))?;
2287    for path in paths_to_check {
2288        if mtime(path).map_err(|e| LoaderError::ModifiedTime(Box::new(e)))? > lib_mtime {
2289            return Ok(true);
2290        }
2291    }
2292    Ok(false)
2293}
2294
2295fn mtime(path: &Path) -> LoaderResult<SystemTime> {
2296    fs::metadata(path)
2297        .map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))?
2298        .modified()
2299        .map_err(|e| LoaderError::IO(IoError::new(e, Some(path))))
2300}