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::warn;
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
126const 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                warn!("Updating package.json");
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                    contents = contents.replace("module.exports =", "export default");
382                    write_file(path, contents)?;
383                }
384
385                Ok(())
386            },
387        )?;
388    }
389
390    // Write .gitignore file
391    missing_path_else(
392        repo_path.join(".gitignore"),
393        allow_update,
394        |path| generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts),
395        |path| {
396            let contents = fs::read_to_string(path)?;
397            if !contents.contains("Zig artifacts") {
398                warn!("Replacing .gitignore");
399                generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts)?;
400            }
401            Ok(())
402        },
403    )?;
404
405    // Write .gitattributes file
406    missing_path_else(
407        repo_path.join(".gitattributes"),
408        allow_update,
409        |path| generate_file(path, GITATTRIBUTES_TEMPLATE, language_name, &generate_opts),
410        |path| {
411            let mut contents = fs::read_to_string(path)?;
412            contents = contents.replace("bindings/c/* ", "bindings/c/** ");
413            if !contents.contains("Zig bindings") {
414                contents.push('\n');
415                contents.push_str(indoc! {"
416                # Zig bindings
417                build.zig linguist-generated
418                build.zig.zon linguist-generated
419                "});
420            }
421            write_file(path, contents)?;
422            Ok(())
423        },
424    )?;
425
426    // Write .editorconfig file
427    missing_path(repo_path.join(".editorconfig"), |path| {
428        generate_file(path, EDITORCONFIG_TEMPLATE, language_name, &generate_opts)
429    })?;
430
431    let bindings_dir = repo_path.join("bindings");
432
433    // Generate Rust bindings
434    if tree_sitter_config.bindings.rust {
435        missing_path(bindings_dir.join("rust"), create_dir)?.apply(|path| {
436            missing_path_else(path.join("lib.rs"), allow_update, |path| {
437                generate_file(path, LIB_RS_TEMPLATE, language_name, &generate_opts)
438            }, |path| {
439                let mut contents = fs::read_to_string(path)?;
440                if !contents.contains("#[cfg(with_highlights_query)]") {
441                let replacement = indoc! {r#"
442                    #[cfg(with_highlights_query)]
443                    /// The syntax highlighting query for this grammar.
444                    pub const HIGHLIGHTS_QUERY: &str = include_str!("../../HIGHLIGHTS_QUERY_PATH");
445
446                    #[cfg(with_injections_query)]
447                    /// The language injection query for this grammar.
448                    pub const INJECTIONS_QUERY: &str = include_str!("../../INJECTIONS_QUERY_PATH");
449
450                    #[cfg(with_locals_query)]
451                    /// The local variable query for this grammar.
452                    pub const LOCALS_QUERY: &str = include_str!("../../LOCALS_QUERY_PATH");
453
454                    #[cfg(with_tags_query)]
455                    /// The symbol tagging query for this grammar.
456                    pub const TAGS_QUERY: &str = include_str!("../../TAGS_QUERY_PATH");
457                    "#}
458                    .replace("HIGHLIGHTS_QUERY_PATH", generate_opts.highlights_query_path)
459                    .replace("INJECTIONS_QUERY_PATH", generate_opts.injections_query_path)
460                    .replace("LOCALS_QUERY_PATH", generate_opts.locals_query_path)
461                    .replace("TAGS_QUERY_PATH", generate_opts.tags_query_path);
462                contents = contents
463                    .replace(
464                        indoc! {r#"
465                        // NOTE: uncomment these to include any queries that this grammar contains:
466
467                        // pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
468                        // pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
469                        // pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
470                        // pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
471                        "#},
472                        &replacement,
473                    );
474                }
475                write_file(path, contents)?;
476                Ok(())
477            })?;
478
479            missing_path_else(
480                path.join("build.rs"),
481                allow_update,
482                |path| generate_file(path, BUILD_RS_TEMPLATE, language_name, &generate_opts),
483                |path| {
484                    let mut contents = fs::read_to_string(path)?;
485                    if !contents.contains("wasm32-unknown-unknown") {
486                        let replacement = indoc!{r#"
487                            c_config.flag("-utf-8");
488
489                            if std::env::var("TARGET").unwrap() == "wasm32-unknown-unknown" {
490                                let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
491                                    panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
492                                };
493                                let Ok(wasm_src) =
494                                    std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
495                                else {
496                                    panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
497                                };
498
499                                c_config.include(&wasm_headers);
500                                c_config.files([
501                                    wasm_src.join("stdio.c"),
502                                    wasm_src.join("stdlib.c"),
503                                    wasm_src.join("string.c"),
504                                ]);
505                            }
506                        "#};
507
508                        let indented_replacement = replacement
509                            .lines()
510                            .map(|line| if line.is_empty() { line.to_string() } else { format!("    {line}") })
511                            .collect::<Vec<_>>()
512                            .join("\n");
513
514                        contents = contents.replace(r#"    c_config.flag("-utf-8");"#, &indented_replacement);
515                    }
516
517                    // Introduce configuration variables for dynamic query inclusion
518                    if !contents.contains("with_highlights_query") {
519                        let replaced = indoc! {r#"
520                                c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
521                            }"#}
522                            .replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case());
523
524                        let replacement = indoc! {r#"
525                                c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
526
527                                println!("cargo:rustc-check-cfg=cfg(with_highlights_query)");
528                                if !"HIGHLIGHTS_QUERY_PATH".is_empty() && std::path::Path::new("HIGHLIGHTS_QUERY_PATH").exists() {
529                                    println!("cargo:rustc-cfg=with_highlights_query");
530                                }
531                                println!("cargo:rustc-check-cfg=cfg(with_injections_query)");
532                                if !"INJECTIONS_QUERY_PATH".is_empty() && std::path::Path::new("INJECTIONS_QUERY_PATH").exists() {
533                                    println!("cargo:rustc-cfg=with_injections_query");
534                                }
535                                println!("cargo:rustc-check-cfg=cfg(with_locals_query)");
536                                if !"LOCALS_QUERY_PATH".is_empty() && std::path::Path::new("LOCALS_QUERY_PATH").exists() {
537                                    println!("cargo:rustc-cfg=with_locals_query");
538                                }
539                                println!("cargo:rustc-check-cfg=cfg(with_tags_query)");
540                                if !"TAGS_QUERY_PATH".is_empty() && std::path::Path::new("TAGS_QUERY_PATH").exists() {
541                                    println!("cargo:rustc-cfg=with_tags_query");
542                                }
543                            }"#}
544                            .replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case())
545                            .replace("HIGHLIGHTS_QUERY_PATH", generate_opts.highlights_query_path)
546                            .replace("INJECTIONS_QUERY_PATH", generate_opts.injections_query_path)
547                            .replace("LOCALS_QUERY_PATH", generate_opts.locals_query_path)
548                            .replace("TAGS_QUERY_PATH", generate_opts.tags_query_path);
549
550                        contents = contents.replace(
551                            &replaced,
552                            &replacement,
553                        );
554                    }
555
556                    write_file(path, contents)?;
557                    Ok(())
558                },
559            )?;
560
561            missing_path_else(
562                repo_path.join("Cargo.toml"),
563                allow_update,
564                |path| {
565                    generate_file(
566                        path,
567                        CARGO_TOML_TEMPLATE,
568                        dashed_language_name.as_str(),
569                        &generate_opts,
570                    )
571                },
572                |path| {
573                    let contents = fs::read_to_string(path)?;
574                    if contents.contains("\"LICENSE\"") {
575                        write_file(path, contents.replace("\"LICENSE\"", "\"/LICENSE\""))?;
576                    }
577                    Ok(())
578                },
579            )?;
580
581            Ok(())
582        })?;
583    }
584
585    // Generate Node bindings
586    if tree_sitter_config.bindings.node {
587        missing_path(bindings_dir.join("node"), create_dir)?.apply(|path| {
588            missing_path_else(
589                path.join("index.js"),
590                allow_update,
591                |path| generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts),
592                |path| {
593                    let contents = fs::read_to_string(path)?;
594                    if !contents.contains("Object.defineProperty") {
595                        warn!("Replacing index.js");
596                        generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts)?;
597                    }
598                    Ok(())
599                },
600            )?;
601
602            missing_path_else(
603                path.join("index.d.ts"),
604                allow_update,
605                |path| generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts),
606                |path| {
607                    let contents = fs::read_to_string(path)?;
608                    if !contents.contains("export default binding") {
609                        warn!("Replacing index.d.ts");
610                        generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts)?;
611                    }
612                    Ok(())
613                },
614            )?;
615
616            missing_path_else(
617                path.join("binding_test.js"),
618                allow_update,
619                |path| {
620                    generate_file(
621                        path,
622                        BINDING_TEST_JS_TEMPLATE,
623                        language_name,
624                        &generate_opts,
625                    )
626                },
627                |path| {
628                    let contents = fs::read_to_string(path)?;
629                    if !contents.contains("import") {
630                        warn!("Replacing binding_test.js");
631                        generate_file(
632                            path,
633                            BINDING_TEST_JS_TEMPLATE,
634                            language_name,
635                            &generate_opts,
636                        )?;
637                    }
638                    Ok(())
639                },
640            )?;
641
642            missing_path(path.join("binding.cc"), |path| {
643                generate_file(path, JS_BINDING_CC_TEMPLATE, language_name, &generate_opts)
644            })?;
645
646            missing_path_else(
647                repo_path.join("binding.gyp"),
648                allow_update,
649                |path| generate_file(path, BINDING_GYP_TEMPLATE, language_name, &generate_opts),
650                |path| {
651                    let contents = fs::read_to_string(path)?;
652                    if contents.contains("fs.exists(") {
653                        write_file(path, contents.replace("fs.exists(", "fs.existsSync("))?;
654                    }
655                    Ok(())
656                },
657            )?;
658
659            Ok(())
660        })?;
661    }
662
663    // Generate C bindings
664    if tree_sitter_config.bindings.c {
665        missing_path(bindings_dir.join("c"), create_dir)?.apply(|path| {
666            let old_file = &path.join(format!("tree-sitter-{}.h", language_name.to_kebab_case()));
667            if allow_update && fs::exists(old_file).unwrap_or(false) {
668                fs::remove_file(old_file)?;
669            }
670            missing_path(path.join("tree_sitter"), create_dir)?.apply(|include_path| {
671                missing_path(
672                    include_path.join(format!("tree-sitter-{}.h", language_name.to_kebab_case())),
673                    |path| {
674                        generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
675                    },
676                )?;
677                Ok(())
678            })?;
679
680            missing_path(
681                path.join(format!("tree-sitter-{}.pc.in", language_name.to_kebab_case())),
682                |path| {
683                    generate_file(
684                        path,
685                        PARSER_NAME_PC_IN_TEMPLATE,
686                        language_name,
687                        &generate_opts,
688                    )
689                },
690            )?;
691
692            missing_path_else(
693                repo_path.join("Makefile"),
694                allow_update,
695                |path| {
696                    generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)
697                },
698                |path| {
699                    let mut contents = fs::read_to_string(path)?;
700                    if !contents.contains("cd '$(DESTDIR)$(LIBDIR)' && ln -sf") {
701                        warn!("Replacing Makefile");
702                        generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)?;
703                    } else {
704                        contents = contents
705                            .replace(
706                                indoc! {r"
707                                $(PARSER): $(SRC_DIR)/grammar.json
708                                        $(TS) generate $^
709                                "},
710                                indoc! {r"
711                                $(SRC_DIR)/grammar.json: grammar.js
712                                        $(TS) generate --no-parser $^
713
714                                $(PARSER): $(SRC_DIR)/grammar.json
715                                        $(TS) generate $^
716                                "}
717                            );
718                        write_file(path, contents)?;
719                    }
720                    Ok(())
721                },
722            )?;
723
724            missing_path_else(
725                repo_path.join("CMakeLists.txt"),
726                allow_update,
727                |path| generate_file(path, CMAKELISTS_TXT_TEMPLATE, language_name, &generate_opts),
728                |path| {
729                    let mut contents = fs::read_to_string(path)?;
730                    contents = contents
731                        .replace("add_custom_target(test", "add_custom_target(ts-test")
732                        .replace(
733                            &formatdoc! {r#"
734                            install(FILES bindings/c/tree-sitter-{language_name}.h
735                                    DESTINATION "${{CMAKE_INSTALL_INCLUDEDIR}}/tree_sitter")
736                            "#},
737                            indoc! {r#"
738                            install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bindings/c/tree_sitter"
739                                    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
740                                    FILES_MATCHING PATTERN "*.h")
741                            "#}
742                        ).replace(
743                            &format!("target_include_directories(tree-sitter-{language_name} PRIVATE src)"),
744                            &formatdoc! {"
745                            target_include_directories(tree-sitter-{language_name}
746                                                       PRIVATE src
747                                                       INTERFACE $<BUILD_INTERFACE:${{CMAKE_CURRENT_SOURCE_DIR}}/bindings/c>
748                                                                 $<INSTALL_INTERFACE:${{CMAKE_INSTALL_INCLUDEDIR}}>)
749                            "}
750                        ).replace(
751                            indoc! {r#"
752                            add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
753                                               DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
754                                               COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
755                                                        --abi=${TREE_SITTER_ABI_VERSION}
756                                               WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
757                                               COMMENT "Generating parser.c")
758                            "#},
759                            indoc! {r#"
760                            add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
761                                                      "${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"
762                                               DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js"
763                                               COMMAND "${TREE_SITTER_CLI}" generate grammar.js --no-parser
764                                               WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
765                                               COMMENT "Generating grammar.json")
766
767                            add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
768                                               BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/parser.h"
769                                                          "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/alloc.h"
770                                                          "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/array.h"
771                                               DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
772                                               COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
773                                                        --abi=${TREE_SITTER_ABI_VERSION}
774                                               WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
775                                               COMMENT "Generating parser.c")
776                            "#}
777                        );
778                    write_file(path, contents)?;
779                    Ok(())
780                },
781            )?;
782
783            Ok(())
784        })?;
785    }
786
787    // Generate Go bindings
788    if tree_sitter_config.bindings.go {
789        missing_path(bindings_dir.join("go"), create_dir)?.apply(|path| {
790            missing_path(path.join("binding.go"), |path| {
791                generate_file(path, BINDING_GO_TEMPLATE, language_name, &generate_opts)
792            })?;
793
794            missing_path(path.join("binding_test.go"), |path| {
795                generate_file(
796                    path,
797                    BINDING_TEST_GO_TEMPLATE,
798                    language_name,
799                    &generate_opts,
800                )
801            })?;
802
803            missing_path(repo_path.join("go.mod"), |path| {
804                generate_file(path, GO_MOD_TEMPLATE, language_name, &generate_opts)
805            })?;
806
807            Ok(())
808        })?;
809    }
810
811    // Generate Python bindings
812    if tree_sitter_config.bindings.python {
813        missing_path(bindings_dir.join("python"), create_dir)?.apply(|path| {
814            let lang_path = path.join(format!("tree_sitter_{}", language_name.to_snake_case()));
815            missing_path(&lang_path, create_dir)?;
816
817            missing_path_else(
818                lang_path.join("binding.c"),
819                allow_update,
820                |path| generate_file(path, PY_BINDING_C_TEMPLATE, language_name, &generate_opts),
821                |path| {
822                    let mut contents = fs::read_to_string(path)?;
823                    if !contents.contains("PyModuleDef_Init") {
824                        contents = contents
825                            .replace("PyModule_Create", "PyModuleDef_Init")
826                            .replace(
827                                "static PyMethodDef methods[] = {\n",
828                                indoc! {"
829                                static struct PyModuleDef_Slot slots[] = {
830                                #ifdef Py_GIL_DISABLED
831                                    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
832                                #endif
833                                    {0, NULL}
834                                };
835
836                                static PyMethodDef methods[] = {
837                                "},
838                            )
839                            .replace(
840                                indoc! {"
841                                .m_size = -1,
842                                    .m_methods = methods
843                                "},
844                                indoc! {"
845                                .m_size = 0,
846                                    .m_methods = methods,
847                                    .m_slots = slots,
848                                "},
849                            );
850                        write_file(path, contents)?;
851                    }
852                    Ok(())
853                },
854            )?;
855
856            missing_path_else(
857                lang_path.join("__init__.py"),
858                allow_update,
859                |path| {
860                    generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)
861                },
862                |path| {
863                    let contents = fs::read_to_string(path)?;
864                    if !contents.contains("uncomment these to include any queries") {
865                        warn!("Replacing __init__.py");
866                        generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)?;
867                    }
868                    Ok(())
869                },
870            )?;
871
872            missing_path_else(
873                lang_path.join("__init__.pyi"),
874                allow_update,
875                |path| generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts),
876                |path| {
877                    let mut contents = fs::read_to_string(path)?;
878                    if contents.contains("uncomment these to include any queries") {
879                        warn!("Replacing __init__.pyi");
880                        generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts)?;
881                    } else if !contents.contains("CapsuleType") {
882                        contents = contents
883                            .replace(
884                                "from typing import Final",
885                                "from typing import Final\nfrom typing_extensions import CapsuleType"
886                            )
887                            .replace("-> object:", "-> CapsuleType:");
888                        write_file(path, contents)?;
889                    }
890                    Ok(())
891                },
892            )?;
893
894            missing_path(lang_path.join("py.typed"), |path| {
895                generate_file(path, "", language_name, &generate_opts) // py.typed is empty
896            })?;
897
898            missing_path(path.join("tests"), create_dir)?.apply(|path| {
899                missing_path_else(
900                    path.join("test_binding.py"),
901                    allow_update,
902                    |path| {
903                        generate_file(
904                            path,
905                            TEST_BINDING_PY_TEMPLATE,
906                            language_name,
907                            &generate_opts,
908                        )
909                    },
910                    |path| {
911                        let mut contents = fs::read_to_string(path)?;
912                        if !contents.contains("Parser(Language(") {
913                            contents = contents
914                                .replace("tree_sitter.Language(", "Parser(Language(")
915                                .replace(".language())\n", ".language()))\n")
916                                .replace(
917                                    "import tree_sitter\n",
918                                    "from tree_sitter import Language, Parser\n",
919                                );
920                            write_file(path, contents)?;
921                        }
922                        Ok(())
923                    },
924                )?;
925                Ok(())
926            })?;
927
928            missing_path_else(
929                repo_path.join("setup.py"),
930                allow_update,
931                |path| generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts),
932                |path| {
933                    let contents = fs::read_to_string(path)?;
934                    if !contents.contains("build_ext") {
935                        warn!("Replacing setup.py");
936                        generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts)?;
937                    }
938                    Ok(())
939                },
940            )?;
941
942            missing_path_else(
943                repo_path.join("pyproject.toml"),
944                allow_update,
945                |path| {
946                    generate_file(
947                        path,
948                        PYPROJECT_TOML_TEMPLATE,
949                        dashed_language_name.as_str(),
950                        &generate_opts,
951                    )
952                },
953                |path| {
954                    let mut contents = fs::read_to_string(path)?;
955                    if !contents.contains("cp310-*") {
956                        contents = contents
957                            .replace(r#"build = "cp39-*""#, r#"build = "cp310-*""#)
958                            .replace(r#"python = ">=3.9""#, r#"python = ">=3.10""#)
959                            .replace("tree-sitter~=0.22", "tree-sitter~=0.24");
960                        write_file(path, contents)?;
961                    }
962                    Ok(())
963                },
964            )?;
965
966            Ok(())
967        })?;
968    }
969
970    // Generate Swift bindings
971    if tree_sitter_config.bindings.swift {
972        missing_path(bindings_dir.join("swift"), create_dir)?.apply(|path| {
973            let lang_path = path.join(&class_name);
974            missing_path(&lang_path, create_dir)?;
975
976            missing_path(lang_path.join(format!("{language_name}.h")), |path| {
977                generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
978            })?;
979
980            missing_path(path.join(format!("{class_name}Tests")), create_dir)?.apply(|path| {
981                missing_path(path.join(format!("{class_name}Tests.swift")), |path| {
982                    generate_file(path, TESTS_SWIFT_TEMPLATE, language_name, &generate_opts)
983                })?;
984
985                Ok(())
986            })?;
987
988            missing_path_else(
989                repo_path.join("Package.swift"),
990                allow_update,
991                |path| generate_file(path, PACKAGE_SWIFT_TEMPLATE, language_name, &generate_opts),
992                |path| {
993                    let mut contents = fs::read_to_string(path)?;
994                    contents = contents
995                        .replace(
996                            "https://github.com/ChimeHQ/SwiftTreeSitter",
997                            "https://github.com/tree-sitter/swift-tree-sitter",
998                        )
999                        .replace("version: \"0.8.0\")", "version: \"0.9.0\")")
1000                        .replace("(url:", "(name: \"SwiftTreeSitter\", url:");
1001                    write_file(path, contents)?;
1002                    Ok(())
1003                },
1004            )?;
1005
1006            Ok(())
1007        })?;
1008    }
1009
1010    // Generate Zig bindings
1011    if tree_sitter_config.bindings.zig {
1012        missing_path_else(
1013            repo_path.join("build.zig"),
1014            allow_update,
1015            |path| generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts),
1016            |path| {
1017                let contents = fs::read_to_string(path)?;
1018                if !contents.contains("b.pkg_hash.len") {
1019                    warn!("Replacing build.zig");
1020                    generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts)
1021                } else {
1022                    Ok(())
1023                }
1024            },
1025        )?;
1026
1027        missing_path_else(
1028            repo_path.join("build.zig.zon"),
1029            allow_update,
1030            |path| generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts),
1031            |path| {
1032                let contents = fs::read_to_string(path)?;
1033                if !contents.contains(".name = .tree_sitter_") {
1034                    warn!("Replacing build.zig.zon");
1035                    generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts)
1036                } else {
1037                    Ok(())
1038                }
1039            },
1040        )?;
1041
1042        missing_path(bindings_dir.join("zig"), create_dir)?.apply(|path| {
1043            missing_path_else(
1044                path.join("root.zig"),
1045                allow_update,
1046                |path| generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts),
1047                |path| {
1048                    let contents = fs::read_to_string(path)?;
1049                    if contents.contains("ts.Language") {
1050                        warn!("Replacing root.zig");
1051                        generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts)
1052                    } else {
1053                        Ok(())
1054                    }
1055                },
1056            )?;
1057
1058            missing_path(path.join("test.zig"), |path| {
1059                generate_file(path, TEST_ZIG_TEMPLATE, language_name, &generate_opts)
1060            })?;
1061
1062            Ok(())
1063        })?;
1064    }
1065
1066    // Generate Java bindings
1067    if tree_sitter_config.bindings.java {
1068        missing_path(repo_path.join("pom.xml"), |path| {
1069            generate_file(path, POM_XML_TEMPLATE, language_name, &generate_opts)
1070        })?;
1071
1072        missing_path(bindings_dir.join("java"), create_dir)?.apply(|path| {
1073            missing_path(path.join("main"), create_dir)?.apply(|path| {
1074                let package_path = generate_opts
1075                    .namespace
1076                    .unwrap_or("io.github.treesitter")
1077                    .replace(['-', '_'], "")
1078                    .split('.')
1079                    .fold(path.to_path_buf(), |path, dir| path.join(dir))
1080                    .join("jtreesitter")
1081                    .join(language_name.to_lowercase().replace('_', ""));
1082                missing_path(package_path, create_dir)?.apply(|path| {
1083                    missing_path(path.join(format!("{class_name}.java")), |path| {
1084                        generate_file(path, BINDING_JAVA_TEMPLATE, language_name, &generate_opts)
1085                    })?;
1086
1087                    Ok(())
1088                })?;
1089
1090                Ok(())
1091            })?;
1092
1093            missing_path(path.join("test"), create_dir)?.apply(|path| {
1094                missing_path(path.join(format!("{class_name}Test.java")), |path| {
1095                    generate_file(path, TEST_JAVA_TEMPLATE, language_name, &generate_opts)
1096                })?;
1097
1098                Ok(())
1099            })?;
1100
1101            Ok(())
1102        })?;
1103    }
1104
1105    Ok(())
1106}
1107
1108pub fn get_root_path(path: &Path) -> Result<PathBuf> {
1109    let mut pathbuf = path.to_owned();
1110    let filename = path.file_name().unwrap().to_str().unwrap();
1111    let is_package_json = filename == "package.json";
1112    loop {
1113        let json = pathbuf
1114            .exists()
1115            .then(|| {
1116                let contents = fs::read_to_string(pathbuf.as_path())
1117                    .with_context(|| format!("Failed to read {filename}"))?;
1118                if is_package_json {
1119                    serde_json::from_str::<Map<String, Value>>(&contents)
1120                        .context(format!("Failed to parse {filename}"))
1121                        .map(|v| v.contains_key("tree-sitter"))
1122                } else {
1123                    serde_json::from_str::<TreeSitterJSON>(&contents)
1124                        .context(format!("Failed to parse {filename}"))
1125                        .map(|_| true)
1126                }
1127            })
1128            .transpose()?;
1129        if json == Some(true) {
1130            return Ok(pathbuf.parent().unwrap().to_path_buf());
1131        }
1132        pathbuf.pop(); // filename
1133        if !pathbuf.pop() {
1134            return Err(anyhow!(format!(
1135                concat!(
1136                    "Failed to locate a {} file,",
1137                    " please ensure you have one, and if you don't then consult the docs",
1138                ),
1139                filename
1140            )));
1141        }
1142        pathbuf.push(filename);
1143    }
1144}
1145
1146fn generate_file(
1147    path: &Path,
1148    template: &str,
1149    language_name: &str,
1150    generate_opts: &GenerateOpts,
1151) -> Result<()> {
1152    let filename = path.file_name().unwrap().to_str().unwrap();
1153
1154    let lower_parser_name = if path
1155        .extension()
1156        .is_some_and(|e| e.eq_ignore_ascii_case("java"))
1157    {
1158        language_name.to_snake_case().replace('_', "")
1159    } else {
1160        language_name.to_snake_case()
1161    };
1162
1163    let mut replacement = template
1164        .replace(
1165            CAMEL_PARSER_NAME_PLACEHOLDER,
1166            generate_opts.camel_parser_name,
1167        )
1168        .replace(
1169            TITLE_PARSER_NAME_PLACEHOLDER,
1170            generate_opts.title_parser_name,
1171        )
1172        .replace(
1173            UPPER_PARSER_NAME_PLACEHOLDER,
1174            &language_name.to_shouty_snake_case(),
1175        )
1176        .replace(
1177            KEBAB_PARSER_NAME_PLACEHOLDER,
1178            &language_name.to_kebab_case(),
1179        )
1180        .replace(LOWER_PARSER_NAME_PLACEHOLDER, &lower_parser_name)
1181        .replace(PARSER_NAME_PLACEHOLDER, language_name)
1182        .replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION)
1183        .replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION)
1184        .replace(ABI_VERSION_MAX_PLACEHOLDER, &ABI_VERSION_MAX.to_string())
1185        .replace(
1186            PARSER_VERSION_PLACEHOLDER,
1187            &generate_opts.version.to_string(),
1188        )
1189        .replace(PARSER_CLASS_NAME_PLACEHOLDER, generate_opts.class_name)
1190        .replace(
1191            HIGHLIGHTS_QUERY_PATH_PLACEHOLDER,
1192            generate_opts.highlights_query_path,
1193        )
1194        .replace(
1195            INJECTIONS_QUERY_PATH_PLACEHOLDER,
1196            generate_opts.injections_query_path,
1197        )
1198        .replace(
1199            LOCALS_QUERY_PATH_PLACEHOLDER,
1200            generate_opts.locals_query_path,
1201        )
1202        .replace(TAGS_QUERY_PATH_PLACEHOLDER, generate_opts.tags_query_path);
1203
1204    if let Some(name) = generate_opts.author_name {
1205        replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER, name);
1206    } else {
1207        match filename {
1208            "package.json" => {
1209                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JS, "");
1210            }
1211            "pyproject.toml" => {
1212                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_PY, "");
1213            }
1214            "grammar.js" => {
1215                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_GRAMMAR, "");
1216            }
1217            "Cargo.toml" => {
1218                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, "");
1219            }
1220            "pom.xml" => {
1221                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JAVA, "");
1222            }
1223            _ => {}
1224        }
1225    }
1226
1227    if let Some(email) = generate_opts.author_email {
1228        replacement = match filename {
1229            "Cargo.toml" | "grammar.js" => {
1230                replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, &format!("<{email}>"))
1231            }
1232            _ => replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, email),
1233        }
1234    } else {
1235        match filename {
1236            "package.json" => {
1237                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JS, "");
1238            }
1239            "pyproject.toml" => {
1240                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_PY, "");
1241            }
1242            "grammar.js" => {
1243                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR, "");
1244            }
1245            "Cargo.toml" => {
1246                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, "");
1247            }
1248            "pom.xml" => {
1249                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JAVA, "");
1250            }
1251            _ => {}
1252        }
1253    }
1254
1255    match (generate_opts.author_url, filename) {
1256        (Some(url), "package.json" | "pom.xml") => {
1257            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url);
1258        }
1259        (None, "package.json") => {
1260            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, "");
1261        }
1262        (None, "pom.xml") => {
1263            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JAVA, "");
1264        }
1265        _ => {}
1266    }
1267
1268    if generate_opts.author_name.is_none()
1269        && generate_opts.author_email.is_none()
1270        && generate_opts.author_url.is_none()
1271    {
1272        match filename {
1273            "package.json" => {
1274                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) {
1275                    if let Some(end_idx) = replacement[start_idx..]
1276                        .find("},")
1277                        .map(|i| i + start_idx + 2)
1278                    {
1279                        replacement.replace_range(start_idx..end_idx, "");
1280                    }
1281                }
1282            }
1283            "pom.xml" => {
1284                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JAVA) {
1285                    if let Some(end_idx) = replacement[start_idx..]
1286                        .find("</developer>")
1287                        .map(|i| i + start_idx + 12)
1288                    {
1289                        replacement.replace_range(start_idx..end_idx, "");
1290                    }
1291                }
1292            }
1293            _ => {}
1294        }
1295    } else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() {
1296        match filename {
1297            "pyproject.toml" => {
1298                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_PY) {
1299                    if let Some(end_idx) = replacement[start_idx..]
1300                        .find("}]")
1301                        .map(|i| i + start_idx + 2)
1302                    {
1303                        replacement.replace_range(start_idx..end_idx, "");
1304                    }
1305                }
1306            }
1307            "grammar.js" => {
1308                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_GRAMMAR) {
1309                    if let Some(end_idx) = replacement[start_idx..]
1310                        .find(" \n")
1311                        .map(|i| i + start_idx + 1)
1312                    {
1313                        replacement.replace_range(start_idx..end_idx, "");
1314                    }
1315                }
1316            }
1317            "Cargo.toml" => {
1318                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_RS) {
1319                    if let Some(end_idx) = replacement[start_idx..]
1320                        .find("\"]")
1321                        .map(|i| i + start_idx + 2)
1322                    {
1323                        replacement.replace_range(start_idx..end_idx, "");
1324                    }
1325                }
1326            }
1327            _ => {}
1328        }
1329    }
1330
1331    if let Some(license) = generate_opts.license {
1332        replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, license);
1333    } else {
1334        replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, "MIT");
1335    }
1336
1337    if let Some(description) = generate_opts.description {
1338        replacement = replacement.replace(PARSER_DESCRIPTION_PLACEHOLDER, description);
1339    } else {
1340        replacement = replacement.replace(
1341            PARSER_DESCRIPTION_PLACEHOLDER,
1342            &format!(
1343                "{} grammar for tree-sitter",
1344                generate_opts.camel_parser_name,
1345            ),
1346        );
1347    }
1348
1349    if let Some(repository) = generate_opts.repository {
1350        replacement = replacement
1351            .replace(
1352                PARSER_URL_STRIPPED_PLACEHOLDER,
1353                &repository.replace("https://", "").to_lowercase(),
1354            )
1355            .replace(PARSER_URL_PLACEHOLDER, &repository.to_lowercase());
1356    } else {
1357        replacement = replacement
1358            .replace(
1359                PARSER_URL_STRIPPED_PLACEHOLDER,
1360                &format!(
1361                    "github.com/tree-sitter/tree-sitter-{}",
1362                    language_name.to_lowercase()
1363                ),
1364            )
1365            .replace(
1366                PARSER_URL_PLACEHOLDER,
1367                &format!(
1368                    "https://github.com/tree-sitter/tree-sitter-{}",
1369                    language_name.to_lowercase()
1370                ),
1371            );
1372    }
1373
1374    if let Some(namespace) = generate_opts.namespace {
1375        replacement = replacement
1376            .replace(
1377                PARSER_NS_CLEANED_PLACEHOLDER,
1378                &namespace.replace(['-', '_'], ""),
1379            )
1380            .replace(PARSER_NS_PLACEHOLDER, namespace);
1381    } else {
1382        replacement = replacement
1383            .replace(PARSER_NS_CLEANED_PLACEHOLDER, "io.github.treesitter")
1384            .replace(PARSER_NS_PLACEHOLDER, "io.github.tree-sitter");
1385    }
1386
1387    if let Some(funding_url) = generate_opts.funding {
1388        match filename {
1389            "pyproject.toml" | "package.json" => {
1390                replacement = replacement.replace(FUNDING_URL_PLACEHOLDER, funding_url);
1391            }
1392            _ => {}
1393        }
1394    } else {
1395        match filename {
1396            "package.json" => {
1397                replacement = replacement.replace("  \"funding\": \"FUNDING_URL\",\n", "");
1398            }
1399            "pyproject.toml" => {
1400                replacement = replacement.replace("Funding = \"FUNDING_URL\"\n", "");
1401            }
1402            _ => {}
1403        }
1404    }
1405
1406    if filename == "build.zig.zon" {
1407        let id = thread_rng().gen_range(1u32..0xFFFF_FFFFu32);
1408        let checksum = crc32(format!("tree_sitter_{language_name}").as_bytes());
1409        replacement = replacement.replace(
1410            PARSER_FINGERPRINT_PLACEHOLDER,
1411            #[cfg(target_endian = "little")]
1412            &format!("0x{checksum:x}{id:x}"),
1413            #[cfg(target_endian = "big")]
1414            &format!("0x{id:x}{checksum:x}"),
1415        );
1416    }
1417
1418    write_file(path, replacement)?;
1419    Ok(())
1420}
1421
1422fn create_dir(path: &Path) -> Result<()> {
1423    fs::create_dir_all(path)
1424        .with_context(|| format!("Failed to create {:?}", path.to_string_lossy()))
1425}
1426
1427#[derive(PartialEq, Eq, Debug)]
1428enum PathState<P>
1429where
1430    P: AsRef<Path>,
1431{
1432    Exists(P),
1433    Missing(P),
1434}
1435
1436#[allow(dead_code)]
1437impl<P> PathState<P>
1438where
1439    P: AsRef<Path>,
1440{
1441    fn exists(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1442        if let Self::Exists(path) = self {
1443            action(path.as_ref())?;
1444        }
1445        Ok(self)
1446    }
1447
1448    fn missing(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1449        if let Self::Missing(path) = self {
1450            action(path.as_ref())?;
1451        }
1452        Ok(self)
1453    }
1454
1455    fn apply(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1456        action(self.as_path())?;
1457        Ok(self)
1458    }
1459
1460    fn apply_state(&self, mut action: impl FnMut(&Self) -> Result<()>) -> Result<&Self> {
1461        action(self)?;
1462        Ok(self)
1463    }
1464
1465    fn as_path(&self) -> &Path {
1466        match self {
1467            Self::Exists(path) | Self::Missing(path) => path.as_ref(),
1468        }
1469    }
1470}
1471
1472fn missing_path<P, F>(path: P, mut action: F) -> Result<PathState<P>>
1473where
1474    P: AsRef<Path>,
1475    F: FnMut(&Path) -> Result<()>,
1476{
1477    let path_ref = path.as_ref();
1478    if !path_ref.exists() {
1479        action(path_ref)?;
1480        Ok(PathState::Missing(path))
1481    } else {
1482        Ok(PathState::Exists(path))
1483    }
1484}
1485
1486fn missing_path_else<P, T, F>(
1487    path: P,
1488    allow_update: bool,
1489    mut action: T,
1490    mut else_action: F,
1491) -> Result<PathState<P>>
1492where
1493    P: AsRef<Path>,
1494    T: FnMut(&Path) -> Result<()>,
1495    F: FnMut(&Path) -> Result<()>,
1496{
1497    let path_ref = path.as_ref();
1498    if !path_ref.exists() {
1499        action(path_ref)?;
1500        Ok(PathState::Missing(path))
1501    } else {
1502        if allow_update {
1503            else_action(path_ref)?;
1504        }
1505        Ok(PathState::Exists(path))
1506    }
1507}