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
683                        .replace(
684                            "https://github.com/ChimeHQ/SwiftTreeSitter",
685                            "https://github.com/tree-sitter/swift-tree-sitter",
686                        )
687                        .replace("version: \"0.8.0\")", "version: \"0.9.0\")")
688                        .replace("(url:", "(name: \"SwiftTreeSitter\", url:");
689                    write_file(path, contents)?;
690                    Ok(())
691                },
692            )?;
693
694            Ok(())
695        })?;
696    }
697
698    // Generate Zig bindings
699    if tree_sitter_config.bindings.zig {
700        missing_path(repo_path.join("build.zig"), |path| {
701            generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts)
702        })?;
703
704        missing_path(repo_path.join("build.zig.zon"), |path| {
705            generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts)
706        })?;
707
708        missing_path(bindings_dir.join("zig"), create_dir)?.apply(|path| {
709            missing_path(path.join("root.zig"), |path| {
710                generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts)
711            })?;
712
713            Ok(())
714        })?;
715    }
716
717    Ok(())
718}
719
720pub fn get_root_path(path: &Path) -> Result<PathBuf> {
721    let mut pathbuf = path.to_owned();
722    let filename = path.file_name().unwrap().to_str().unwrap();
723    let is_package_json = filename == "package.json";
724    loop {
725        let json = pathbuf
726            .exists()
727            .then(|| {
728                let contents = fs::read_to_string(pathbuf.as_path())
729                    .with_context(|| format!("Failed to read {filename}"))?;
730                if is_package_json {
731                    serde_json::from_str::<Map<String, Value>>(&contents)
732                        .context(format!("Failed to parse {filename}"))
733                        .map(|v| v.contains_key("tree-sitter"))
734                } else {
735                    serde_json::from_str::<TreeSitterJSON>(&contents)
736                        .context(format!("Failed to parse {filename}"))
737                        .map(|_| true)
738                }
739            })
740            .transpose()?;
741        if json == Some(true) {
742            return Ok(pathbuf.parent().unwrap().to_path_buf());
743        }
744        pathbuf.pop(); // filename
745        if !pathbuf.pop() {
746            return Err(anyhow!(format!(
747                concat!(
748                    "Failed to locate a {} file,",
749                    " please ensure you have one, and if you don't then consult the docs",
750                ),
751                filename
752            )));
753        }
754        pathbuf.push(filename);
755    }
756}
757
758fn generate_file(
759    path: &Path,
760    template: &str,
761    language_name: &str,
762    generate_opts: &GenerateOpts,
763) -> Result<()> {
764    let filename = path.file_name().unwrap().to_str().unwrap();
765
766    let mut replacement = template
767        .replace(
768            CAMEL_PARSER_NAME_PLACEHOLDER,
769            generate_opts.camel_parser_name,
770        )
771        .replace(
772            TITLE_PARSER_NAME_PLACEHOLDER,
773            generate_opts.title_parser_name,
774        )
775        .replace(
776            UPPER_PARSER_NAME_PLACEHOLDER,
777            &language_name.to_shouty_snake_case(),
778        )
779        .replace(
780            LOWER_PARSER_NAME_PLACEHOLDER,
781            &language_name.to_snake_case(),
782        )
783        .replace(
784            KEBAB_PARSER_NAME_PLACEHOLDER,
785            &language_name.to_kebab_case(),
786        )
787        .replace(PARSER_NAME_PLACEHOLDER, language_name)
788        .replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION)
789        .replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION)
790        .replace(ABI_VERSION_MAX_PLACEHOLDER, &ABI_VERSION_MAX.to_string())
791        .replace(
792            PARSER_VERSION_PLACEHOLDER,
793            &generate_opts.version.to_string(),
794        )
795        .replace(PARSER_CLASS_NAME_PLACEHOLDER, generate_opts.class_name);
796
797    if let Some(name) = generate_opts.author_name {
798        replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER, name);
799    } else {
800        match filename {
801            "package.json" => {
802                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JS, "");
803            }
804            "pyproject.toml" => {
805                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_PY, "");
806            }
807            "grammar.js" => {
808                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_GRAMMAR, "");
809            }
810            "Cargo.toml" => {
811                replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, "");
812            }
813            _ => {}
814        }
815    }
816
817    if let Some(email) = generate_opts.author_email {
818        replacement = match filename {
819            "Cargo.toml" | "grammar.js" => {
820                replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, &format!("<{email}>"))
821            }
822            _ => replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, email),
823        }
824    } else {
825        match filename {
826            "package.json" => {
827                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JS, "");
828            }
829            "pyproject.toml" => {
830                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_PY, "");
831            }
832            "grammar.js" => {
833                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR, "");
834            }
835            "Cargo.toml" => {
836                replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, "");
837            }
838            _ => {}
839        }
840    }
841
842    if filename == "package.json" {
843        if let Some(url) = generate_opts.author_url {
844            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url);
845        } else {
846            replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, "");
847        }
848    }
849
850    if generate_opts.author_name.is_none()
851        && generate_opts.author_email.is_none()
852        && generate_opts.author_url.is_none()
853        && filename == "package.json"
854    {
855        if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) {
856            if let Some(end_idx) = replacement[start_idx..]
857                .find("},")
858                .map(|i| i + start_idx + 2)
859            {
860                replacement.replace_range(start_idx..end_idx, "");
861            }
862        }
863    } else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() {
864        match filename {
865            "pyproject.toml" => {
866                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_PY) {
867                    if let Some(end_idx) = replacement[start_idx..]
868                        .find("}]")
869                        .map(|i| i + start_idx + 2)
870                    {
871                        replacement.replace_range(start_idx..end_idx, "");
872                    }
873                }
874            }
875            "grammar.js" => {
876                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_GRAMMAR) {
877                    if let Some(end_idx) = replacement[start_idx..]
878                        .find(" \n")
879                        .map(|i| i + start_idx + 1)
880                    {
881                        replacement.replace_range(start_idx..end_idx, "");
882                    }
883                }
884            }
885            "Cargo.toml" => {
886                if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_RS) {
887                    if let Some(end_idx) = replacement[start_idx..]
888                        .find("\"]")
889                        .map(|i| i + start_idx + 2)
890                    {
891                        replacement.replace_range(start_idx..end_idx, "");
892                    }
893                }
894            }
895            _ => {}
896        }
897    }
898
899    match generate_opts.license {
900        Some(license) => replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, license),
901        _ => replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, "MIT"),
902    }
903
904    match generate_opts.description {
905        Some(description) => {
906            replacement = replacement.replace(PARSER_DESCRIPTION_PLACEHOLDER, description);
907        }
908        _ => {
909            replacement = replacement.replace(
910                PARSER_DESCRIPTION_PLACEHOLDER,
911                &format!(
912                    "{} grammar for tree-sitter",
913                    generate_opts.camel_parser_name,
914                ),
915            );
916        }
917    }
918
919    match generate_opts.repository {
920        Some(repository) => {
921            replacement = replacement
922                .replace(
923                    PARSER_URL_STRIPPED_PLACEHOLDER,
924                    &repository.replace("https://", "").to_lowercase(),
925                )
926                .replace(PARSER_URL_PLACEHOLDER, &repository.to_lowercase());
927        }
928        _ => {
929            replacement = replacement
930                .replace(
931                    PARSER_URL_STRIPPED_PLACEHOLDER,
932                    &format!(
933                        "github.com/tree-sitter/tree-sitter-{}",
934                        language_name.to_lowercase()
935                    ),
936                )
937                .replace(
938                    PARSER_URL_PLACEHOLDER,
939                    &format!(
940                        "https://github.com/tree-sitter/tree-sitter-{}",
941                        language_name.to_lowercase()
942                    ),
943                );
944        }
945    }
946
947    if let Some(funding_url) = generate_opts.funding {
948        match filename {
949            "pyproject.toml" | "package.json" => {
950                replacement = replacement.replace(FUNDING_URL_PLACEHOLDER, funding_url);
951            }
952            _ => {}
953        }
954    } else {
955        match filename {
956            "package.json" => {
957                replacement = replacement.replace("  \"funding\": \"FUNDING_URL\",\n", "");
958            }
959            "pyproject.toml" => {
960                replacement = replacement.replace("Funding = \"FUNDING_URL\"\n", "");
961            }
962            _ => {}
963        }
964    }
965
966    write_file(path, replacement)?;
967    Ok(())
968}
969
970fn create_dir(path: &Path) -> Result<()> {
971    fs::create_dir_all(path)
972        .with_context(|| format!("Failed to create {:?}", path.to_string_lossy()))
973}
974
975#[derive(PartialEq, Eq, Debug)]
976enum PathState<P>
977where
978    P: AsRef<Path>,
979{
980    Exists(P),
981    Missing(P),
982}
983
984#[allow(dead_code)]
985impl<P> PathState<P>
986where
987    P: AsRef<Path>,
988{
989    fn exists(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
990        if let Self::Exists(path) = self {
991            action(path.as_ref())?;
992        }
993        Ok(self)
994    }
995
996    fn missing(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
997        if let Self::Missing(path) = self {
998            action(path.as_ref())?;
999        }
1000        Ok(self)
1001    }
1002
1003    fn apply(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1004        action(self.as_path())?;
1005        Ok(self)
1006    }
1007
1008    fn apply_state(&self, mut action: impl FnMut(&Self) -> Result<()>) -> Result<&Self> {
1009        action(self)?;
1010        Ok(self)
1011    }
1012
1013    fn as_path(&self) -> &Path {
1014        match self {
1015            Self::Exists(path) | Self::Missing(path) => path.as_ref(),
1016        }
1017    }
1018}
1019
1020fn missing_path<P, F>(path: P, mut action: F) -> Result<PathState<P>>
1021where
1022    P: AsRef<Path>,
1023    F: FnMut(&Path) -> Result<()>,
1024{
1025    let path_ref = path.as_ref();
1026    if !path_ref.exists() {
1027        action(path_ref)?;
1028        Ok(PathState::Missing(path))
1029    } else {
1030        Ok(PathState::Exists(path))
1031    }
1032}
1033
1034fn missing_path_else<P, T, F>(
1035    path: P,
1036    allow_update: bool,
1037    mut action: T,
1038    mut else_action: F,
1039) -> Result<PathState<P>>
1040where
1041    P: AsRef<Path>,
1042    T: FnMut(&Path) -> Result<()>,
1043    F: FnMut(&Path) -> Result<()>,
1044{
1045    let path_ref = path.as_ref();
1046    if !path_ref.exists() {
1047        action(path_ref)?;
1048        Ok(PathState::Missing(path))
1049    } else {
1050        if allow_update {
1051            else_action(path_ref)?;
1052        }
1053        Ok(PathState::Exists(path))
1054    }
1055}