diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 4a00747e..657e9d9c 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -13,6 +13,7 @@ pub mod test; pub mod test_highlight; pub mod test_tags; pub mod util; +pub mod version; pub mod wasm; #[cfg(test)] diff --git a/cli/src/main.rs b/cli/src/main.rs index b8027ea8..7ff5c929 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -12,7 +12,7 @@ use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input}; use glob::glob; use heck::ToUpperCamelCase; use regex::Regex; -use semver::Version; +use semver::Version as SemverVersion; use tree_sitter::{ffi, Parser, Point}; use tree_sitter_cli::{ fuzz::{ @@ -25,7 +25,7 @@ use tree_sitter_cli::{ parse::{self, ParseFileOptions, ParseOutput, ParseTheme}, playground, query, tags, test::{self, TestOptions}, - test_highlight, test_tags, util, wasm, + test_highlight, test_tags, util, version, wasm, }; use tree_sitter_config::Config; use tree_sitter_highlight::Highlighter; @@ -46,6 +46,7 @@ enum Commands { Build(Build), Parse(Parse), Test(Test), + Version(Version), Fuzz(Fuzz), Query(Query), Highlight(Highlight), @@ -279,6 +280,15 @@ struct Test { pub overview_only: bool, } +#[derive(Args)] +#[command(alias = "publish")] +/// Increment the version of a grammar +struct Version { + #[arg(num_args = 1)] + /// The version to bump to + pub version: SemverVersion, +} + #[derive(Args)] #[command(about = "Fuzz a parser", alias = "f")] struct Fuzz { @@ -555,9 +565,9 @@ impl Init { }; let initial_version = || { - Input::::with_theme(&ColorfulTheme::default()) + Input::::with_theme(&ColorfulTheme::default()) .with_prompt("Version") - .default(Version::new(0, 1, 0)) + .default(SemverVersion::new(0, 1, 0)) .interact_text() }; @@ -1041,6 +1051,12 @@ impl Test { } } +impl Version { + fn run(self, current_dir: PathBuf) -> Result<()> { + version::Version::new(self.version.to_string(), current_dir).run() + } +} + impl Fuzz { fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> { loader.sanitize_build(true); @@ -1346,6 +1362,7 @@ fn run() -> Result<()> { Commands::Build(build_options) => build_options.run(loader, ¤t_dir)?, Commands::Parse(parse_options) => parse_options.run(loader, ¤t_dir)?, Commands::Test(test_options) => test_options.run(loader, ¤t_dir)?, + Commands::Version(version_options) => version_options.run(current_dir)?, Commands::Fuzz(fuzz_options) => fuzz_options.run(loader, ¤t_dir)?, Commands::Query(query_options) => query_options.run(loader, ¤t_dir)?, Commands::Highlight(highlight_options) => highlight_options.run(loader)?, diff --git a/cli/src/tests/detect_language.rs b/cli/src/tests/detect_language.rs index aed4ae18..eeb24855 100644 --- a/cli/src/tests/detect_language.rs +++ b/cli/src/tests/detect_language.rs @@ -101,7 +101,7 @@ fn tree_sitter_dir(tree_sitter_json: &str, name: &str) -> tempfile::TempDir { fs::write( temp_dir.path().join("src/parser.c"), format!( - r##" + r#" #include "tree_sitter/parser.h" #ifdef _WIN32 #define TS_PUBLIC __declspec(dllexport) @@ -109,7 +109,7 @@ fn tree_sitter_dir(tree_sitter_json: &str, name: &str) -> tempfile::TempDir { #define TS_PUBLIC __attribute__((visibility("default"))) #endif TS_PUBLIC const TSLanguage *tree_sitter_{name}() {{}} - "## + "# ), ) .unwrap(); diff --git a/cli/src/version.rs b/cli/src/version.rs new file mode 100644 index 00000000..11871265 --- /dev/null +++ b/cli/src/version.rs @@ -0,0 +1,264 @@ +use std::{fs, path::PathBuf, process::Command}; + +use anyhow::{anyhow, Context, Result}; +use regex::Regex; +use tree_sitter_loader::TreeSitterJSON; + +pub struct Version { + pub version: String, + pub current_dir: PathBuf, +} + +impl Version { + #[must_use] + pub const fn new(version: String, current_dir: PathBuf) -> Self { + Self { + version, + current_dir, + } + } + + pub fn run(self) -> Result<()> { + let tree_sitter_json = self.current_dir.join("tree-sitter.json"); + + let tree_sitter_json = + serde_json::from_str::(&fs::read_to_string(tree_sitter_json)?)?; + + let is_multigrammar = tree_sitter_json.grammars.len() > 1; + + self.update_treesitter_json().with_context(|| { + format!( + "Failed to update tree-sitter.json at {}", + self.current_dir.display() + ) + })?; + self.update_cargo_toml().with_context(|| { + format!( + "Failed to update Cargo.toml at {}", + self.current_dir.display() + ) + })?; + self.update_package_json().with_context(|| { + format!( + "Failed to update package.json at {}", + self.current_dir.display() + ) + })?; + self.update_makefile(is_multigrammar).with_context(|| { + format!( + "Failed to update Makefile at {}", + self.current_dir.display() + ) + })?; + self.update_cmakelists_txt().with_context(|| { + format!( + "Failed to update CMakeLists.txt at {}", + self.current_dir.display() + ) + })?; + self.update_pyproject_toml().with_context(|| { + format!( + "Failed to update pyproject.toml at {}", + self.current_dir.display() + ) + })?; + + Ok(()) + } + + fn update_treesitter_json(&self) -> Result<()> { + let tree_sitter_json = &fs::read_to_string(self.current_dir.join("tree-sitter.json"))?; + + let tree_sitter_json = tree_sitter_json + .lines() + .map(|line| { + if line.contains("\"version\":") { + let prefix_index = line.find("\"version\":").unwrap() + "\"version\":".len(); + let start_quote = line[prefix_index..].find('"').unwrap() + prefix_index + 1; + let end_quote = line[start_quote + 1..].find('"').unwrap() + start_quote + 1; + + format!( + "{}{}{}", + &line[..start_quote], + self.version, + &line[end_quote..] + ) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") + + "\n"; + + fs::write(self.current_dir.join("tree-sitter.json"), tree_sitter_json)?; + + Ok(()) + } + + fn update_cargo_toml(&self) -> Result<()> { + if !self.current_dir.join("Cargo.toml").exists() { + return Ok(()); + } + + let cargo_toml = fs::read_to_string(self.current_dir.join("Cargo.toml"))?; + + let cargo_toml = cargo_toml + .lines() + .map(|line| { + if line.starts_with("version =") { + format!("version = \"{}\"", self.version) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") + + "\n"; + + fs::write(self.current_dir.join("Cargo.toml"), cargo_toml)?; + + if self.current_dir.join("Cargo.lock").exists() { + let Ok(cmd) = Command::new("cargo") + .arg("generate-lockfile") + .arg("--offline") + .current_dir(&self.current_dir) + .output() + else { + return Ok(()); // cargo is not `executable`, ignore + }; + + if !cmd.status.success() { + let stderr = String::from_utf8_lossy(&cmd.stderr); + return Err(anyhow!( + "Failed to run `cargo generate-lockfile`:\n{stderr}" + )); + } + } + + Ok(()) + } + + fn update_package_json(&self) -> Result<()> { + if !self.current_dir.join("package.json").exists() { + return Ok(()); + } + + let package_json = &fs::read_to_string(self.current_dir.join("package.json"))?; + + let package_json = package_json + .lines() + .map(|line| { + if line.contains("\"version\":") { + let prefix_index = line.find("\"version\":").unwrap() + "\"version\":".len(); + let start_quote = line[prefix_index..].find('"').unwrap() + prefix_index + 1; + let end_quote = line[start_quote + 1..].find('"').unwrap() + start_quote + 1; + + format!( + "{}{}{}", + &line[..start_quote], + self.version, + &line[end_quote..] + ) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") + + "\n"; + + fs::write(self.current_dir.join("package.json"), package_json)?; + + if self.current_dir.join("package-lock.json").exists() { + let Ok(cmd) = Command::new("npm") + .arg("install") + .arg("--package-lock-only") + .current_dir(&self.current_dir) + .output() + else { + return Ok(()); // npm is not `executable`, ignore + }; + + if !cmd.status.success() { + let stderr = String::from_utf8_lossy(&cmd.stderr); + return Err(anyhow!("Failed to run `npm install`:\n{stderr}")); + } + } + + Ok(()) + } + + fn update_makefile(&self, is_multigrammar: bool) -> Result<()> { + let makefile = if is_multigrammar { + if !self.current_dir.join("common").join("common.mak").exists() { + return Ok(()); + } + + fs::read_to_string(self.current_dir.join("Makefile"))? + } else { + if !self.current_dir.join("Makefile").exists() { + return Ok(()); + } + + fs::read_to_string(self.current_dir.join("Makefile"))? + }; + + let makefile = makefile + .lines() + .map(|line| { + if line.starts_with("VERSION") { + format!("VERSION := {}", self.version) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") + + "\n"; + + fs::write(self.current_dir.join("Makefile"), makefile)?; + + Ok(()) + } + + fn update_cmakelists_txt(&self) -> Result<()> { + if !self.current_dir.join("CMakeLists.txt").exists() { + return Ok(()); + } + + let cmake = fs::read_to_string(self.current_dir.join("CMakeLists.txt"))?; + + let re = Regex::new(r#"(\s*VERSION\s+)"[0-9]+\.[0-9]+\.[0-9]+""#)?; + let cmake = re.replace(&cmake, format!(r#"$1"{}""#, self.version)); + + fs::write(self.current_dir.join("CMakeLists.txt"), cmake.as_bytes())?; + + Ok(()) + } + + fn update_pyproject_toml(&self) -> Result<()> { + if !self.current_dir.join("pyproject.toml").exists() { + return Ok(()); + } + + let pyproject_toml = fs::read_to_string(self.current_dir.join("pyproject.toml"))?; + + let pyproject_toml = pyproject_toml + .lines() + .map(|line| { + if line.starts_with("version =") { + format!("version = \"{}\"", self.version) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") + + "\n"; + + fs::write(self.current_dir.join("pyproject.toml"), pyproject_toml)?; + + Ok(()) + } +} diff --git a/docs/section-3-creating-parsers.md b/docs/section-3-creating-parsers.md index 2c32a10f..7f635a4e 100644 --- a/docs/section-3-creating-parsers.md +++ b/docs/section-3-creating-parsers.md @@ -164,6 +164,28 @@ This field controls what bindings are generated when the `init` command is run. * `rust` (default: `true`) * `swift` (default: `false`) +### Command: `version` + +The `version` command prints the version of the `tree-sitter` CLI tool that you have installed. + +```sh +tree-sitter version 1.0.0 +``` + +The only argument is the version itself, which is the first positional argument. +This will update the version in several files, if they exist: + +* tree-sitter.json +* Cargo.toml +* package.json +* Makefile +* CMakeLists.txt +* pyproject.toml + +As a grammar author, you should keep the version of your grammar in sync across +different bindings. However, doing so manually is error-prone and tedious, so +this command takes care of the burden. + ### Command: `generate` The most important command you'll use is `tree-sitter generate`. This command reads the `grammar.js` file in your current working directory and creates a file called `src/parser.c`, which implements the parser. After making changes to your grammar, just run `tree-sitter generate` again.