From 61c21aa408710019fc81c14a9b213dd762d5c5bd Mon Sep 17 00:00:00 2001 From: WillLillis Date: Sat, 4 Oct 2025 03:21:20 -0400 Subject: [PATCH] refactor(generate)!: include path when available in IO errors --- crates/generate/src/generate.rs | 138 +++++++++++++++++++------------- crates/generate/src/quickjs.rs | 6 +- 2 files changed, 87 insertions(+), 57 deletions(-) diff --git a/crates/generate/src/generate.rs b/crates/generate/src/generate.rs index 612a5fa8..6a005637 100644 --- a/crates/generate/src/generate.rs +++ b/crates/generate/src/generate.rs @@ -80,8 +80,8 @@ pub type GenerateResult = Result; pub enum GenerateError { #[error("Error with specified path -- {0}")] GrammarPath(String), - #[error("{0}")] - IO(String), + #[error(transparent)] + IO(IoError), #[cfg(feature = "load")] #[error(transparent)] LoadGrammarFile(#[from] LoadGrammarError), @@ -100,9 +100,28 @@ pub enum GenerateError { SuperTypeCycle(#[from] SuperTypeCycleError), } -impl From for GenerateError { - fn from(value: std::io::Error) -> Self { - Self::IO(value.to_string()) +#[derive(Debug, Error, Serialize)] +pub struct IoError { + pub error: String, + pub path: Option, +} + +impl IoError { + fn new(error: &std::io::Error, path: Option<&Path>) -> Self { + Self { + error: error.to_string(), + path: path.map(|p| p.to_string_lossy().to_string()), + } + } +} + +impl std::fmt::Display for IoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.error)?; + if let Some(ref path) = self.path { + write!(f, " ({path})")?; + } + Ok(()) } } @@ -117,18 +136,11 @@ pub enum LoadGrammarError { #[error("Failed to load grammar.js -- {0}")] LoadJSGrammarFile(#[from] JSError), #[error("Failed to load grammar.json -- {0}")] - IO(String), + IO(IoError), #[error("Unknown grammar file extension: {0:?}")] FileExtension(PathBuf), } -#[cfg(feature = "load")] -impl From for LoadGrammarError { - fn from(value: std::io::Error) -> Self { - Self::IO(value.to_string()) - } -} - #[cfg(feature = "load")] #[derive(Debug, Error, Serialize)] pub enum ParseVersionError { @@ -136,8 +148,8 @@ pub enum ParseVersionError { Version(String), #[error("{0}")] JSON(String), - #[error("{0}")] - IO(String), + #[error(transparent)] + IO(IoError), } #[cfg(feature = "load")] @@ -152,8 +164,21 @@ pub enum JSError { JSRuntimeUtf8 { runtime: String, error: String }, #[error("`{runtime}` process exited with status {code}")] JSRuntimeExit { runtime: String, code: i32 }, - #[error("{0}")] - IO(String), + #[error("Failed to open stdin for `{runtime}`")] + JSRuntimeStdin { runtime: String }, + #[error("Failed to write {item} to `{runtime}`'s stdin -- {error}")] + JSRuntimeWrite { + runtime: String, + item: String, + error: String, + }, + #[error("Failed to read output from `{runtime}` -- {error}")] + JSRuntimeRead { runtime: String, error: String }, + #[error(transparent)] + IO(IoError), + #[cfg(feature = "qjs-rt")] + #[error("Failed to get relative path")] + RelativePath, #[error("Could not parse this package's version as semver -- {0}")] Semver(String), #[error("Failed to serialze grammar JSON -- {0}")] @@ -163,13 +188,6 @@ pub enum JSError { QuickJS(String), } -#[cfg(feature = "load")] -impl From for JSError { - fn from(value: std::io::Error) -> Self { - Self::IO(value.to_string()) - } -} - #[cfg(feature = "load")] impl From for JSError { fn from(value: serde_json::Error) -> Self { @@ -230,7 +248,8 @@ where .try_exists() .map_err(|e| GenerateError::GrammarPath(e.to_string()))? { - fs::create_dir_all(&path_buf)?; + fs::create_dir_all(&path_buf) + .map_err(|e| GenerateError::IO(IoError::new(&e, Some(path_buf.as_path()))))?; repo_path = path_buf; repo_path.join("grammar.js") } else { @@ -247,15 +266,12 @@ where let header_path = src_path.join("tree_sitter"); // Ensure that the output directory exists - fs::create_dir_all(&src_path)?; + fs::create_dir_all(&src_path) + .map_err(|e| GenerateError::IO(IoError::new(&e, Some(src_path.as_path()))))?; if grammar_path.file_name().unwrap() != "grammar.json" { - fs::write(src_path.join("grammar.json"), &grammar_json).map_err(|e| { - GenerateError::IO(format!( - "Failed to write grammar.json to {} -- {e}", - src_path.display() - )) - })?; + fs::write(src_path.join("grammar.json"), &grammar_json) + .map_err(|e| GenerateError::IO(IoError::new(&e, Some(src_path.as_path()))))?; } // If our job is only to generate `grammar.json` and not `parser.c`, stop here. @@ -297,7 +313,8 @@ where write_file(&src_path.join("parser.c"), c_code)?; write_file(&src_path.join("node-types.json"), node_types_json)?; - fs::create_dir_all(&header_path)?; + fs::create_dir_all(&header_path) + .map_err(|e| GenerateError::IO(IoError::new(&e, Some(header_path.as_path()))))?; write_file(&header_path.join("alloc.h"), ALLOC_HEADER)?; write_file(&header_path.join("array.h"), ARRAY_HEADER)?; write_file(&header_path.join("parser.h"), PARSER_HEADER)?; @@ -413,9 +430,8 @@ fn read_grammar_version(repo_path: &Path) -> Result, ParseVersio let json = path .exists() .then(|| { - let contents = fs::read_to_string(path.as_path()).map_err(|e| { - ParseVersionError::IO(format!("Failed to read `{}` -- {e}", path.display())) - })?; + let contents = fs::read_to_string(path.as_path()) + .map_err(|e| ParseVersionError::IO(IoError::new(&e, Some(path.as_path()))))?; serde_json::from_str::(&contents).map_err(|e| { ParseVersionError::JSON(format!("Failed to parse `{}` -- {e}", path.display())) }) @@ -449,14 +465,16 @@ pub fn load_grammar_file( } match grammar_path.extension().and_then(|e| e.to_str()) { Some("js") => Ok(load_js_grammar_file(grammar_path, js_runtime)?), - Some("json") => Ok(fs::read_to_string(grammar_path)?), + Some("json") => Ok(fs::read_to_string(grammar_path) + .map_err(|e| LoadGrammarError::IO(IoError::new(&e, Some(grammar_path))))?), _ => Err(LoadGrammarError::FileExtension(grammar_path.to_owned()))?, } } #[cfg(feature = "load")] fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResult { - let grammar_path = dunce::canonicalize(grammar_path)?; + let grammar_path = dunce::canonicalize(grammar_path) + .map_err(|e| JSError::IO(IoError::new(&e, Some(grammar_path))))?; #[cfg(feature = "qjs-rt")] if js_runtime == Some("native") { @@ -497,7 +515,9 @@ fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResu let mut js_stdin = js_process .stdin .take() - .ok_or_else(|| JSError::IO(format!("Failed to open stdin for `{js_runtime}`")))?; + .ok_or_else(|| JSError::JSRuntimeStdin { + runtime: js_runtime.to_string(), + })?; let cli_version = Version::parse(env!("CARGO_PKG_VERSION"))?; write!( @@ -507,21 +527,26 @@ fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResu globalThis.TREE_SITTER_CLI_VERSION_PATCH = {};", cli_version.major, cli_version.minor, cli_version.patch, ) - .map_err(|e| { - JSError::IO(format!( - "Failed to write tree-sitter version to `{js_runtime}`'s stdin -- {e}" - )) - })?; - js_stdin.write(include_bytes!("./dsl.js")).map_err(|e| { - JSError::IO(format!( - "Failed to write grammar dsl to `{js_runtime}`'s stdin -- {e}" - )) + .map_err(|e| JSError::JSRuntimeWrite { + runtime: js_runtime.to_string(), + item: "tree-sitter version".to_string(), + error: e.to_string(), })?; + js_stdin + .write(include_bytes!("./dsl.js")) + .map_err(|e| JSError::JSRuntimeWrite { + runtime: js_runtime.to_string(), + item: "grammar dsl".to_string(), + error: e.to_string(), + })?; drop(js_stdin); let output = js_process .wait_with_output() - .map_err(|e| JSError::IO(format!("Failed to read output from `{js_runtime}` -- {e}")))?; + .map_err(|e| JSError::JSRuntimeRead { + runtime: js_runtime.to_string(), + error: e.to_string(), + })?; match output.status.code() { Some(0) => { let stdout = String::from_utf8(output.stdout).map_err(|e| JSError::JSRuntimeUtf8 { @@ -537,9 +562,15 @@ fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResu grammar_json = &stdout[pos + 1..]; let mut stdout = std::io::stdout().lock(); - stdout.write_all(node_output.as_bytes())?; - stdout.write_all(b"\n")?; - stdout.flush()?; + stdout + .write_all(node_output.as_bytes()) + .map_err(|e| JSError::IO(IoError::new(&e, None)))?; + stdout + .write_all(b"\n") + .map_err(|e| JSError::IO(IoError::new(&e, None)))?; + stdout + .flush() + .map_err(|e| JSError::IO(IoError::new(&e, None)))?; } Ok(serde_json::to_string_pretty(&serde_json::from_str::< @@ -559,8 +590,7 @@ fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResu #[cfg(feature = "load")] pub fn write_file(path: &Path, body: impl AsRef<[u8]>) -> GenerateResult<()> { - fs::write(path, body) - .map_err(|e| GenerateError::IO(format!("Failed to write {:?} -- {e}", path.file_name()))) + fs::write(path, body).map_err(|e| GenerateError::IO(IoError::new(&e, Some(path)))) } #[cfg(test)] diff --git a/crates/generate/src/quickjs.rs b/crates/generate/src/quickjs.rs index 689615fc..848030e8 100644 --- a/crates/generate/src/quickjs.rs +++ b/crates/generate/src/quickjs.rs @@ -10,7 +10,7 @@ use rquickjs::{ Context, Ctx, Function, Module, Object, Runtime, Type, Value, }; -use super::{JSError, JSResult}; +use super::{IoError, JSError, JSResult}; const DSL: &[u8] = include_bytes!("dsl.js"); @@ -266,10 +266,10 @@ pub fn execute_native_runtime(grammar_path: &Path) -> JSResult { let loader = ScriptLoader::default().with_extension("mjs"); runtime.set_loader(resolver, loader); - let cwd = std::env::current_dir()?; + let cwd = std::env::current_dir().map_err(|e| JSError::IO(IoError::new(&e, None)))?; let relative_path = pathdiff::diff_paths(grammar_path, &cwd) .map(|p| p.to_string_lossy().to_string()) - .ok_or_else(|| JSError::IO("Failed to get relative path".to_string()))?; + .ok_or(JSError::RelativePath)?; context.with(|ctx| -> JSResult { let globals = ctx.globals();