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