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