Skip to main content

tree_sitter_cli/
init.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4    str::{self, FromStr},
5};
6
7use anyhow::{anyhow, Context, Result};
8use crc32fast::hash as crc32;
9use heck::{ToKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
10use indoc::{formatdoc, indoc};
11use log::info;
12use rand::{thread_rng, Rng};
13use semver::Version;
14use serde::{Deserialize, Serialize};
15use serde_json::{Map, Value};
16use tree_sitter_generate::write_file;
17use tree_sitter_loader::{
18    Author, Bindings, Grammar, Links, Metadata, PathsJSON, TreeSitterJSON,
19    DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME, DEFAULT_INJECTIONS_QUERY_FILE_NAME,
20    DEFAULT_LOCALS_QUERY_FILE_NAME, DEFAULT_TAGS_QUERY_FILE_NAME,
21};
22
23const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
24const CLI_VERSION_PLACEHOLDER: &str = "CLI_VERSION";
25
26const ABI_VERSION_MAX: usize = tree_sitter::LANGUAGE_VERSION;
27const ABI_VERSION_MAX_PLACEHOLDER: &str = "ABI_VERSION_MAX";
28
29const PARSER_NAME_PLACEHOLDER: &str = "PARSER_NAME";
30const CAMEL_PARSER_NAME_PLACEHOLDER: &str = "CAMEL_PARSER_NAME";
31const TITLE_PARSER_NAME_PLACEHOLDER: &str = "TITLE_PARSER_NAME";
32const UPPER_PARSER_NAME_PLACEHOLDER: &str = "UPPER_PARSER_NAME";
33const LOWER_PARSER_NAME_PLACEHOLDER: &str = "LOWER_PARSER_NAME";
34const KEBAB_PARSER_NAME_PLACEHOLDER: &str = "KEBAB_PARSER_NAME";
35const PARSER_CLASS_NAME_PLACEHOLDER: &str = "PARSER_CLASS_NAME";
36
37const PARSER_DESCRIPTION_PLACEHOLDER: &str = "PARSER_DESCRIPTION";
38const PARSER_LICENSE_PLACEHOLDER: &str = "PARSER_LICENSE";
39const PARSER_NS_PLACEHOLDER: &str = "PARSER_NS";
40const PARSER_NS_CLEANED_PLACEHOLDER: &str = "PARSER_NS_CLEANED";
41const PARSER_URL_PLACEHOLDER: &str = "PARSER_URL";
42const PARSER_URL_STRIPPED_PLACEHOLDER: &str = "PARSER_URL_STRIPPED";
43const PARSER_VERSION_PLACEHOLDER: &str = "PARSER_VERSION";
44const PARSER_FINGERPRINT_PLACEHOLDER: &str = "PARSER_FINGERPRINT";
45
46const AUTHOR_NAME_PLACEHOLDER: &str = "PARSER_AUTHOR_NAME";
47const AUTHOR_EMAIL_PLACEHOLDER: &str = "PARSER_AUTHOR_EMAIL";
48const AUTHOR_URL_PLACEHOLDER: &str = "PARSER_AUTHOR_URL";
49
50const AUTHOR_BLOCK_JS: &str = "\n  \"author\": {";
51const AUTHOR_NAME_PLACEHOLDER_JS: &str = "\n    \"name\": \"PARSER_AUTHOR_NAME\",";
52const AUTHOR_EMAIL_PLACEHOLDER_JS: &str = ",\n    \"email\": \"PARSER_AUTHOR_EMAIL\"";
53const AUTHOR_URL_PLACEHOLDER_JS: &str = ",\n    \"url\": \"PARSER_AUTHOR_URL\"";
54
55const AUTHOR_BLOCK_PY: &str = "\nauthors = [{";
56const AUTHOR_NAME_PLACEHOLDER_PY: &str = "name = \"PARSER_AUTHOR_NAME\"";
57const AUTHOR_EMAIL_PLACEHOLDER_PY: &str = ", email = \"PARSER_AUTHOR_EMAIL\"";
58
59const AUTHOR_BLOCK_RS: &str = "\nauthors = [";
60const AUTHOR_NAME_PLACEHOLDER_RS: &str = "PARSER_AUTHOR_NAME";
61const AUTHOR_EMAIL_PLACEHOLDER_RS: &str = " PARSER_AUTHOR_EMAIL";
62
63const AUTHOR_BLOCK_JAVA: &str = "\n    <developer>";
64const AUTHOR_NAME_PLACEHOLDER_JAVA: &str = "\n      <name>PARSER_AUTHOR_NAME</name>";
65const AUTHOR_EMAIL_PLACEHOLDER_JAVA: &str = "\n      <email>PARSER_AUTHOR_EMAIL</email>";
66const AUTHOR_URL_PLACEHOLDER_JAVA: &str = "\n      <url>PARSER_AUTHOR_URL</url>";
67
68const AUTHOR_BLOCK_GRAMMAR: &str = "\n * @author ";
69const AUTHOR_NAME_PLACEHOLDER_GRAMMAR: &str = "PARSER_AUTHOR_NAME";
70const AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR: &str = " PARSER_AUTHOR_EMAIL";
71
72const FUNDING_URL_PLACEHOLDER: &str = "FUNDING_URL";
73
74const HIGHLIGHTS_QUERY_PATH_PLACEHOLDER: &str = "HIGHLIGHTS_QUERY_PATH";
75const INJECTIONS_QUERY_PATH_PLACEHOLDER: &str = "INJECTIONS_QUERY_PATH";
76const LOCALS_QUERY_PATH_PLACEHOLDER: &str = "LOCALS_QUERY_PATH";
77const TAGS_QUERY_PATH_PLACEHOLDER: &str = "TAGS_QUERY_PATH";
78
79const GRAMMAR_JS_TEMPLATE: &str = include_str!("./templates/grammar.js");
80const PACKAGE_JSON_TEMPLATE: &str = include_str!("./templates/package.json");
81const GITIGNORE_TEMPLATE: &str = include_str!("./templates/gitignore");
82const GITATTRIBUTES_TEMPLATE: &str = include_str!("./templates/gitattributes");
83const EDITORCONFIG_TEMPLATE: &str = include_str!("./templates/.editorconfig");
84
85const RUST_BINDING_VERSION: &str = env!("CARGO_PKG_VERSION");
86const RUST_BINDING_VERSION_PLACEHOLDER: &str = "RUST_BINDING_VERSION";
87
88const LIB_RS_TEMPLATE: &str = include_str!("./templates/lib.rs");
89const BUILD_RS_TEMPLATE: &str = include_str!("./templates/build.rs");
90const CARGO_TOML_TEMPLATE: &str = include_str!("./templates/_cargo.toml");
91
92const INDEX_JS_TEMPLATE: &str = include_str!("./templates/index.js");
93const INDEX_D_TS_TEMPLATE: &str = include_str!("./templates/index.d.ts");
94const JS_BINDING_CC_TEMPLATE: &str = include_str!("./templates/js-binding.cc");
95const BINDING_GYP_TEMPLATE: &str = include_str!("./templates/binding.gyp");
96const BINDING_TEST_JS_TEMPLATE: &str = include_str!("./templates/binding_test.js");
97
98const MAKEFILE_TEMPLATE: &str = include_str!("./templates/makefile");
99const CMAKELISTS_TXT_TEMPLATE: &str = include_str!("./templates/cmakelists.cmake");
100const PARSER_NAME_H_TEMPLATE: &str = include_str!("./templates/PARSER_NAME.h");
101const PARSER_NAME_PC_IN_TEMPLATE: &str = include_str!("./templates/PARSER_NAME.pc.in");
102
103const GO_MOD_TEMPLATE: &str = include_str!("./templates/go.mod");
104const BINDING_GO_TEMPLATE: &str = include_str!("./templates/binding.go");
105const BINDING_TEST_GO_TEMPLATE: &str = include_str!("./templates/binding_test.go");
106
107const SETUP_PY_TEMPLATE: &str = include_str!("./templates/setup.py");
108const INIT_PY_TEMPLATE: &str = include_str!("./templates/__init__.py");
109const INIT_PYI_TEMPLATE: &str = include_str!("./templates/__init__.pyi");
110const PYPROJECT_TOML_TEMPLATE: &str = include_str!("./templates/pyproject.toml");
111const PY_BINDING_C_TEMPLATE: &str = include_str!("./templates/py-binding.c");
112const TEST_BINDING_PY_TEMPLATE: &str = include_str!("./templates/test_binding.py");
113
114const PACKAGE_SWIFT_TEMPLATE: &str = include_str!("./templates/package.swift");
115const TESTS_SWIFT_TEMPLATE: &str = include_str!("./templates/tests.swift");
116
117const POM_XML_TEMPLATE: &str = include_str!("./templates/pom.xml");
118const BINDING_JAVA_TEMPLATE: &str = include_str!("./templates/binding.java");
119const TEST_JAVA_TEMPLATE: &str = include_str!("./templates/test.java");
120
121const BUILD_ZIG_TEMPLATE: &str = include_str!("./templates/build.zig");
122const BUILD_ZIG_ZON_TEMPLATE: &str = include_str!("./templates/build.zig.zon");
123const ROOT_ZIG_TEMPLATE: &str = include_str!("./templates/root.zig");
124const TEST_ZIG_TEMPLATE: &str = include_str!("./templates/test.zig");
125
126pub const TREE_SITTER_JSON_SCHEMA: &str =
127    "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json";
128
129#[derive(Serialize, Deserialize, Clone)]
130pub struct JsonConfigOpts {
131    pub name: String,
132    pub camelcase: String,
133    pub title: String,
134    pub description: String,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub repository: Option<String>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub funding: Option<String>,
139    pub scope: String,
140    pub file_types: Vec<String>,
141    pub version: Version,
142    pub license: String,
143    pub author: String,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub email: Option<String>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub url: Option<String>,
148    pub namespace: Option<String>,
149    pub bindings: Bindings,
150}
151
152impl JsonConfigOpts {
153    #[must_use]
154    pub fn to_tree_sitter_json(self) -> TreeSitterJSON {
155        TreeSitterJSON {
156            schema: Some(TREE_SITTER_JSON_SCHEMA.to_string()),
157            grammars: vec![Grammar {
158                name: self.name.clone(),
159                camelcase: Some(self.camelcase),
160                title: Some(self.title),
161                scope: self.scope,
162                path: None,
163                external_files: PathsJSON::Empty,
164                file_types: Some(self.file_types),
165                highlights: PathsJSON::Empty,
166                injections: PathsJSON::Empty,
167                locals: PathsJSON::Empty,
168                tags: PathsJSON::Empty,
169                injection_regex: Some(format!("^{}$", self.name)),
170                first_line_regex: None,
171                content_regex: None,
172                class_name: Some(format!("TreeSitter{}", self.name.to_upper_camel_case())),
173            }],
174            metadata: Metadata {
175                version: self.version,
176                license: Some(self.license),
177                description: Some(self.description),
178                authors: Some(vec![Author {
179                    name: self.author,
180                    email: self.email,
181                    url: self.url,
182                }]),
183                links: Some(Links {
184                    repository: self.repository.unwrap_or_else(|| {
185                        format!("https://github.com/tree-sitter/tree-sitter-{}", self.name)
186                    }),
187                    funding: self.funding,
188                }),
189                namespace: self.namespace,
190            },
191            bindings: self.bindings,
192        }
193    }
194}
195
196impl Default for JsonConfigOpts {
197    fn default() -> Self {
198        Self {
199            name: String::new(),
200            camelcase: String::new(),
201            title: String::new(),
202            description: String::new(),
203            repository: None,
204            funding: None,
205            scope: String::new(),
206            file_types: vec![],
207            version: Version::from_str("0.1.0").unwrap(),
208            license: String::new(),
209            author: String::new(),
210            email: None,
211            url: None,
212            namespace: None,
213            bindings: Bindings::default(),
214        }
215    }
216}
217
218struct GenerateOpts<'a> {
219    author_name: Option<&'a str>,
220    author_email: Option<&'a str>,
221    author_url: Option<&'a str>,
222    license: Option<&'a str>,
223    description: Option<&'a str>,
224    repository: Option<&'a str>,
225    funding: Option<&'a str>,
226    version: &'a Version,
227    camel_parser_name: &'a str,
228    title_parser_name: &'a str,
229    class_name: &'a str,
230    highlights_query_path: &'a str,
231    injections_query_path: &'a str,
232    locals_query_path: &'a str,
233    tags_query_path: &'a str,
234    namespace: Option<&'a str>,
235}
236
237pub fn generate_grammar_files(
238    repo_path: &Path,
239    language_name: &str,
240    allow_update: bool,
241    opts: Option<&JsonConfigOpts>,
242) -> Result<()> {
243    let dashed_language_name = language_name.to_kebab_case();
244
245    let tree_sitter_config = missing_path_else(
246        repo_path.join("tree-sitter.json"),
247        true,
248        |path| {
249            // invariant: opts is always Some when `tree-sitter.json` doesn't exist
250            let Some(opts) = opts else { unreachable!() };
251
252            let tree_sitter_json = opts.clone().to_tree_sitter_json();
253            write_file(path, serde_json::to_string_pretty(&tree_sitter_json)?)?;
254            Ok(())
255        },
256        |path| {
257            // updating the config, if needed
258            if let Some(opts) = opts {
259                let tree_sitter_json = opts.clone().to_tree_sitter_json();
260                write_file(path, serde_json::to_string_pretty(&tree_sitter_json)?)?;
261            }
262            Ok(())
263        },
264    )?;
265
266    let tree_sitter_config = serde_json::from_str::<TreeSitterJSON>(
267        &fs::read_to_string(tree_sitter_config.as_path())
268            .with_context(|| "Failed to read tree-sitter.json")?,
269    )?;
270
271    let authors = tree_sitter_config.metadata.authors.as_ref();
272    let camel_name = tree_sitter_config.grammars[0]
273        .camelcase
274        .clone()
275        .unwrap_or_else(|| language_name.to_upper_camel_case());
276    let title_name = tree_sitter_config.grammars[0]
277        .title
278        .clone()
279        .unwrap_or_else(|| language_name.to_upper_camel_case());
280    let class_name = tree_sitter_config.grammars[0]
281        .class_name
282        .clone()
283        .unwrap_or_else(|| format!("TreeSitter{}", language_name.to_upper_camel_case()));
284
285    let default_highlights_path = Path::new("queries").join(DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME);
286    let default_injections_path = Path::new("queries").join(DEFAULT_INJECTIONS_QUERY_FILE_NAME);
287    let default_locals_path = Path::new("queries").join(DEFAULT_LOCALS_QUERY_FILE_NAME);
288    let default_tags_path = Path::new("queries").join(DEFAULT_TAGS_QUERY_FILE_NAME);
289
290    let generate_opts = GenerateOpts {
291        author_name: authors
292            .map(|a| a.first().map(|a| a.name.as_str()))
293            .unwrap_or_default(),
294        author_email: authors
295            .map(|a| a.first().and_then(|a| a.email.as_deref()))
296            .unwrap_or_default(),
297        author_url: authors
298            .map(|a| a.first().and_then(|a| a.url.as_deref()))
299            .unwrap_or_default(),
300        license: tree_sitter_config.metadata.license.as_deref(),
301        description: tree_sitter_config.metadata.description.as_deref(),
302        repository: tree_sitter_config
303            .metadata
304            .links
305            .as_ref()
306            .map(|l| l.repository.as_str()),
307        funding: tree_sitter_config
308            .metadata
309            .links
310            .as_ref()
311            .and_then(|l| l.funding.as_deref()),
312        version: &tree_sitter_config.metadata.version,
313        camel_parser_name: &camel_name,
314        title_parser_name: &title_name,
315        class_name: &class_name,
316        highlights_query_path: tree_sitter_config.grammars[0]
317            .highlights
318            .to_variable_value(&default_highlights_path),
319        injections_query_path: tree_sitter_config.grammars[0]
320            .injections
321            .to_variable_value(&default_injections_path),
322        locals_query_path: tree_sitter_config.grammars[0]
323            .locals
324            .to_variable_value(&default_locals_path),
325        tags_query_path: tree_sitter_config.grammars[0]
326            .tags
327            .to_variable_value(&default_tags_path),
328        namespace: tree_sitter_config.metadata.namespace.as_deref(),
329    };
330
331    // Create package.json
332    missing_path_else(
333        repo_path.join("package.json"),
334        allow_update,
335        |path| {
336            generate_file(
337                path,
338                PACKAGE_JSON_TEMPLATE,
339                dashed_language_name.as_str(),
340                &generate_opts,
341            )
342        },
343        |path| {
344            let mut contents = fs::read_to_string(path)?
345                .replace(
346                    r#""node-addon-api": "^8.3.1""#,
347                    r#""node-addon-api": "^8.5.0""#,
348                )
349                .replace(
350                    indoc! {r#"
351                    "prebuildify": "^6.0.1",
352                    "tree-sitter-cli":"#},
353                    indoc! {r#"
354                    "prebuildify": "^6.0.1",
355                    "tree-sitter": "^0.25.0",
356                    "tree-sitter-cli":"#},
357                );
358            if !contents.contains("module") {
359                info!("Migrating package.json to ESM");
360                contents = contents.replace(
361                    r#""repository":"#,
362                    indoc! {r#"
363                    "type": "module",
364                      "repository":"#},
365                );
366            }
367            write_file(path, contents)?;
368            Ok(())
369        },
370    )?;
371
372    // Do not create a grammar.js file in a repo with multiple language configs
373    if !tree_sitter_config.has_multiple_language_configs() {
374        missing_path_else(
375            repo_path.join("grammar.js"),
376            allow_update,
377            |path| generate_file(path, GRAMMAR_JS_TEMPLATE, language_name, &generate_opts),
378            |path| {
379                let mut contents = fs::read_to_string(path)?;
380                if contents.contains("module.exports") {
381                    info!("Migrating grammars.js to ESM");
382                    contents = contents.replace("module.exports =", "export default");
383                    write_file(path, contents)?;
384                }
385
386                Ok(())
387            },
388        )?;
389    }
390
391    // Write .gitignore file
392    missing_path_else(
393        repo_path.join(".gitignore"),
394        allow_update,
395        |path| generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts),
396        |path| {
397            let mut contents = fs::read_to_string(path)?;
398            if !contents.contains("Zig artifacts") {
399                info!("Adding zig entries to .gitignore");
400                contents.push('\n');
401                contents.push_str(indoc! {"
402                # Zig artifacts
403                .zig-cache/
404                zig-cache/
405                zig-out/
406                "});
407            }
408            Ok(())
409        },
410    )?;
411
412    // Write .gitattributes file
413    missing_path_else(
414        repo_path.join(".gitattributes"),
415        allow_update,
416        |path| generate_file(path, GITATTRIBUTES_TEMPLATE, language_name, &generate_opts),
417        |path| {
418            let mut contents = fs::read_to_string(path)?;
419            let c_bindings_entry = "bindings/c/* ";
420            if contents.contains(c_bindings_entry) {
421                info!("Updating c bindings entry in .gitattributes");
422                contents = contents.replace(c_bindings_entry, "bindings/c/** ");
423            }
424            if !contents.contains("Zig bindings") {
425                info!("Adding zig entries to .gitattributes");
426                contents.push('\n');
427                contents.push_str(indoc! {"
428                # Zig bindings
429                build.zig linguist-generated
430                build.zig.zon linguist-generated
431                "});
432            }
433            write_file(path, contents)?;
434            Ok(())
435        },
436    )?;
437
438    // Write .editorconfig file
439    missing_path(repo_path.join(".editorconfig"), |path| {
440        generate_file(path, EDITORCONFIG_TEMPLATE, language_name, &generate_opts)
441    })?;
442
443    let bindings_dir = repo_path.join("bindings");
444
445    // Generate Rust bindings
446    if tree_sitter_config.bindings.rust {
447        missing_path(bindings_dir.join("rust"), create_dir)?.apply(|path| {
448            missing_path_else(path.join("lib.rs"), allow_update, |path| {
449                generate_file(path, LIB_RS_TEMPLATE, language_name, &generate_opts)
450            }, |path| {
451                let mut contents = fs::read_to_string(path)?;
452                if !contents.contains("#[cfg(with_highlights_query)]") {
453                    info!("Updating query constants in bindings/rust/lib.rs");
454                    let replacement = indoc! {r#"
455                        #[cfg(with_highlights_query)]
456                        /// The syntax highlighting query for this grammar.
457                        pub const HIGHLIGHTS_QUERY: &str = include_str!("../../HIGHLIGHTS_QUERY_PATH");
458
459                        #[cfg(with_injections_query)]
460                        /// The language injection query for this grammar.
461                        pub const INJECTIONS_QUERY: &str = include_str!("../../INJECTIONS_QUERY_PATH");
462
463                        #[cfg(with_locals_query)]
464                        /// The local variable query for this grammar.
465                        pub const LOCALS_QUERY: &str = include_str!("../../LOCALS_QUERY_PATH");
466
467                        #[cfg(with_tags_query)]
468                        /// The symbol tagging query for this grammar.
469                        pub const TAGS_QUERY: &str = include_str!("../../TAGS_QUERY_PATH");
470                        "#}
471                        .replace(HIGHLIGHTS_QUERY_PATH_PLACEHOLDER, &generate_opts.highlights_query_path.replace('\\', "/"))
472                        .replace(INJECTIONS_QUERY_PATH_PLACEHOLDER, &generate_opts.injections_query_path.replace('\\', "/"))
473                        .replace(LOCALS_QUERY_PATH_PLACEHOLDER, &generate_opts.locals_query_path.replace('\\', "/"))
474                        .replace(TAGS_QUERY_PATH_PLACEHOLDER, &generate_opts.tags_query_path.replace('\\', "/"));
475                    contents = contents
476                        .replace(
477                            indoc! {r#"
478                            // NOTE: uncomment these to include any queries that this grammar contains:
479
480                            // pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
481                            // pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
482                            // pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
483                            // pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
484                            "#},
485                            &replacement,
486                        );
487                }
488                write_file(path, contents)?;
489                Ok(())
490            })?;
491
492            missing_path_else(
493                path.join("build.rs"),
494                allow_update,
495                |path| generate_file(path, BUILD_RS_TEMPLATE, language_name, &generate_opts),
496                |path| {
497                    let mut contents = fs::read_to_string(path)?;
498                    if !contents.contains("wasm32-unknown-unknown") {
499                        info!("Adding wasm32-unknown-unknown target to bindings/rust/build.rs");
500                        let replacement = indoc!{r#"
501                            c_config.flag("-utf-8");
502
503                            if std::env::var("TARGET").unwrap() == "wasm32-unknown-unknown" {
504                                let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
505                                    panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
506                                };
507                                let Ok(wasm_src) =
508                                    std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
509                                else {
510                                    panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
511                                };
512
513                                c_config.include(&wasm_headers);
514                                c_config.files([
515                                    wasm_src.join("stdio.c"),
516                                    wasm_src.join("stdlib.c"),
517                                    wasm_src.join("string.c"),
518                                ]);
519                            }
520                        "#}
521                            .lines()
522                            .map(|line| if line.is_empty() { line.to_string() } else { format!("    {line}") })
523                            .collect::<Vec<_>>()
524                            .join("\n");
525
526                        contents = contents.replace(r#"    c_config.flag("-utf-8");"#, &replacement);
527                    }
528
529                    // Introduce configuration variables for dynamic query inclusion
530                    if !contents.contains("with_highlights_query") {
531                        info!("Adding support for dynamic query inclusion to bindings/rust/build.rs");
532                        let replaced = indoc! {r#"
533                                c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
534                            }"#}
535                            .replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case());
536
537                        let replacement = indoc! {r#"
538                                c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
539
540                                println!("cargo:rustc-check-cfg=cfg(with_highlights_query)");
541                                if !"HIGHLIGHTS_QUERY_PATH".is_empty() && std::path::Path::new("HIGHLIGHTS_QUERY_PATH").exists() {
542                                    println!("cargo:rustc-cfg=with_highlights_query");
543                                }
544                                println!("cargo:rustc-check-cfg=cfg(with_injections_query)");
545                                if !"INJECTIONS_QUERY_PATH".is_empty() && std::path::Path::new("INJECTIONS_QUERY_PATH").exists() {
546                                    println!("cargo:rustc-cfg=with_injections_query");
547                                }
548                                println!("cargo:rustc-check-cfg=cfg(with_locals_query)");
549                                if !"LOCALS_QUERY_PATH".is_empty() && std::path::Path::new("LOCALS_QUERY_PATH").exists() {
550                                    println!("cargo:rustc-cfg=with_locals_query");
551                                }
552                                println!("cargo:rustc-check-cfg=cfg(with_tags_query)");
553                                if !"TAGS_QUERY_PATH".is_empty() && std::path::Path::new("TAGS_QUERY_PATH").exists() {
554                                    println!("cargo:rustc-cfg=with_tags_query");
555                                }
556                            }"#}
557                            .replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case())
558                            .replace(HIGHLIGHTS_QUERY_PATH_PLACEHOLDER, &generate_opts.highlights_query_path.replace('\\', "/"))
559                            .replace(INJECTIONS_QUERY_PATH_PLACEHOLDER, &generate_opts.injections_query_path.replace('\\', "/"))
560                            .replace(LOCALS_QUERY_PATH_PLACEHOLDER, &generate_opts.locals_query_path.replace('\\', "/"))
561                            .replace(TAGS_QUERY_PATH_PLACEHOLDER, &generate_opts.tags_query_path.replace('\\', "/"));
562
563                        contents = contents.replace(
564                            &replaced,
565                            &replacement,
566                        );
567                    }
568
569                    write_file(path, contents)?;
570                    Ok(())
571                },
572            )?;
573
574            missing_path_else(
575                repo_path.join("Cargo.toml"),
576                allow_update,
577                |path| {
578                    generate_file(
579                        path,
580                        CARGO_TOML_TEMPLATE,
581                        dashed_language_name.as_str(),
582                        &generate_opts,
583                    )
584                },
585                |path| {
586                    let contents = fs::read_to_string(path)?;
587                    if contents.contains("\"LICENSE\"") {
588                        info!("Adding LICENSE entry to bindings/rust/Cargo.toml");
589                        write_file(path, contents.replace("\"LICENSE\"", "\"/LICENSE\""))?;
590                    }
591                    Ok(())
592                },
593            )?;
594
595            Ok(())
596        })?;
597    }
598
599    // Generate Node bindings
600    if tree_sitter_config.bindings.node {
601        missing_path(bindings_dir.join("node"), create_dir)?.apply(|path| {
602            missing_path_else(
603                path.join("index.js"),
604                allow_update,
605                |path| generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts),
606                |path| {
607                    let contents = fs::read_to_string(path)?;
608                    if !contents.contains("Object.defineProperty") {
609                        info!("Replacing index.js");
610                        generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts)?;
611                    }
612                    Ok(())
613                },
614            )?;
615
616            missing_path_else(
617                path.join("index.d.ts"),
618                allow_update,
619                |path| generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts),
620                |path| {
621                    let contents = fs::read_to_string(path)?;
622                    if !contents.contains("export default binding") {
623                        info!("Replacing index.d.ts");
624                        generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts)?;
625                    }
626                    Ok(())
627                },
628            )?;
629
630            missing_path_else(
631                path.join("binding_test.js"),
632                allow_update,
633                |path| {
634                    generate_file(
635                        path,
636                        BINDING_TEST_JS_TEMPLATE,
637                        language_name,
638                        &generate_opts,
639                    )
640                },
641                |path| {
642                    let contents = fs::read_to_string(path)?;
643                    if !contents.contains("import") {
644                        info!("Replacing binding_test.js");
645                        generate_file(
646                            path,
647                            BINDING_TEST_JS_TEMPLATE,
648                            language_name,
649                            &generate_opts,
650                        )?;
651                    }
652                    Ok(())
653                },
654            )?;
655
656            missing_path(path.join("binding.cc"), |path| {
657                generate_file(path, JS_BINDING_CC_TEMPLATE, language_name, &generate_opts)
658            })?;
659
660            missing_path_else(
661                repo_path.join("binding.gyp"),
662                allow_update,
663                |path| generate_file(path, BINDING_GYP_TEMPLATE, language_name, &generate_opts),
664                |path| {
665                    let contents = fs::read_to_string(path)?;
666                    if contents.contains("fs.exists(") {
667                        info!("Replacing `fs.exists` calls in binding.gyp");
668                        write_file(path, contents.replace("fs.exists(", "fs.existsSync("))?;
669                    }
670                    Ok(())
671                },
672            )?;
673
674            Ok(())
675        })?;
676    }
677
678    // Generate C bindings
679    if tree_sitter_config.bindings.c {
680        let kebab_case_name = language_name.to_kebab_case();
681        missing_path(bindings_dir.join("c"), create_dir)?.apply(|path| {
682            let header_name = format!("tree-sitter-{kebab_case_name}.h");
683            let old_file = &path.join(&header_name);
684            if allow_update && fs::exists(old_file).unwrap_or(false) {
685                info!("Removing bindings/c/{header_name}");
686                fs::remove_file(old_file)?;
687            }
688            missing_path(path.join("tree_sitter"), create_dir)?.apply(|include_path| {
689                missing_path(
690                    include_path.join(&header_name),
691                    |path| {
692                        generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
693                    },
694                )?;
695                Ok(())
696            })?;
697
698            missing_path(
699                path.join(format!("tree-sitter-{kebab_case_name}.pc.in")),
700                |path| {
701                    generate_file(
702                        path,
703                        PARSER_NAME_PC_IN_TEMPLATE,
704                        language_name,
705                        &generate_opts,
706                    )
707                },
708            )?;
709
710            missing_path_else(
711                repo_path.join("Makefile"),
712                allow_update,
713                |path| {
714                    generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)
715                },
716                |path| {
717                    let mut contents = fs::read_to_string(path)?;
718                    if !contents.contains("cd '$(DESTDIR)$(LIBDIR)' && ln -sf") {
719                        info!("Replacing Makefile");
720                        generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)?;
721                    } else {
722                        let replaced = indoc! {r"
723                            $(PARSER): $(SRC_DIR)/grammar.json
724                                    $(TS) generate $^
725                            "};
726                        if contents.contains(replaced) {
727                            info!("Adding --no-parser target to Makefile");
728                            contents = contents
729                                .replace(
730                                    replaced,
731                                    indoc! {r"
732                                    $(SRC_DIR)/grammar.json: grammar.js
733                                            $(TS) generate --no-parser $^
734
735                                    $(PARSER): $(SRC_DIR)/grammar.json
736                                            $(TS) generate $^
737                                    "}
738                                );
739                        }
740                        if !contents.contains("\nDESCRIPTION :=") {
741                            if let Some(version_line) = contents.lines().find(|l| l.starts_with("VERSION := ")) {
742                                info!("Adding DESCRIPTION to Makefile");
743                                let description = generate_opts.description.map_or_else(
744                                    || format!("{} grammar for tree-sitter", generate_opts.camel_parser_name),
745                                    str::to_string,
746                                );
747                                contents = contents.replace(
748                                    version_line,
749                                    &format!("{version_line}\nDESCRIPTION := {description}"),
750                                );
751                            }
752                        }
753                        write_file(path, contents)?;
754                    }
755                    Ok(())
756                },
757            )?;
758
759            missing_path_else(
760                repo_path.join("CMakeLists.txt"),
761                allow_update,
762                |path| generate_file(path, CMAKELISTS_TXT_TEMPLATE, language_name, &generate_opts),
763                |path| {
764                    let contents = fs::read_to_string(path)?;
765                    let replaced_contents = contents
766                        .replace("add_custom_target(test", "add_custom_target(ts-test")
767                        .replace(
768                            &formatdoc! {r#"
769                            install(FILES bindings/c/tree-sitter-{language_name}.h
770                                    DESTINATION "${{CMAKE_INSTALL_INCLUDEDIR}}/tree_sitter")
771                            "#},
772                            indoc! {r#"
773                            install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bindings/c/tree_sitter"
774                                    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
775                                    FILES_MATCHING PATTERN "*.h")
776                            "#}
777                        ).replace(
778                            &format!("target_include_directories(tree-sitter-{language_name} PRIVATE src)"),
779                            &formatdoc! {"
780                            target_include_directories(tree-sitter-{language_name}
781                                                       PRIVATE src
782                                                       INTERFACE $<BUILD_INTERFACE:${{CMAKE_CURRENT_SOURCE_DIR}}/bindings/c>
783                                                                 $<INSTALL_INTERFACE:${{CMAKE_INSTALL_INCLUDEDIR}}>)
784                            "}
785                        ).replace(
786                            indoc! {r#"
787                            add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
788                                               DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
789                                               COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
790                                                        --abi=${TREE_SITTER_ABI_VERSION}
791                                               WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
792                                               COMMENT "Generating parser.c")
793                            "#},
794                            indoc! {r#"
795                            add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
796                                                      "${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"
797                                               DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js"
798                                               COMMAND "${TREE_SITTER_CLI}" generate grammar.js --no-parser
799                                               WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
800                                               COMMENT "Generating grammar.json")
801
802                            add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
803                                               BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/parser.h"
804                                                          "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/alloc.h"
805                                                          "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/array.h"
806                                               DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
807                                               COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
808                                                        --abi=${TREE_SITTER_ABI_VERSION}
809                                               WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
810                                               COMMENT "Generating parser.c")
811                            "#}
812                        );
813                    if !replaced_contents.eq(&contents) {
814                        info!("Updating CMakeLists.txt");
815                        write_file(path, replaced_contents)?;
816                    }
817                    Ok(())
818                },
819            )?;
820
821            Ok(())
822        })?;
823    }
824
825    // Generate Go bindings
826    if tree_sitter_config.bindings.go {
827        missing_path(bindings_dir.join("go"), create_dir)?.apply(|path| {
828            missing_path(path.join("binding.go"), |path| {
829                generate_file(path, BINDING_GO_TEMPLATE, language_name, &generate_opts)
830            })?;
831
832            missing_path(path.join("binding_test.go"), |path| {
833                generate_file(
834                    path,
835                    BINDING_TEST_GO_TEMPLATE,
836                    language_name,
837                    &generate_opts,
838                )
839            })?;
840
841            missing_path(repo_path.join("go.mod"), |path| {
842                generate_file(path, GO_MOD_TEMPLATE, language_name, &generate_opts)
843            })?;
844
845            Ok(())
846        })?;
847    }
848
849    // Generate Python bindings
850    if tree_sitter_config.bindings.python {
851        missing_path(bindings_dir.join("python"), create_dir)?.apply(|path| {
852            let snake_case_grammar_name = format!("tree_sitter_{}", language_name.to_snake_case());
853            let lang_path = path.join(&snake_case_grammar_name);
854            missing_path(&lang_path, create_dir)?;
855
856            missing_path_else(
857                lang_path.join("binding.c"),
858                allow_update,
859                |path| generate_file(path, PY_BINDING_C_TEMPLATE, language_name, &generate_opts),
860                |path| {
861                    let mut contents = fs::read_to_string(path)?;
862                    if !contents.contains("PyModuleDef_Init") {
863                        info!("Updating bindings/python/{snake_case_grammar_name}/binding.c");
864                        contents = contents
865                            .replace("PyModule_Create", "PyModuleDef_Init")
866                            .replace(
867                                "static PyMethodDef methods[] = {\n",
868                                indoc! {"
869                                static struct PyModuleDef_Slot slots[] = {
870                                #ifdef Py_GIL_DISABLED
871                                    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
872                                #endif
873                                    {0, NULL}
874                                };
875
876                                static PyMethodDef methods[] = {
877                                "},
878                            )
879                            .replace(
880                                indoc! {"
881                                .m_size = -1,
882                                    .m_methods = methods
883                                "},
884                                indoc! {"
885                                .m_size = 0,
886                                    .m_methods = methods,
887                                    .m_slots = slots,
888                                "},
889                            );
890                        write_file(path, contents)?;
891                    }
892                    Ok(())
893                },
894            )?;
895
896            missing_path_else(
897                lang_path.join("__init__.py"),
898                allow_update,
899                |path| {
900                    generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)
901                },
902                |path| {
903                    let contents = fs::read_to_string(path)?;
904                    if contents.contains("uncomment these to include any queries") {
905                        info!("Replacing __init__.py");
906                        generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)?;
907                    }
908                    Ok(())
909                },
910            )?;
911
912            missing_path_else(
913                lang_path.join("__init__.pyi"),
914                allow_update,
915                |path| generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts),
916                |path| {
917                    let mut contents = fs::read_to_string(path)?;
918                    if contents.contains("uncomment these to include any queries") {
919                        info!("Replacing __init__.pyi");
920                        generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts)?;
921                    } else if !contents.contains("CapsuleType") {
922                        info!("Updating __init__.pyi");
923                        contents = contents
924                            .replace(
925                                "from typing import Final",
926                                "from typing import Final\nfrom typing_extensions import CapsuleType"
927                            )
928                            .replace("-> object:", "-> CapsuleType:");
929                        write_file(path, contents)?;
930                    }
931                    Ok(())
932                },
933            )?;
934
935            missing_path(lang_path.join("py.typed"), |path| {
936                generate_file(path, "", language_name, &generate_opts) // py.typed is empty
937            })?;
938
939            missing_path(path.join("tests"), create_dir)?.apply(|path| {
940                missing_path_else(
941                    path.join("test_binding.py"),
942                    allow_update,
943                    |path| {
944                        generate_file(
945                            path,
946                            TEST_BINDING_PY_TEMPLATE,
947                            language_name,
948                            &generate_opts,
949                        )
950                    },
951                    |path| {
952                        let mut contents = fs::read_to_string(path)?;
953                        if !contents.contains("Parser(Language(") {
954                            info!("Updating Language function in bindings/python/tests/test_binding.py");
955                            contents = contents
956                                .replace("tree_sitter.Language(", "Parser(Language(")
957                                .replace(".language())\n", ".language()))\n")
958                                .replace(
959                                    "import tree_sitter\n",
960                                    "from tree_sitter import Language, Parser\n",
961                                );
962                            write_file(path, contents)?;
963                        }
964                        Ok(())
965                    },
966                )?;
967                Ok(())
968            })?;
969
970            missing_path_else(
971                repo_path.join("setup.py"),
972                allow_update,
973                |path| generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts),
974                |path| {
975                    let mut contents = fs::read_to_string(path)?;
976                    if !contents.contains("build_ext") {
977                        info!("Replacing setup.py");
978                        generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts)?;
979                    } else {
980                        if !contents.contains(" and not get_config_var") {
981                            info!("Updating Python free-threading support in setup.py");
982                            contents = contents.replace(
983                                r#"startswith("cp"):"#,
984                                r#"startswith("cp") and not get_config_var("Py_GIL_DISABLED"):"#
985                            );
986                            write_file(path, &contents)?;
987                        }
988                        if !contents.contains("include(\"src/*.c\")") {
989                            info!("Updating sdist file list in setup.py");
990                            let contents = contents.replace(
991                               "include(\"src/tree_sitter/*.h\")",
992                               "include(\"src/tree_sitter/*.h\")\n        self.filelist.include(\"src/*.c\")",
993                            );
994                            write_file(path, &contents)?;
995                        }
996                    }
997
998                    Ok(())
999                },
1000            )?;
1001
1002            missing_path_else(
1003                repo_path.join("pyproject.toml"),
1004                allow_update,
1005                |path| {
1006                    generate_file(
1007                        path,
1008                        PYPROJECT_TOML_TEMPLATE,
1009                        dashed_language_name.as_str(),
1010                        &generate_opts,
1011                    )
1012                },
1013                |path| {
1014                    let mut contents = fs::read_to_string(path)?;
1015                    if !contents.contains("cp310-*") {
1016                        info!("Updating dependencies in pyproject.toml");
1017                        contents = contents
1018                            .replace(r#"build = "cp39-*""#, r#"build = "cp310-*""#)
1019                            .replace(r#"python = ">=3.9""#, r#"python = ">=3.10""#)
1020                            .replace("tree-sitter~=0.22", "tree-sitter~=0.24");
1021                        write_file(path, contents)?;
1022                    }
1023                    Ok(())
1024                },
1025            )?;
1026
1027            Ok(())
1028        })?;
1029    }
1030
1031    // Generate Swift bindings
1032    if tree_sitter_config.bindings.swift {
1033        missing_path(bindings_dir.join("swift"), create_dir)?.apply(|path| {
1034            let lang_path = path.join(&class_name);
1035            missing_path(&lang_path, create_dir)?;
1036
1037            missing_path(lang_path.join(format!("{language_name}.h")), |path| {
1038                generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
1039            })?;
1040
1041            missing_path(path.join(format!("{class_name}Tests")), create_dir)?.apply(|path| {
1042                missing_path(path.join(format!("{class_name}Tests.swift")), |path| {
1043                    generate_file(path, TESTS_SWIFT_TEMPLATE, language_name, &generate_opts)
1044                })?;
1045
1046                Ok(())
1047            })?;
1048
1049            missing_path_else(
1050                repo_path.join("Package.swift"),
1051                allow_update,
1052                |path| generate_file(path, PACKAGE_SWIFT_TEMPLATE, language_name, &generate_opts),
1053                |path| {
1054                    let contents = fs::read_to_string(path)?;
1055                    let replaced_contents = contents
1056                        .replace(
1057                            "https://github.com/ChimeHQ/SwiftTreeSitter",
1058                            "https://github.com/tree-sitter/swift-tree-sitter",
1059                        )
1060                        .replace("version: \"0.8.0\")", "version: \"0.10.0\")")
1061                        .replace("version: \"0.9.0\")", "version: \"0.10.0\")")
1062                        .replace("(name: \"SwiftTreeSitter\", url:", "(url:")
1063                        .replace(
1064                            "    \"SwiftTreeSitter\"",
1065                            "    .product(name: \"SwiftTreeSitter\", package: \"swift-tree-sitter\")",
1066                        );
1067                    if !replaced_contents.eq(&contents) {
1068                        info!("Updating tree-sitter dependency in Package.swift");
1069                        write_file(path, replaced_contents)?;
1070                    }
1071                    Ok(())
1072                },
1073            )?;
1074
1075            Ok(())
1076        })?;
1077    }
1078
1079    // Generate Zig bindings
1080    if tree_sitter_config.bindings.zig {
1081        missing_path_else(
1082            repo_path.join("build.zig"),
1083            allow_update,
1084            |path| generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts),
1085            |path| {
1086                let contents = fs::read_to_string(path)?;
1087                if !contents.contains("b.pkg_hash.len") {
1088                    info!("Replacing build.zig");
1089                    generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts)
1090                } else {
1091                    Ok(())
1092                }
1093            },
1094        )?;
1095
1096        missing_path_else(
1097            repo_path.join("build.zig.zon"),
1098            allow_update,
1099            |path| generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts),
1100            |path| {
1101                let contents = fs::read_to_string(path)?;
1102                if !contents.contains(".name = .tree_sitter_") {
1103                    info!("Replacing build.zig.zon");
1104                    generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts)
1105                } else {
1106                    Ok(())
1107                }
1108            },
1109        )?;
1110
1111        missing_path(bindings_dir.join("zig"), create_dir)?.apply(|path| {
1112            missing_path_else(
1113                path.join("root.zig"),
1114                allow_update,
1115                |path| generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts),
1116                |path| {
1117                    let contents = fs::read_to_string(path)?;
1118                    if contents.contains("ts.Language") {
1119                        info!("Replacing root.zig");
1120                        generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts)
1121                    } else {
1122                        Ok(())
1123                    }
1124                },
1125            )?;
1126
1127            missing_path(path.join("test.zig"), |path| {
1128                generate_file(path, TEST_ZIG_TEMPLATE, language_name, &generate_opts)
1129            })?;
1130
1131            Ok(())
1132        })?;
1133    }
1134
1135    // Generate Java bindings
1136    if tree_sitter_config.bindings.java {
1137        missing_path(repo_path.join("pom.xml"), |path| {
1138            generate_file(path, POM_XML_TEMPLATE, language_name, &generate_opts)
1139        })?;
1140
1141        missing_path(bindings_dir.join("java"), create_dir)?.apply(|path| {
1142            missing_path(path.join("main"), create_dir)?.apply(|path| {
1143                let package_path = generate_opts
1144                    .namespace
1145                    .unwrap_or("io.github.treesitter")
1146                    .replace(['-', '_'], "")
1147                    .split('.')
1148                    .fold(path.to_path_buf(), |path, dir| path.join(dir))
1149                    .join("jtreesitter")
1150                    .join(language_name.to_lowercase().replace('_', ""));
1151                missing_path(package_path, create_dir)?.apply(|path| {
1152                    missing_path(path.join(format!("{class_name}.java")), |path| {
1153                        generate_file(path, BINDING_JAVA_TEMPLATE, language_name, &generate_opts)
1154                    })?;
1155
1156                    Ok(())
1157                })?;
1158
1159                Ok(())
1160            })?;
1161
1162            missing_path(path.join("test"), create_dir)?.apply(|path| {
1163                missing_path(path.join(format!("{class_name}Test.java")), |path| {
1164                    generate_file(path, TEST_JAVA_TEMPLATE, language_name, &generate_opts)
1165                })?;
1166
1167                Ok(())
1168            })?;
1169
1170            Ok(())
1171        })?;
1172    }
1173
1174    Ok(())
1175}
1176
1177pub fn get_root_path(path: &Path) -> Result<PathBuf> {
1178    let mut pathbuf = path.to_owned();
1179    let filename = path.file_name().unwrap().to_str().unwrap();
1180    let is_package_json = filename == "package.json";
1181    loop {
1182        let json = pathbuf
1183            .exists()
1184            .then(|| {
1185                let contents = fs::read_to_string(pathbuf.as_path())
1186                    .with_context(|| format!("Failed to read {filename}"))?;
1187                if is_package_json {
1188                    serde_json::from_str::<Map<String, Value>>(&contents)
1189                        .context(format!("Failed to parse {filename}"))
1190                        .map(|v| v.contains_key("tree-sitter"))
1191                } else {
1192                    serde_json::from_str::<TreeSitterJSON>(&contents)
1193                        .context(format!("Failed to parse {filename}"))
1194                        .map(|_| true)
1195                }
1196            })
1197            .transpose()?;
1198        if json == Some(true) {
1199            return Ok(pathbuf.parent().unwrap().to_path_buf());
1200        }
1201        pathbuf.pop(); // filename
1202        if !pathbuf.pop() {
1203            return Err(anyhow!(format!(
1204                concat!(
1205                    "Failed to locate a {} file,",
1206                    " please ensure you have one, and if you don't then consult the docs",
1207                ),
1208                filename
1209            )));
1210        }
1211        pathbuf.push(filename);
1212    }
1213}
1214
1215fn generate_file(
1216    path: &Path,
1217    template: &str,
1218    language_name: &str,
1219    generate_opts: &GenerateOpts,
1220) -> Result<()> {
1221    let filename = path.file_name().unwrap().to_str().unwrap();
1222
1223    let lower_parser_name = if path
1224        .extension()
1225        .is_some_and(|e| e.eq_ignore_ascii_case("java"))
1226    {
1227        language_name.to_snake_case().replace('_', "")
1228    } else {
1229        language_name.to_snake_case()
1230    };
1231
1232    let mut replacement = template
1233        .replace(
1234            CAMEL_PARSER_NAME_PLACEHOLDER,
1235            generate_opts.camel_parser_name,
1236        )
1237        .replace(
1238            TITLE_PARSER_NAME_PLACEHOLDER,
1239            generate_opts.title_parser_name,
1240        )
1241        .replace(
1242            UPPER_PARSER_NAME_PLACEHOLDER,
1243            &language_name.to_shouty_snake_case(),
1244        )
1245        .replace(
1246            KEBAB_PARSER_NAME_PLACEHOLDER,
1247            &language_name.to_kebab_case(),
1248        )
1249        .replace(LOWER_PARSER_NAME_PLACEHOLDER, &lower_parser_name)
1250        .replace(PARSER_NAME_PLACEHOLDER, language_name)
1251        .replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION)
1252        .replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION)
1253        .replace(ABI_VERSION_MAX_PLACEHOLDER, &ABI_VERSION_MAX.to_string())
1254        .replace(
1255            PARSER_VERSION_PLACEHOLDER,
1256            &generate_opts.version.to_string(),
1257        )
1258        .replace(PARSER_CLASS_NAME_PLACEHOLDER, generate_opts.class_name)
1259        .replace(
1260            HIGHLIGHTS_QUERY_PATH_PLACEHOLDER,
1261            &generate_opts.highlights_query_path.replace('\\', "/"),
1262        )
1263        .replace(
1264            INJECTIONS_QUERY_PATH_PLACEHOLDER,
1265            &generate_opts.injections_query_path.replace('\\', "/"),
1266        )
1267        .replace(
1268            LOCALS_QUERY_PATH_PLACEHOLDER,
1269            &generate_opts.locals_query_path.replace('\\', "/"),
1270        )
1271        .replace(
1272            TAGS_QUERY_PATH_PLACEHOLDER,
1273            &generate_opts.tags_query_path.replace('\\', "/"),
1274        );
1275
1276    if let Some(name) = generate_opts.author_name {
1277        replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER, name);
1278    } else {
1279        match filename {
1280            "package.json" => {
1281                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JS, "");
1282            }
1283            "pyproject.toml" => {
1284                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_PY, "");
1285            }
1286            "grammar.js" => {
1287                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_GRAMMAR, "");
1288            }
1289            "Cargo.toml" => {
1290                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, "");
1291            }
1292            "pom.xml" => {
1293                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JAVA, "");
1294            }
1295            _ => {}
1296        }
1297    }
1298
1299    if let Some(email) = generate_opts.author_email {
1300        replacement = match filename {
1301            "Cargo.toml" | "grammar.js" => {
1302                replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, &format!("<{email}>"))
1303            }
1304            _ => replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, email),
1305        }
1306    } else {
1307        match filename {
1308            "package.json" => {
1309                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JS, "");
1310            }
1311            "pyproject.toml" => {
1312                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_PY, "");
1313            }
1314            "grammar.js" => {
1315                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR, "");
1316            }
1317            "Cargo.toml" => {
1318                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, "");
1319            }
1320            "pom.xml" => {
1321                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JAVA, "");
1322            }
1323            _ => {}
1324        }
1325    }
1326
1327    match (generate_opts.author_url, filename) {
1328        (Some(url), "package.json" | "pom.xml") => {
1329            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url);
1330        }
1331        (None, "package.json") => {
1332            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, "");
1333        }
1334        (None, "pom.xml") => {
1335            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JAVA, "");
1336        }
1337        _ => {}
1338    }
1339
1340    if generate_opts.author_name.is_none()
1341        && generate_opts.author_email.is_none()
1342        && generate_opts.author_url.is_none()
1343    {
1344        match filename {
1345            "package.json" => {
1346                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) {
1347                    if let Some(end_idx) = replacement[start_idx..]
1348                        .find("},")
1349                        .map(|i| i + start_idx + 2)
1350                    {
1351                        replacement.replace_range(start_idx..end_idx, "");
1352                    }
1353                }
1354            }
1355            "pom.xml" => {
1356                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JAVA) {
1357                    if let Some(end_idx) = replacement[start_idx..]
1358                        .find("</developer>")
1359                        .map(|i| i + start_idx + 12)
1360                    {
1361                        replacement.replace_range(start_idx..end_idx, "");
1362                    }
1363                }
1364            }
1365            _ => {}
1366        }
1367    } else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() {
1368        match filename {
1369            "pyproject.toml" => {
1370                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_PY) {
1371                    if let Some(end_idx) = replacement[start_idx..]
1372                        .find("}]")
1373                        .map(|i| i + start_idx + 2)
1374                    {
1375                        replacement.replace_range(start_idx..end_idx, "");
1376                    }
1377                }
1378            }
1379            "grammar.js" => {
1380                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_GRAMMAR) {
1381                    if let Some(end_idx) = replacement[start_idx..]
1382                        .find(" \n")
1383                        .map(|i| i + start_idx + 1)
1384                    {
1385                        replacement.replace_range(start_idx..end_idx, "");
1386                    }
1387                }
1388            }
1389            "Cargo.toml" => {
1390                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_RS) {
1391                    if let Some(end_idx) = replacement[start_idx..]
1392                        .find("\"]")
1393                        .map(|i| i + start_idx + 2)
1394                    {
1395                        replacement.replace_range(start_idx..end_idx, "");
1396                    }
1397                }
1398            }
1399            _ => {}
1400        }
1401    }
1402
1403    if let Some(license) = generate_opts.license {
1404        replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, license);
1405    } else {
1406        replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, "MIT");
1407    }
1408
1409    if let Some(description) = generate_opts.description {
1410        replacement = replacement.replace(PARSER_DESCRIPTION_PLACEHOLDER, description);
1411    } else {
1412        replacement = replacement.replace(
1413            PARSER_DESCRIPTION_PLACEHOLDER,
1414            &format!(
1415                "{} grammar for tree-sitter",
1416                generate_opts.camel_parser_name,
1417            ),
1418        );
1419    }
1420
1421    if let Some(repository) = generate_opts.repository {
1422        replacement = replacement
1423            .replace(
1424                PARSER_URL_STRIPPED_PLACEHOLDER,
1425                &repository.replace("https://", ""),
1426            )
1427            .replace(PARSER_URL_PLACEHOLDER, repository);
1428    } else {
1429        replacement = replacement
1430            .replace(
1431                PARSER_URL_STRIPPED_PLACEHOLDER,
1432                &format!("github.com/tree-sitter/tree-sitter-{language_name}"),
1433            )
1434            .replace(
1435                PARSER_URL_PLACEHOLDER,
1436                &format!("https://github.com/tree-sitter/tree-sitter-{language_name}"),
1437            );
1438    }
1439
1440    if let Some(namespace) = generate_opts.namespace {
1441        replacement = replacement
1442            .replace(
1443                PARSER_NS_CLEANED_PLACEHOLDER,
1444                &namespace.replace(['-', '_'], ""),
1445            )
1446            .replace(PARSER_NS_PLACEHOLDER, namespace);
1447    } else {
1448        replacement = replacement
1449            .replace(PARSER_NS_CLEANED_PLACEHOLDER, "io.github.treesitter")
1450            .replace(PARSER_NS_PLACEHOLDER, "io.github.tree-sitter");
1451    }
1452
1453    if let Some(funding_url) = generate_opts.funding {
1454        match filename {
1455            "pyproject.toml" | "package.json" => {
1456                replacement = replacement.replace(FUNDING_URL_PLACEHOLDER, funding_url);
1457            }
1458            _ => {}
1459        }
1460    } else {
1461        match filename {
1462            "package.json" => {
1463                replacement = replacement.replace("  \"funding\": \"FUNDING_URL\",\n", "");
1464            }
1465            "pyproject.toml" => {
1466                replacement = replacement.replace("Funding = \"FUNDING_URL\"\n", "");
1467            }
1468            _ => {}
1469        }
1470    }
1471
1472    if filename == "build.zig.zon" {
1473        let id = thread_rng().gen_range(1u32..0xFFFF_FFFFu32);
1474        let checksum = crc32(format!("tree_sitter_{language_name}").as_bytes());
1475        replacement = replacement.replace(
1476            PARSER_FINGERPRINT_PLACEHOLDER,
1477            #[cfg(target_endian = "little")]
1478            &format!("0x{checksum:x}{id:x}"),
1479            #[cfg(target_endian = "big")]
1480            &format!("0x{id:x}{checksum:x}"),
1481        );
1482    }
1483
1484    write_file(path, replacement)?;
1485    Ok(())
1486}
1487
1488fn create_dir(path: &Path) -> Result<()> {
1489    fs::create_dir_all(path)
1490        .with_context(|| format!("Failed to create {:?}", path.to_string_lossy()))
1491}
1492
1493#[derive(PartialEq, Eq, Debug)]
1494enum PathState<P>
1495where
1496    P: AsRef<Path>,
1497{
1498    Exists(P),
1499    Missing(P),
1500}
1501
1502#[allow(dead_code)]
1503impl<P> PathState<P>
1504where
1505    P: AsRef<Path>,
1506{
1507    fn exists(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1508        if let Self::Exists(path) = self {
1509            action(path.as_ref())?;
1510        }
1511        Ok(self)
1512    }
1513
1514    fn missing(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1515        if let Self::Missing(path) = self {
1516            action(path.as_ref())?;
1517        }
1518        Ok(self)
1519    }
1520
1521    fn apply(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1522        action(self.as_path())?;
1523        Ok(self)
1524    }
1525
1526    fn apply_state(&self, mut action: impl FnMut(&Self) -> Result<()>) -> Result<&Self> {
1527        action(self)?;
1528        Ok(self)
1529    }
1530
1531    fn as_path(&self) -> &Path {
1532        match self {
1533            Self::Exists(path) | Self::Missing(path) => path.as_ref(),
1534        }
1535    }
1536}
1537
1538fn missing_path<P, F>(path: P, mut action: F) -> Result<PathState<P>>
1539where
1540    P: AsRef<Path>,
1541    F: FnMut(&Path) -> Result<()>,
1542{
1543    let path_ref = path.as_ref();
1544    if !path_ref.exists() {
1545        action(path_ref)?;
1546        Ok(PathState::Missing(path))
1547    } else {
1548        Ok(PathState::Exists(path))
1549    }
1550}
1551
1552fn missing_path_else<P, T, F>(
1553    path: P,
1554    allow_update: bool,
1555    mut action: T,
1556    mut else_action: F,
1557) -> Result<PathState<P>>
1558where
1559    P: AsRef<Path>,
1560    T: FnMut(&Path) -> Result<()>,
1561    F: FnMut(&Path) -> Result<()>,
1562{
1563    let path_ref = path.as_ref();
1564    if !path_ref.exists() {
1565        action(path_ref)?;
1566        Ok(PathState::Missing(path))
1567    } else {
1568        if allow_update {
1569            else_action(path_ref)?;
1570        }
1571        Ok(PathState::Exists(path))
1572    }
1573}