feat: add version subcommand for versioning grammars

This commit is contained in:
Amaan Qureshi 2024-10-19 02:08:57 +00:00 committed by GitHub
parent c03977a87e
commit 40606dd632
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 310 additions and 6 deletions

View file

@ -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)]

View file

@ -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::<Version>::with_theme(&ColorfulTheme::default())
Input::<SemverVersion>::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, &current_dir)?,
Commands::Parse(parse_options) => parse_options.run(loader, &current_dir)?,
Commands::Test(test_options) => test_options.run(loader, &current_dir)?,
Commands::Version(version_options) => version_options.run(current_dir)?,
Commands::Fuzz(fuzz_options) => fuzz_options.run(loader, &current_dir)?,
Commands::Query(query_options) => query_options.run(loader, &current_dir)?,
Commands::Highlight(highlight_options) => highlight_options.run(loader)?,

View file

@ -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();

264
cli/src/version.rs Normal file
View file

@ -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::<TreeSitterJSON>(&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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.join("\n")
+ "\n";
fs::write(self.current_dir.join("pyproject.toml"), pyproject_toml)?;
Ok(())
}
}

View file

@ -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.