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 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 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 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 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 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 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 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 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 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 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 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 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) })?;
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 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 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(); 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}