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 heck::{ToKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
9use indoc::{formatdoc, indoc};
10use semver::Version;
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value};
13use tree_sitter_generate::write_file;
14use tree_sitter_loader::{Author, Bindings, Grammar, Links, Metadata, PathsJSON, TreeSitterJSON};
15use url::Url;
16
17const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
18const CLI_VERSION_PLACEHOLDER: &str = "CLI_VERSION";
19
20const ABI_VERSION_MAX: usize = tree_sitter::LANGUAGE_VERSION;
21const ABI_VERSION_MAX_PLACEHOLDER: &str = "ABI_VERSION_MAX";
22
23const PARSER_NAME_PLACEHOLDER: &str = "PARSER_NAME";
24const CAMEL_PARSER_NAME_PLACEHOLDER: &str = "CAMEL_PARSER_NAME";
25const TITLE_PARSER_NAME_PLACEHOLDER: &str = "TITLE_PARSER_NAME";
26const UPPER_PARSER_NAME_PLACEHOLDER: &str = "UPPER_PARSER_NAME";
27const LOWER_PARSER_NAME_PLACEHOLDER: &str = "LOWER_PARSER_NAME";
28const KEBAB_PARSER_NAME_PLACEHOLDER: &str = "KEBAB_PARSER_NAME";
29const PARSER_CLASS_NAME_PLACEHOLDER: &str = "PARSER_CLASS_NAME";
30
31const PARSER_DESCRIPTION_PLACEHOLDER: &str = "PARSER_DESCRIPTION";
32const PARSER_LICENSE_PLACEHOLDER: &str = "PARSER_LICENSE";
33const PARSER_URL_PLACEHOLDER: &str = "PARSER_URL";
34const PARSER_URL_STRIPPED_PLACEHOLDER: &str = "PARSER_URL_STRIPPED";
35const PARSER_VERSION_PLACEHOLDER: &str = "PARSER_VERSION";
36
37const AUTHOR_NAME_PLACEHOLDER: &str = "PARSER_AUTHOR_NAME";
38const AUTHOR_EMAIL_PLACEHOLDER: &str = "PARSER_AUTHOR_EMAIL";
39const AUTHOR_URL_PLACEHOLDER: &str = "PARSER_AUTHOR_URL";
40
41const AUTHOR_BLOCK_JS: &str = "\n  \"author\": {";
42const AUTHOR_NAME_PLACEHOLDER_JS: &str = "\n    \"name\": \"PARSER_AUTHOR_NAME\",";
43const AUTHOR_EMAIL_PLACEHOLDER_JS: &str = ",\n    \"email\": \"PARSER_AUTHOR_EMAIL\"";
44const AUTHOR_URL_PLACEHOLDER_JS: &str = ",\n    \"url\": \"PARSER_AUTHOR_URL\"";
45
46const AUTHOR_BLOCK_PY: &str = "\nauthors = [{";
47const AUTHOR_NAME_PLACEHOLDER_PY: &str = "name = \"PARSER_AUTHOR_NAME\"";
48const AUTHOR_EMAIL_PLACEHOLDER_PY: &str = ", email = \"PARSER_AUTHOR_EMAIL\"";
49
50const AUTHOR_BLOCK_RS: &str = "\nauthors = [";
51const AUTHOR_NAME_PLACEHOLDER_RS: &str = "PARSER_AUTHOR_NAME";
52const AUTHOR_EMAIL_PLACEHOLDER_RS: &str = " PARSER_AUTHOR_EMAIL";
53
54const AUTHOR_BLOCK_GRAMMAR: &str = "\n * @author ";
55const AUTHOR_NAME_PLACEHOLDER_GRAMMAR: &str = "PARSER_AUTHOR_NAME";
56const AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR: &str = " PARSER_AUTHOR_EMAIL";
57
58const FUNDING_URL_PLACEHOLDER: &str = "FUNDING_URL";
59
60const GRAMMAR_JS_TEMPLATE: &str = include_str!("./templates/grammar.js");
61const PACKAGE_JSON_TEMPLATE: &str = include_str!("./templates/package.json");
62const GITIGNORE_TEMPLATE: &str = include_str!("./templates/gitignore");
63const GITATTRIBUTES_TEMPLATE: &str = include_str!("./templates/gitattributes");
64const EDITORCONFIG_TEMPLATE: &str = include_str!("./templates/.editorconfig");
65
66const RUST_BINDING_VERSION: &str = env!("CARGO_PKG_VERSION");
67const RUST_BINDING_VERSION_PLACEHOLDER: &str = "RUST_BINDING_VERSION";
68
69const LIB_RS_TEMPLATE: &str = include_str!("./templates/lib.rs");
70const BUILD_RS_TEMPLATE: &str = include_str!("./templates/build.rs");
71const CARGO_TOML_TEMPLATE: &str = include_str!("./templates/_cargo.toml");
72
73const INDEX_JS_TEMPLATE: &str = include_str!("./templates/index.js");
74const INDEX_D_TS_TEMPLATE: &str = include_str!("./templates/index.d.ts");
75const JS_BINDING_CC_TEMPLATE: &str = include_str!("./templates/js-binding.cc");
76const BINDING_GYP_TEMPLATE: &str = include_str!("./templates/binding.gyp");
77const BINDING_TEST_JS_TEMPLATE: &str = include_str!("./templates/binding_test.js");
78
79const MAKEFILE_TEMPLATE: &str = include_str!("./templates/makefile");
80const CMAKELISTS_TXT_TEMPLATE: &str = include_str!("./templates/cmakelists.cmake");
81const PARSER_NAME_H_TEMPLATE: &str = include_str!("./templates/PARSER_NAME.h");
82const PARSER_NAME_PC_IN_TEMPLATE: &str = include_str!("./templates/PARSER_NAME.pc.in");
83
84const GO_MOD_TEMPLATE: &str = include_str!("./templates/go.mod");
85const BINDING_GO_TEMPLATE: &str = include_str!("./templates/binding.go");
86const BINDING_TEST_GO_TEMPLATE: &str = include_str!("./templates/binding_test.go");
87
88const SETUP_PY_TEMPLATE: &str = include_str!("./templates/setup.py");
89const INIT_PY_TEMPLATE: &str = include_str!("./templates/__init__.py");
90const INIT_PYI_TEMPLATE: &str = include_str!("./templates/__init__.pyi");
91const PYPROJECT_TOML_TEMPLATE: &str = include_str!("./templates/pyproject.toml");
92const PY_BINDING_C_TEMPLATE: &str = include_str!("./templates/py-binding.c");
93const TEST_BINDING_PY_TEMPLATE: &str = include_str!("./templates/test_binding.py");
94
95const PACKAGE_SWIFT_TEMPLATE: &str = include_str!("./templates/package.swift");
96const TESTS_SWIFT_TEMPLATE: &str = include_str!("./templates/tests.swift");
97
98const BUILD_ZIG_TEMPLATE: &str = include_str!("./templates/build.zig");
99const BUILD_ZIG_ZON_TEMPLATE: &str = include_str!("./templates/build.zig.zon");
100const ROOT_ZIG_TEMPLATE: &str = include_str!("./templates/root.zig");
101const TEST_ZIG_TEMPLATE: &str = include_str!("./templates/test.zig");
102
103const TREE_SITTER_JSON_SCHEMA: &str =
104    "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json";
105
106#[must_use]
107pub fn path_in_ignore(repo_path: &Path) -> bool {
108    [
109        "bindings",
110        "build",
111        "examples",
112        "node_modules",
113        "queries",
114        "script",
115        "src",
116        "target",
117        "test",
118        "types",
119    ]
120    .iter()
121    .any(|dir| repo_path.ends_with(dir))
122}
123
124#[derive(Serialize, Deserialize, Clone)]
125pub struct JsonConfigOpts {
126    pub name: String,
127    pub camelcase: String,
128    pub title: String,
129    pub description: String,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub repository: Option<Url>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub funding: Option<Url>,
134    pub scope: String,
135    pub file_types: Vec<String>,
136    pub version: Version,
137    pub license: String,
138    pub author: String,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub email: Option<String>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub url: Option<Url>,
143}
144
145impl JsonConfigOpts {
146    #[must_use]
147    pub fn to_tree_sitter_json(self) -> TreeSitterJSON {
148        TreeSitterJSON {
149            schema: Some(TREE_SITTER_JSON_SCHEMA.to_string()),
150            grammars: vec![Grammar {
151                name: self.name.clone(),
152                camelcase: Some(self.camelcase),
153                title: Some(self.title),
154                scope: self.scope,
155                path: None,
156                external_files: PathsJSON::Empty,
157                file_types: Some(self.file_types),
158                highlights: PathsJSON::Empty,
159                injections: PathsJSON::Empty,
160                locals: PathsJSON::Empty,
161                tags: PathsJSON::Empty,
162                injection_regex: Some(format!("^{}$", self.name)),
163                first_line_regex: None,
164                content_regex: None,
165                class_name: Some(format!("TreeSitter{}", self.name.to_upper_camel_case())),
166            }],
167            metadata: Metadata {
168                version: self.version,
169                license: Some(self.license),
170                description: Some(self.description),
171                authors: Some(vec![Author {
172                    name: self.author,
173                    email: self.email,
174                    url: self.url.map(|url| url.to_string()),
175                }]),
176                links: Some(Links {
177                    repository: self.repository.unwrap_or_else(|| {
178                        Url::parse(&format!(
179                            "https://github.com/tree-sitter/tree-sitter-{}",
180                            self.name
181                        ))
182                        .expect("Failed to parse default repository URL")
183                    }),
184                    funding: self.funding,
185                    homepage: None,
186                }),
187                namespace: None,
188            },
189            bindings: Bindings::default(),
190        }
191    }
192}
193
194impl Default for JsonConfigOpts {
195    fn default() -> Self {
196        Self {
197            name: String::new(),
198            camelcase: String::new(),
199            title: String::new(),
200            description: String::new(),
201            repository: None,
202            funding: None,
203            scope: String::new(),
204            file_types: vec![],
205            version: Version::from_str("0.1.0").unwrap(),
206            license: String::new(),
207            author: String::new(),
208            email: None,
209            url: None,
210        }
211    }
212}
213
214struct GenerateOpts<'a> {
215    author_name: Option<&'a str>,
216    author_email: Option<&'a str>,
217    author_url: Option<&'a str>,
218    license: Option<&'a str>,
219    description: Option<&'a str>,
220    repository: Option<&'a str>,
221    funding: Option<&'a str>,
222    version: &'a Version,
223    camel_parser_name: &'a str,
224    title_parser_name: &'a str,
225    class_name: &'a str,
226}
227
228pub fn generate_grammar_files(
229    repo_path: &Path,
230    language_name: &str,
231    allow_update: bool,
232    opts: Option<&JsonConfigOpts>,
233) -> Result<()> {
234    let dashed_language_name = language_name.to_kebab_case();
235
236    let tree_sitter_config = missing_path_else(
237        repo_path.join("tree-sitter.json"),
238        true,
239        |path| {
240            // invariant: opts is always Some when `tree-sitter.json` doesn't exist
241            let Some(opts) = opts else { unreachable!() };
242
243            let tree_sitter_json = opts.clone().to_tree_sitter_json();
244            write_file(path, serde_json::to_string_pretty(&tree_sitter_json)?)?;
245            Ok(())
246        },
247        |path| {
248            // updating the config, if needed
249            if let Some(opts) = opts {
250                let tree_sitter_json = opts.clone().to_tree_sitter_json();
251                write_file(path, serde_json::to_string_pretty(&tree_sitter_json)?)?;
252            }
253            Ok(())
254        },
255    )?;
256
257    let tree_sitter_config = serde_json::from_str::<TreeSitterJSON>(
258        &fs::read_to_string(tree_sitter_config.as_path())
259            .with_context(|| "Failed to read tree-sitter.json")?,
260    )?;
261
262    let authors = tree_sitter_config.metadata.authors.as_ref();
263    let camel_name = tree_sitter_config.grammars[0]
264        .camelcase
265        .clone()
266        .unwrap_or_else(|| language_name.to_upper_camel_case());
267    let title_name = tree_sitter_config.grammars[0]
268        .title
269        .clone()
270        .unwrap_or_else(|| language_name.to_upper_camel_case());
271    let class_name = tree_sitter_config.grammars[0]
272        .class_name
273        .clone()
274        .unwrap_or_else(|| format!("TreeSitter{}", language_name.to_upper_camel_case()));
275
276    let generate_opts = GenerateOpts {
277        author_name: authors
278            .map(|a| a.first().map(|a| a.name.as_str()))
279            .unwrap_or_default(),
280        author_email: authors
281            .map(|a| a.first().and_then(|a| a.email.as_deref()))
282            .unwrap_or_default(),
283        author_url: authors
284            .map(|a| a.first().and_then(|a| a.url.as_deref()))
285            .unwrap_or_default(),
286        license: tree_sitter_config.metadata.license.as_deref(),
287        description: tree_sitter_config.metadata.description.as_deref(),
288        repository: tree_sitter_config
289            .metadata
290            .links
291            .as_ref()
292            .map(|l| l.repository.as_str()),
293        funding: tree_sitter_config
294            .metadata
295            .links
296            .as_ref()
297            .and_then(|l| l.funding.as_ref().map(|f| f.as_str())),
298        version: &tree_sitter_config.metadata.version,
299        camel_parser_name: &camel_name,
300        title_parser_name: &title_name,
301        class_name: &class_name,
302    };
303
304    // Create package.json
305    missing_path_else(
306        repo_path.join("package.json"),
307        allow_update,
308        |path| {
309            generate_file(
310                path,
311                PACKAGE_JSON_TEMPLATE,
312                dashed_language_name.as_str(),
313                &generate_opts,
314            )
315        },
316        |path| {
317            let contents = fs::read_to_string(path)?
318                .replace(
319                    r#""node-addon-api": "^8.3.1"#,
320                    r#""node-addon-api": "^8.5.0""#,
321                )
322                .replace(
323                    indoc! {r#"
324                    "prebuildify": "^6.0.1",
325                    "tree-sitter-cli":"#},
326                    indoc! {r#"
327                    "prebuildify": "^6.0.1",
328                    "tree-sitter": "^0.22.4",
329                    "tree-sitter-cli":"#},
330                );
331            write_file(path, contents)?;
332            Ok(())
333        },
334    )?;
335
336    // Do not create a grammar.js file in a repo with multiple language configs
337    if !tree_sitter_config.has_multiple_language_configs() {
338        missing_path(repo_path.join("grammar.js"), |path| {
339            generate_file(path, GRAMMAR_JS_TEMPLATE, language_name, &generate_opts)
340        })?;
341    }
342
343    // Write .gitignore file
344    missing_path_else(
345        repo_path.join(".gitignore"),
346        allow_update,
347        |path| generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts),
348        |path| {
349            let contents = fs::read_to_string(path)?;
350            if !contents.contains("Zig artifacts") {
351                eprintln!("Replacing .gitignore");
352                generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts)?;
353            }
354            Ok(())
355        },
356    )?;
357
358    // Write .gitattributes file
359    missing_path_else(
360        repo_path.join(".gitattributes"),
361        allow_update,
362        |path| generate_file(path, GITATTRIBUTES_TEMPLATE, language_name, &generate_opts),
363        |path| {
364            let mut contents = fs::read_to_string(path)?;
365            contents = contents.replace("bindings/c/* ", "bindings/c/** ");
366            if !contents.contains("Zig bindings") {
367                contents.push('\n');
368                contents.push_str(indoc! {"
369                # Zig bindings
370                build.zig linguist-generated
371                build.zig.zon linguist-generated
372                "});
373            }
374            write_file(path, contents)?;
375            Ok(())
376        },
377    )?;
378
379    // Write .editorconfig file
380    missing_path(repo_path.join(".editorconfig"), |path| {
381        generate_file(path, EDITORCONFIG_TEMPLATE, language_name, &generate_opts)
382    })?;
383
384    let bindings_dir = repo_path.join("bindings");
385
386    // Generate Rust bindings
387    if tree_sitter_config.bindings.rust {
388        missing_path(bindings_dir.join("rust"), create_dir)?.apply(|path| {
389            missing_path(path.join("lib.rs"), |path| {
390                generate_file(path, LIB_RS_TEMPLATE, language_name, &generate_opts)
391            })?;
392
393            missing_path(path.join("build.rs"), |path| {
394                generate_file(path, BUILD_RS_TEMPLATE, language_name, &generate_opts)
395            })?;
396
397            missing_path_else(
398                repo_path.join("Cargo.toml"),
399                allow_update,
400                |path| {
401                    generate_file(
402                        path,
403                        CARGO_TOML_TEMPLATE,
404                        dashed_language_name.as_str(),
405                        &generate_opts,
406                    )
407                },
408                |path| {
409                    let contents = fs::read_to_string(path)?;
410                    if contents.contains("\"LICENSE\"") {
411                        write_file(path, contents.replace("\"LICENSE\"", "\"/LICENSE\""))?;
412                    }
413                    Ok(())
414                },
415            )?;
416
417            Ok(())
418        })?;
419    }
420
421    // Generate Node bindings
422    if tree_sitter_config.bindings.node {
423        missing_path(bindings_dir.join("node"), create_dir)?.apply(|path| {
424            missing_path_else(
425                path.join("index.js"),
426                allow_update,
427                |path| generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts),
428                |path| {
429                    let contents = fs::read_to_string(path)?;
430                    if !contents.contains("bun") {
431                        eprintln!("Replacing index.js");
432                        generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts)?;
433                    }
434                    Ok(())
435                },
436            )?;
437
438            missing_path(path.join("index.d.ts"), |path| {
439                generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts)
440            })?;
441
442            missing_path(path.join("binding_test.js"), |path| {
443                generate_file(
444                    path,
445                    BINDING_TEST_JS_TEMPLATE,
446                    language_name,
447                    &generate_opts,
448                )
449            })?;
450
451            missing_path(path.join("binding.cc"), |path| {
452                generate_file(path, JS_BINDING_CC_TEMPLATE, language_name, &generate_opts)
453            })?;
454
455            missing_path_else(
456                repo_path.join("binding.gyp"),
457                allow_update,
458                |path| generate_file(path, BINDING_GYP_TEMPLATE, language_name, &generate_opts),
459                |path| {
460                    let contents = fs::read_to_string(path)?;
461                    if contents.contains("fs.exists(") {
462                        write_file(path, contents.replace("fs.exists(", "fs.existsSync("))?;
463                    }
464                    Ok(())
465                },
466            )?;
467
468            Ok(())
469        })?;
470    }
471
472    // Generate C bindings
473    if tree_sitter_config.bindings.c {
474        missing_path(bindings_dir.join("c"), create_dir)?.apply(|path| {
475            let old_file = &path.join(format!("tree-sitter-{}.h", language_name.to_kebab_case()));
476            if allow_update && fs::exists(old_file).unwrap_or(false) {
477                fs::remove_file(old_file)?;
478            }
479            missing_path(path.join("tree_sitter"), create_dir)?.apply(|include_path| {
480                missing_path(
481                    include_path.join(format!("tree-sitter-{}.h", language_name.to_kebab_case())),
482                    |path| {
483                        generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
484                    },
485                )?;
486                Ok(())
487            })?;
488
489            missing_path(
490                path.join(format!("tree-sitter-{}.pc.in", language_name.to_kebab_case())),
491                |path| {
492                    generate_file(
493                        path,
494                        PARSER_NAME_PC_IN_TEMPLATE,
495                        language_name,
496                        &generate_opts,
497                    )
498                },
499            )?;
500
501            missing_path_else(
502                repo_path.join("Makefile"),
503                allow_update,
504                |path| {
505                    generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)
506                },
507                |path| {
508                    let contents = fs::read_to_string(path)?.replace(
509                        "-m644 bindings/c/$(LANGUAGE_NAME).h",
510                        "-m644 bindings/c/tree_sitter/$(LANGUAGE_NAME).h"
511                    );
512                    write_file(path, contents)?;
513                    Ok(())
514                },
515            )?;
516
517            missing_path_else(
518                repo_path.join("CMakeLists.txt"),
519                allow_update,
520                |path| generate_file(path, CMAKELISTS_TXT_TEMPLATE, language_name, &generate_opts),
521                |path| {
522                    let mut contents = fs::read_to_string(path)?;
523                    contents = contents
524                        .replace("add_custom_target(test", "add_custom_target(ts-test")
525                        .replace(
526                            &formatdoc! {r#"
527                            install(FILES bindings/c/tree-sitter-{language_name}.h
528                                    DESTINATION "${{CMAKE_INSTALL_INCLUDEDIR}}/tree_sitter")
529                            "#},
530                            indoc! {r#"
531                            install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bindings/c/tree_sitter"
532                                    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
533                                    FILES_MATCHING PATTERN "*.h")
534                            "#}
535                        ).replace(
536                            &format!("target_include_directories(tree-sitter-{language_name} PRIVATE src)"),
537                            &formatdoc! {"
538                            target_include_directories(tree-sitter-{language_name}
539                                                       PRIVATE src
540                                                       INTERFACE $<BUILD_INTERFACE:${{CMAKE_CURRENT_SOURCE_DIR}}/bindings/c>
541                                                                 $<INSTALL_INTERFACE:${{CMAKE_INSTALL_INCLUDEDIR}}>)
542                            "}
543                        );
544                    write_file(path, contents)?;
545                    Ok(())
546                },
547            )?;
548
549            Ok(())
550        })?;
551    }
552
553    // Generate Go bindings
554    if tree_sitter_config.bindings.go {
555        missing_path(bindings_dir.join("go"), create_dir)?.apply(|path| {
556            missing_path(path.join("binding.go"), |path| {
557                generate_file(path, BINDING_GO_TEMPLATE, language_name, &generate_opts)
558            })?;
559
560            missing_path(path.join("binding_test.go"), |path| {
561                generate_file(
562                    path,
563                    BINDING_TEST_GO_TEMPLATE,
564                    language_name,
565                    &generate_opts,
566                )
567            })?;
568
569            missing_path(repo_path.join("go.mod"), |path| {
570                generate_file(path, GO_MOD_TEMPLATE, language_name, &generate_opts)
571            })?;
572
573            Ok(())
574        })?;
575    }
576
577    // Generate Python bindings
578    if tree_sitter_config.bindings.python {
579        missing_path(bindings_dir.join("python"), create_dir)?.apply(|path| {
580            let lang_path = path.join(format!("tree_sitter_{}", language_name.to_snake_case()));
581            missing_path(&lang_path, create_dir)?;
582
583            missing_path_else(
584                lang_path.join("binding.c"),
585                allow_update,
586                |path| generate_file(path, PY_BINDING_C_TEMPLATE, language_name, &generate_opts),
587                |path| {
588                    let mut contents = fs::read_to_string(path)?;
589                    if !contents.contains("PyModuleDef_Init") {
590                        contents = contents
591                            .replace("PyModule_Create", "PyModuleDef_Init")
592                            .replace(
593                                "static PyMethodDef methods[] = {\n",
594                                indoc! {"
595                                static struct PyModuleDef_Slot slots[] = {
596                                #ifdef Py_GIL_DISABLED
597                                    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
598                                #endif
599                                    {0, NULL}
600                                };
601
602                                static PyMethodDef methods[] = {
603                                "},
604                            )
605                            .replace(
606                                indoc! {"
607                                .m_size = -1,
608                                    .m_methods = methods
609                                "},
610                                indoc! {"
611                                .m_size = 0,
612                                    .m_methods = methods,
613                                    .m_slots = slots,
614                                "},
615                            );
616                        write_file(path, contents)?;
617                    }
618                    Ok(())
619                },
620            )?;
621
622            missing_path(lang_path.join("__init__.py"), |path| {
623                generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)
624            })?;
625
626            missing_path(lang_path.join("__init__.pyi"), |path| {
627                generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts)
628            })?;
629
630            missing_path(lang_path.join("py.typed"), |path| {
631                generate_file(path, "", language_name, &generate_opts) // py.typed is empty
632            })?;
633
634            missing_path(path.join("tests"), create_dir)?.apply(|path| {
635                missing_path_else(
636                    path.join("test_binding.py"),
637                    allow_update,
638                    |path| {
639                        generate_file(
640                            path,
641                            TEST_BINDING_PY_TEMPLATE,
642                            language_name,
643                            &generate_opts,
644                        )
645                    },
646                    |path| {
647                        let mut contents = fs::read_to_string(path)?;
648                        if !contents.contains("Parser(Language(") {
649                            contents = contents
650                                .replace("tree_sitter.Language(", "Parser(Language(")
651                                .replace(".language())\n", ".language()))\n")
652                                .replace(
653                                    "import tree_sitter\n",
654                                    "from tree_sitter import Language, Parser\n",
655                                );
656                            write_file(path, contents)?;
657                        }
658                        Ok(())
659                    },
660                )?;
661                Ok(())
662            })?;
663
664            missing_path_else(
665                repo_path.join("setup.py"),
666                allow_update,
667                |path| generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts),
668                |path| {
669                    let contents = fs::read_to_string(path)?;
670                    if !contents.contains("build_ext") {
671                        eprintln!("Replacing setup.py");
672                        generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts)?;
673                    }
674                    Ok(())
675                },
676            )?;
677
678            missing_path_else(
679                repo_path.join("pyproject.toml"),
680                allow_update,
681                |path| {
682                    generate_file(
683                        path,
684                        PYPROJECT_TOML_TEMPLATE,
685                        dashed_language_name.as_str(),
686                        &generate_opts,
687                    )
688                },
689                |path| {
690                    let mut contents = fs::read_to_string(path)?;
691                    if !contents.contains("cp310-*") {
692                        contents = contents
693                            .replace(r#"build = "cp39-*""#, r#"build = "cp310-*""#)
694                            .replace(r#"python = ">=3.9""#, r#"python = ">=3.10""#)
695                            .replace("tree-sitter~=0.22", "tree-sitter~=0.24");
696                        write_file(path, contents)?;
697                    }
698                    Ok(())
699                },
700            )?;
701
702            Ok(())
703        })?;
704    }
705
706    // Generate Swift bindings
707    if tree_sitter_config.bindings.swift {
708        missing_path(bindings_dir.join("swift"), create_dir)?.apply(|path| {
709            let lang_path = path.join(&class_name);
710            missing_path(&lang_path, create_dir)?;
711
712            missing_path(lang_path.join(format!("{language_name}.h")), |path| {
713                generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
714            })?;
715
716            missing_path(path.join(format!("{class_name}Tests")), create_dir)?.apply(|path| {
717                missing_path(path.join(format!("{class_name}Tests.swift")), |path| {
718                    generate_file(path, TESTS_SWIFT_TEMPLATE, language_name, &generate_opts)
719                })?;
720
721                Ok(())
722            })?;
723
724            missing_path_else(
725                repo_path.join("Package.swift"),
726                allow_update,
727                |path| generate_file(path, PACKAGE_SWIFT_TEMPLATE, language_name, &generate_opts),
728                |path| {
729                    let mut contents = fs::read_to_string(path)?;
730                    contents = contents
731                        .replace(
732                            "https://github.com/ChimeHQ/SwiftTreeSitter",
733                            "https://github.com/tree-sitter/swift-tree-sitter",
734                        )
735                        .replace("version: \"0.8.0\")", "version: \"0.9.0\")")
736                        .replace("(url:", "(name: \"SwiftTreeSitter\", url:");
737                    write_file(path, contents)?;
738                    Ok(())
739                },
740            )?;
741
742            Ok(())
743        })?;
744    }
745
746    // Generate Zig bindings
747    if tree_sitter_config.bindings.zig {
748        missing_path_else(
749            repo_path.join("build.zig"),
750            allow_update,
751            |path| generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts),
752            |path| {
753                let contents = fs::read_to_string(path)?;
754                if !contents.contains("b.pkg_hash.len") {
755                    eprintln!("Replacing build.zig");
756                    generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts)
757                } else {
758                    Ok(())
759                }
760            },
761        )?;
762
763        missing_path_else(
764            repo_path.join("build.zig.zon"),
765            allow_update,
766            |path| generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts),
767            |path| {
768                let contents = fs::read_to_string(path)?;
769                if !contents.contains(".name = .tree_sitter_") {
770                    eprintln!("Replacing build.zig.zon");
771                    generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts)
772                } else {
773                    Ok(())
774                }
775            },
776        )?;
777
778        missing_path(bindings_dir.join("zig"), create_dir)?.apply(|path| {
779            missing_path_else(
780                path.join("root.zig"),
781                allow_update,
782                |path| generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts),
783                |path| {
784                    let contents = fs::read_to_string(path)?;
785                    if contents.contains("ts.Language") {
786                        eprintln!("Replacing root.zig");
787                        generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts)
788                    } else {
789                        Ok(())
790                    }
791                },
792            )?;
793
794            missing_path(path.join("test.zig"), |path| {
795                generate_file(path, TEST_ZIG_TEMPLATE, language_name, &generate_opts)
796            })?;
797
798            Ok(())
799        })?;
800    }
801
802    Ok(())
803}
804
805pub fn get_root_path(path: &Path) -> Result<PathBuf> {
806    let mut pathbuf = path.to_owned();
807    let filename = path.file_name().unwrap().to_str().unwrap();
808    let is_package_json = filename == "package.json";
809    loop {
810        let json = pathbuf
811            .exists()
812            .then(|| {
813                let contents = fs::read_to_string(pathbuf.as_path())
814                    .with_context(|| format!("Failed to read {filename}"))?;
815                if is_package_json {
816                    serde_json::from_str::<Map<String, Value>>(&contents)
817                        .context(format!("Failed to parse {filename}"))
818                        .map(|v| v.contains_key("tree-sitter"))
819                } else {
820                    serde_json::from_str::<TreeSitterJSON>(&contents)
821                        .context(format!("Failed to parse {filename}"))
822                        .map(|_| true)
823                }
824            })
825            .transpose()?;
826        if json == Some(true) {
827            return Ok(pathbuf.parent().unwrap().to_path_buf());
828        }
829        pathbuf.pop(); // filename
830        if !pathbuf.pop() {
831            return Err(anyhow!(format!(
832                concat!(
833                    "Failed to locate a {} file,",
834                    " please ensure you have one, and if you don't then consult the docs",
835                ),
836                filename
837            )));
838        }
839        pathbuf.push(filename);
840    }
841}
842
843fn generate_file(
844    path: &Path,
845    template: &str,
846    language_name: &str,
847    generate_opts: &GenerateOpts,
848) -> Result<()> {
849    let filename = path.file_name().unwrap().to_str().unwrap();
850
851    let mut replacement = template
852        .replace(
853            CAMEL_PARSER_NAME_PLACEHOLDER,
854            generate_opts.camel_parser_name,
855        )
856        .replace(
857            TITLE_PARSER_NAME_PLACEHOLDER,
858            generate_opts.title_parser_name,
859        )
860        .replace(
861            UPPER_PARSER_NAME_PLACEHOLDER,
862            &language_name.to_shouty_snake_case(),
863        )
864        .replace(
865            LOWER_PARSER_NAME_PLACEHOLDER,
866            &language_name.to_snake_case(),
867        )
868        .replace(
869            KEBAB_PARSER_NAME_PLACEHOLDER,
870            &language_name.to_kebab_case(),
871        )
872        .replace(PARSER_NAME_PLACEHOLDER, language_name)
873        .replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION)
874        .replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION)
875        .replace(ABI_VERSION_MAX_PLACEHOLDER, &ABI_VERSION_MAX.to_string())
876        .replace(
877            PARSER_VERSION_PLACEHOLDER,
878            &generate_opts.version.to_string(),
879        )
880        .replace(PARSER_CLASS_NAME_PLACEHOLDER, generate_opts.class_name);
881
882    if let Some(name) = generate_opts.author_name {
883        replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER, name);
884    } else {
885        match filename {
886            "package.json" => {
887                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JS, "");
888            }
889            "pyproject.toml" => {
890                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_PY, "");
891            }
892            "grammar.js" => {
893                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_GRAMMAR, "");
894            }
895            "Cargo.toml" => {
896                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, "");
897            }
898            _ => {}
899        }
900    }
901
902    if let Some(email) = generate_opts.author_email {
903        replacement = match filename {
904            "Cargo.toml" | "grammar.js" => {
905                replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, &format!("<{email}>"))
906            }
907            _ => replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, email),
908        }
909    } else {
910        match filename {
911            "package.json" => {
912                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JS, "");
913            }
914            "pyproject.toml" => {
915                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_PY, "");
916            }
917            "grammar.js" => {
918                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR, "");
919            }
920            "Cargo.toml" => {
921                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, "");
922            }
923            _ => {}
924        }
925    }
926
927    if filename == "package.json" {
928        if let Some(url) = generate_opts.author_url {
929            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url);
930        } else {
931            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, "");
932        }
933    }
934
935    if generate_opts.author_name.is_none()
936        && generate_opts.author_email.is_none()
937        && generate_opts.author_url.is_none()
938        && filename == "package.json"
939    {
940        if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) {
941            if let Some(end_idx) = replacement[start_idx..]
942                .find("},")
943                .map(|i| i + start_idx + 2)
944            {
945                replacement.replace_range(start_idx..end_idx, "");
946            }
947        }
948    } else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() {
949        match filename {
950            "pyproject.toml" => {
951                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_PY) {
952                    if let Some(end_idx) = replacement[start_idx..]
953                        .find("}]")
954                        .map(|i| i + start_idx + 2)
955                    {
956                        replacement.replace_range(start_idx..end_idx, "");
957                    }
958                }
959            }
960            "grammar.js" => {
961                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_GRAMMAR) {
962                    if let Some(end_idx) = replacement[start_idx..]
963                        .find(" \n")
964                        .map(|i| i + start_idx + 1)
965                    {
966                        replacement.replace_range(start_idx..end_idx, "");
967                    }
968                }
969            }
970            "Cargo.toml" => {
971                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_RS) {
972                    if let Some(end_idx) = replacement[start_idx..]
973                        .find("\"]")
974                        .map(|i| i + start_idx + 2)
975                    {
976                        replacement.replace_range(start_idx..end_idx, "");
977                    }
978                }
979            }
980            _ => {}
981        }
982    }
983
984    match generate_opts.license {
985        Some(license) => replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, license),
986        _ => replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, "MIT"),
987    }
988
989    match generate_opts.description {
990        Some(description) => {
991            replacement = replacement.replace(PARSER_DESCRIPTION_PLACEHOLDER, description);
992        }
993        _ => {
994            replacement = replacement.replace(
995                PARSER_DESCRIPTION_PLACEHOLDER,
996                &format!(
997                    "{} grammar for tree-sitter",
998                    generate_opts.camel_parser_name,
999                ),
1000            );
1001        }
1002    }
1003
1004    match generate_opts.repository {
1005        Some(repository) => {
1006            replacement = replacement
1007                .replace(
1008                    PARSER_URL_STRIPPED_PLACEHOLDER,
1009                    &repository.replace("https://", "").to_lowercase(),
1010                )
1011                .replace(PARSER_URL_PLACEHOLDER, &repository.to_lowercase());
1012        }
1013        _ => {
1014            replacement = replacement
1015                .replace(
1016                    PARSER_URL_STRIPPED_PLACEHOLDER,
1017                    &format!(
1018                        "github.com/tree-sitter/tree-sitter-{}",
1019                        language_name.to_lowercase()
1020                    ),
1021                )
1022                .replace(
1023                    PARSER_URL_PLACEHOLDER,
1024                    &format!(
1025                        "https://github.com/tree-sitter/tree-sitter-{}",
1026                        language_name.to_lowercase()
1027                    ),
1028                );
1029        }
1030    }
1031
1032    if let Some(funding_url) = generate_opts.funding {
1033        match filename {
1034            "pyproject.toml" | "package.json" => {
1035                replacement = replacement.replace(FUNDING_URL_PLACEHOLDER, funding_url);
1036            }
1037            _ => {}
1038        }
1039    } else {
1040        match filename {
1041            "package.json" => {
1042                replacement = replacement.replace("  \"funding\": \"FUNDING_URL\",\n", "");
1043            }
1044            "pyproject.toml" => {
1045                replacement = replacement.replace("Funding = \"FUNDING_URL\"\n", "");
1046            }
1047            _ => {}
1048        }
1049    }
1050
1051    write_file(path, replacement)?;
1052    Ok(())
1053}
1054
1055fn create_dir(path: &Path) -> Result<()> {
1056    fs::create_dir_all(path)
1057        .with_context(|| format!("Failed to create {:?}", path.to_string_lossy()))
1058}
1059
1060#[derive(PartialEq, Eq, Debug)]
1061enum PathState<P>
1062where
1063    P: AsRef<Path>,
1064{
1065    Exists(P),
1066    Missing(P),
1067}
1068
1069#[allow(dead_code)]
1070impl<P> PathState<P>
1071where
1072    P: AsRef<Path>,
1073{
1074    fn exists(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1075        if let Self::Exists(path) = self {
1076            action(path.as_ref())?;
1077        }
1078        Ok(self)
1079    }
1080
1081    fn missing(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1082        if let Self::Missing(path) = self {
1083            action(path.as_ref())?;
1084        }
1085        Ok(self)
1086    }
1087
1088    fn apply(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1089        action(self.as_path())?;
1090        Ok(self)
1091    }
1092
1093    fn apply_state(&self, mut action: impl FnMut(&Self) -> Result<()>) -> Result<&Self> {
1094        action(self)?;
1095        Ok(self)
1096    }
1097
1098    fn as_path(&self) -> &Path {
1099        match self {
1100            Self::Exists(path) | Self::Missing(path) => path.as_ref(),
1101        }
1102    }
1103}
1104
1105fn missing_path<P, F>(path: P, mut action: F) -> Result<PathState<P>>
1106where
1107    P: AsRef<Path>,
1108    F: FnMut(&Path) -> Result<()>,
1109{
1110    let path_ref = path.as_ref();
1111    if !path_ref.exists() {
1112        action(path_ref)?;
1113        Ok(PathState::Missing(path))
1114    } else {
1115        Ok(PathState::Exists(path))
1116    }
1117}
1118
1119fn missing_path_else<P, T, F>(
1120    path: P,
1121    allow_update: bool,
1122    mut action: T,
1123    mut else_action: F,
1124) -> Result<PathState<P>>
1125where
1126    P: AsRef<Path>,
1127    T: FnMut(&Path) -> Result<()>,
1128    F: FnMut(&Path) -> Result<()>,
1129{
1130    let path_ref = path.as_ref();
1131    if !path_ref.exists() {
1132        action(path_ref)?;
1133        Ok(PathState::Missing(path))
1134    } else {
1135        if allow_update {
1136            else_action(path_ref)?;
1137        }
1138        Ok(PathState::Exists(path))
1139    }
1140}