1use std::{
2 fs,
3 path::{Path, PathBuf},
4 str::{self, FromStr},
5};
6
7use anyhow::{anyhow, Context, Result};
8use crc32fast::hash as crc32;
9use heck::{ToKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
10use indoc::{formatdoc, indoc};
11use log::warn;
12use rand::{thread_rng, Rng};
13use semver::Version;
14use serde::{Deserialize, Serialize};
15use serde_json::{Map, Value};
16use tree_sitter_generate::write_file;
17use tree_sitter_loader::{
18 Author, Bindings, Grammar, Links, Metadata, PathsJSON, TreeSitterJSON,
19 DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME, DEFAULT_INJECTIONS_QUERY_FILE_NAME,
20 DEFAULT_LOCALS_QUERY_FILE_NAME, DEFAULT_TAGS_QUERY_FILE_NAME,
21};
22
23const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
24const CLI_VERSION_PLACEHOLDER: &str = "CLI_VERSION";
25
26const ABI_VERSION_MAX: usize = tree_sitter::LANGUAGE_VERSION;
27const ABI_VERSION_MAX_PLACEHOLDER: &str = "ABI_VERSION_MAX";
28
29const PARSER_NAME_PLACEHOLDER: &str = "PARSER_NAME";
30const CAMEL_PARSER_NAME_PLACEHOLDER: &str = "CAMEL_PARSER_NAME";
31const TITLE_PARSER_NAME_PLACEHOLDER: &str = "TITLE_PARSER_NAME";
32const UPPER_PARSER_NAME_PLACEHOLDER: &str = "UPPER_PARSER_NAME";
33const LOWER_PARSER_NAME_PLACEHOLDER: &str = "LOWER_PARSER_NAME";
34const KEBAB_PARSER_NAME_PLACEHOLDER: &str = "KEBAB_PARSER_NAME";
35const PARSER_CLASS_NAME_PLACEHOLDER: &str = "PARSER_CLASS_NAME";
36
37const PARSER_DESCRIPTION_PLACEHOLDER: &str = "PARSER_DESCRIPTION";
38const PARSER_LICENSE_PLACEHOLDER: &str = "PARSER_LICENSE";
39const PARSER_NS_PLACEHOLDER: &str = "PARSER_NS";
40const PARSER_NS_CLEANED_PLACEHOLDER: &str = "PARSER_NS_CLEANED";
41const PARSER_URL_PLACEHOLDER: &str = "PARSER_URL";
42const PARSER_URL_STRIPPED_PLACEHOLDER: &str = "PARSER_URL_STRIPPED";
43const PARSER_VERSION_PLACEHOLDER: &str = "PARSER_VERSION";
44const PARSER_FINGERPRINT_PLACEHOLDER: &str = "PARSER_FINGERPRINT";
45
46const AUTHOR_NAME_PLACEHOLDER: &str = "PARSER_AUTHOR_NAME";
47const AUTHOR_EMAIL_PLACEHOLDER: &str = "PARSER_AUTHOR_EMAIL";
48const AUTHOR_URL_PLACEHOLDER: &str = "PARSER_AUTHOR_URL";
49
50const AUTHOR_BLOCK_JS: &str = "\n \"author\": {";
51const AUTHOR_NAME_PLACEHOLDER_JS: &str = "\n \"name\": \"PARSER_AUTHOR_NAME\",";
52const AUTHOR_EMAIL_PLACEHOLDER_JS: &str = ",\n \"email\": \"PARSER_AUTHOR_EMAIL\"";
53const AUTHOR_URL_PLACEHOLDER_JS: &str = ",\n \"url\": \"PARSER_AUTHOR_URL\"";
54
55const AUTHOR_BLOCK_PY: &str = "\nauthors = [{";
56const AUTHOR_NAME_PLACEHOLDER_PY: &str = "name = \"PARSER_AUTHOR_NAME\"";
57const AUTHOR_EMAIL_PLACEHOLDER_PY: &str = ", email = \"PARSER_AUTHOR_EMAIL\"";
58
59const AUTHOR_BLOCK_RS: &str = "\nauthors = [";
60const AUTHOR_NAME_PLACEHOLDER_RS: &str = "PARSER_AUTHOR_NAME";
61const AUTHOR_EMAIL_PLACEHOLDER_RS: &str = " PARSER_AUTHOR_EMAIL";
62
63const AUTHOR_BLOCK_JAVA: &str = "\n <developer>";
64const AUTHOR_NAME_PLACEHOLDER_JAVA: &str = "\n <name>PARSER_AUTHOR_NAME</name>";
65const AUTHOR_EMAIL_PLACEHOLDER_JAVA: &str = "\n <email>PARSER_AUTHOR_EMAIL</email>";
66const AUTHOR_URL_PLACEHOLDER_JAVA: &str = "\n <url>PARSER_AUTHOR_URL</url>";
67
68const AUTHOR_BLOCK_GRAMMAR: &str = "\n * @author ";
69const AUTHOR_NAME_PLACEHOLDER_GRAMMAR: &str = "PARSER_AUTHOR_NAME";
70const AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR: &str = " PARSER_AUTHOR_EMAIL";
71
72const FUNDING_URL_PLACEHOLDER: &str = "FUNDING_URL";
73
74const HIGHLIGHTS_QUERY_PATH_PLACEHOLDER: &str = "HIGHLIGHTS_QUERY_PATH";
75const INJECTIONS_QUERY_PATH_PLACEHOLDER: &str = "INJECTIONS_QUERY_PATH";
76const LOCALS_QUERY_PATH_PLACEHOLDER: &str = "LOCALS_QUERY_PATH";
77const TAGS_QUERY_PATH_PLACEHOLDER: &str = "TAGS_QUERY_PATH";
78
79const GRAMMAR_JS_TEMPLATE: &str = include_str!("./templates/grammar.js");
80const PACKAGE_JSON_TEMPLATE: &str = include_str!("./templates/package.json");
81const GITIGNORE_TEMPLATE: &str = include_str!("./templates/gitignore");
82const GITATTRIBUTES_TEMPLATE: &str = include_str!("./templates/gitattributes");
83const EDITORCONFIG_TEMPLATE: &str = include_str!("./templates/.editorconfig");
84
85const RUST_BINDING_VERSION: &str = env!("CARGO_PKG_VERSION");
86const RUST_BINDING_VERSION_PLACEHOLDER: &str = "RUST_BINDING_VERSION";
87
88const LIB_RS_TEMPLATE: &str = include_str!("./templates/lib.rs");
89const BUILD_RS_TEMPLATE: &str = include_str!("./templates/build.rs");
90const CARGO_TOML_TEMPLATE: &str = include_str!("./templates/_cargo.toml");
91
92const INDEX_JS_TEMPLATE: &str = include_str!("./templates/index.js");
93const INDEX_D_TS_TEMPLATE: &str = include_str!("./templates/index.d.ts");
94const JS_BINDING_CC_TEMPLATE: &str = include_str!("./templates/js-binding.cc");
95const BINDING_GYP_TEMPLATE: &str = include_str!("./templates/binding.gyp");
96const BINDING_TEST_JS_TEMPLATE: &str = include_str!("./templates/binding_test.js");
97
98const MAKEFILE_TEMPLATE: &str = include_str!("./templates/makefile");
99const CMAKELISTS_TXT_TEMPLATE: &str = include_str!("./templates/cmakelists.cmake");
100const PARSER_NAME_H_TEMPLATE: &str = include_str!("./templates/PARSER_NAME.h");
101const PARSER_NAME_PC_IN_TEMPLATE: &str = include_str!("./templates/PARSER_NAME.pc.in");
102
103const GO_MOD_TEMPLATE: &str = include_str!("./templates/go.mod");
104const BINDING_GO_TEMPLATE: &str = include_str!("./templates/binding.go");
105const BINDING_TEST_GO_TEMPLATE: &str = include_str!("./templates/binding_test.go");
106
107const SETUP_PY_TEMPLATE: &str = include_str!("./templates/setup.py");
108const INIT_PY_TEMPLATE: &str = include_str!("./templates/__init__.py");
109const INIT_PYI_TEMPLATE: &str = include_str!("./templates/__init__.pyi");
110const PYPROJECT_TOML_TEMPLATE: &str = include_str!("./templates/pyproject.toml");
111const PY_BINDING_C_TEMPLATE: &str = include_str!("./templates/py-binding.c");
112const TEST_BINDING_PY_TEMPLATE: &str = include_str!("./templates/test_binding.py");
113
114const PACKAGE_SWIFT_TEMPLATE: &str = include_str!("./templates/package.swift");
115const TESTS_SWIFT_TEMPLATE: &str = include_str!("./templates/tests.swift");
116
117const POM_XML_TEMPLATE: &str = include_str!("./templates/pom.xml");
118const BINDING_JAVA_TEMPLATE: &str = include_str!("./templates/binding.java");
119const TEST_JAVA_TEMPLATE: &str = include_str!("./templates/test.java");
120
121const BUILD_ZIG_TEMPLATE: &str = include_str!("./templates/build.zig");
122const BUILD_ZIG_ZON_TEMPLATE: &str = include_str!("./templates/build.zig.zon");
123const ROOT_ZIG_TEMPLATE: &str = include_str!("./templates/root.zig");
124const TEST_ZIG_TEMPLATE: &str = include_str!("./templates/test.zig");
125
126const TREE_SITTER_JSON_SCHEMA: &str =
127 "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json";
128
129#[derive(Serialize, Deserialize, Clone)]
130pub struct JsonConfigOpts {
131 pub name: String,
132 pub camelcase: String,
133 pub title: String,
134 pub description: String,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub repository: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub funding: Option<String>,
139 pub scope: String,
140 pub file_types: Vec<String>,
141 pub version: Version,
142 pub license: String,
143 pub author: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub email: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub url: Option<String>,
148 pub namespace: Option<String>,
149 pub bindings: Bindings,
150}
151
152impl JsonConfigOpts {
153 #[must_use]
154 pub fn to_tree_sitter_json(self) -> TreeSitterJSON {
155 TreeSitterJSON {
156 schema: Some(TREE_SITTER_JSON_SCHEMA.to_string()),
157 grammars: vec![Grammar {
158 name: self.name.clone(),
159 camelcase: Some(self.camelcase),
160 title: Some(self.title),
161 scope: self.scope,
162 path: None,
163 external_files: PathsJSON::Empty,
164 file_types: Some(self.file_types),
165 highlights: PathsJSON::Empty,
166 injections: PathsJSON::Empty,
167 locals: PathsJSON::Empty,
168 tags: PathsJSON::Empty,
169 injection_regex: Some(format!("^{}$", self.name)),
170 first_line_regex: None,
171 content_regex: None,
172 class_name: Some(format!("TreeSitter{}", self.name.to_upper_camel_case())),
173 }],
174 metadata: Metadata {
175 version: self.version,
176 license: Some(self.license),
177 description: Some(self.description),
178 authors: Some(vec![Author {
179 name: self.author,
180 email: self.email,
181 url: self.url,
182 }]),
183 links: Some(Links {
184 repository: self.repository.unwrap_or_else(|| {
185 format!("https://github.com/tree-sitter/tree-sitter-{}", self.name)
186 }),
187 funding: self.funding,
188 }),
189 namespace: self.namespace,
190 },
191 bindings: self.bindings,
192 }
193 }
194}
195
196impl Default for JsonConfigOpts {
197 fn default() -> Self {
198 Self {
199 name: String::new(),
200 camelcase: String::new(),
201 title: String::new(),
202 description: String::new(),
203 repository: None,
204 funding: None,
205 scope: String::new(),
206 file_types: vec![],
207 version: Version::from_str("0.1.0").unwrap(),
208 license: String::new(),
209 author: String::new(),
210 email: None,
211 url: None,
212 namespace: None,
213 bindings: Bindings::default(),
214 }
215 }
216}
217
218struct GenerateOpts<'a> {
219 author_name: Option<&'a str>,
220 author_email: Option<&'a str>,
221 author_url: Option<&'a str>,
222 license: Option<&'a str>,
223 description: Option<&'a str>,
224 repository: Option<&'a str>,
225 funding: Option<&'a str>,
226 version: &'a Version,
227 camel_parser_name: &'a str,
228 title_parser_name: &'a str,
229 class_name: &'a str,
230 highlights_query_path: &'a str,
231 injections_query_path: &'a str,
232 locals_query_path: &'a str,
233 tags_query_path: &'a str,
234 namespace: Option<&'a str>,
235}
236
237pub fn generate_grammar_files(
238 repo_path: &Path,
239 language_name: &str,
240 allow_update: bool,
241 opts: Option<&JsonConfigOpts>,
242) -> Result<()> {
243 let dashed_language_name = language_name.to_kebab_case();
244
245 let tree_sitter_config = missing_path_else(
246 repo_path.join("tree-sitter.json"),
247 true,
248 |path| {
249 let Some(opts) = opts else { unreachable!() };
251
252 let tree_sitter_json = opts.clone().to_tree_sitter_json();
253 write_file(path, serde_json::to_string_pretty(&tree_sitter_json)?)?;
254 Ok(())
255 },
256 |path| {
257 if let Some(opts) = opts {
259 let tree_sitter_json = opts.clone().to_tree_sitter_json();
260 write_file(path, serde_json::to_string_pretty(&tree_sitter_json)?)?;
261 }
262 Ok(())
263 },
264 )?;
265
266 let tree_sitter_config = serde_json::from_str::<TreeSitterJSON>(
267 &fs::read_to_string(tree_sitter_config.as_path())
268 .with_context(|| "Failed to read tree-sitter.json")?,
269 )?;
270
271 let authors = tree_sitter_config.metadata.authors.as_ref();
272 let camel_name = tree_sitter_config.grammars[0]
273 .camelcase
274 .clone()
275 .unwrap_or_else(|| language_name.to_upper_camel_case());
276 let title_name = tree_sitter_config.grammars[0]
277 .title
278 .clone()
279 .unwrap_or_else(|| language_name.to_upper_camel_case());
280 let class_name = tree_sitter_config.grammars[0]
281 .class_name
282 .clone()
283 .unwrap_or_else(|| format!("TreeSitter{}", language_name.to_upper_camel_case()));
284
285 let default_highlights_path = Path::new("queries").join(DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME);
286 let default_injections_path = Path::new("queries").join(DEFAULT_INJECTIONS_QUERY_FILE_NAME);
287 let default_locals_path = Path::new("queries").join(DEFAULT_LOCALS_QUERY_FILE_NAME);
288 let default_tags_path = Path::new("queries").join(DEFAULT_TAGS_QUERY_FILE_NAME);
289
290 let generate_opts = GenerateOpts {
291 author_name: authors
292 .map(|a| a.first().map(|a| a.name.as_str()))
293 .unwrap_or_default(),
294 author_email: authors
295 .map(|a| a.first().and_then(|a| a.email.as_deref()))
296 .unwrap_or_default(),
297 author_url: authors
298 .map(|a| a.first().and_then(|a| a.url.as_deref()))
299 .unwrap_or_default(),
300 license: tree_sitter_config.metadata.license.as_deref(),
301 description: tree_sitter_config.metadata.description.as_deref(),
302 repository: tree_sitter_config
303 .metadata
304 .links
305 .as_ref()
306 .map(|l| l.repository.as_str()),
307 funding: tree_sitter_config
308 .metadata
309 .links
310 .as_ref()
311 .and_then(|l| l.funding.as_deref()),
312 version: &tree_sitter_config.metadata.version,
313 camel_parser_name: &camel_name,
314 title_parser_name: &title_name,
315 class_name: &class_name,
316 highlights_query_path: tree_sitter_config.grammars[0]
317 .highlights
318 .to_variable_value(&default_highlights_path),
319 injections_query_path: tree_sitter_config.grammars[0]
320 .injections
321 .to_variable_value(&default_injections_path),
322 locals_query_path: tree_sitter_config.grammars[0]
323 .locals
324 .to_variable_value(&default_locals_path),
325 tags_query_path: tree_sitter_config.grammars[0]
326 .tags
327 .to_variable_value(&default_tags_path),
328 namespace: tree_sitter_config.metadata.namespace.as_deref(),
329 };
330
331 missing_path_else(
333 repo_path.join("package.json"),
334 allow_update,
335 |path| {
336 generate_file(
337 path,
338 PACKAGE_JSON_TEMPLATE,
339 dashed_language_name.as_str(),
340 &generate_opts,
341 )
342 },
343 |path| {
344 let mut contents = fs::read_to_string(path)?
345 .replace(
346 r#""node-addon-api": "^8.3.1""#,
347 r#""node-addon-api": "^8.5.0""#,
348 )
349 .replace(
350 indoc! {r#"
351 "prebuildify": "^6.0.1",
352 "tree-sitter-cli":"#},
353 indoc! {r#"
354 "prebuildify": "^6.0.1",
355 "tree-sitter": "^0.25.0",
356 "tree-sitter-cli":"#},
357 );
358 if !contents.contains("module") {
359 warn!("Updating package.json");
360 contents = contents.replace(
361 r#""repository":"#,
362 indoc! {r#"
363 "type": "module",
364 "repository":"#},
365 );
366 }
367 write_file(path, contents)?;
368 Ok(())
369 },
370 )?;
371
372 if !tree_sitter_config.has_multiple_language_configs() {
374 missing_path_else(
375 repo_path.join("grammar.js"),
376 allow_update,
377 |path| generate_file(path, GRAMMAR_JS_TEMPLATE, language_name, &generate_opts),
378 |path| {
379 let mut contents = fs::read_to_string(path)?;
380 if contents.contains("module.exports") {
381 contents = contents.replace("module.exports =", "export default");
382 write_file(path, contents)?;
383 }
384
385 Ok(())
386 },
387 )?;
388 }
389
390 missing_path_else(
392 repo_path.join(".gitignore"),
393 allow_update,
394 |path| generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts),
395 |path| {
396 let contents = fs::read_to_string(path)?;
397 if !contents.contains("Zig artifacts") {
398 warn!("Replacing .gitignore");
399 generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts)?;
400 }
401 Ok(())
402 },
403 )?;
404
405 missing_path_else(
407 repo_path.join(".gitattributes"),
408 allow_update,
409 |path| generate_file(path, GITATTRIBUTES_TEMPLATE, language_name, &generate_opts),
410 |path| {
411 let mut contents = fs::read_to_string(path)?;
412 contents = contents.replace("bindings/c/* ", "bindings/c/** ");
413 if !contents.contains("Zig bindings") {
414 contents.push('\n');
415 contents.push_str(indoc! {"
416 # Zig bindings
417 build.zig linguist-generated
418 build.zig.zon linguist-generated
419 "});
420 }
421 write_file(path, contents)?;
422 Ok(())
423 },
424 )?;
425
426 missing_path(repo_path.join(".editorconfig"), |path| {
428 generate_file(path, EDITORCONFIG_TEMPLATE, language_name, &generate_opts)
429 })?;
430
431 let bindings_dir = repo_path.join("bindings");
432
433 if tree_sitter_config.bindings.rust {
435 missing_path(bindings_dir.join("rust"), create_dir)?.apply(|path| {
436 missing_path_else(path.join("lib.rs"), allow_update, |path| {
437 generate_file(path, LIB_RS_TEMPLATE, language_name, &generate_opts)
438 }, |path| {
439 let mut contents = fs::read_to_string(path)?;
440 if !contents.contains("#[cfg(with_highlights_query)]") {
441 let replacement = indoc! {r#"
442 #[cfg(with_highlights_query)]
443 /// The syntax highlighting query for this grammar.
444 pub const HIGHLIGHTS_QUERY: &str = include_str!("../../HIGHLIGHTS_QUERY_PATH");
445
446 #[cfg(with_injections_query)]
447 /// The language injection query for this grammar.
448 pub const INJECTIONS_QUERY: &str = include_str!("../../INJECTIONS_QUERY_PATH");
449
450 #[cfg(with_locals_query)]
451 /// The local variable query for this grammar.
452 pub const LOCALS_QUERY: &str = include_str!("../../LOCALS_QUERY_PATH");
453
454 #[cfg(with_tags_query)]
455 /// The symbol tagging query for this grammar.
456 pub const TAGS_QUERY: &str = include_str!("../../TAGS_QUERY_PATH");
457 "#}
458 .replace("HIGHLIGHTS_QUERY_PATH", generate_opts.highlights_query_path)
459 .replace("INJECTIONS_QUERY_PATH", generate_opts.injections_query_path)
460 .replace("LOCALS_QUERY_PATH", generate_opts.locals_query_path)
461 .replace("TAGS_QUERY_PATH", generate_opts.tags_query_path);
462 contents = contents
463 .replace(
464 indoc! {r#"
465 // NOTE: uncomment these to include any queries that this grammar contains:
466
467 // pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
468 // pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
469 // pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
470 // pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
471 "#},
472 &replacement,
473 );
474 }
475 write_file(path, contents)?;
476 Ok(())
477 })?;
478
479 missing_path_else(
480 path.join("build.rs"),
481 allow_update,
482 |path| generate_file(path, BUILD_RS_TEMPLATE, language_name, &generate_opts),
483 |path| {
484 let mut contents = fs::read_to_string(path)?;
485 if !contents.contains("wasm32-unknown-unknown") {
486 let replacement = indoc!{r#"
487 c_config.flag("-utf-8");
488
489 if std::env::var("TARGET").unwrap() == "wasm32-unknown-unknown" {
490 let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
491 panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
492 };
493 let Ok(wasm_src) =
494 std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
495 else {
496 panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
497 };
498
499 c_config.include(&wasm_headers);
500 c_config.files([
501 wasm_src.join("stdio.c"),
502 wasm_src.join("stdlib.c"),
503 wasm_src.join("string.c"),
504 ]);
505 }
506 "#};
507
508 let indented_replacement = replacement
509 .lines()
510 .map(|line| if line.is_empty() { line.to_string() } else { format!(" {line}") })
511 .collect::<Vec<_>>()
512 .join("\n");
513
514 contents = contents.replace(r#" c_config.flag("-utf-8");"#, &indented_replacement);
515 }
516
517 if !contents.contains("with_highlights_query") {
519 let replaced = indoc! {r#"
520 c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
521 }"#}
522 .replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case());
523
524 let replacement = indoc! {r#"
525 c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
526
527 println!("cargo:rustc-check-cfg=cfg(with_highlights_query)");
528 if !"HIGHLIGHTS_QUERY_PATH".is_empty() && std::path::Path::new("HIGHLIGHTS_QUERY_PATH").exists() {
529 println!("cargo:rustc-cfg=with_highlights_query");
530 }
531 println!("cargo:rustc-check-cfg=cfg(with_injections_query)");
532 if !"INJECTIONS_QUERY_PATH".is_empty() && std::path::Path::new("INJECTIONS_QUERY_PATH").exists() {
533 println!("cargo:rustc-cfg=with_injections_query");
534 }
535 println!("cargo:rustc-check-cfg=cfg(with_locals_query)");
536 if !"LOCALS_QUERY_PATH".is_empty() && std::path::Path::new("LOCALS_QUERY_PATH").exists() {
537 println!("cargo:rustc-cfg=with_locals_query");
538 }
539 println!("cargo:rustc-check-cfg=cfg(with_tags_query)");
540 if !"TAGS_QUERY_PATH".is_empty() && std::path::Path::new("TAGS_QUERY_PATH").exists() {
541 println!("cargo:rustc-cfg=with_tags_query");
542 }
543 }"#}
544 .replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case())
545 .replace("HIGHLIGHTS_QUERY_PATH", generate_opts.highlights_query_path)
546 .replace("INJECTIONS_QUERY_PATH", generate_opts.injections_query_path)
547 .replace("LOCALS_QUERY_PATH", generate_opts.locals_query_path)
548 .replace("TAGS_QUERY_PATH", generate_opts.tags_query_path);
549
550 contents = contents.replace(
551 &replaced,
552 &replacement,
553 );
554 }
555
556 write_file(path, contents)?;
557 Ok(())
558 },
559 )?;
560
561 missing_path_else(
562 repo_path.join("Cargo.toml"),
563 allow_update,
564 |path| {
565 generate_file(
566 path,
567 CARGO_TOML_TEMPLATE,
568 dashed_language_name.as_str(),
569 &generate_opts,
570 )
571 },
572 |path| {
573 let contents = fs::read_to_string(path)?;
574 if contents.contains("\"LICENSE\"") {
575 write_file(path, contents.replace("\"LICENSE\"", "\"/LICENSE\""))?;
576 }
577 Ok(())
578 },
579 )?;
580
581 Ok(())
582 })?;
583 }
584
585 if tree_sitter_config.bindings.node {
587 missing_path(bindings_dir.join("node"), create_dir)?.apply(|path| {
588 missing_path_else(
589 path.join("index.js"),
590 allow_update,
591 |path| generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts),
592 |path| {
593 let contents = fs::read_to_string(path)?;
594 if !contents.contains("Object.defineProperty") {
595 warn!("Replacing index.js");
596 generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts)?;
597 }
598 Ok(())
599 },
600 )?;
601
602 missing_path_else(
603 path.join("index.d.ts"),
604 allow_update,
605 |path| generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts),
606 |path| {
607 let contents = fs::read_to_string(path)?;
608 if !contents.contains("export default binding") {
609 warn!("Replacing index.d.ts");
610 generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts)?;
611 }
612 Ok(())
613 },
614 )?;
615
616 missing_path_else(
617 path.join("binding_test.js"),
618 allow_update,
619 |path| {
620 generate_file(
621 path,
622 BINDING_TEST_JS_TEMPLATE,
623 language_name,
624 &generate_opts,
625 )
626 },
627 |path| {
628 let contents = fs::read_to_string(path)?;
629 if !contents.contains("import") {
630 warn!("Replacing binding_test.js");
631 generate_file(
632 path,
633 BINDING_TEST_JS_TEMPLATE,
634 language_name,
635 &generate_opts,
636 )?;
637 }
638 Ok(())
639 },
640 )?;
641
642 missing_path(path.join("binding.cc"), |path| {
643 generate_file(path, JS_BINDING_CC_TEMPLATE, language_name, &generate_opts)
644 })?;
645
646 missing_path_else(
647 repo_path.join("binding.gyp"),
648 allow_update,
649 |path| generate_file(path, BINDING_GYP_TEMPLATE, language_name, &generate_opts),
650 |path| {
651 let contents = fs::read_to_string(path)?;
652 if contents.contains("fs.exists(") {
653 write_file(path, contents.replace("fs.exists(", "fs.existsSync("))?;
654 }
655 Ok(())
656 },
657 )?;
658
659 Ok(())
660 })?;
661 }
662
663 if tree_sitter_config.bindings.c {
665 missing_path(bindings_dir.join("c"), create_dir)?.apply(|path| {
666 let old_file = &path.join(format!("tree-sitter-{}.h", language_name.to_kebab_case()));
667 if allow_update && fs::exists(old_file).unwrap_or(false) {
668 fs::remove_file(old_file)?;
669 }
670 missing_path(path.join("tree_sitter"), create_dir)?.apply(|include_path| {
671 missing_path(
672 include_path.join(format!("tree-sitter-{}.h", language_name.to_kebab_case())),
673 |path| {
674 generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
675 },
676 )?;
677 Ok(())
678 })?;
679
680 missing_path(
681 path.join(format!("tree-sitter-{}.pc.in", language_name.to_kebab_case())),
682 |path| {
683 generate_file(
684 path,
685 PARSER_NAME_PC_IN_TEMPLATE,
686 language_name,
687 &generate_opts,
688 )
689 },
690 )?;
691
692 missing_path_else(
693 repo_path.join("Makefile"),
694 allow_update,
695 |path| {
696 generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)
697 },
698 |path| {
699 let mut contents = fs::read_to_string(path)?;
700 if !contents.contains("cd '$(DESTDIR)$(LIBDIR)' && ln -sf") {
701 warn!("Replacing Makefile");
702 generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)?;
703 } else {
704 contents = contents
705 .replace(
706 indoc! {r"
707 $(PARSER): $(SRC_DIR)/grammar.json
708 $(TS) generate $^
709 "},
710 indoc! {r"
711 $(SRC_DIR)/grammar.json: grammar.js
712 $(TS) generate --no-parser $^
713
714 $(PARSER): $(SRC_DIR)/grammar.json
715 $(TS) generate $^
716 "}
717 );
718 write_file(path, contents)?;
719 }
720 Ok(())
721 },
722 )?;
723
724 missing_path_else(
725 repo_path.join("CMakeLists.txt"),
726 allow_update,
727 |path| generate_file(path, CMAKELISTS_TXT_TEMPLATE, language_name, &generate_opts),
728 |path| {
729 let mut contents = fs::read_to_string(path)?;
730 contents = contents
731 .replace("add_custom_target(test", "add_custom_target(ts-test")
732 .replace(
733 &formatdoc! {r#"
734 install(FILES bindings/c/tree-sitter-{language_name}.h
735 DESTINATION "${{CMAKE_INSTALL_INCLUDEDIR}}/tree_sitter")
736 "#},
737 indoc! {r#"
738 install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bindings/c/tree_sitter"
739 DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
740 FILES_MATCHING PATTERN "*.h")
741 "#}
742 ).replace(
743 &format!("target_include_directories(tree-sitter-{language_name} PRIVATE src)"),
744 &formatdoc! {"
745 target_include_directories(tree-sitter-{language_name}
746 PRIVATE src
747 INTERFACE $<BUILD_INTERFACE:${{CMAKE_CURRENT_SOURCE_DIR}}/bindings/c>
748 $<INSTALL_INTERFACE:${{CMAKE_INSTALL_INCLUDEDIR}}>)
749 "}
750 ).replace(
751 indoc! {r#"
752 add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
753 DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
754 COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
755 --abi=${TREE_SITTER_ABI_VERSION}
756 WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
757 COMMENT "Generating parser.c")
758 "#},
759 indoc! {r#"
760 add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
761 "${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"
762 DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js"
763 COMMAND "${TREE_SITTER_CLI}" generate grammar.js --no-parser
764 WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
765 COMMENT "Generating grammar.json")
766
767 add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
768 BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/parser.h"
769 "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/alloc.h"
770 "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/array.h"
771 DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
772 COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
773 --abi=${TREE_SITTER_ABI_VERSION}
774 WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
775 COMMENT "Generating parser.c")
776 "#}
777 );
778 write_file(path, contents)?;
779 Ok(())
780 },
781 )?;
782
783 Ok(())
784 })?;
785 }
786
787 if tree_sitter_config.bindings.go {
789 missing_path(bindings_dir.join("go"), create_dir)?.apply(|path| {
790 missing_path(path.join("binding.go"), |path| {
791 generate_file(path, BINDING_GO_TEMPLATE, language_name, &generate_opts)
792 })?;
793
794 missing_path(path.join("binding_test.go"), |path| {
795 generate_file(
796 path,
797 BINDING_TEST_GO_TEMPLATE,
798 language_name,
799 &generate_opts,
800 )
801 })?;
802
803 missing_path(repo_path.join("go.mod"), |path| {
804 generate_file(path, GO_MOD_TEMPLATE, language_name, &generate_opts)
805 })?;
806
807 Ok(())
808 })?;
809 }
810
811 if tree_sitter_config.bindings.python {
813 missing_path(bindings_dir.join("python"), create_dir)?.apply(|path| {
814 let lang_path = path.join(format!("tree_sitter_{}", language_name.to_snake_case()));
815 missing_path(&lang_path, create_dir)?;
816
817 missing_path_else(
818 lang_path.join("binding.c"),
819 allow_update,
820 |path| generate_file(path, PY_BINDING_C_TEMPLATE, language_name, &generate_opts),
821 |path| {
822 let mut contents = fs::read_to_string(path)?;
823 if !contents.contains("PyModuleDef_Init") {
824 contents = contents
825 .replace("PyModule_Create", "PyModuleDef_Init")
826 .replace(
827 "static PyMethodDef methods[] = {\n",
828 indoc! {"
829 static struct PyModuleDef_Slot slots[] = {
830 #ifdef Py_GIL_DISABLED
831 {Py_mod_gil, Py_MOD_GIL_NOT_USED},
832 #endif
833 {0, NULL}
834 };
835
836 static PyMethodDef methods[] = {
837 "},
838 )
839 .replace(
840 indoc! {"
841 .m_size = -1,
842 .m_methods = methods
843 "},
844 indoc! {"
845 .m_size = 0,
846 .m_methods = methods,
847 .m_slots = slots,
848 "},
849 );
850 write_file(path, contents)?;
851 }
852 Ok(())
853 },
854 )?;
855
856 missing_path_else(
857 lang_path.join("__init__.py"),
858 allow_update,
859 |path| {
860 generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)
861 },
862 |path| {
863 let contents = fs::read_to_string(path)?;
864 if !contents.contains("uncomment these to include any queries") {
865 warn!("Replacing __init__.py");
866 generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)?;
867 }
868 Ok(())
869 },
870 )?;
871
872 missing_path_else(
873 lang_path.join("__init__.pyi"),
874 allow_update,
875 |path| generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts),
876 |path| {
877 let mut contents = fs::read_to_string(path)?;
878 if contents.contains("uncomment these to include any queries") {
879 warn!("Replacing __init__.pyi");
880 generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts)?;
881 } else if !contents.contains("CapsuleType") {
882 contents = contents
883 .replace(
884 "from typing import Final",
885 "from typing import Final\nfrom typing_extensions import CapsuleType"
886 )
887 .replace("-> object:", "-> CapsuleType:");
888 write_file(path, contents)?;
889 }
890 Ok(())
891 },
892 )?;
893
894 missing_path(lang_path.join("py.typed"), |path| {
895 generate_file(path, "", language_name, &generate_opts) })?;
897
898 missing_path(path.join("tests"), create_dir)?.apply(|path| {
899 missing_path_else(
900 path.join("test_binding.py"),
901 allow_update,
902 |path| {
903 generate_file(
904 path,
905 TEST_BINDING_PY_TEMPLATE,
906 language_name,
907 &generate_opts,
908 )
909 },
910 |path| {
911 let mut contents = fs::read_to_string(path)?;
912 if !contents.contains("Parser(Language(") {
913 contents = contents
914 .replace("tree_sitter.Language(", "Parser(Language(")
915 .replace(".language())\n", ".language()))\n")
916 .replace(
917 "import tree_sitter\n",
918 "from tree_sitter import Language, Parser\n",
919 );
920 write_file(path, contents)?;
921 }
922 Ok(())
923 },
924 )?;
925 Ok(())
926 })?;
927
928 missing_path_else(
929 repo_path.join("setup.py"),
930 allow_update,
931 |path| generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts),
932 |path| {
933 let contents = fs::read_to_string(path)?;
934 if !contents.contains("build_ext") {
935 warn!("Replacing setup.py");
936 generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts)?;
937 }
938 Ok(())
939 },
940 )?;
941
942 missing_path_else(
943 repo_path.join("pyproject.toml"),
944 allow_update,
945 |path| {
946 generate_file(
947 path,
948 PYPROJECT_TOML_TEMPLATE,
949 dashed_language_name.as_str(),
950 &generate_opts,
951 )
952 },
953 |path| {
954 let mut contents = fs::read_to_string(path)?;
955 if !contents.contains("cp310-*") {
956 contents = contents
957 .replace(r#"build = "cp39-*""#, r#"build = "cp310-*""#)
958 .replace(r#"python = ">=3.9""#, r#"python = ">=3.10""#)
959 .replace("tree-sitter~=0.22", "tree-sitter~=0.24");
960 write_file(path, contents)?;
961 }
962 Ok(())
963 },
964 )?;
965
966 Ok(())
967 })?;
968 }
969
970 if tree_sitter_config.bindings.swift {
972 missing_path(bindings_dir.join("swift"), create_dir)?.apply(|path| {
973 let lang_path = path.join(&class_name);
974 missing_path(&lang_path, create_dir)?;
975
976 missing_path(lang_path.join(format!("{language_name}.h")), |path| {
977 generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
978 })?;
979
980 missing_path(path.join(format!("{class_name}Tests")), create_dir)?.apply(|path| {
981 missing_path(path.join(format!("{class_name}Tests.swift")), |path| {
982 generate_file(path, TESTS_SWIFT_TEMPLATE, language_name, &generate_opts)
983 })?;
984
985 Ok(())
986 })?;
987
988 missing_path_else(
989 repo_path.join("Package.swift"),
990 allow_update,
991 |path| generate_file(path, PACKAGE_SWIFT_TEMPLATE, language_name, &generate_opts),
992 |path| {
993 let mut contents = fs::read_to_string(path)?;
994 contents = contents
995 .replace(
996 "https://github.com/ChimeHQ/SwiftTreeSitter",
997 "https://github.com/tree-sitter/swift-tree-sitter",
998 )
999 .replace("version: \"0.8.0\")", "version: \"0.9.0\")")
1000 .replace("(url:", "(name: \"SwiftTreeSitter\", url:");
1001 write_file(path, contents)?;
1002 Ok(())
1003 },
1004 )?;
1005
1006 Ok(())
1007 })?;
1008 }
1009
1010 if tree_sitter_config.bindings.zig {
1012 missing_path_else(
1013 repo_path.join("build.zig"),
1014 allow_update,
1015 |path| generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts),
1016 |path| {
1017 let contents = fs::read_to_string(path)?;
1018 if !contents.contains("b.pkg_hash.len") {
1019 warn!("Replacing build.zig");
1020 generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts)
1021 } else {
1022 Ok(())
1023 }
1024 },
1025 )?;
1026
1027 missing_path_else(
1028 repo_path.join("build.zig.zon"),
1029 allow_update,
1030 |path| generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts),
1031 |path| {
1032 let contents = fs::read_to_string(path)?;
1033 if !contents.contains(".name = .tree_sitter_") {
1034 warn!("Replacing build.zig.zon");
1035 generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts)
1036 } else {
1037 Ok(())
1038 }
1039 },
1040 )?;
1041
1042 missing_path(bindings_dir.join("zig"), create_dir)?.apply(|path| {
1043 missing_path_else(
1044 path.join("root.zig"),
1045 allow_update,
1046 |path| generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts),
1047 |path| {
1048 let contents = fs::read_to_string(path)?;
1049 if contents.contains("ts.Language") {
1050 warn!("Replacing root.zig");
1051 generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts)
1052 } else {
1053 Ok(())
1054 }
1055 },
1056 )?;
1057
1058 missing_path(path.join("test.zig"), |path| {
1059 generate_file(path, TEST_ZIG_TEMPLATE, language_name, &generate_opts)
1060 })?;
1061
1062 Ok(())
1063 })?;
1064 }
1065
1066 if tree_sitter_config.bindings.java {
1068 missing_path(repo_path.join("pom.xml"), |path| {
1069 generate_file(path, POM_XML_TEMPLATE, language_name, &generate_opts)
1070 })?;
1071
1072 missing_path(bindings_dir.join("java"), create_dir)?.apply(|path| {
1073 missing_path(path.join("main"), create_dir)?.apply(|path| {
1074 let package_path = generate_opts
1075 .namespace
1076 .unwrap_or("io.github.treesitter")
1077 .replace(['-', '_'], "")
1078 .split('.')
1079 .fold(path.to_path_buf(), |path, dir| path.join(dir))
1080 .join("jtreesitter")
1081 .join(language_name.to_lowercase().replace('_', ""));
1082 missing_path(package_path, create_dir)?.apply(|path| {
1083 missing_path(path.join(format!("{class_name}.java")), |path| {
1084 generate_file(path, BINDING_JAVA_TEMPLATE, language_name, &generate_opts)
1085 })?;
1086
1087 Ok(())
1088 })?;
1089
1090 Ok(())
1091 })?;
1092
1093 missing_path(path.join("test"), create_dir)?.apply(|path| {
1094 missing_path(path.join(format!("{class_name}Test.java")), |path| {
1095 generate_file(path, TEST_JAVA_TEMPLATE, language_name, &generate_opts)
1096 })?;
1097
1098 Ok(())
1099 })?;
1100
1101 Ok(())
1102 })?;
1103 }
1104
1105 Ok(())
1106}
1107
1108pub fn get_root_path(path: &Path) -> Result<PathBuf> {
1109 let mut pathbuf = path.to_owned();
1110 let filename = path.file_name().unwrap().to_str().unwrap();
1111 let is_package_json = filename == "package.json";
1112 loop {
1113 let json = pathbuf
1114 .exists()
1115 .then(|| {
1116 let contents = fs::read_to_string(pathbuf.as_path())
1117 .with_context(|| format!("Failed to read {filename}"))?;
1118 if is_package_json {
1119 serde_json::from_str::<Map<String, Value>>(&contents)
1120 .context(format!("Failed to parse {filename}"))
1121 .map(|v| v.contains_key("tree-sitter"))
1122 } else {
1123 serde_json::from_str::<TreeSitterJSON>(&contents)
1124 .context(format!("Failed to parse {filename}"))
1125 .map(|_| true)
1126 }
1127 })
1128 .transpose()?;
1129 if json == Some(true) {
1130 return Ok(pathbuf.parent().unwrap().to_path_buf());
1131 }
1132 pathbuf.pop(); if !pathbuf.pop() {
1134 return Err(anyhow!(format!(
1135 concat!(
1136 "Failed to locate a {} file,",
1137 " please ensure you have one, and if you don't then consult the docs",
1138 ),
1139 filename
1140 )));
1141 }
1142 pathbuf.push(filename);
1143 }
1144}
1145
1146fn generate_file(
1147 path: &Path,
1148 template: &str,
1149 language_name: &str,
1150 generate_opts: &GenerateOpts,
1151) -> Result<()> {
1152 let filename = path.file_name().unwrap().to_str().unwrap();
1153
1154 let lower_parser_name = if path
1155 .extension()
1156 .is_some_and(|e| e.eq_ignore_ascii_case("java"))
1157 {
1158 language_name.to_snake_case().replace('_', "")
1159 } else {
1160 language_name.to_snake_case()
1161 };
1162
1163 let mut replacement = template
1164 .replace(
1165 CAMEL_PARSER_NAME_PLACEHOLDER,
1166 generate_opts.camel_parser_name,
1167 )
1168 .replace(
1169 TITLE_PARSER_NAME_PLACEHOLDER,
1170 generate_opts.title_parser_name,
1171 )
1172 .replace(
1173 UPPER_PARSER_NAME_PLACEHOLDER,
1174 &language_name.to_shouty_snake_case(),
1175 )
1176 .replace(
1177 KEBAB_PARSER_NAME_PLACEHOLDER,
1178 &language_name.to_kebab_case(),
1179 )
1180 .replace(LOWER_PARSER_NAME_PLACEHOLDER, &lower_parser_name)
1181 .replace(PARSER_NAME_PLACEHOLDER, language_name)
1182 .replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION)
1183 .replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION)
1184 .replace(ABI_VERSION_MAX_PLACEHOLDER, &ABI_VERSION_MAX.to_string())
1185 .replace(
1186 PARSER_VERSION_PLACEHOLDER,
1187 &generate_opts.version.to_string(),
1188 )
1189 .replace(PARSER_CLASS_NAME_PLACEHOLDER, generate_opts.class_name)
1190 .replace(
1191 HIGHLIGHTS_QUERY_PATH_PLACEHOLDER,
1192 generate_opts.highlights_query_path,
1193 )
1194 .replace(
1195 INJECTIONS_QUERY_PATH_PLACEHOLDER,
1196 generate_opts.injections_query_path,
1197 )
1198 .replace(
1199 LOCALS_QUERY_PATH_PLACEHOLDER,
1200 generate_opts.locals_query_path,
1201 )
1202 .replace(TAGS_QUERY_PATH_PLACEHOLDER, generate_opts.tags_query_path);
1203
1204 if let Some(name) = generate_opts.author_name {
1205 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER, name);
1206 } else {
1207 match filename {
1208 "package.json" => {
1209 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JS, "");
1210 }
1211 "pyproject.toml" => {
1212 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_PY, "");
1213 }
1214 "grammar.js" => {
1215 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_GRAMMAR, "");
1216 }
1217 "Cargo.toml" => {
1218 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, "");
1219 }
1220 "pom.xml" => {
1221 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JAVA, "");
1222 }
1223 _ => {}
1224 }
1225 }
1226
1227 if let Some(email) = generate_opts.author_email {
1228 replacement = match filename {
1229 "Cargo.toml" | "grammar.js" => {
1230 replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, &format!("<{email}>"))
1231 }
1232 _ => replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, email),
1233 }
1234 } else {
1235 match filename {
1236 "package.json" => {
1237 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JS, "");
1238 }
1239 "pyproject.toml" => {
1240 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_PY, "");
1241 }
1242 "grammar.js" => {
1243 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR, "");
1244 }
1245 "Cargo.toml" => {
1246 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, "");
1247 }
1248 "pom.xml" => {
1249 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JAVA, "");
1250 }
1251 _ => {}
1252 }
1253 }
1254
1255 match (generate_opts.author_url, filename) {
1256 (Some(url), "package.json" | "pom.xml") => {
1257 replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url);
1258 }
1259 (None, "package.json") => {
1260 replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, "");
1261 }
1262 (None, "pom.xml") => {
1263 replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JAVA, "");
1264 }
1265 _ => {}
1266 }
1267
1268 if generate_opts.author_name.is_none()
1269 && generate_opts.author_email.is_none()
1270 && generate_opts.author_url.is_none()
1271 {
1272 match filename {
1273 "package.json" => {
1274 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) {
1275 if let Some(end_idx) = replacement[start_idx..]
1276 .find("},")
1277 .map(|i| i + start_idx + 2)
1278 {
1279 replacement.replace_range(start_idx..end_idx, "");
1280 }
1281 }
1282 }
1283 "pom.xml" => {
1284 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JAVA) {
1285 if let Some(end_idx) = replacement[start_idx..]
1286 .find("</developer>")
1287 .map(|i| i + start_idx + 12)
1288 {
1289 replacement.replace_range(start_idx..end_idx, "");
1290 }
1291 }
1292 }
1293 _ => {}
1294 }
1295 } else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() {
1296 match filename {
1297 "pyproject.toml" => {
1298 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_PY) {
1299 if let Some(end_idx) = replacement[start_idx..]
1300 .find("}]")
1301 .map(|i| i + start_idx + 2)
1302 {
1303 replacement.replace_range(start_idx..end_idx, "");
1304 }
1305 }
1306 }
1307 "grammar.js" => {
1308 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_GRAMMAR) {
1309 if let Some(end_idx) = replacement[start_idx..]
1310 .find(" \n")
1311 .map(|i| i + start_idx + 1)
1312 {
1313 replacement.replace_range(start_idx..end_idx, "");
1314 }
1315 }
1316 }
1317 "Cargo.toml" => {
1318 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_RS) {
1319 if let Some(end_idx) = replacement[start_idx..]
1320 .find("\"]")
1321 .map(|i| i + start_idx + 2)
1322 {
1323 replacement.replace_range(start_idx..end_idx, "");
1324 }
1325 }
1326 }
1327 _ => {}
1328 }
1329 }
1330
1331 if let Some(license) = generate_opts.license {
1332 replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, license);
1333 } else {
1334 replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, "MIT");
1335 }
1336
1337 if let Some(description) = generate_opts.description {
1338 replacement = replacement.replace(PARSER_DESCRIPTION_PLACEHOLDER, description);
1339 } else {
1340 replacement = replacement.replace(
1341 PARSER_DESCRIPTION_PLACEHOLDER,
1342 &format!(
1343 "{} grammar for tree-sitter",
1344 generate_opts.camel_parser_name,
1345 ),
1346 );
1347 }
1348
1349 if let Some(repository) = generate_opts.repository {
1350 replacement = replacement
1351 .replace(
1352 PARSER_URL_STRIPPED_PLACEHOLDER,
1353 &repository.replace("https://", "").to_lowercase(),
1354 )
1355 .replace(PARSER_URL_PLACEHOLDER, &repository.to_lowercase());
1356 } else {
1357 replacement = replacement
1358 .replace(
1359 PARSER_URL_STRIPPED_PLACEHOLDER,
1360 &format!(
1361 "github.com/tree-sitter/tree-sitter-{}",
1362 language_name.to_lowercase()
1363 ),
1364 )
1365 .replace(
1366 PARSER_URL_PLACEHOLDER,
1367 &format!(
1368 "https://github.com/tree-sitter/tree-sitter-{}",
1369 language_name.to_lowercase()
1370 ),
1371 );
1372 }
1373
1374 if let Some(namespace) = generate_opts.namespace {
1375 replacement = replacement
1376 .replace(
1377 PARSER_NS_CLEANED_PLACEHOLDER,
1378 &namespace.replace(['-', '_'], ""),
1379 )
1380 .replace(PARSER_NS_PLACEHOLDER, namespace);
1381 } else {
1382 replacement = replacement
1383 .replace(PARSER_NS_CLEANED_PLACEHOLDER, "io.github.treesitter")
1384 .replace(PARSER_NS_PLACEHOLDER, "io.github.tree-sitter");
1385 }
1386
1387 if let Some(funding_url) = generate_opts.funding {
1388 match filename {
1389 "pyproject.toml" | "package.json" => {
1390 replacement = replacement.replace(FUNDING_URL_PLACEHOLDER, funding_url);
1391 }
1392 _ => {}
1393 }
1394 } else {
1395 match filename {
1396 "package.json" => {
1397 replacement = replacement.replace(" \"funding\": \"FUNDING_URL\",\n", "");
1398 }
1399 "pyproject.toml" => {
1400 replacement = replacement.replace("Funding = \"FUNDING_URL\"\n", "");
1401 }
1402 _ => {}
1403 }
1404 }
1405
1406 if filename == "build.zig.zon" {
1407 let id = thread_rng().gen_range(1u32..0xFFFF_FFFFu32);
1408 let checksum = crc32(format!("tree_sitter_{language_name}").as_bytes());
1409 replacement = replacement.replace(
1410 PARSER_FINGERPRINT_PLACEHOLDER,
1411 #[cfg(target_endian = "little")]
1412 &format!("0x{checksum:x}{id:x}"),
1413 #[cfg(target_endian = "big")]
1414 &format!("0x{id:x}{checksum:x}"),
1415 );
1416 }
1417
1418 write_file(path, replacement)?;
1419 Ok(())
1420}
1421
1422fn create_dir(path: &Path) -> Result<()> {
1423 fs::create_dir_all(path)
1424 .with_context(|| format!("Failed to create {:?}", path.to_string_lossy()))
1425}
1426
1427#[derive(PartialEq, Eq, Debug)]
1428enum PathState<P>
1429where
1430 P: AsRef<Path>,
1431{
1432 Exists(P),
1433 Missing(P),
1434}
1435
1436#[allow(dead_code)]
1437impl<P> PathState<P>
1438where
1439 P: AsRef<Path>,
1440{
1441 fn exists(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1442 if let Self::Exists(path) = self {
1443 action(path.as_ref())?;
1444 }
1445 Ok(self)
1446 }
1447
1448 fn missing(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1449 if let Self::Missing(path) = self {
1450 action(path.as_ref())?;
1451 }
1452 Ok(self)
1453 }
1454
1455 fn apply(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1456 action(self.as_path())?;
1457 Ok(self)
1458 }
1459
1460 fn apply_state(&self, mut action: impl FnMut(&Self) -> Result<()>) -> Result<&Self> {
1461 action(self)?;
1462 Ok(self)
1463 }
1464
1465 fn as_path(&self) -> &Path {
1466 match self {
1467 Self::Exists(path) | Self::Missing(path) => path.as_ref(),
1468 }
1469 }
1470}
1471
1472fn missing_path<P, F>(path: P, mut action: F) -> Result<PathState<P>>
1473where
1474 P: AsRef<Path>,
1475 F: FnMut(&Path) -> Result<()>,
1476{
1477 let path_ref = path.as_ref();
1478 if !path_ref.exists() {
1479 action(path_ref)?;
1480 Ok(PathState::Missing(path))
1481 } else {
1482 Ok(PathState::Exists(path))
1483 }
1484}
1485
1486fn missing_path_else<P, T, F>(
1487 path: P,
1488 allow_update: bool,
1489 mut action: T,
1490 mut else_action: F,
1491) -> Result<PathState<P>>
1492where
1493 P: AsRef<Path>,
1494 T: FnMut(&Path) -> Result<()>,
1495 F: FnMut(&Path) -> Result<()>,
1496{
1497 let path_ref = path.as_ref();
1498 if !path_ref.exists() {
1499 action(path_ref)?;
1500 Ok(PathState::Missing(path))
1501 } else {
1502 if allow_update {
1503 else_action(path_ref)?;
1504 }
1505 Ok(PathState::Exists(path))
1506 }
1507}