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