--- a/cmds/jrsonnet/Cargo.toml +++ b/cmds/jrsonnet/Cargo.toml @@ -11,11 +11,9 @@ [dependencies] jrsonnet-evaluator = { path = "../../crates/jrsonnet-evaluator", version = "1.0.0" } jrsonnet-parser = { path = "../../crates/jrsonnet-parser", version = "1.0.0" } -annotate-snippets = "0.8.0" +jrsonnet-trace = { path = "../../crates/jrsonnet-trace", version = "1.0.0" } # TODO: Fix mimalloc compile errors, and use them mimallocator = "0.1.3" [dependencies.clap] version = "3.0.0-beta.1" -default-features = false -features = ["std", "derive"] --- a/cmds/jrsonnet/src/main.rs +++ b/cmds/jrsonnet/src/main.rs @@ -1,6 +1,7 @@ use clap::Clap; -use jrsonnet_evaluator::{trace::CodeLocation, EvaluationState, LocError, StackTrace, Val}; +use jrsonnet_evaluator::Val; use jrsonnet_parser::{el, Arg, ArgsDesc, Expr, LocExpr, ParserSettings}; +use jrsonnet_trace::{CompactFormat, ExplainingFormat, PathResolver, TraceFormat}; use std::env::current_dir; use std::{collections::HashMap, path::PathBuf, rc::Rc, str::FromStr}; @@ -26,18 +27,16 @@ } #[derive(PartialEq)] -enum TraceFormat { - CppJsonnet, - GoJsonnet, - Custom, +enum TraceFormatName { + Compact, + Explaining, } -impl FromStr for TraceFormat { +impl FromStr for TraceFormatName { type Err = &'static str; fn from_str(s: &str) -> Result { Ok(match s { - "cpp" => TraceFormat::CppJsonnet, - "go" => TraceFormat::GoJsonnet, - "default" => TraceFormat::Custom, + "compact" => TraceFormatName::Compact, + "explaining" => TraceFormatName::Explaining, _ => return Err("no such format"), }) } @@ -67,8 +66,8 @@ } #[derive(Clap)] -#[clap(version = "0.1.0", author = "Lach ")] -struct Opts { +#[clap(name = "jrsonnet", version, author)] +pub struct Opts { #[clap(long, about = "Disable global std variable")] no_stdlib: bool, #[clap(long, about = "Add external string", number_of_values = 1)] @@ -81,8 +80,8 @@ tla_code: Vec, #[clap(long, short = "f", default_value = "json", possible_values = &["none", "json", "yaml"], about = "Output format, wraps resulting value to corresponding std.manifest call")] format: Format, - #[clap(long, default_value = "default", possible_values = &["cpp", "go", "default"], about = "Emulated needed stacktrace display")] - trace_format: TraceFormat, + #[clap(long, default_value = "compact", possible_values = &["compact", "explaining"], about = "Choose format of displayed stacktraces")] + trace_format: TraceFormatName, #[clap( long, @@ -149,11 +148,18 @@ for ExtStr { name, value } in opts.ext_code.iter().cloned() { evaluator.add_ext_var(name.into(), evaluator.parse_evaluate_raw(&value).unwrap()); } + + let resolver = PathResolver::Relative(std::env::current_dir().unwrap()); + let trace_format: Box = match opts.trace_format { + TraceFormatName::Compact => Box::new(CompactFormat { resolver }), + TraceFormatName::Explaining => Box::new(ExplainingFormat { resolver }), + }; + let mut input = current_dir().unwrap(); input.push(opts.input.clone()); let code_string = String::from_utf8(std::fs::read(opts.input.clone()).unwrap()).unwrap(); - if let Err(e) = evaluator.add_file(Rc::new(input.clone()), code_string.clone().into()) { - print_syntax_error(e, &input, &code_string); + if let Err(e) = evaluator.add_file(Rc::new(input.clone()), code_string.into()) { + trace_format.print_trace(&evaluator, &e).unwrap(); std::process::exit(1); } let result = evaluator.evaluate_file(&input); @@ -191,16 +197,13 @@ }; let v = evaluator.run_in_state(|| match opts.format { Format::Json => Ok(Val::Str(v.into_json(opts.line_padding)?)), - Format::Yaml => { - evaluator.add_global("__tmp__to_yaml__".into(), v); - evaluator.parse_evaluate_raw("std.manifestYamlDoc(__tmp__to_yaml__, \" \")") - } + Format::Yaml => Ok(Val::Str(v.into_yaml(opts.line_padding)?)), _ => Ok(v), }); let v = match v { Ok(v) => v, Err(err) => { - print_error(&err, evaluator, &opts); + trace_format.print_trace(&evaluator, &err).unwrap(); std::process::exit(1); } }; @@ -213,128 +216,8 @@ } } Err(err) => { - print_error(&err, evaluator, &opts); + trace_format.print_trace(&evaluator, &err).unwrap(); std::process::exit(1); } - } -} - -fn print_error(err: &LocError, evaluator: EvaluationState, opts: &Opts) { - println!("Error: {:?}", err.0); - print_trace(&(err.1), evaluator, &opts); -} - -fn print_syntax_error(error: jrsonnet_parser::ParseError, file: &PathBuf, code: &str) { - use annotate_snippets::{ - display_list::{DisplayList, FormatOptions}, - snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}, - }; - //&("Expected: ".to_owned() + error.expected) - let origin = file.to_str().unwrap(); - let error_message = format!("Expected: {}", error.expected); - let snippet = Snippet { - opt: FormatOptions { - color: true, - ..Default::default() - }, - title: Some(Annotation { - label: Some(&error_message), - id: None, - annotation_type: AnnotationType::Error, - }), - footer: vec![], - slices: vec![Slice { - source: &code, - line_start: 1, - origin: Some(origin), - fold: false, - annotations: vec![SourceAnnotation { - label: "At this position", - annotation_type: AnnotationType::Error, - range: (error.location.offset, error.location.offset + 1), - }], - }], - }; - - let dl = DisplayList::from(snippet); - println!("{}", dl); -} - -fn print_trace(trace: &StackTrace, evaluator: EvaluationState, opts: &Opts) { - use annotate_snippets::{ - display_list::{DisplayList, FormatOptions}, - snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}, - }; - for item in trace.0.iter() { - let desc = &item.1; - let source = item.0.clone(); - let start_end = evaluator.map_source_locations(&source.0, &[source.1, source.2]); - if opts.trace_format == TraceFormat::Custom { - let source_fragment: String = evaluator - .get_source(&source.0) - .unwrap() - .chars() - .skip(start_end[0].line_start_offset) - .take(start_end[1].line_end_offset - start_end[0].line_start_offset) - .collect(); - let snippet = Snippet { - opt: FormatOptions { - color: true, - ..Default::default() - }, - title: Some(Annotation { - label: Some(&item.1), - id: None, - annotation_type: AnnotationType::Error, - }), - footer: vec![], - slices: vec![Slice { - source: &source_fragment, - line_start: start_end[0].line, - origin: Some(&source.0.to_str().unwrap()), - fold: false, - annotations: vec![SourceAnnotation { - label: desc, - annotation_type: AnnotationType::Error, - range: ( - source.1 - start_end[0].line_start_offset, - source.2 - start_end[0].line_start_offset, - ), - }], - }], - }; - - let dl = DisplayList::from(snippet); - println!("{}", dl); - } else { - print_jsonnet_pair( - source.0.to_str().unwrap(), - &start_end[0], - &start_end[1], - opts.trace_format == TraceFormat::GoJsonnet, - ); - } - } -} - -fn print_jsonnet_pair(file: &str, start: &CodeLocation, end: &CodeLocation, is_go: bool) { - if is_go { - print!(" "); - } else { - print!(" "); - } - print!("{}:", file); - if start.line == end.line { - // IDK why, but this is the behavior original jsonnet cpp impl shows - if start.column == end.column || !is_go && start.column + 1 == end.column { - println!("{}:{}", start.line, end.column) - } else { - println!("{}:{}-{}", start.line, start.column, end.column); - } - } else { - println!( - "({}:{})-({}:{})", - start.line, end.column, start.line, end.column - ); } } --- /dev/null +++ b/crates/jrsonnet-trace/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jrsonnet-trace" +version = "1.0.0" +authors = ["Лач "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +jrsonnet-evaluator = { path = "../jrsonnet-evaluator", version = "1.0.0" } +jrsonnet-parser = { path = "../jrsonnet-parser", version = "1.0.0" } +pathdiff = "0.2.0" +annotate-snippets = "0.9.0" --- /dev/null +++ b/crates/jrsonnet-trace/src/lib.rs @@ -0,0 +1,176 @@ +use jrsonnet_evaluator::{ + trace::{offset_to_location, CodeLocation}, + EvaluationState, LocError, +}; +use std::path::PathBuf; + +/// How paths should be displayed +pub enum PathResolver { + /// Only filename will be shown + FileName, + /// Absolute path of file + Absolute, + /// Relative path from base directory + Relative(PathBuf), +} + +impl PathResolver { + pub fn resolve(&self, from: &PathBuf) -> String { + match self { + PathResolver::FileName => from.file_name().unwrap().to_string_lossy().into_owned(), + PathResolver::Absolute => from.to_string_lossy().into_owned(), + PathResolver::Relative(base) => { + if from.is_relative() { + return from.to_string_lossy().into_owned(); + } + pathdiff::diff_paths(from, base) + .unwrap() + .to_string_lossy() + .into_owned() + } + } + } +} + +/// Implements trace to string pretty-printing +pub trait TraceFormat { + fn write_trace( + &self, + out: &mut dyn std::io::Write, + evaluation_state: &EvaluationState, + error: &LocError, + ) -> Result<(), std::io::Error>; + fn print_trace( + &self, + evaluation_state: &EvaluationState, + error: &LocError, + ) -> Result<(), std::io::Error> { + self.write_trace(&mut std::io::stdout(), evaluation_state, error) + } +} + +fn print_code_location( + out: &mut impl std::fmt::Write, + start: &CodeLocation, + end: &CodeLocation, +) -> Result<(), std::fmt::Error> { + if start.line == end.line { + if start.column == end.column { + write!(out, "{}:{}", start.line, end.column - 1)?; + } else { + write!(out, "{}:{}-{}", start.line, start.column - 1, end.column)?; + } + } else { + write!( + out, + "{}:{}-{}:{}", + start.line, + end.column - 1, + start.line, + end.column + )?; + } + Ok(()) +} + +/// vanilla jsonnet like formatting +pub struct CompactFormat { + pub resolver: PathResolver, +} + +impl TraceFormat for CompactFormat { + fn write_trace( + &self, + out: &mut dyn std::io::Write, + evaluation_state: &EvaluationState, + error: &LocError, + ) -> Result<(), std::io::Error> { + writeln!(out, "{:?}", error.0)?; + let file_names = (error.1) + .0 + .iter() + .map(|el| { + let resolved_path = self.resolver.resolve(&(el.0).0); + // TODO: Process all trace elements first + let location = + evaluation_state.map_source_locations(&(el.0).0, &[(el.0).1, (el.0).2]); + (resolved_path, location) + }) + .map(|(mut n, location)| { + use std::fmt::Write; + write!(n, ":").unwrap(); + print_code_location(&mut n, &location[0], &location[1]).unwrap(); + n + }) + .collect::>(); + let align = file_names.iter().map(|e| e.len()).max().unwrap_or(0); + for (i, (el, file)) in (error.1).0.iter().zip(file_names).enumerate() { + if i != 0 { + writeln!(out)?; + } + write!(out, "{: Result<(), std::io::Error> { + use annotate_snippets::{ + display_list::{DisplayList, FormatOptions}, + snippet::{AnnotationType, Slice, Snippet, SourceAnnotation}, + }; + writeln!(out, "{:?}", error.0)?; + let trace = &error.1; + for item in trace.0.iter() { + let desc = &item.1; + let source = item.0.clone(); + let start_end = evaluation_state.map_source_locations(&source.0, &[source.1, source.2]); + + let source_fragment: String = evaluation_state + .get_source(&source.0) + .unwrap() + .chars() + .skip(start_end[0].line_start_offset) + .take(start_end[1].line_end_offset - start_end[0].line_start_offset) + .collect(); + + let origin = self.resolver.resolve(&source.0); + let snippet = Snippet { + opt: FormatOptions { + color: true, + ..Default::default() + }, + title: None, + footer: vec![], + slices: vec![Slice { + source: &source_fragment, + line_start: start_end[0].line, + origin: Some(&origin), + fold: false, + annotations: vec![SourceAnnotation { + label: desc, + annotation_type: AnnotationType::Error, + range: ( + source.1 - start_end[0].line_start_offset, + source.2 - start_end[0].line_start_offset, + ), + }], + }], + }; + + let dl = DisplayList::from(snippet); + writeln!(out, "{}", dl)?; + } + Ok(()) + } +}