From 9f8d4fc520b33fc7a7591061e83805237670a229 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Thu, 26 May 2022 18:05:31 +0000 Subject: [PATCH] feat: friendlier errors --- --- a/Cargo.lock +++ b/Cargo.lock @@ -3,6 +3,17 @@ version = 3 [[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] name = "annotate-snippets" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -69,6 +80,12 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] name = "clap" version = "3.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -152,12 +169,32 @@ ] [[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] +name = "hashbrown" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +dependencies = [ + "ahash", +] + +[[package]] name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -179,7 +216,7 @@ checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.11.2", ] [[package]] @@ -221,6 +258,7 @@ "base64", "bincode", "gcmodule", + "hashbrown 0.12.1", "jrsonnet-interner", "jrsonnet-macros", "jrsonnet-parser", @@ -232,6 +270,8 @@ "serde", "serde_json", "serde_yaml", + "static_assertions", + "strsim", "thiserror", ] @@ -240,6 +280,7 @@ version = "0.4.2" dependencies = [ "gcmodule", + "hashbrown 0.12.1", "rustc-hash", "serde", ] @@ -262,6 +303,7 @@ "jrsonnet-stdlib", "peg", "serde", + "static_assertions", ] [[package]] @@ -293,9 +335,9 @@ [[package]] name = "libc" -version = "0.2.108" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" [[package]] name = "linked-hash-map" @@ -338,6 +380,12 @@ ] [[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -359,7 +407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "cloudabi", "libc", "redox_syscall", @@ -516,6 +564,12 @@ checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -598,6 +652,12 @@ checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,5 @@ [workspace] -members = [ - "crates/*", - "bindings/jsonnet", - "cmds/jrsonnet", -] +members = ["crates/*", "bindings/jsonnet", "cmds/jrsonnet"] [profile.test] opt-level = 1 @@ -14,3 +10,4 @@ codegen-units = 1 debug = 0 panic = "abort" +strip = true --- a/bindings/jsonnet/src/import.rs +++ b/bindings/jsonnet/src/import.rs @@ -32,9 +32,9 @@ out: RefCell>>, } impl ImportResolver for CallbackImportResolver { - fn resolve_file(&self, from: &Path, path: &Path) -> Result { + fn resolve_file(&self, from: &Path, path: &str) -> Result { let base = CString::new(from.to_str().unwrap()).unwrap().into_raw(); - let rel = CString::new(path.to_str().unwrap()).unwrap().into_raw(); + let rel = CString::new(path).unwrap().into_raw(); let found_here: *mut c_char = null_mut(); let mut success: i32 = 0; let result_ptr = unsafe { @@ -108,17 +108,17 @@ } } impl ImportResolver for NativeImportResolver { - fn resolve_file(&self, from: &Path, path: &Path) -> Result { + fn resolve_file(&self, from: &Path, path: &str) -> Result { let mut new_path = from.to_owned(); new_path.push(path); if new_path.exists() { - Ok(new_path.into()) + Ok(new_path) } else { for library_path in self.library_paths.borrow().iter() { let mut cloned = library_path.clone(); cloned.push(path); if cloned.exists() { - return Ok(cloned.into()); + return Ok(cloned); } } throw!(ImportFileNotFound(from.to_owned(), path.to_owned())) --- a/bindings/jsonnet/src/vars_tlas.rs +++ b/bindings/jsonnet/src/vars_tlas.rs @@ -20,11 +20,8 @@ pub unsafe extern "C" fn jsonnet_ext_code(vm: &State, name: *const c_char, value: *const c_char) { let name = CStr::from_ptr(name); let value = CStr::from_ptr(value); - vm.add_ext_code( - name.to_str().unwrap().into(), - value.to_str().unwrap().into(), - ) - .unwrap() + vm.add_ext_code(name.to_str().unwrap(), value.to_str().unwrap().into()) + .unwrap() } /// # Safety #[no_mangle] @@ -41,9 +38,6 @@ pub unsafe extern "C" fn jsonnet_tla_code(vm: &State, name: *const c_char, value: *const c_char) { let name = CStr::from_ptr(name); let value = CStr::from_ptr(value); - vm.add_tla_code( - name.to_str().unwrap().into(), - value.to_str().unwrap().into(), - ) - .unwrap() + vm.add_tla_code(name.to_str().unwrap().into(), value.to_str().unwrap()) + .unwrap() } --- a/crates/jrsonnet-evaluator/Cargo.toml +++ b/crates/jrsonnet-evaluator/Cargo.toml @@ -7,13 +7,16 @@ edition = "2021" [features] -default = ["serialized-stdlib", "explaining-traces"] +default = ["serialized-stdlib", "explaining-traces", "friendly-errors"] # Serializes standard library AST instead of parsing them every run serialized-stdlib = ["bincode", "jrsonnet-parser/serde"] # Rustc-like trace visualization explaining-traces = ["annotate-snippets"] # Allows library authors to throw custom errors anyhow-error = ["anyhow"] +# Provides helpful explaintations to errors, at cost of adding +# more dependencies and slowing down error path +friendly-errors = ["strsim"] # Allows to preserve field order in objects exp-preserve-order = [] @@ -42,20 +45,13 @@ serde_json = "1.0" serde_yaml = { git = "https://github.com/CertainLach/serde-yaml", branch = "feature/old-octals-quirk" } -[dependencies.anyhow] -version = "1.0" -optional = true - +anyhow = { version = "1.0", optional = true } +# Friendly errors +strsim = { version = "0.10.0", optional = true } # Serialized stdlib -[dependencies.bincode] -version = "1.3" -optional = true - +bincode = { version = "1.3", optional = true } # Explaining traces -[dependencies.annotate-snippets] -version = "0.9.1" -features = ["color"] -optional = true +annotate-snippets = { version = "0.9.1", features = ["color"], optional = true } [build-dependencies] jrsonnet-stdlib = { path = "../jrsonnet-stdlib", version = "0.4.2" } --- a/crates/jrsonnet-evaluator/src/ctx.rs +++ b/crates/jrsonnet-evaluator/src/ctx.rs @@ -49,13 +49,40 @@ })) } + #[cfg(not(feature = "friendly-errors"))] pub fn binding(&self, name: IStr) -> Result> { Ok(self .0 .bindings .get(&name) .cloned() - .ok_or(VariableIsNotDefined(name))?) + .ok_or(VariableIsNotDefined(name, vec![]))?) + } + + #[cfg(feature = "friendly-errors")] + pub fn binding(&self, name: IStr) -> Result> { + use std::cmp::Ordering; + + use crate::throw; + + if let Some(val) = self.0.bindings.get(&name).cloned() { + return Ok(val); + } + + let mut heap = Vec::new(); + self.0.bindings.clone().iter_keys(|k| { + let conf = strsim::jaro_winkler(&k as &str, &name as &str); + if conf < 0.8 { + return; + } + heap.push((conf, k)); + }); + heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); + + throw!(VariableIsNotDefined( + name, + heap.into_iter().map(|(_, k)| k).collect() + )) } pub fn contains_binding(&self, name: IStr) -> bool { self.0.bindings.contains_key(&name) --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -11,6 +11,30 @@ typed::TypeLocError, }; +fn format_found(list: &[IStr], what: &str) -> String { + if list.is_empty() { + return String::new(); + } + let mut out = String::new(); + out.push_str("\nThere is "); + out.push_str(what); + if list.len() > 1 { + out.push('s'); + } + out.push_str(" with similar name"); + if list.len() > 1 { + out.push('s'); + } + out.push_str(" present: "); + for (i, v) in list.iter().enumerate() { + if i != 0 { + out.push_str(", "); + } + out.push_str(v as &str); + } + out +} + #[derive(Error, Debug, Clone, Trace)] pub enum Error { #[error("intrinsic not found: {0}")] @@ -39,15 +63,15 @@ #[error("assert failed: {0}")] AssertionFailed(IStr), - #[error("variable is not defined: {0}")] - VariableIsNotDefined(IStr), + #[error("variable is not defined: {0}{}", format_found(.1, "variable"))] + VariableIsNotDefined(IStr, Vec), #[error("duplicate local var: {0}")] DuplicateLocalVar(IStr), #[error("type mismatch: expected {}, got {2} {0}", .1.iter().map(|e| format!("{}", e)).collect::>().join(", "))] TypeMismatch(&'static str, Vec, ValType), - #[error("no such field: {0}")] - NoSuchField(IStr), + #[error("no such field: {0}{}", format_found(.1, "field"))] + NoSuchField(IStr, Vec), #[error("only functions can be called, got {0}")] OnlyFunctionsCanBeCalledGot(ValType), --- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::{cmp::Ordering, rc::Rc}; use gcmodule::{Cc, Trace}; use jrsonnet_interner::IStr; @@ -450,7 +450,29 @@ || format!("field <{}> access", key), || match v.get(s.clone(), key.clone()) { Ok(Some(v)) => Ok(v), - Ok(None) => throw!(NoSuchField(key.clone())), + #[cfg(not(feature = "friendly-errors"))] + Ok(None) => throw!(NoSuchField(key.clone(), vec![])), + #[cfg(feature = "friendly-errors")] + Ok(None) => { + let mut heap = Vec::new(); + for field in v.fields_ex( + true, + #[cfg(feature = "exp-preserve-order")] + false, + ) { + let conf = strsim::jaro_winkler(&field as &str, &key as &str); + if conf < 0.8 { + continue; + } + heap.push((conf, field)); + } + heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); + + throw!(NoSuchField( + key.clone(), + heap.into_iter().map(|(_, v)| v).collect() + )) + } Err(e) if matches!(e.error(), MagicThisFileUsed) => { Ok(Val::Str(loc.0.full_path().into())) } @@ -630,14 +652,14 @@ let path = s.resolve_file(&import_location, path as &str)?; match i { Import(_) => s.push( - CallLocation::new(loc), + CallLocation::new(loc), || format!("import {:?}", path.clone()), || s.import(path.clone()), )?, ImportStr(_) => Val::Str(s.import_str(path)?), ImportBin(_) => Val::Arr(ArrValue::Bytes(s.import_bin(path)?)), _ => unreachable!(), - } + } } }) } -- gitstuff