Skip to main content

tree_sitter_cli/
init.rs

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