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::info;
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
126pub const 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 info!("Migrating package.json to ESM");
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 info!("Migrating grammars.js to ESM");
382 contents = contents.replace("module.exports =", "export default");
383 write_file(path, contents)?;
384 }
385
386 Ok(())
387 },
388 )?;
389 }
390
391 missing_path_else(
393 repo_path.join(".gitignore"),
394 allow_update,
395 |path| generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts),
396 |path| {
397 let mut contents = fs::read_to_string(path)?;
398 if !contents.contains("Zig artifacts") {
399 info!("Adding zig entries to .gitignore");
400 contents.push('\n');
401 contents.push_str(indoc! {"
402 # Zig artifacts
403 .zig-cache/
404 zig-cache/
405 zig-out/
406 "});
407 }
408 Ok(())
409 },
410 )?;
411
412 missing_path_else(
414 repo_path.join(".gitattributes"),
415 allow_update,
416 |path| generate_file(path, GITATTRIBUTES_TEMPLATE, language_name, &generate_opts),
417 |path| {
418 let mut contents = fs::read_to_string(path)?;
419 let c_bindings_entry = "bindings/c/* ";
420 if contents.contains(c_bindings_entry) {
421 info!("Updating c bindings entry in .gitattributes");
422 contents = contents.replace(c_bindings_entry, "bindings/c/** ");
423 }
424 if !contents.contains("Zig bindings") {
425 info!("Adding zig entries to .gitattributes");
426 contents.push('\n');
427 contents.push_str(indoc! {"
428 # Zig bindings
429 build.zig linguist-generated
430 build.zig.zon linguist-generated
431 "});
432 }
433 write_file(path, contents)?;
434 Ok(())
435 },
436 )?;
437
438 missing_path(repo_path.join(".editorconfig"), |path| {
440 generate_file(path, EDITORCONFIG_TEMPLATE, language_name, &generate_opts)
441 })?;
442
443 let bindings_dir = repo_path.join("bindings");
444
445 if tree_sitter_config.bindings.rust {
447 missing_path(bindings_dir.join("rust"), create_dir)?.apply(|path| {
448 missing_path_else(path.join("lib.rs"), allow_update, |path| {
449 generate_file(path, LIB_RS_TEMPLATE, language_name, &generate_opts)
450 }, |path| {
451 let mut contents = fs::read_to_string(path)?;
452 if !contents.contains("#[cfg(with_highlights_query)]") {
453 info!("Updating query constants in bindings/rust/lib.rs");
454 let replacement = indoc! {r#"
455 #[cfg(with_highlights_query)]
456 /// The syntax highlighting query for this grammar.
457 pub const HIGHLIGHTS_QUERY: &str = include_str!("../../HIGHLIGHTS_QUERY_PATH");
458
459 #[cfg(with_injections_query)]
460 /// The language injection query for this grammar.
461 pub const INJECTIONS_QUERY: &str = include_str!("../../INJECTIONS_QUERY_PATH");
462
463 #[cfg(with_locals_query)]
464 /// The local variable query for this grammar.
465 pub const LOCALS_QUERY: &str = include_str!("../../LOCALS_QUERY_PATH");
466
467 #[cfg(with_tags_query)]
468 /// The symbol tagging query for this grammar.
469 pub const TAGS_QUERY: &str = include_str!("../../TAGS_QUERY_PATH");
470 "#}
471 .replace(HIGHLIGHTS_QUERY_PATH_PLACEHOLDER, &generate_opts.highlights_query_path.replace('\\', "/"))
472 .replace(INJECTIONS_QUERY_PATH_PLACEHOLDER, &generate_opts.injections_query_path.replace('\\', "/"))
473 .replace(LOCALS_QUERY_PATH_PLACEHOLDER, &generate_opts.locals_query_path.replace('\\', "/"))
474 .replace(TAGS_QUERY_PATH_PLACEHOLDER, &generate_opts.tags_query_path.replace('\\', "/"));
475 contents = contents
476 .replace(
477 indoc! {r#"
478 // NOTE: uncomment these to include any queries that this grammar contains:
479
480 // pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
481 // pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
482 // pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
483 // pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
484 "#},
485 &replacement,
486 );
487 }
488 write_file(path, contents)?;
489 Ok(())
490 })?;
491
492 missing_path_else(
493 path.join("build.rs"),
494 allow_update,
495 |path| generate_file(path, BUILD_RS_TEMPLATE, language_name, &generate_opts),
496 |path| {
497 let mut contents = fs::read_to_string(path)?;
498 if !contents.contains("wasm32-unknown-unknown") {
499 info!("Adding wasm32-unknown-unknown target to bindings/rust/build.rs");
500 let replacement = indoc!{r#"
501 c_config.flag("-utf-8");
502
503 if std::env::var("TARGET").unwrap() == "wasm32-unknown-unknown" {
504 let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
505 panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
506 };
507 let Ok(wasm_src) =
508 std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
509 else {
510 panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
511 };
512
513 c_config.include(&wasm_headers);
514 c_config.files([
515 wasm_src.join("stdio.c"),
516 wasm_src.join("stdlib.c"),
517 wasm_src.join("string.c"),
518 ]);
519 }
520 "#}
521 .lines()
522 .map(|line| if line.is_empty() { line.to_string() } else { format!(" {line}") })
523 .collect::<Vec<_>>()
524 .join("\n");
525
526 contents = contents.replace(r#" c_config.flag("-utf-8");"#, &replacement);
527 }
528
529 if !contents.contains("with_highlights_query") {
531 info!("Adding support for dynamic query inclusion to bindings/rust/build.rs");
532 let replaced = indoc! {r#"
533 c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
534 }"#}
535 .replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case());
536
537 let replacement = indoc! {r#"
538 c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
539
540 println!("cargo:rustc-check-cfg=cfg(with_highlights_query)");
541 if !"HIGHLIGHTS_QUERY_PATH".is_empty() && std::path::Path::new("HIGHLIGHTS_QUERY_PATH").exists() {
542 println!("cargo:rustc-cfg=with_highlights_query");
543 }
544 println!("cargo:rustc-check-cfg=cfg(with_injections_query)");
545 if !"INJECTIONS_QUERY_PATH".is_empty() && std::path::Path::new("INJECTIONS_QUERY_PATH").exists() {
546 println!("cargo:rustc-cfg=with_injections_query");
547 }
548 println!("cargo:rustc-check-cfg=cfg(with_locals_query)");
549 if !"LOCALS_QUERY_PATH".is_empty() && std::path::Path::new("LOCALS_QUERY_PATH").exists() {
550 println!("cargo:rustc-cfg=with_locals_query");
551 }
552 println!("cargo:rustc-check-cfg=cfg(with_tags_query)");
553 if !"TAGS_QUERY_PATH".is_empty() && std::path::Path::new("TAGS_QUERY_PATH").exists() {
554 println!("cargo:rustc-cfg=with_tags_query");
555 }
556 }"#}
557 .replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case())
558 .replace(HIGHLIGHTS_QUERY_PATH_PLACEHOLDER, &generate_opts.highlights_query_path.replace('\\', "/"))
559 .replace(INJECTIONS_QUERY_PATH_PLACEHOLDER, &generate_opts.injections_query_path.replace('\\', "/"))
560 .replace(LOCALS_QUERY_PATH_PLACEHOLDER, &generate_opts.locals_query_path.replace('\\', "/"))
561 .replace(TAGS_QUERY_PATH_PLACEHOLDER, &generate_opts.tags_query_path.replace('\\', "/"));
562
563 contents = contents.replace(
564 &replaced,
565 &replacement,
566 );
567 }
568
569 write_file(path, contents)?;
570 Ok(())
571 },
572 )?;
573
574 missing_path_else(
575 repo_path.join("Cargo.toml"),
576 allow_update,
577 |path| {
578 generate_file(
579 path,
580 CARGO_TOML_TEMPLATE,
581 dashed_language_name.as_str(),
582 &generate_opts,
583 )
584 },
585 |path| {
586 let contents = fs::read_to_string(path)?;
587 if contents.contains("\"LICENSE\"") {
588 info!("Adding LICENSE entry to bindings/rust/Cargo.toml");
589 write_file(path, contents.replace("\"LICENSE\"", "\"/LICENSE\""))?;
590 }
591 Ok(())
592 },
593 )?;
594
595 Ok(())
596 })?;
597 }
598
599 if tree_sitter_config.bindings.node {
601 missing_path(bindings_dir.join("node"), create_dir)?.apply(|path| {
602 missing_path_else(
603 path.join("index.js"),
604 allow_update,
605 |path| generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts),
606 |path| {
607 let contents = fs::read_to_string(path)?;
608 if !contents.contains("Object.defineProperty") {
609 info!("Replacing index.js");
610 generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts)?;
611 }
612 Ok(())
613 },
614 )?;
615
616 missing_path_else(
617 path.join("index.d.ts"),
618 allow_update,
619 |path| generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts),
620 |path| {
621 let contents = fs::read_to_string(path)?;
622 if !contents.contains("export default binding") {
623 info!("Replacing index.d.ts");
624 generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts)?;
625 }
626 Ok(())
627 },
628 )?;
629
630 missing_path_else(
631 path.join("binding_test.js"),
632 allow_update,
633 |path| {
634 generate_file(
635 path,
636 BINDING_TEST_JS_TEMPLATE,
637 language_name,
638 &generate_opts,
639 )
640 },
641 |path| {
642 let contents = fs::read_to_string(path)?;
643 if !contents.contains("import") {
644 info!("Replacing binding_test.js");
645 generate_file(
646 path,
647 BINDING_TEST_JS_TEMPLATE,
648 language_name,
649 &generate_opts,
650 )?;
651 }
652 Ok(())
653 },
654 )?;
655
656 missing_path(path.join("binding.cc"), |path| {
657 generate_file(path, JS_BINDING_CC_TEMPLATE, language_name, &generate_opts)
658 })?;
659
660 missing_path_else(
661 repo_path.join("binding.gyp"),
662 allow_update,
663 |path| generate_file(path, BINDING_GYP_TEMPLATE, language_name, &generate_opts),
664 |path| {
665 let contents = fs::read_to_string(path)?;
666 if contents.contains("fs.exists(") {
667 info!("Replacing `fs.exists` calls in binding.gyp");
668 write_file(path, contents.replace("fs.exists(", "fs.existsSync("))?;
669 }
670 Ok(())
671 },
672 )?;
673
674 Ok(())
675 })?;
676 }
677
678 if tree_sitter_config.bindings.c {
680 let kebab_case_name = language_name.to_kebab_case();
681 missing_path(bindings_dir.join("c"), create_dir)?.apply(|path| {
682 let header_name = format!("tree-sitter-{kebab_case_name}.h");
683 let old_file = &path.join(&header_name);
684 if allow_update && fs::exists(old_file).unwrap_or(false) {
685 info!("Removing bindings/c/{header_name}");
686 fs::remove_file(old_file)?;
687 }
688 missing_path(path.join("tree_sitter"), create_dir)?.apply(|include_path| {
689 missing_path(
690 include_path.join(&header_name),
691 |path| {
692 generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
693 },
694 )?;
695 Ok(())
696 })?;
697
698 missing_path(
699 path.join(format!("tree-sitter-{kebab_case_name}.pc.in")),
700 |path| {
701 generate_file(
702 path,
703 PARSER_NAME_PC_IN_TEMPLATE,
704 language_name,
705 &generate_opts,
706 )
707 },
708 )?;
709
710 missing_path_else(
711 repo_path.join("Makefile"),
712 allow_update,
713 |path| {
714 generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)
715 },
716 |path| {
717 let mut contents = fs::read_to_string(path)?;
718 if !contents.contains("cd '$(DESTDIR)$(LIBDIR)' && ln -sf") {
719 info!("Replacing Makefile");
720 generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)?;
721 } else {
722 let replaced = indoc! {r"
723 $(PARSER): $(SRC_DIR)/grammar.json
724 $(TS) generate $^
725 "};
726 if contents.contains(replaced) {
727 info!("Adding --no-parser target to Makefile");
728 contents = contents
729 .replace(
730 replaced,
731 indoc! {r"
732 $(SRC_DIR)/grammar.json: grammar.js
733 $(TS) generate --no-parser $^
734
735 $(PARSER): $(SRC_DIR)/grammar.json
736 $(TS) generate $^
737 "}
738 );
739 }
740 if !contents.contains("\nDESCRIPTION :=") {
741 if let Some(version_line) = contents.lines().find(|l| l.starts_with("VERSION := ")) {
742 info!("Adding DESCRIPTION to Makefile");
743 let description = generate_opts.description.map_or_else(
744 || format!("{} grammar for tree-sitter", generate_opts.camel_parser_name),
745 str::to_string,
746 );
747 contents = contents.replace(
748 version_line,
749 &format!("{version_line}\nDESCRIPTION := {description}"),
750 );
751 }
752 }
753 write_file(path, contents)?;
754 }
755 Ok(())
756 },
757 )?;
758
759 missing_path_else(
760 repo_path.join("CMakeLists.txt"),
761 allow_update,
762 |path| generate_file(path, CMAKELISTS_TXT_TEMPLATE, language_name, &generate_opts),
763 |path| {
764 let contents = fs::read_to_string(path)?;
765 let replaced_contents = contents
766 .replace("add_custom_target(test", "add_custom_target(ts-test")
767 .replace(
768 &formatdoc! {r#"
769 install(FILES bindings/c/tree-sitter-{language_name}.h
770 DESTINATION "${{CMAKE_INSTALL_INCLUDEDIR}}/tree_sitter")
771 "#},
772 indoc! {r#"
773 install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bindings/c/tree_sitter"
774 DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
775 FILES_MATCHING PATTERN "*.h")
776 "#}
777 ).replace(
778 &format!("target_include_directories(tree-sitter-{language_name} PRIVATE src)"),
779 &formatdoc! {"
780 target_include_directories(tree-sitter-{language_name}
781 PRIVATE src
782 INTERFACE $<BUILD_INTERFACE:${{CMAKE_CURRENT_SOURCE_DIR}}/bindings/c>
783 $<INSTALL_INTERFACE:${{CMAKE_INSTALL_INCLUDEDIR}}>)
784 "}
785 ).replace(
786 indoc! {r#"
787 add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
788 DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
789 COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
790 --abi=${TREE_SITTER_ABI_VERSION}
791 WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
792 COMMENT "Generating parser.c")
793 "#},
794 indoc! {r#"
795 add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
796 "${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"
797 DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js"
798 COMMAND "${TREE_SITTER_CLI}" generate grammar.js --no-parser
799 WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
800 COMMENT "Generating grammar.json")
801
802 add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
803 BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/parser.h"
804 "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/alloc.h"
805 "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/array.h"
806 DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
807 COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
808 --abi=${TREE_SITTER_ABI_VERSION}
809 WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
810 COMMENT "Generating parser.c")
811 "#}
812 );
813 if !replaced_contents.eq(&contents) {
814 info!("Updating CMakeLists.txt");
815 write_file(path, replaced_contents)?;
816 }
817 Ok(())
818 },
819 )?;
820
821 Ok(())
822 })?;
823 }
824
825 if tree_sitter_config.bindings.go {
827 missing_path(bindings_dir.join("go"), create_dir)?.apply(|path| {
828 missing_path(path.join("binding.go"), |path| {
829 generate_file(path, BINDING_GO_TEMPLATE, language_name, &generate_opts)
830 })?;
831
832 missing_path(path.join("binding_test.go"), |path| {
833 generate_file(
834 path,
835 BINDING_TEST_GO_TEMPLATE,
836 language_name,
837 &generate_opts,
838 )
839 })?;
840
841 missing_path(repo_path.join("go.mod"), |path| {
842 generate_file(path, GO_MOD_TEMPLATE, language_name, &generate_opts)
843 })?;
844
845 Ok(())
846 })?;
847 }
848
849 if tree_sitter_config.bindings.python {
851 missing_path(bindings_dir.join("python"), create_dir)?.apply(|path| {
852 let snake_case_grammar_name = format!("tree_sitter_{}", language_name.to_snake_case());
853 let lang_path = path.join(&snake_case_grammar_name);
854 missing_path(&lang_path, create_dir)?;
855
856 missing_path_else(
857 lang_path.join("binding.c"),
858 allow_update,
859 |path| generate_file(path, PY_BINDING_C_TEMPLATE, language_name, &generate_opts),
860 |path| {
861 let mut contents = fs::read_to_string(path)?;
862 if !contents.contains("PyModuleDef_Init") {
863 info!("Updating bindings/python/{snake_case_grammar_name}/binding.c");
864 contents = contents
865 .replace("PyModule_Create", "PyModuleDef_Init")
866 .replace(
867 "static PyMethodDef methods[] = {\n",
868 indoc! {"
869 static struct PyModuleDef_Slot slots[] = {
870 #ifdef Py_GIL_DISABLED
871 {Py_mod_gil, Py_MOD_GIL_NOT_USED},
872 #endif
873 {0, NULL}
874 };
875
876 static PyMethodDef methods[] = {
877 "},
878 )
879 .replace(
880 indoc! {"
881 .m_size = -1,
882 .m_methods = methods
883 "},
884 indoc! {"
885 .m_size = 0,
886 .m_methods = methods,
887 .m_slots = slots,
888 "},
889 );
890 write_file(path, contents)?;
891 }
892 Ok(())
893 },
894 )?;
895
896 missing_path_else(
897 lang_path.join("__init__.py"),
898 allow_update,
899 |path| {
900 generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)
901 },
902 |path| {
903 let contents = fs::read_to_string(path)?;
904 if contents.contains("uncomment these to include any queries") {
905 info!("Replacing __init__.py");
906 generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)?;
907 }
908 Ok(())
909 },
910 )?;
911
912 missing_path_else(
913 lang_path.join("__init__.pyi"),
914 allow_update,
915 |path| generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts),
916 |path| {
917 let mut contents = fs::read_to_string(path)?;
918 if contents.contains("uncomment these to include any queries") {
919 info!("Replacing __init__.pyi");
920 generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts)?;
921 } else if !contents.contains("CapsuleType") {
922 info!("Updating __init__.pyi");
923 contents = contents
924 .replace(
925 "from typing import Final",
926 "from typing import Final\nfrom typing_extensions import CapsuleType"
927 )
928 .replace("-> object:", "-> CapsuleType:");
929 write_file(path, contents)?;
930 }
931 Ok(())
932 },
933 )?;
934
935 missing_path(lang_path.join("py.typed"), |path| {
936 generate_file(path, "", language_name, &generate_opts) })?;
938
939 missing_path(path.join("tests"), create_dir)?.apply(|path| {
940 missing_path_else(
941 path.join("test_binding.py"),
942 allow_update,
943 |path| {
944 generate_file(
945 path,
946 TEST_BINDING_PY_TEMPLATE,
947 language_name,
948 &generate_opts,
949 )
950 },
951 |path| {
952 let mut contents = fs::read_to_string(path)?;
953 if !contents.contains("Parser(Language(") {
954 info!("Updating Language function in bindings/python/tests/test_binding.py");
955 contents = contents
956 .replace("tree_sitter.Language(", "Parser(Language(")
957 .replace(".language())\n", ".language()))\n")
958 .replace(
959 "import tree_sitter\n",
960 "from tree_sitter import Language, Parser\n",
961 );
962 write_file(path, contents)?;
963 }
964 Ok(())
965 },
966 )?;
967 Ok(())
968 })?;
969
970 missing_path_else(
971 repo_path.join("setup.py"),
972 allow_update,
973 |path| generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts),
974 |path| {
975 let mut contents = fs::read_to_string(path)?;
976 if !contents.contains("build_ext") {
977 info!("Replacing setup.py");
978 generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts)?;
979 } else {
980 if !contents.contains(" and not get_config_var") {
981 info!("Updating Python free-threading support in setup.py");
982 contents = contents.replace(
983 r#"startswith("cp"):"#,
984 r#"startswith("cp") and not get_config_var("Py_GIL_DISABLED"):"#
985 );
986 write_file(path, &contents)?;
987 }
988 if !contents.contains("include(\"src/*.c\")") {
989 info!("Updating sdist file list in setup.py");
990 let contents = contents.replace(
991 "include(\"src/tree_sitter/*.h\")",
992 "include(\"src/tree_sitter/*.h\")\n self.filelist.include(\"src/*.c\")",
993 );
994 write_file(path, &contents)?;
995 }
996 }
997
998 Ok(())
999 },
1000 )?;
1001
1002 missing_path_else(
1003 repo_path.join("pyproject.toml"),
1004 allow_update,
1005 |path| {
1006 generate_file(
1007 path,
1008 PYPROJECT_TOML_TEMPLATE,
1009 dashed_language_name.as_str(),
1010 &generate_opts,
1011 )
1012 },
1013 |path| {
1014 let mut contents = fs::read_to_string(path)?;
1015 if !contents.contains("cp310-*") {
1016 info!("Updating dependencies in pyproject.toml");
1017 contents = contents
1018 .replace(r#"build = "cp39-*""#, r#"build = "cp310-*""#)
1019 .replace(r#"python = ">=3.9""#, r#"python = ">=3.10""#)
1020 .replace("tree-sitter~=0.22", "tree-sitter~=0.24");
1021 write_file(path, contents)?;
1022 }
1023 Ok(())
1024 },
1025 )?;
1026
1027 Ok(())
1028 })?;
1029 }
1030
1031 if tree_sitter_config.bindings.swift {
1033 missing_path(bindings_dir.join("swift"), create_dir)?.apply(|path| {
1034 let lang_path = path.join(&class_name);
1035 missing_path(&lang_path, create_dir)?;
1036
1037 missing_path(lang_path.join(format!("{language_name}.h")), |path| {
1038 generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
1039 })?;
1040
1041 missing_path(path.join(format!("{class_name}Tests")), create_dir)?.apply(|path| {
1042 missing_path(path.join(format!("{class_name}Tests.swift")), |path| {
1043 generate_file(path, TESTS_SWIFT_TEMPLATE, language_name, &generate_opts)
1044 })?;
1045
1046 Ok(())
1047 })?;
1048
1049 missing_path_else(
1050 repo_path.join("Package.swift"),
1051 allow_update,
1052 |path| generate_file(path, PACKAGE_SWIFT_TEMPLATE, language_name, &generate_opts),
1053 |path| {
1054 let contents = fs::read_to_string(path)?;
1055 let replaced_contents = contents
1056 .replace(
1057 "https://github.com/ChimeHQ/SwiftTreeSitter",
1058 "https://github.com/tree-sitter/swift-tree-sitter",
1059 )
1060 .replace("version: \"0.8.0\")", "version: \"0.10.0\")")
1061 .replace("version: \"0.9.0\")", "version: \"0.10.0\")")
1062 .replace("(name: \"SwiftTreeSitter\", url:", "(url:")
1063 .replace(
1064 " \"SwiftTreeSitter\"",
1065 " .product(name: \"SwiftTreeSitter\", package: \"swift-tree-sitter\")",
1066 );
1067 if !replaced_contents.eq(&contents) {
1068 info!("Updating tree-sitter dependency in Package.swift");
1069 write_file(path, replaced_contents)?;
1070 }
1071 Ok(())
1072 },
1073 )?;
1074
1075 Ok(())
1076 })?;
1077 }
1078
1079 if tree_sitter_config.bindings.zig {
1081 missing_path_else(
1082 repo_path.join("build.zig"),
1083 allow_update,
1084 |path| generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts),
1085 |path| {
1086 let contents = fs::read_to_string(path)?;
1087 if !contents.contains("b.pkg_hash.len") {
1088 info!("Replacing build.zig");
1089 generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts)
1090 } else {
1091 Ok(())
1092 }
1093 },
1094 )?;
1095
1096 missing_path_else(
1097 repo_path.join("build.zig.zon"),
1098 allow_update,
1099 |path| generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts),
1100 |path| {
1101 let contents = fs::read_to_string(path)?;
1102 if !contents.contains(".name = .tree_sitter_") {
1103 info!("Replacing build.zig.zon");
1104 generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts)
1105 } else {
1106 Ok(())
1107 }
1108 },
1109 )?;
1110
1111 missing_path(bindings_dir.join("zig"), create_dir)?.apply(|path| {
1112 missing_path_else(
1113 path.join("root.zig"),
1114 allow_update,
1115 |path| generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts),
1116 |path| {
1117 let contents = fs::read_to_string(path)?;
1118 if contents.contains("ts.Language") {
1119 info!("Replacing root.zig");
1120 generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts)
1121 } else {
1122 Ok(())
1123 }
1124 },
1125 )?;
1126
1127 missing_path(path.join("test.zig"), |path| {
1128 generate_file(path, TEST_ZIG_TEMPLATE, language_name, &generate_opts)
1129 })?;
1130
1131 Ok(())
1132 })?;
1133 }
1134
1135 if tree_sitter_config.bindings.java {
1137 missing_path(repo_path.join("pom.xml"), |path| {
1138 generate_file(path, POM_XML_TEMPLATE, language_name, &generate_opts)
1139 })?;
1140
1141 missing_path(bindings_dir.join("java"), create_dir)?.apply(|path| {
1142 missing_path(path.join("main"), create_dir)?.apply(|path| {
1143 let package_path = generate_opts
1144 .namespace
1145 .unwrap_or("io.github.treesitter")
1146 .replace(['-', '_'], "")
1147 .split('.')
1148 .fold(path.to_path_buf(), |path, dir| path.join(dir))
1149 .join("jtreesitter")
1150 .join(language_name.to_lowercase().replace('_', ""));
1151 missing_path(package_path, create_dir)?.apply(|path| {
1152 missing_path(path.join(format!("{class_name}.java")), |path| {
1153 generate_file(path, BINDING_JAVA_TEMPLATE, language_name, &generate_opts)
1154 })?;
1155
1156 Ok(())
1157 })?;
1158
1159 Ok(())
1160 })?;
1161
1162 missing_path(path.join("test"), create_dir)?.apply(|path| {
1163 missing_path(path.join(format!("{class_name}Test.java")), |path| {
1164 generate_file(path, TEST_JAVA_TEMPLATE, language_name, &generate_opts)
1165 })?;
1166
1167 Ok(())
1168 })?;
1169
1170 Ok(())
1171 })?;
1172 }
1173
1174 Ok(())
1175}
1176
1177pub fn get_root_path(path: &Path) -> Result<PathBuf> {
1178 let mut pathbuf = path.to_owned();
1179 let filename = path.file_name().unwrap().to_str().unwrap();
1180 let is_package_json = filename == "package.json";
1181 loop {
1182 let json = pathbuf
1183 .exists()
1184 .then(|| {
1185 let contents = fs::read_to_string(pathbuf.as_path())
1186 .with_context(|| format!("Failed to read {filename}"))?;
1187 if is_package_json {
1188 serde_json::from_str::<Map<String, Value>>(&contents)
1189 .context(format!("Failed to parse {filename}"))
1190 .map(|v| v.contains_key("tree-sitter"))
1191 } else {
1192 serde_json::from_str::<TreeSitterJSON>(&contents)
1193 .context(format!("Failed to parse {filename}"))
1194 .map(|_| true)
1195 }
1196 })
1197 .transpose()?;
1198 if json == Some(true) {
1199 return Ok(pathbuf.parent().unwrap().to_path_buf());
1200 }
1201 pathbuf.pop(); if !pathbuf.pop() {
1203 return Err(anyhow!(format!(
1204 concat!(
1205 "Failed to locate a {} file,",
1206 " please ensure you have one, and if you don't then consult the docs",
1207 ),
1208 filename
1209 )));
1210 }
1211 pathbuf.push(filename);
1212 }
1213}
1214
1215fn generate_file(
1216 path: &Path,
1217 template: &str,
1218 language_name: &str,
1219 generate_opts: &GenerateOpts,
1220) -> Result<()> {
1221 let filename = path.file_name().unwrap().to_str().unwrap();
1222
1223 let lower_parser_name = if path
1224 .extension()
1225 .is_some_and(|e| e.eq_ignore_ascii_case("java"))
1226 {
1227 language_name.to_snake_case().replace('_', "")
1228 } else {
1229 language_name.to_snake_case()
1230 };
1231
1232 let mut replacement = template
1233 .replace(
1234 CAMEL_PARSER_NAME_PLACEHOLDER,
1235 generate_opts.camel_parser_name,
1236 )
1237 .replace(
1238 TITLE_PARSER_NAME_PLACEHOLDER,
1239 generate_opts.title_parser_name,
1240 )
1241 .replace(
1242 UPPER_PARSER_NAME_PLACEHOLDER,
1243 &language_name.to_shouty_snake_case(),
1244 )
1245 .replace(
1246 KEBAB_PARSER_NAME_PLACEHOLDER,
1247 &language_name.to_kebab_case(),
1248 )
1249 .replace(LOWER_PARSER_NAME_PLACEHOLDER, &lower_parser_name)
1250 .replace(PARSER_NAME_PLACEHOLDER, language_name)
1251 .replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION)
1252 .replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION)
1253 .replace(ABI_VERSION_MAX_PLACEHOLDER, &ABI_VERSION_MAX.to_string())
1254 .replace(
1255 PARSER_VERSION_PLACEHOLDER,
1256 &generate_opts.version.to_string(),
1257 )
1258 .replace(PARSER_CLASS_NAME_PLACEHOLDER, generate_opts.class_name)
1259 .replace(
1260 HIGHLIGHTS_QUERY_PATH_PLACEHOLDER,
1261 &generate_opts.highlights_query_path.replace('\\', "/"),
1262 )
1263 .replace(
1264 INJECTIONS_QUERY_PATH_PLACEHOLDER,
1265 &generate_opts.injections_query_path.replace('\\', "/"),
1266 )
1267 .replace(
1268 LOCALS_QUERY_PATH_PLACEHOLDER,
1269 &generate_opts.locals_query_path.replace('\\', "/"),
1270 )
1271 .replace(
1272 TAGS_QUERY_PATH_PLACEHOLDER,
1273 &generate_opts.tags_query_path.replace('\\', "/"),
1274 );
1275
1276 if let Some(name) = generate_opts.author_name {
1277 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER, name);
1278 } else {
1279 match filename {
1280 "package.json" => {
1281 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JS, "");
1282 }
1283 "pyproject.toml" => {
1284 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_PY, "");
1285 }
1286 "grammar.js" => {
1287 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_GRAMMAR, "");
1288 }
1289 "Cargo.toml" => {
1290 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, "");
1291 }
1292 "pom.xml" => {
1293 replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JAVA, "");
1294 }
1295 _ => {}
1296 }
1297 }
1298
1299 if let Some(email) = generate_opts.author_email {
1300 replacement = match filename {
1301 "Cargo.toml" | "grammar.js" => {
1302 replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, &format!("<{email}>"))
1303 }
1304 _ => replacement.replace(AUTHOR_EMAIL_PLACEHOLDER, email),
1305 }
1306 } else {
1307 match filename {
1308 "package.json" => {
1309 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JS, "");
1310 }
1311 "pyproject.toml" => {
1312 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_PY, "");
1313 }
1314 "grammar.js" => {
1315 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR, "");
1316 }
1317 "Cargo.toml" => {
1318 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, "");
1319 }
1320 "pom.xml" => {
1321 replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JAVA, "");
1322 }
1323 _ => {}
1324 }
1325 }
1326
1327 match (generate_opts.author_url, filename) {
1328 (Some(url), "package.json" | "pom.xml") => {
1329 replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url);
1330 }
1331 (None, "package.json") => {
1332 replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, "");
1333 }
1334 (None, "pom.xml") => {
1335 replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JAVA, "");
1336 }
1337 _ => {}
1338 }
1339
1340 if generate_opts.author_name.is_none()
1341 && generate_opts.author_email.is_none()
1342 && generate_opts.author_url.is_none()
1343 {
1344 match filename {
1345 "package.json" => {
1346 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) {
1347 if let Some(end_idx) = replacement[start_idx..]
1348 .find("},")
1349 .map(|i| i + start_idx + 2)
1350 {
1351 replacement.replace_range(start_idx..end_idx, "");
1352 }
1353 }
1354 }
1355 "pom.xml" => {
1356 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JAVA) {
1357 if let Some(end_idx) = replacement[start_idx..]
1358 .find("</developer>")
1359 .map(|i| i + start_idx + 12)
1360 {
1361 replacement.replace_range(start_idx..end_idx, "");
1362 }
1363 }
1364 }
1365 _ => {}
1366 }
1367 } else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() {
1368 match filename {
1369 "pyproject.toml" => {
1370 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_PY) {
1371 if let Some(end_idx) = replacement[start_idx..]
1372 .find("}]")
1373 .map(|i| i + start_idx + 2)
1374 {
1375 replacement.replace_range(start_idx..end_idx, "");
1376 }
1377 }
1378 }
1379 "grammar.js" => {
1380 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_GRAMMAR) {
1381 if let Some(end_idx) = replacement[start_idx..]
1382 .find(" \n")
1383 .map(|i| i + start_idx + 1)
1384 {
1385 replacement.replace_range(start_idx..end_idx, "");
1386 }
1387 }
1388 }
1389 "Cargo.toml" => {
1390 if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_RS) {
1391 if let Some(end_idx) = replacement[start_idx..]
1392 .find("\"]")
1393 .map(|i| i + start_idx + 2)
1394 {
1395 replacement.replace_range(start_idx..end_idx, "");
1396 }
1397 }
1398 }
1399 _ => {}
1400 }
1401 }
1402
1403 if let Some(license) = generate_opts.license {
1404 replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, license);
1405 } else {
1406 replacement = replacement.replace(PARSER_LICENSE_PLACEHOLDER, "MIT");
1407 }
1408
1409 if let Some(description) = generate_opts.description {
1410 replacement = replacement.replace(PARSER_DESCRIPTION_PLACEHOLDER, description);
1411 } else {
1412 replacement = replacement.replace(
1413 PARSER_DESCRIPTION_PLACEHOLDER,
1414 &format!(
1415 "{} grammar for tree-sitter",
1416 generate_opts.camel_parser_name,
1417 ),
1418 );
1419 }
1420
1421 if let Some(repository) = generate_opts.repository {
1422 replacement = replacement
1423 .replace(
1424 PARSER_URL_STRIPPED_PLACEHOLDER,
1425 &repository.replace("https://", ""),
1426 )
1427 .replace(PARSER_URL_PLACEHOLDER, repository);
1428 } else {
1429 replacement = replacement
1430 .replace(
1431 PARSER_URL_STRIPPED_PLACEHOLDER,
1432 &format!("github.com/tree-sitter/tree-sitter-{language_name}"),
1433 )
1434 .replace(
1435 PARSER_URL_PLACEHOLDER,
1436 &format!("https://github.com/tree-sitter/tree-sitter-{language_name}"),
1437 );
1438 }
1439
1440 if let Some(namespace) = generate_opts.namespace {
1441 replacement = replacement
1442 .replace(
1443 PARSER_NS_CLEANED_PLACEHOLDER,
1444 &namespace.replace(['-', '_'], ""),
1445 )
1446 .replace(PARSER_NS_PLACEHOLDER, namespace);
1447 } else {
1448 replacement = replacement
1449 .replace(PARSER_NS_CLEANED_PLACEHOLDER, "io.github.treesitter")
1450 .replace(PARSER_NS_PLACEHOLDER, "io.github.tree-sitter");
1451 }
1452
1453 if let Some(funding_url) = generate_opts.funding {
1454 match filename {
1455 "pyproject.toml" | "package.json" => {
1456 replacement = replacement.replace(FUNDING_URL_PLACEHOLDER, funding_url);
1457 }
1458 _ => {}
1459 }
1460 } else {
1461 match filename {
1462 "package.json" => {
1463 replacement = replacement.replace(" \"funding\": \"FUNDING_URL\",\n", "");
1464 }
1465 "pyproject.toml" => {
1466 replacement = replacement.replace("Funding = \"FUNDING_URL\"\n", "");
1467 }
1468 _ => {}
1469 }
1470 }
1471
1472 if filename == "build.zig.zon" {
1473 let id = thread_rng().gen_range(1u32..0xFFFF_FFFFu32);
1474 let checksum = crc32(format!("tree_sitter_{language_name}").as_bytes());
1475 replacement = replacement.replace(
1476 PARSER_FINGERPRINT_PLACEHOLDER,
1477 #[cfg(target_endian = "little")]
1478 &format!("0x{checksum:x}{id:x}"),
1479 #[cfg(target_endian = "big")]
1480 &format!("0x{id:x}{checksum:x}"),
1481 );
1482 }
1483
1484 write_file(path, replacement)?;
1485 Ok(())
1486}
1487
1488fn create_dir(path: &Path) -> Result<()> {
1489 fs::create_dir_all(path)
1490 .with_context(|| format!("Failed to create {:?}", path.to_string_lossy()))
1491}
1492
1493#[derive(PartialEq, Eq, Debug)]
1494enum PathState<P>
1495where
1496 P: AsRef<Path>,
1497{
1498 Exists(P),
1499 Missing(P),
1500}
1501
1502#[allow(dead_code)]
1503impl<P> PathState<P>
1504where
1505 P: AsRef<Path>,
1506{
1507 fn exists(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1508 if let Self::Exists(path) = self {
1509 action(path.as_ref())?;
1510 }
1511 Ok(self)
1512 }
1513
1514 fn missing(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1515 if let Self::Missing(path) = self {
1516 action(path.as_ref())?;
1517 }
1518 Ok(self)
1519 }
1520
1521 fn apply(&self, mut action: impl FnMut(&Path) -> Result<()>) -> Result<&Self> {
1522 action(self.as_path())?;
1523 Ok(self)
1524 }
1525
1526 fn apply_state(&self, mut action: impl FnMut(&Self) -> Result<()>) -> Result<&Self> {
1527 action(self)?;
1528 Ok(self)
1529 }
1530
1531 fn as_path(&self) -> &Path {
1532 match self {
1533 Self::Exists(path) | Self::Missing(path) => path.as_ref(),
1534 }
1535 }
1536}
1537
1538fn missing_path<P, F>(path: P, mut action: F) -> Result<PathState<P>>
1539where
1540 P: AsRef<Path>,
1541 F: FnMut(&Path) -> Result<()>,
1542{
1543 let path_ref = path.as_ref();
1544 if !path_ref.exists() {
1545 action(path_ref)?;
1546 Ok(PathState::Missing(path))
1547 } else {
1548 Ok(PathState::Exists(path))
1549 }
1550}
1551
1552fn missing_path_else<P, T, F>(
1553 path: P,
1554 allow_update: bool,
1555 mut action: T,
1556 mut else_action: F,
1557) -> Result<PathState<P>>
1558where
1559 P: AsRef<Path>,
1560 T: FnMut(&Path) -> Result<()>,
1561 F: FnMut(&Path) -> Result<()>,
1562{
1563 let path_ref = path.as_ref();
1564 if !path_ref.exists() {
1565 action(path_ref)?;
1566 Ok(PathState::Missing(path))
1567 } else {
1568 if allow_update {
1569 else_action(path_ref)?;
1570 }
1571 Ok(PathState::Exists(path))
1572 }
1573}