From d2d01e77e39aeec1c344f32c6a8b1c1faeca9f6f Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 9 Jun 2021 12:32:22 -0400 Subject: [PATCH] cli: Use anyhow and thiserror for errors This patch updates the CLI to use anyhow and thiserror for error management. The main feature that our custom `Error` type was providing was a _list_ of messages, which would allow us to annotate "lower-level" errors with more contextual information. This is exactly what's provided by anyhow's `Context` trait. (This is setup work for a future PR that will pull the `config` and `loader` modules out into separate crates; by using `anyhow` we wouldn't have to deal with a circular dependency between with the new crates.) --- Cargo.lock | 29 ++++ cli/Cargo.toml | 1 + cli/benches/benchmark.rs | 8 +- cli/src/error.rs | 156 ------------------ cli/src/generate/binding_files.rs | 13 +- .../build_tables/build_parse_table.rs | 10 +- cli/src/generate/build_tables/mod.rs | 2 +- cli/src/generate/mod.rs | 13 +- cli/src/generate/node_types.rs | 11 +- cli/src/generate/parse_grammar.rs | 5 +- .../generate/prepare_grammar/expand_tokens.rs | 37 ++--- .../prepare_grammar/extract_tokens.rs | 10 +- .../prepare_grammar/flatten_grammar.rs | 4 +- .../prepare_grammar/intern_symbols.rs | 14 +- cli/src/generate/prepare_grammar/mod.rs | 20 ++- cli/src/highlight.rs | 2 +- cli/src/lib.rs | 1 - cli/src/loader.rs | 48 +++--- cli/src/main.rs | 82 ++++----- cli/src/parse.rs | 13 +- cli/src/query.rs | 17 +- cli/src/query_testing.rs | 11 +- cli/src/tags.rs | 4 +- cli/src/test.rs | 14 +- cli/src/test_highlight.rs | 32 ++-- cli/src/tests/corpus_test.rs | 8 +- cli/src/util.rs | 9 +- cli/src/wasm.rs | 31 ++-- cli/src/web_ui.rs | 8 +- highlight/Cargo.toml | 1 + highlight/src/lib.rs | 6 +- tags/Cargo.toml | 1 + tags/src/lib.rs | 35 ++-- 33 files changed, 237 insertions(+), 419 deletions(-) delete mode 100644 cli/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index f735619c..97e7fea6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" + [[package]] name = "arrayref" version = "0.3.6" @@ -585,6 +591,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.3" @@ -647,6 +673,7 @@ name = "tree-sitter-cli" version = "0.19.5" dependencies = [ "ansi_term 0.12.1", + "anyhow", "atty", "cc", "clap", @@ -680,6 +707,7 @@ name = "tree-sitter-highlight" version = "0.19.2" dependencies = [ "regex", + "thiserror", "tree-sitter", ] @@ -689,6 +717,7 @@ version = "0.19.2" dependencies = [ "memchr", "regex", + "thiserror", "tree-sitter", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 693ac817..99dbb8c7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,6 +20,7 @@ harness = false [dependencies] ansi_term = "0.12" +anyhow = "1.0" atty = "0.2" cc = "^1.0.58" clap = "2.32" diff --git a/cli/benches/benchmark.rs b/cli/benches/benchmark.rs index 53ab3fea..9e3ed833 100644 --- a/cli/benches/benchmark.rs +++ b/cli/benches/benchmark.rs @@ -1,10 +1,10 @@ +use anyhow::Context; use lazy_static::lazy_static; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::time::Instant; use std::{env, fs, str, usize}; use tree_sitter::{Language, Parser, Query}; -use tree_sitter_cli::error::Error; use tree_sitter_cli::loader::Loader; include!("../src/tests/helpers/dirs.rs"); @@ -192,7 +192,7 @@ fn parse(path: &Path, max_path_length: usize, mut action: impl FnMut(&[u8])) -> ); let source_code = fs::read(path) - .map_err(Error::wrap(|| format!("Failed to read {:?}", path))) + .with_context(|| format!("Failed to read {:?}", path)) .unwrap(); let time = Instant::now(); for _ in 0..*REPETITION_COUNT { @@ -209,8 +209,6 @@ fn get_language(path: &Path) -> Language { let src_dir = GRAMMARS_DIR.join(path).join("src"); TEST_LOADER .load_language_at_path(&src_dir, &src_dir) - .map_err(Error::wrap(|| { - format!("Failed to load language at path {:?}", src_dir) - })) + .with_context(|| format!("Failed to load language at path {:?}", src_dir)) .unwrap() } diff --git a/cli/src/error.rs b/cli/src/error.rs deleted file mode 100644 index a595ad01..00000000 --- a/cli/src/error.rs +++ /dev/null @@ -1,156 +0,0 @@ -use super::test_highlight; -use std::fmt::Write; -use std::io; -use std::io::ErrorKind; -use tree_sitter::{QueryError, QueryErrorKind}; -use walkdir; - -#[derive(Debug)] -pub struct Error(Option>); - -pub type Result = std::result::Result; - -impl Error { - pub fn grammar(message: &str) -> Self { - Error(Some(vec![format!("Grammar error: {}", message)])) - } - - pub fn regex(mut message: String) -> Self { - message.insert_str(0, "Regex error: "); - Error(Some(vec![message])) - } - - pub fn undefined_symbol(name: &str) -> Self { - Error(Some(vec![format!("Undefined symbol `{}`", name)])) - } - - pub fn new(message: String) -> Self { - Error(Some(vec![message])) - } - - pub fn new_ignored() -> Self { - Self(None) - } - - pub fn is_ignored(&self) -> bool { - self.0.is_none() - } - - pub fn err(message: String) -> Result { - Err(Error::new(message)) - } - - pub fn wrap, M: ToString, F: FnOnce() -> M>( - message_fn: F, - ) -> impl FnOnce(E) -> Self { - |e| { - let mut result = e.into(); - match result.0 { - Some(ref mut e) => e.push(message_fn().to_string()), - None => panic!("It's not allowed to wrap an ignored error"), - } - result - } - } - - pub fn message(&self) -> String { - match self.0 { - None => "Ignored error".to_string(), - Some(ref e) => { - let mut result = e.last().unwrap().clone(); - if e.len() > 1 { - result.push_str("\nDetails:\n"); - for msg in e[0..e.len() - 1].iter().rev() { - writeln!(&mut result, " {}", msg).unwrap(); - } - } - result - } - } - } -} - -impl<'a> From<(&str, QueryError)> for Error { - fn from((path, error): (&str, QueryError)) -> Self { - let mut msg = format!("Query error at {}:{}. ", path, error.row + 1); - match error.kind { - QueryErrorKind::Capture => write!(&mut msg, "Invalid capture name {}", error.message), - QueryErrorKind::Field => write!(&mut msg, "Invalid field name {}", error.message), - QueryErrorKind::NodeType => write!(&mut msg, "Invalid node type {}", error.message), - QueryErrorKind::Syntax => write!(&mut msg, "Invalid syntax:\n{}", error.message), - QueryErrorKind::Structure => write!(&mut msg, "Impossible pattern:\n{}", error.message), - QueryErrorKind::Predicate => write!(&mut msg, "Invalid predicate: {}", error.message), - } - .unwrap(); - Self::new(msg) - } -} - -impl<'a> From for Error { - fn from(error: tree_sitter_highlight::Error) -> Self { - Error::new(format!("{:?}", error)) - } -} - -impl<'a> From for Error { - fn from(error: tree_sitter_tags::Error) -> Self { - Error::new(format!("{}", error)) - } -} - -impl From for Error { - fn from(error: serde_json::Error) -> Self { - Error::new(error.to_string()) - } -} - -impl From for Error { - fn from(error: io::Error) -> Self { - match error.kind() { - ErrorKind::BrokenPipe => Error::new_ignored(), - _ => Error::new(error.to_string()), - } - } -} - -impl From for Error { - fn from(error: glob::PatternError) -> Self { - Error::new(error.to_string()) - } -} - -impl From for Error { - fn from(error: glob::GlobError) -> Self { - Error::new(error.to_string()) - } -} - -impl From for Error { - fn from(error: libloading::Error) -> Self { - Error::new(error.to_string()) - } -} - -impl From for Error { - fn from(error: regex_syntax::ast::Error) -> Self { - Error::new(error.to_string()) - } -} - -impl From for Error { - fn from(error: test_highlight::Failure) -> Self { - Error::new(error.message()) - } -} - -impl From for Error { - fn from(error: String) -> Self { - Error::new(error) - } -} - -impl From for Error { - fn from(error: walkdir::Error) -> Self { - Error::new(error.to_string()) - } -} diff --git a/cli/src/generate/binding_files.rs b/cli/src/generate/binding_files.rs index 0a55e35a..3ea9b356 100644 --- a/cli/src/generate/binding_files.rs +++ b/cli/src/generate/binding_files.rs @@ -1,5 +1,5 @@ use super::write_file; -use crate::error::{Error, Result}; +use anyhow::{Context, Result}; use std::path::Path; use std::{fs, str}; @@ -61,7 +61,7 @@ pub fn generate_binding_files(repo_path: &Path, language_name: &str) -> Result<( eprintln!("Updating binding.gyp with new binding path"); let binding_gyp = fs::read_to_string(&binding_gyp_path) - .map_err(Error::wrap(|| "Failed to read binding.gyp"))?; + .with_context(|| "Failed to read binding.gyp")?; let binding_gyp = binding_gyp.replace("src/binding.cc", "bindings/node/binding.cc"); write_file(&binding_gyp_path, binding_gyp)?; } else { @@ -72,12 +72,12 @@ pub fn generate_binding_files(repo_path: &Path, language_name: &str) -> Result<( let package_json_path = repo_path.join("package.json"); if package_json_path.exists() { let package_json_str = fs::read_to_string(&package_json_path) - .map_err(Error::wrap(|| "Failed to read package.json"))?; + .with_context(|| "Failed to read package.json")?; let mut package_json = serde_json::from_str::>( &package_json_str, ) - .map_err(Error::wrap(|| "Failed to parse package.json"))?; + .with_context(|| "Failed to parse package.json")?; let package_json_main = package_json.get("main"); let package_json_needs_update = package_json_main.map_or(true, |v| { let main_string = v.as_str(); @@ -126,7 +126,6 @@ fn generate_file(path: &Path, template: &str, language_name: &str) -> Result<()> } fn create_dir(path: &Path) -> Result<()> { - fs::create_dir_all(&path).map_err(Error::wrap(|| { - format!("Failed to create {:?}", path.to_string_lossy()) - })) + fs::create_dir_all(&path) + .with_context(|| format!("Failed to create {:?}", path.to_string_lossy())) } diff --git a/cli/src/generate/build_tables/build_parse_table.rs b/cli/src/generate/build_tables/build_parse_table.rs index 2231d5b0..bcce614a 100644 --- a/cli/src/generate/build_tables/build_parse_table.rs +++ b/cli/src/generate/build_tables/build_parse_table.rs @@ -1,5 +1,6 @@ use super::item::{ParseItem, ParseItemSet, ParseItemSetCore}; use super::item_set_builder::ParseItemSetBuilder; +use crate::generate::grammars::PrecedenceEntry; use crate::generate::grammars::{ InlinedProductionMap, LexicalGrammar, SyntaxGrammar, VariableType, }; @@ -9,10 +10,7 @@ use crate::generate::tables::{ FieldLocation, GotoAction, ParseAction, ParseState, ParseStateId, ParseTable, ParseTableEntry, ProductionInfo, ProductionInfoId, }; -use crate::{ - error::{Error, Result}, - generate::grammars::PrecedenceEntry, -}; +use anyhow::{anyhow, Result}; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::fmt::Write; use std::u32; @@ -387,7 +385,7 @@ impl<'a> ParseTableBuilder<'a> { } message += &self.syntax_grammar.variables[*variable_index as usize].name; } - return Err(Error::new(message)); + return Err(anyhow!(message)); } } // Add actions for the start tokens of each non-terminal extra rule. @@ -762,7 +760,7 @@ impl<'a> ParseTableBuilder<'a> { } write!(&mut msg, "\n").unwrap(); - Err(Error::new(msg)) + Err(anyhow!(msg)) } fn compare_precedence( diff --git a/cli/src/generate/build_tables/mod.rs b/cli/src/generate/build_tables/mod.rs index fba0fef3..fe996254 100644 --- a/cli/src/generate/build_tables/mod.rs +++ b/cli/src/generate/build_tables/mod.rs @@ -11,12 +11,12 @@ use self::build_parse_table::{build_parse_table, ParseStateInfo}; use self::coincident_tokens::CoincidentTokenIndex; use self::minimize_parse_table::minimize_parse_table; use self::token_conflicts::TokenConflictMap; -use crate::error::Result; use crate::generate::grammars::{InlinedProductionMap, LexicalGrammar, SyntaxGrammar}; use crate::generate::nfa::NfaCursor; use crate::generate::node_types::VariableInfo; use crate::generate::rules::{AliasMap, Symbol, SymbolType, TokenSet}; use crate::generate::tables::{LexTable, ParseAction, ParseTable, ParseTableEntry}; +use anyhow::Result; use log::info; use std::collections::{BTreeSet, HashMap}; diff --git a/cli/src/generate/mod.rs b/cli/src/generate/mod.rs index cc92e56d..123e6ffa 100644 --- a/cli/src/generate/mod.rs +++ b/cli/src/generate/mod.rs @@ -17,7 +17,7 @@ use self::parse_grammar::parse_grammar; use self::prepare_grammar::prepare_grammar; use self::render::render_c_code; use self::rules::AliasMap; -use crate::error::{Error, Result}; +use anyhow::{anyhow, Context, Result}; use lazy_static::lazy_static; use regex::{Regex, RegexBuilder}; use std::fs; @@ -161,10 +161,10 @@ fn load_grammar_file(grammar_path: &Path) -> Result { match grammar_path.extension().and_then(|e| e.to_str()) { Some("js") => Ok(load_js_grammar_file(grammar_path)?), Some("json") => Ok(fs::read_to_string(grammar_path)?), - _ => Err(Error::new(format!( + _ => Err(anyhow!( "Unknown grammar file extension: {:?}", grammar_path - ))), + )), } } @@ -191,7 +191,7 @@ fn load_js_grammar_file(grammar_path: &Path) -> Result { match output.status.code() { None => panic!("Node process was killed"), Some(0) => {} - Some(code) => return Error::err(format!("Node process exited with status {}", code)), + Some(code) => return Err(anyhow!("Node process exited with status {}", code)), } let mut result = String::from_utf8(output.stdout).expect("Got invalid UTF8 from node"); @@ -200,7 +200,6 @@ fn load_js_grammar_file(grammar_path: &Path) -> Result { } fn write_file(path: &Path, body: impl AsRef<[u8]>) -> Result<()> { - fs::write(path, body).map_err(Error::wrap(|| { - format!("Failed to write {:?}", path.file_name().unwrap()) - })) + fs::write(path, body) + .with_context(|| format!("Failed to write {:?}", path.file_name().unwrap())) } diff --git a/cli/src/generate/node_types.rs b/cli/src/generate/node_types.rs index 97aed607..f675fc13 100644 --- a/cli/src/generate/node_types.rs +++ b/cli/src/generate/node_types.rs @@ -1,6 +1,6 @@ use super::grammars::{LexicalGrammar, SyntaxGrammar, VariableType}; use super::rules::{Alias, AliasMap, Symbol, SymbolType}; -use crate::error::{Error, Result}; +use anyhow::{anyhow, Result}; use serde_derive::Serialize; use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -328,10 +328,13 @@ pub(crate) fn get_variable_info( for supertype_symbol in &syntax_grammar.supertype_symbols { if result[supertype_symbol.index].has_multi_step_production { let variable = &syntax_grammar.variables[supertype_symbol.index]; - return Err(Error::grammar(&format!( - "Supertype symbols must always have a single visible child, but `{}` can have multiple", + return Err(anyhow!( + concat!( + "Grammar error: Supertype symbols must always ", + "have a single visible child, but `{}` can have multiple" + ), variable.name - ))); + )); } } diff --git a/cli/src/generate/parse_grammar.rs b/cli/src/generate/parse_grammar.rs index 5b189f24..c91ebd64 100644 --- a/cli/src/generate/parse_grammar.rs +++ b/cli/src/generate/parse_grammar.rs @@ -1,6 +1,6 @@ use super::grammars::{InputGrammar, PrecedenceEntry, Variable, VariableType}; use super::rules::{Precedence, Rule}; -use crate::error::{Error, Result}; +use anyhow::{anyhow, Result}; use serde_derive::Deserialize; use serde_json::{Map, Value}; @@ -109,9 +109,8 @@ pub(crate) fn parse_grammar(input: &str) -> Result { RuleJSON::STRING { value } => PrecedenceEntry::Name(value), RuleJSON::SYMBOL { name } => PrecedenceEntry::Symbol(name), _ => { - return Err(Error::new( + return Err(anyhow!( "Invalid rule in precedences array. Only strings and symbols are allowed" - .to_string(), )) } }) diff --git a/cli/src/generate/prepare_grammar/expand_tokens.rs b/cli/src/generate/prepare_grammar/expand_tokens.rs index b4c53a27..a5fe318b 100644 --- a/cli/src/generate/prepare_grammar/expand_tokens.rs +++ b/cli/src/generate/prepare_grammar/expand_tokens.rs @@ -1,11 +1,8 @@ use super::ExtractedLexicalGrammar; use crate::generate::grammars::{LexicalGrammar, LexicalVariable}; use crate::generate::nfa::{CharacterSet, Nfa, NfaState}; -use crate::generate::rules::Rule; -use crate::{ - error::{Error, Result}, - generate::rules::Precedence, -}; +use crate::generate::rules::{Precedence, Rule}; +use anyhow::{anyhow, Context, Result}; use lazy_static::lazy_static; use regex::Regex; use regex_syntax::ast::{ @@ -111,9 +108,7 @@ pub(crate) fn expand_tokens(mut grammar: ExtractedLexicalGrammar) -> Result Ok(false), - _ => Err(Error::grammar(&format!("Unexpected rule {:?}", rule))), + _ => Err(anyhow!("Grammar error: Unexpected rule {:?}", rule)), } } fn expand_regex(&mut self, ast: &Ast, mut next_state_id: u32) -> Result { match ast { Ast::Empty(_) => Ok(false), - Ast::Flags(_) => Err(Error::regex("Flags are not supported".to_string())), + Ast::Flags(_) => Err(anyhow!("Regex error: Flags are not supported")), Ast::Literal(literal) => { self.push_advance(CharacterSet::from_char(literal.c), next_state_id); Ok(true) @@ -221,7 +216,7 @@ impl NfaBuilder { self.push_advance(CharacterSet::from_char('\n').negate(), next_state_id); Ok(true) } - Ast::Assertion(_) => Err(Error::regex("Assertions are not supported".to_string())), + Ast::Assertion(_) => Err(anyhow!("Regex error: Assertions are not supported")), Ast::Class(class) => match class { Class::Unicode(class) => { let mut chars = self.expand_unicode_character_class(&class.kind)?; @@ -248,8 +243,8 @@ impl NfaBuilder { self.push_advance(chars, next_state_id); Ok(true) } - ClassSet::BinaryOp(_) => Err(Error::regex( - "Binary operators in character classes aren't supported".to_string(), + ClassSet::BinaryOp(_) => Err(anyhow!( + "Regex error: Binary operators in character classes aren't supported" )), }, }, @@ -383,10 +378,10 @@ impl NfaBuilder { } Ok(set) } - _ => Err(Error::regex(format!( - "Unsupported character class syntax {:?}", + _ => Err(anyhow!( + "Regex error: Unsupported character class syntax {:?}", item - ))), + )), } } @@ -406,10 +401,10 @@ impl NfaBuilder { .get(class_name.as_str()) .or_else(|| UNICODE_PROPERTIES.get(class_name.as_str())) .ok_or_else(|| { - Error::regex(format!( - "Unsupported unicode character class {}", + anyhow!( + "Regex error: Unsupported unicode character class {}", class_name - )) + ) })?; for c in code_points { if let Some(c) = std::char::from_u32(*c) { @@ -421,8 +416,8 @@ impl NfaBuilder { } } ClassUnicodeKind::NamedValue { .. } => { - return Err(Error::regex( - "Key-value unicode properties are not supported".to_string(), + return Err(anyhow!( + "Regex error: Key-value unicode properties are not supported" )) } } diff --git a/cli/src/generate/prepare_grammar/extract_tokens.rs b/cli/src/generate/prepare_grammar/extract_tokens.rs index ae9e4547..acf34401 100644 --- a/cli/src/generate/prepare_grammar/extract_tokens.rs +++ b/cli/src/generate/prepare_grammar/extract_tokens.rs @@ -1,7 +1,7 @@ use super::{ExtractedLexicalGrammar, ExtractedSyntaxGrammar, InternedGrammar}; -use crate::error::{Error, Result}; use crate::generate::grammars::{ExternalToken, Variable, VariableType}; use crate::generate::rules::{MetadataParams, Rule, Symbol, SymbolType}; +use anyhow::{anyhow, Result}; use std::collections::HashMap; use std::mem; @@ -108,7 +108,7 @@ pub(super) fn extract_tokens( let rule = symbol_replacer.replace_symbols_in_rule(&external_token.rule); if let Rule::Symbol(symbol) = rule { if symbol.is_non_terminal() { - return Error::err(format!( + return Err(anyhow!( "Rule '{}' cannot be used as both an external token and a non-terminal rule", &variables[symbol.index].name, )); @@ -128,7 +128,7 @@ pub(super) fn extract_tokens( }) } } else { - return Error::err(format!( + return Err(anyhow!( "Non-symbol rules cannot be used as external tokens" )); } @@ -138,7 +138,7 @@ pub(super) fn extract_tokens( if let Some(token) = grammar.word_token { let token = symbol_replacer.replace_symbol(token); if token.is_non_terminal() { - return Error::err(format!( + return Err(anyhow!( "Non-terminal symbol '{}' cannot be used as the word token", &variables[token.index].name )); @@ -482,7 +482,7 @@ mod test { match extract_tokens(grammar) { Err(e) => { - assert_eq!(e.message(), "Rule 'rule_1' cannot be used as both an external token and a non-terminal rule"); + assert_eq!(e.to_string(), "Rule 'rule_1' cannot be used as both an external token and a non-terminal rule"); } _ => { panic!("Expected an error but got no error"); diff --git a/cli/src/generate/prepare_grammar/flatten_grammar.rs b/cli/src/generate/prepare_grammar/flatten_grammar.rs index 4d50335f..e9950e1b 100644 --- a/cli/src/generate/prepare_grammar/flatten_grammar.rs +++ b/cli/src/generate/prepare_grammar/flatten_grammar.rs @@ -1,9 +1,9 @@ use super::ExtractedSyntaxGrammar; -use crate::error::{Error, Result}; use crate::generate::grammars::{ Production, ProductionStep, SyntaxGrammar, SyntaxVariable, Variable, }; use crate::generate::rules::{Alias, Associativity, Precedence, Rule, Symbol}; +use anyhow::{anyhow, Result}; struct RuleFlattener { production: Production, @@ -193,7 +193,7 @@ pub(super) fn flatten_grammar(grammar: ExtractedSyntaxGrammar) -> Result Result { let interner = Interner { grammar }; if variable_type_for_name(&grammar.variables[0].name) == VariableType::Hidden { - return Error::err("A grammar's start rule must be visible.".to_string()); + return Err(anyhow!("A grammar's start rule must be visible.")); } let mut variables = Vec::with_capacity(grammar.variables.len()); @@ -40,7 +40,7 @@ pub(super) fn intern_symbols(grammar: &InputGrammar) -> Result supertype_symbols.push( interner .intern_name(supertype_symbol_name) - .ok_or_else(|| Error::undefined_symbol(supertype_symbol_name))?, + .ok_or_else(|| anyhow!("Undefined symbol `{}`", supertype_symbol_name))?, ); } @@ -51,7 +51,7 @@ pub(super) fn intern_symbols(grammar: &InputGrammar) -> Result interned_conflict.push( interner .intern_name(&name) - .ok_or_else(|| Error::undefined_symbol(name))?, + .ok_or_else(|| anyhow!("Undefined symbol `{}`", name))?, ); } expected_conflicts.push(interned_conflict); @@ -69,7 +69,7 @@ pub(super) fn intern_symbols(grammar: &InputGrammar) -> Result word_token = Some( interner .intern_name(&name) - .ok_or_else(|| Error::undefined_symbol(&name))?, + .ok_or_else(|| anyhow!("Undefined symbol `{}`", &name))?, ); } @@ -122,7 +122,7 @@ impl<'a> Interner<'a> { if let Some(symbol) = self.intern_name(&name) { Ok(Rule::Symbol(symbol)) } else { - Err(Error::undefined_symbol(name)) + Err(anyhow!("Undefined symbol `{}`", name)) } } @@ -234,7 +234,7 @@ mod tests { let result = intern_symbols(&build_grammar(vec![Variable::named("x", Rule::named("y"))])); match result { - Err(e) => assert_eq!(e.message(), "Undefined symbol `y`"), + Err(e) => assert_eq!(e.to_string(), "Undefined symbol `y`"), _ => panic!("Expected an error but got none"), } } diff --git a/cli/src/generate/prepare_grammar/mod.rs b/cli/src/generate/prepare_grammar/mod.rs index d3687919..283c80c5 100644 --- a/cli/src/generate/prepare_grammar/mod.rs +++ b/cli/src/generate/prepare_grammar/mod.rs @@ -19,7 +19,7 @@ use super::grammars::{ SyntaxGrammar, Variable, }; use super::rules::{AliasMap, Precedence, Rule, Symbol}; -use super::{Error, Result}; +use anyhow::{anyhow, Result}; use std::{ cmp::Ordering, collections::{hash_map, HashMap, HashSet}, @@ -93,10 +93,11 @@ fn validate_precedences(grammar: &InputGrammar) -> Result<()> { } hash_map::Entry::Occupied(e) => { if e.get() != &ordering { - return Err(Error::new(format!( + return Err(anyhow!( "Conflicting orderings for precedences {} and {}", - entry1, entry2 - ))); + entry1, + entry2 + )); } } } @@ -116,10 +117,11 @@ fn validate_precedences(grammar: &InputGrammar) -> Result<()> { Rule::Metadata { rule, params } => { if let Precedence::Name(n) = ¶ms.precedence { if !names.contains(n) { - return Err(Error::new(format!( + return Err(anyhow!( "Undeclared precedence '{}' in rule '{}'", - n, rule_name - ))); + n, + rule_name + )); } } validate(rule_name, rule, names)?; @@ -196,7 +198,7 @@ mod tests { let result = validate_precedences(&grammar); assert_eq!( - result.unwrap_err().message(), + result.unwrap_err().to_string(), "Undeclared precedence 'omg' in rule 'v2'", ); } @@ -244,7 +246,7 @@ mod tests { let result = validate_precedences(&grammar); assert_eq!( - result.unwrap_err().message(), + result.unwrap_err().to_string(), "Conflicting orderings for precedences 'a' and 'b'", ); } diff --git a/cli/src/highlight.rs b/cli/src/highlight.rs index 1078e48c..d5b1da97 100644 --- a/cli/src/highlight.rs +++ b/cli/src/highlight.rs @@ -1,7 +1,7 @@ use super::util; -use crate::error::Result; use crate::loader::Loader; use ansi_term::Color; +use anyhow::Result; use lazy_static::lazy_static; use serde::ser::SerializeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e00323b7..b6f01dfe 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,5 +1,4 @@ pub mod config; -pub mod error; pub mod generate; pub mod highlight; pub mod loader; diff --git a/cli/src/loader.rs b/cli/src/loader.rs index 58ccf747..04af4716 100644 --- a/cli/src/loader.rs +++ b/cli/src/loader.rs @@ -1,4 +1,4 @@ -use super::error::{Error, Result}; +use anyhow::{anyhow, Context, Error, Result}; use libloading::{Library, Symbol}; use once_cell::unsync::OnceCell; use regex::{Regex, RegexBuilder}; @@ -162,7 +162,7 @@ impl Loader { // one to use by applying the configurations' content regexes. else { let file_contents = fs::read(path) - .map_err(Error::wrap(|| format!("Failed to read path {:?}", path)))?; + .with_context(|| format!("Failed to read path {:?}", path))?; let file_contents = String::from_utf8_lossy(&file_contents); let mut best_score = -2isize; let mut best_configuration_id = None; @@ -250,9 +250,9 @@ impl Loader { name: String, } let mut grammar_file = - fs::File::open(grammar_path).map_err(Error::wrap(|| "Failed to read grammar.json"))?; + fs::File::open(grammar_path).with_context(|| "Failed to read grammar.json")?; let grammar_json: GrammarJSON = serde_json::from_reader(BufReader::new(&mut grammar_file)) - .map_err(Error::wrap(|| "Failed to parse grammar.json"))?; + .with_context(|| "Failed to parse grammar.json")?; let scanner_path = if scanner_path.exists() { Some(scanner_path) @@ -283,9 +283,8 @@ impl Loader { let mut library_path = self.parser_lib_path.join(name); library_path.set_extension(DYLIB_EXTENSION); - let recompile = needs_recompile(&library_path, &parser_path, &scanner_path).map_err( - Error::wrap(|| "Failed to compare source and binary timestamps"), - )?; + let recompile = needs_recompile(&library_path, &parser_path, &scanner_path) + .with_context(|| "Failed to compare source and binary timestamps")?; if recompile { let mut config = cc::Build::new(); @@ -336,26 +335,23 @@ impl Loader { let output = command .output() - .map_err(Error::wrap(|| "Failed to execute C compiler"))?; + .with_context(|| "Failed to execute C compiler")?; if !output.status.success() { - return Err(Error::new(format!( + return Err(anyhow!( "Parser compilation failed.\nStdout: {}\nStderr: {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) - ))); + )); } } - let library = unsafe { Library::new(&library_path) }.map_err(Error::wrap(|| { - format!("Error opening dynamic library {:?}", &library_path) - }))?; + let library = unsafe { Library::new(&library_path) } + .with_context(|| format!("Error opening dynamic library {:?}", &library_path))?; let language_fn_name = format!("tree_sitter_{}", replace_dashes_with_underscores(name)); let language = unsafe { let language_fn: Symbol Language> = library .get(language_fn_name.as_bytes()) - .map_err(Error::wrap(|| { - format!("Failed to load symbol {}", language_fn_name) - }))?; + .with_context(|| format!("Failed to load symbol {}", language_fn_name))?; language_fn() }; mem::forget(library); @@ -370,8 +366,7 @@ impl Loader { Err(e) => { eprintln!( "Failed to load language for injection string '{}': {}", - string, - e.message() + string, e ); None } @@ -380,8 +375,7 @@ impl Loader { Err(e) => { eprintln!( "Failed to load property sheet for injection string '{}': {}", - string, - e.message() + string, e ); None } @@ -646,7 +640,7 @@ impl<'a> LanguageConfiguration<'a> { ranges: &'b Vec<(String, Range)>, source: &str, start_offset: usize, - ) -> (&'b str, QueryError) { + ) -> Error { let offset_within_section = error.offset - start_offset; let (path, range) = ranges .iter() @@ -657,7 +651,7 @@ impl<'a> LanguageConfiguration<'a> { .chars() .filter(|c| *c == '\n') .count(); - (path.as_ref(), error) + Error::from(error).context(format!("Error in query file {:?}", path)) } fn read_queries( @@ -671,18 +665,16 @@ impl<'a> LanguageConfiguration<'a> { for path in paths { let abs_path = self.root_path.join(path); let prev_query_len = query.len(); - query += &fs::read_to_string(&abs_path).map_err(Error::wrap(|| { - format!("Failed to read query file {:?}", path) - }))?; + query += &fs::read_to_string(&abs_path) + .with_context(|| format!("Failed to read query file {:?}", path))?; path_ranges.push((path.clone(), prev_query_len..query.len())); } } else { let queries_path = self.root_path.join("queries"); let path = queries_path.join(default_path); if path.exists() { - query = fs::read_to_string(&path).map_err(Error::wrap(|| { - format!("Failed to read query file {:?}", path) - }))?; + query = fs::read_to_string(&path) + .with_context(|| format!("Failed to read query file {:?}", path))?; path_ranges.push((default_path.to_string(), 0..query.len())); } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 6be7eb8e..e80fa550 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,32 +1,18 @@ +use anyhow::{anyhow, Context, Result}; use clap::{App, AppSettings, Arg, SubCommand}; -use error::Error; use glob::glob; use std::path::Path; -use std::process::exit; use std::{env, fs, u64}; use tree_sitter::Language; use tree_sitter_cli::{ - config, error, generate, highlight, loader, logger, parse, query, tags, test, test_highlight, - util, wasm, web_ui, + config, generate, highlight, loader, logger, parse, query, tags, test, test_highlight, util, + wasm, web_ui, }; const BUILD_VERSION: &'static str = env!("CARGO_PKG_VERSION"); const BUILD_SHA: Option<&'static str> = option_env!("BUILD_SHA"); -fn main() { - if let Err(e) = run() { - if e.is_ignored() { - exit(0); - } - if !e.message().is_empty() { - eprintln!(""); - eprintln!("{}", e.message()); - } - exit(1); - } -} - -fn run() -> error::Result<()> { +fn main() -> Result<()> { let version = if let Some(build_sha) = BUILD_SHA { format!("{} ({})", BUILD_VERSION, build_sha) } else { @@ -212,7 +198,7 @@ fn run() -> error::Result<()> { let languages = loader.languages_at_path(¤t_dir)?; let language = languages .first() - .ok_or_else(|| "No language found".to_string())?; + .ok_or_else(|| anyhow!("No language found"))?; let test_dir = current_dir.join("test"); // Run the corpus tests. Look for them at two paths: `test/corpus` and `corpus`. @@ -297,7 +283,7 @@ fn run() -> error::Result<()> { } if has_error { - return Error::err(String::new()); + return Err(anyhow!("")); } } else if let Some(matches) = matches.subcommand_matches("query") { let ordered_captures = matches.values_of("captures").is_some(); @@ -352,7 +338,7 @@ fn run() -> error::Result<()> { if let Some(scope) = matches.value_of("scope") { lang = loader.language_configuration_for_scope(scope)?; if lang.is_none() { - return Error::err(format!("Unknown scope '{}'", scope)); + return Err(anyhow!("Unknown scope '{}'", scope)); } } @@ -432,12 +418,10 @@ fn run() -> error::Result<()> { fn collect_paths<'a>( paths_file: Option<&str>, paths: Option>, -) -> error::Result> { +) -> Result> { if let Some(paths_file) = paths_file { return Ok(fs::read_to_string(paths_file) - .map_err(Error::wrap(|| { - format!("Failed to read paths file {}", paths_file) - }))? + .with_context(|| format!("Failed to read paths file {}", paths_file))? .trim() .split_ascii_whitespace() .map(String::from) @@ -467,8 +451,8 @@ fn collect_paths<'a>( if Path::new(path).exists() { incorporate_path(path, positive); } else { - let paths = glob(path) - .map_err(Error::wrap(|| format!("Invalid glob pattern {:?}", path)))?; + let paths = + glob(path).with_context(|| format!("Invalid glob pattern {:?}", path))?; for path in paths { if let Some(path) = path?.to_str() { incorporate_path(path, positive); @@ -478,15 +462,15 @@ fn collect_paths<'a>( } if result.is_empty() { - Error::err( - "No files were found at or matched by the provided pathname/glob".to_string(), - )?; + return Err(anyhow!( + "No files were found at or matched by the provided pathname/glob" + )); } return Ok(result); } - Err(Error::new("Must provide one or more paths".to_string())) + Err(anyhow!("Must provide one or more paths")) } fn select_language( @@ -494,41 +478,35 @@ fn select_language( path: &Path, current_dir: &Path, scope: Option<&str>, -) -> Result { +) -> Result { if let Some(scope) = scope { - if let Some(config) = - loader - .language_configuration_for_scope(scope) - .map_err(Error::wrap(|| { - format!("Failed to load language for scope '{}'", scope) - }))? + if let Some(config) = loader + .language_configuration_for_scope(scope) + .with_context(|| format!("Failed to load language for scope '{}'", scope))? { Ok(config.0) } else { - return Error::err(format!("Unknown scope '{}'", scope)); + return Err(anyhow!("Unknown scope '{}'", scope)); } - } else if let Some((lang, _)) = - loader - .language_configuration_for_file_name(path) - .map_err(Error::wrap(|| { - format!( - "Failed to load language for file name {:?}", - path.file_name().unwrap() - ) - }))? + } else if let Some((lang, _)) = loader + .language_configuration_for_file_name(path) + .with_context(|| { + format!( + "Failed to load language for file name {}", + &path.file_name().unwrap().to_string_lossy() + ) + })? { Ok(lang) } else if let Some(lang) = loader .languages_at_path(¤t_dir) - .map_err(Error::wrap(|| { - "Failed to load language in current directory" - }))? + .with_context(|| "Failed to load language in current directory")? .first() .cloned() { Ok(lang) } else { eprintln!("No language found"); - Error::err("No language found".to_string()) + Err(anyhow!("No language found")) } } diff --git a/cli/src/parse.rs b/cli/src/parse.rs index e843663e..904ee396 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -1,5 +1,5 @@ -use super::error::{Error, Result}; use super::util; +use anyhow::{anyhow, Context, Result}; use std::io::{self, Write}; use std::path::Path; use std::sync::atomic::AtomicUsize; @@ -45,10 +45,9 @@ pub fn parse_file_at_path( ) -> Result { let mut _log_session = None; let mut parser = Parser::new(); - parser.set_language(language).map_err(|e| e.to_string())?; - let mut source_code = fs::read(path).map_err(Error::wrap(|| { - format!("Error reading source file {:?}", path) - }))?; + parser.set_language(language)?; + let mut source_code = + fs::read(path).with_context(|| format!("Error reading source file {:?}", path))?; // If the `--cancel` flag was passed, then cancel the parse // when the user types a newline. @@ -296,10 +295,10 @@ pub fn perform_edit(tree: &mut Tree, input: &mut Vec, edit: &Edit) -> InputE fn parse_edit_flag(source_code: &Vec, flag: &str) -> Result { let error = || { - Error::from(format!(concat!( + anyhow!(concat!( "Invalid edit string '{}'. ", "Edit strings must match the pattern ' '" - ), flag)) + ), flag) }; // Three whitespace-separated parts: diff --git a/cli/src/query.rs b/cli/src/query.rs index 9d8ca578..9039f751 100644 --- a/cli/src/query.rs +++ b/cli/src/query.rs @@ -1,5 +1,5 @@ -use super::error::{Error, Result}; use crate::query_testing; +use anyhow::{Context, Result}; use std::{ fs, io::{self, Write}, @@ -19,11 +19,9 @@ pub fn query_files_at_paths( let stdout = io::stdout(); let mut stdout = stdout.lock(); - let query_source = fs::read_to_string(query_path).map_err(Error::wrap(|| { - format!("Error reading query file {:?}", query_path) - }))?; - let query = Query::new(language, &query_source) - .map_err(|e| Error::new(format!("Query compilation failed: {:?}", e)))?; + let query_source = fs::read_to_string(query_path) + .with_context(|| format!("Error reading query file {:?}", query_path))?; + let query = Query::new(language, &query_source).with_context(|| "Query compilation failed")?; let mut query_cursor = QueryCursor::new(); if let Some(range) = range { @@ -31,16 +29,15 @@ pub fn query_files_at_paths( } let mut parser = Parser::new(); - parser.set_language(language).map_err(|e| e.to_string())?; + parser.set_language(language)?; for path in paths { let mut results = Vec::new(); writeln!(&mut stdout, "{}", path)?; - let source_code = fs::read(&path).map_err(Error::wrap(|| { - format!("Error reading source file {:?}", path) - }))?; + let source_code = + fs::read(&path).with_context(|| format!("Error reading source file {:?}", path))?; let tree = parser.parse(&source_code, None).unwrap(); if ordered_captures { diff --git a/cli/src/query_testing.rs b/cli/src/query_testing.rs index ef02ec69..6dc35c8d 100644 --- a/cli/src/query_testing.rs +++ b/cli/src/query_testing.rs @@ -1,5 +1,4 @@ -use crate::error; -use crate::error::Result; +use anyhow::{anyhow, Result}; use lazy_static::lazy_static; use regex::Regex; use std::fs; @@ -139,10 +138,12 @@ pub fn assert_expected_captures( p.position.row == info.start.row && p.position >= info.start && p.position < info.end }) { if found.expected_capture_name != info.name && info.name != "name" { - Err(error::Error::new(format!( + Err(anyhow!( "Assertion failed: at {}, found {}, expected {}", - info.start, found.expected_capture_name, info.name - )))? + info.start, + found.expected_capture_name, + info.name + ))? } } } diff --git a/cli/src/tags.rs b/cli/src/tags.rs index 802d8d06..aeb1c3cc 100644 --- a/cli/src/tags.rs +++ b/cli/src/tags.rs @@ -1,6 +1,6 @@ use super::loader::Loader; use super::util; -use crate::error::{Error, Result}; +use anyhow::{anyhow, Result}; use std::io::{self, Write}; use std::path::Path; use std::time::Instant; @@ -18,7 +18,7 @@ pub fn generate_tags( if let Some(scope) = scope { lang = loader.language_configuration_for_scope(scope)?; if lang.is_none() { - return Error::err(format!("Unknown scope '{}'", scope)); + return Err(anyhow!("Unknown scope '{}'", scope)); } } diff --git a/cli/src/test.rs b/cli/src/test.rs index d79bec8e..9c6987d7 100644 --- a/cli/src/test.rs +++ b/cli/src/test.rs @@ -1,6 +1,6 @@ -use super::error::{Error, Result}; use super::util; use ansi_term::Colour; +use anyhow::{anyhow, Context, Result}; use difference::{Changeset, Difference}; use lazy_static::lazy_static; use regex::bytes::{Regex as ByteRegex, RegexBuilder as ByteRegexBuilder}; @@ -65,7 +65,7 @@ pub fn run_tests_at_path( let test_entry = parse_tests(path)?; let mut _log_session = None; let mut parser = Parser::new(); - parser.set_language(language).map_err(|e| e.to_string())?; + parser.set_language(language)?; if debug_graph { _log_session = Some(util::log_graphs(&mut parser, "log.html")?); @@ -116,7 +116,7 @@ pub fn run_tests_at_path( println!("\n {}. {}:", i + 1, name); print_diff(actual, expected); } - Error::err(String::new()) + Err(anyhow!("")) } } else { Ok(()) @@ -135,10 +135,10 @@ pub fn check_queries_at_path(language: Language, path: &Path) -> Result<()> { }) { let filepath = entry.file_name().to_str().unwrap_or(""); - let content = fs::read_to_string(entry.path()).map_err(Error::wrap(|| { - format!("Error reading query file {:?}", entry.file_name()) - }))?; - Query::new(language, &content).map_err(|e| (filepath, e))?; + let content = fs::read_to_string(entry.path()) + .with_context(|| format!("Error reading query file {:?}", filepath))?; + Query::new(language, &content) + .with_context(|| format!("Error in query file {:?}", filepath))?; } } Ok(()) diff --git a/cli/src/test_highlight.rs b/cli/src/test_highlight.rs index df870bf6..dcc42081 100644 --- a/cli/src/test_highlight.rs +++ b/cli/src/test_highlight.rs @@ -1,12 +1,13 @@ -use super::error::Result; use crate::loader::Loader; use crate::query_testing::{parse_position_comments, Assertion}; use ansi_term::Colour; +use anyhow::{anyhow, Result}; use std::fs; use std::path::Path; use tree_sitter::Point; use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter}; +#[derive(Debug)] pub struct Failure { row: usize, column: usize, @@ -14,25 +15,26 @@ pub struct Failure { actual_highlights: Vec, } -impl Failure { - pub fn message(&self) -> String { - let mut result = format!( +impl std::error::Error for Failure {} + +impl std::fmt::Display for Failure { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, "Failure - row: {}, column: {}, expected highlight '{}', actual highlights: ", self.row, self.column, self.expected_highlight - ); + )?; if self.actual_highlights.is_empty() { - result += "none."; + write!(f, "none.")?; } else { for (i, actual_highlight) in self.actual_highlights.iter().enumerate() { if i > 0 { - result += ", "; + write!(f, ", ")?; } - result += "'"; - result += actual_highlight; - result += "'"; + write!(f, "'{}'", actual_highlight)?; } } - result + Ok(()) } } @@ -47,10 +49,10 @@ pub fn test_highlights(loader: &Loader, directory: &Path) -> Result<()> { let test_file_name = highlight_test_file.file_name(); let (language, language_config) = loader .language_configuration_for_file_name(&test_file_path)? - .ok_or_else(|| format!("No language found for path {:?}", test_file_path))?; + .ok_or_else(|| anyhow!("No language found for path {:?}", test_file_path))?; let highlight_config = language_config .highlight_config(language)? - .ok_or_else(|| format!("No highlighting config found for {:?}", test_file_path))?; + .ok_or_else(|| anyhow!("No highlighting config found for {:?}", test_file_path))?; match test_highlight( &loader, &mut highlighter, @@ -69,14 +71,14 @@ pub fn test_highlights(loader: &Loader, directory: &Path) -> Result<()> { " ✗ {}", Colour::Red.paint(test_file_name.to_string_lossy().as_ref()) ); - println!(" {}", e.message()); + println!(" {}", e); failed = true; } } } if failed { - Err(String::new().into()) + Err(anyhow!("")) } else { Ok(()) } diff --git a/cli/src/tests/corpus_test.rs b/cli/src/tests/corpus_test.rs index cd6ad061..bfb2b52c 100644 --- a/cli/src/tests/corpus_test.rs +++ b/cli/src/tests/corpus_test.rs @@ -231,11 +231,10 @@ fn test_feature_corpus_files() { let expected_message = fs::read_to_string(&error_message_path).unwrap(); if let Err(e) = generate_result { - if e.message() != expected_message { + if e.to_string() != expected_message { eprintln!( "Unexpected error message.\n\nExpected:\n\n{}\nActual:\n\n{}\n", - expected_message, - e.message() + expected_message, e ); failure_count += 1; } @@ -250,8 +249,7 @@ fn test_feature_corpus_files() { if let Err(e) = &generate_result { eprintln!( "Unexpected error for test grammar '{}':\n{}", - language_name, - e.message() + language_name, e ); failure_count += 1; continue; diff --git a/cli/src/util.rs b/cli/src/util.rs index 58690ce7..396702c8 100644 --- a/cli/src/util.rs +++ b/cli/src/util.rs @@ -1,4 +1,4 @@ -use super::error::Result; +use anyhow::{anyhow, Context, Result}; use std::io; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -40,7 +40,6 @@ pub fn log_graphs(_parser: &mut Parser, _path: &str) -> Result { #[cfg(unix)] pub fn log_graphs(parser: &mut Parser, path: &str) -> Result { - use super::error::Error; use std::io::Write; let mut dot_file = std::fs::File::create(path)?; @@ -50,13 +49,11 @@ pub fn log_graphs(parser: &mut Parser, path: &str) -> Result { .stdin(Stdio::piped()) .stdout(dot_file) .spawn() - .map_err(Error::wrap(|| { - "Failed to run the `dot` command. Check that graphviz is installed." - }))?; + .with_context(|| "Failed to run the `dot` command. Check that graphviz is installed.")?; let dot_stdin = dot_process .stdin .take() - .ok_or_else(|| Error::new("Failed to open stdin for `dot` process.".to_string()))?; + .ok_or_else(|| anyhow!("Failed to open stdin for `dot` process."))?; parser.print_dot_graphs(&dot_stdin); Ok(LogSession( PathBuf::from(path), diff --git a/cli/src/wasm.rs b/cli/src/wasm.rs index c2175938..1abbb042 100644 --- a/cli/src/wasm.rs +++ b/cli/src/wasm.rs @@ -1,5 +1,5 @@ -use super::error::{Error, Result}; use super::generate::parse_grammar::GrammarJSON; +use anyhow::{anyhow, Context, Result}; use std::ffi::{OsStr, OsString}; use std::fs; use std::path::Path; @@ -8,12 +8,10 @@ use which::which; pub fn get_grammar_name(src_dir: &Path) -> Result { let grammar_json_path = src_dir.join("grammar.json"); - let grammar_json = fs::read_to_string(&grammar_json_path).map_err(Error::wrap(|| { - format!("Failed to read grammar file {:?}", grammar_json_path) - }))?; - let grammar: GrammarJSON = serde_json::from_str(&grammar_json).map_err(Error::wrap(|| { - format!("Failed to parse grammar file {:?}", grammar_json_path) - }))?; + let grammar_json = fs::read_to_string(&grammar_json_path) + .with_context(|| format!("Failed to read grammar file {:?}", grammar_json_path))?; + let grammar: GrammarJSON = serde_json::from_str(&grammar_json) + .with_context(|| format!("Failed to parse grammar file {:?}", grammar_json_path))?; Ok(grammar.name) } @@ -56,7 +54,7 @@ pub fn compile_language_to_wasm(language_dir: &Path, force_docker: bool) -> Resu let user_id_output = Command::new("id") .arg("-u") .output() - .map_err(Error::wrap(|| "Failed to get get current user id"))?; + .with_context(|| "Failed to get get current user id")?; let user_id = String::from_utf8_lossy(&user_id_output.stdout); let user_id = user_id.trim(); command.args(&["--user", user_id]); @@ -65,9 +63,9 @@ pub fn compile_language_to_wasm(language_dir: &Path, force_docker: bool) -> Resu // Run `emcc` in a container using the `emscripten-slim` image command.args(&["emscripten/emsdk", "emcc"]); } else { - return Error::err( - "You must have either emcc or docker on your PATH to run this command".to_string(), - ); + return Err(anyhow!( + "You must have either emcc or docker on your PATH to run this command" + )); } command.args(&[ @@ -107,18 +105,17 @@ pub fn compile_language_to_wasm(language_dir: &Path, force_docker: bool) -> Resu let output = command .output() - .map_err(Error::wrap(|| "Failed to run emcc command"))?; + .with_context(|| "Failed to run emcc command")?; if !output.status.success() { - return Err(Error::from(format!( + return Err(anyhow!( "emcc command failed - {}", String::from_utf8_lossy(&output.stderr) - ))); + )); } // Move the created `.wasm` file into the current working directory. - fs::rename(&language_dir.join(&output_filename), &output_filename).map_err(Error::wrap( - || format!("Couldn't find output file {:?}", output_filename), - ))?; + fs::rename(&language_dir.join(&output_filename), &output_filename) + .with_context(|| format!("Couldn't find output file {:?}", output_filename))?; Ok(()) } diff --git a/cli/src/web_ui.rs b/cli/src/web_ui.rs index 9b29a73a..bbdbd381 100644 --- a/cli/src/web_ui.rs +++ b/cli/src/web_ui.rs @@ -1,5 +1,5 @@ -use super::error::Error; use super::wasm; +use anyhow::Context; use std::env; use std::fs; use std::net::TcpListener; @@ -62,16 +62,16 @@ pub fn serve(grammar_path: &Path, open_in_browser: bool) { let url = format!("127.0.0.1:{}", port); let server = Server::http(&url).expect("Failed to start web server"); let grammar_name = wasm::get_grammar_name(&grammar_path.join("src")) - .map_err(Error::wrap(|| "Failed to get wasm filename")) + .with_context(|| "Failed to get wasm filename") .unwrap(); let wasm_filename = format!("tree-sitter-{}.wasm", grammar_name); let language_wasm = fs::read(grammar_path.join(&wasm_filename)) - .map_err(Error::wrap(|| { + .with_context(|| { format!( "Failed to read {}. Run `tree-sitter build-wasm` first.", wasm_filename ) - })) + }) .unwrap(); if open_in_browser { if let Err(_) = webbrowser::open(&format!("http://127.0.0.1:{}", port)) { diff --git a/highlight/Cargo.toml b/highlight/Cargo.toml index dc3c1221..8d356996 100644 --- a/highlight/Cargo.toml +++ b/highlight/Cargo.toml @@ -18,6 +18,7 @@ crate-type = ["lib", "staticlib"] [dependencies] regex = "1" +thiserror = "1.0" [dependencies.tree-sitter] version = ">= 0.3.7" diff --git a/highlight/src/lib.rs b/highlight/src/lib.rs index 3f75c6dc..a0cfc34e 100644 --- a/highlight/src/lib.rs +++ b/highlight/src/lib.rs @@ -4,6 +4,7 @@ pub use c_lib as c; use std::sync::atomic::{AtomicUsize, Ordering}; use std::{iter, mem, ops, str, usize}; +use thiserror::Error; use tree_sitter::{ Language, LossyUtf8, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError, QueryMatch, Range, Tree, @@ -18,10 +19,13 @@ const BUFFER_LINES_RESERVE_CAPACITY: usize = 1000; pub struct Highlight(pub usize); /// Represents the reason why syntax highlighting failed. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Error, PartialEq, Eq)] pub enum Error { + #[error("Cancelled")] Cancelled, + #[error("Invalid language")] InvalidLanguage, + #[error("Unknown error")] Unknown, } diff --git a/tags/Cargo.toml b/tags/Cargo.toml index c1fb7c77..7fa61238 100644 --- a/tags/Cargo.toml +++ b/tags/Cargo.toml @@ -19,6 +19,7 @@ crate-type = ["lib", "staticlib"] [dependencies] regex = "1" memchr = "2.3" +thiserror = "1.0" [dependencies.tree-sitter] version = ">= 0.17.0" diff --git a/tags/src/lib.rs b/tags/src/lib.rs index 23b877c3..26c6d73d 100644 --- a/tags/src/lib.rs +++ b/tags/src/lib.rs @@ -7,7 +7,8 @@ use std::ffi::{CStr, CString}; use std::ops::Range; use std::os::raw::c_char; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::{char, fmt, mem, str}; +use std::{char, mem, str}; +use thiserror::Error; use tree_sitter::{ Language, LossyUtf8, Parser, Point, Query, QueryCursor, QueryError, QueryPredicateArg, Tree, }; @@ -56,12 +57,17 @@ pub struct Tag { pub syntax_type_id: u32, } -#[derive(Debug, PartialEq)] +#[derive(Debug, Error, PartialEq)] pub enum Error { - Query(QueryError), - Regex(regex::Error), + #[error(transparent)] + Query(#[from] QueryError), + #[error(transparent)] + Regex(#[from] regex::Error), + #[error("Cancelled")] Cancelled, + #[error("Invalid language")] InvalidLanguage, + #[error("Invalid capture @{0}. Expected one of: @definition.*, @reference.*, @doc, @name, @local.(scope|definition|reference).")] InvalidCapture(String), } @@ -562,27 +568,6 @@ impl Tag { } } -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::InvalidCapture(name) => write!(f, "Invalid capture @{}. Expected one of: @definition.*, @reference.*, @doc, @name, @local.(scope|definition|reference).", name), - _ => write!(f, "{:?}", self) - } - } -} - -impl From for Error { - fn from(error: regex::Error) -> Self { - Error::Regex(error) - } -} - -impl From for Error { - fn from(error: QueryError) -> Self { - Error::Query(error) - } -} - fn line_range( text: &[u8], start_byte: usize,