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