1#[cfg(feature = "explaining-traces")]2use std::cell::RefCell;3use std::{4 any::Any,5 path::{Component, Path, PathBuf},6};78use jrsonnet_gcmodule::Trace;9use jrsonnet_ir::CodeLocation;10#[cfg(feature = "explaining-traces")]11use jrsonnet_ir::Span;1213use crate::{Error, error::ErrorKind};141516#[derive(Clone, Trace)]17pub enum PathResolver {18 19 FileName,20 21 Absolute,22 23 Relative(PathBuf),24}2526impl PathResolver {27 28 pub fn new_cwd_fallback() -> Self {29 std::env::current_dir().map_or(Self::Absolute, Self::Relative)30 }31 pub fn resolve(&self, from: &Path) -> String {32 match self {33 Self::FileName => from34 .file_name()35 .expect("file name exists")36 .to_string_lossy()37 .into_owned(),38 Self::Absolute => from.to_string_lossy().into_owned(),39 Self::Relative(base) => {40 if from.is_relative() {41 return from.to_string_lossy().into_owned();42 }43 let diff = pathdiff::diff_paths(from, base).expect("base is absolute");44 let parents = diff45 .components()46 .take_while(|c| matches!(c, Component::ParentDir))47 .count();48 let base_depth = base49 .components()50 .filter(|c| matches!(c, Component::Normal(_)))51 .count();52 if parents > 0 && parents >= base_depth {53 return from.to_string_lossy().into_owned();54 }55 diff.to_string_lossy().into_owned()56 }57 }58 }59}606162#[allow(clippy::module_name_repetitions)]63pub trait TraceFormat: Trace {64 fn write_trace(65 &self,66 out: &mut dyn std::fmt::Write,67 error: &Error,68 ) -> Result<(), std::fmt::Error>;69 fn format(&self, error: &Error) -> Result<String, std::fmt::Error> {70 let mut out = String::new();71 self.write_trace(&mut out, error)?;72 Ok(out)73 }74 fn as_any(&self) -> &dyn Any;75 fn as_any_mut(&mut self) -> &mut dyn Any;76}7778fn print_code_location(79 out: &mut impl std::fmt::Write,80 start: &CodeLocation,81 end: &CodeLocation,82) -> Result<(), std::fmt::Error> {83 if start.line == end.line {84 if start.column == end.column {85 write!(out, "{}:{}", start.line, start.column)?;86 } else {87 write!(88 out,89 "{}:{}-{}",90 start.line,91 start.column,92 end.column.saturating_sub(1)93 )?;94 }95 } else {96 write!(97 out,98 "{}:{}-{}:{}",99 start.line,100 start.column,101 end.line,102 end.column.saturating_sub(1)103 )?;104 }105 Ok(())106}107108109#[derive(Trace)]110pub struct CompactFormat {111 pub resolver: PathResolver,112 pub max_trace: usize,113 pub padding: usize,114}115impl Default for CompactFormat {116 fn default() -> Self {117 Self {118 resolver: PathResolver::Absolute,119 max_trace: 20,120 padding: 4,121 }122 }123}124125impl TraceFormat for CompactFormat {126 fn write_trace(127 &self,128 out: &mut dyn std::fmt::Write,129 error: &Error,130 ) -> Result<(), std::fmt::Error> {131 write!(out, "{}", error.error())?;132 if let ErrorKind::ImportSyntaxError { path, error } = error.error() {133 use std::fmt::Write;134135 writeln!(out)?;136 let mut n = path.source_path().path().map_or_else(137 || path.source_path().to_string(),138 |r| self.resolver.resolve(r),139 );140 let offset = (error.location.1 as usize).min(path.code().len());141 #[expect(clippy::cast_possible_truncation, reason = "code is limited by 4gb")]142 let location = path143 .map_source_locations(&[offset as u32])144 .into_iter()145 .next()146 .unwrap();147148 write!(n, ":").unwrap();149 print_code_location(&mut n, &location, &location).unwrap();150 write!(out, "{:<p$}{n}", "", p = self.padding)?;151 }152 let file_names = error153 .trace()154 .0155 .iter()156 .map(|el| &el.location)157 .map(|location| {158 use std::fmt::Write;159 #[allow(clippy::option_if_let_else)]160 if let Some(location) = location {161 let mut resolved_path = match location.0.source_path().path() {162 Some(r) => self.resolver.resolve(r),163 None => location.0.source_path().to_string(),164 };165 166 let location = location.0.map_source_locations(&[location.1, location.2]);167 write!(resolved_path, ":").unwrap();168 print_code_location(&mut resolved_path, &location[0], &location[1]).unwrap();169 write!(resolved_path, ":").unwrap();170 Some(resolved_path)171 } else {172 None173 }174 })175 .collect::<Vec<_>>();176 let align = file_names177 .iter()178 .flatten()179 .map(String::len)180 .max()181 .unwrap_or(0);182 for (el, file) in error.trace().0.iter().zip(file_names) {183 writeln!(out)?;184 if let Some(file) = file {185 write!(186 out,187 "{:<p$}{:<w$} {}",188 "",189 file,190 el.desc,191 p = self.padding,192 w = align193 )?;194 } else {195 write!(out, "{:<p$}{}", "", el.desc, p = self.padding,)?;196 }197 }198 Ok(())199 }200201 fn as_any(&self) -> &dyn Any {202 self203 }204205 fn as_any_mut(&mut self) -> &mut dyn Any {206 self207 }208}209210#[derive(Trace)]211pub struct JsFormat {212 pub max_trace: usize,213}214impl TraceFormat for JsFormat {215 fn write_trace(216 &self,217 out: &mut dyn std::fmt::Write,218 error: &Error,219 ) -> Result<(), std::fmt::Error> {220 write!(out, "{}", error.error())?;221 for item in &error.trace().0 {222 writeln!(out)?;223 let desc = &item.desc;224 if let Some(source) = &item.location {225 let start_end = source.0.map_source_locations(&[source.1, source.2]);226 let resolved_path = source.0.source_path().path().map_or_else(227 || source.0.source_path().to_string(),228 |r| r.display().to_string(),229 );230231 write!(232 out,233 " at {} ({}:{}:{})",234 desc, resolved_path, start_end[0].line, start_end[0].column,235 )?;236 } else {237 write!(out, " during {desc}")?;238 }239 }240 Ok(())241 }242243 fn as_any(&self) -> &dyn Any {244 self245 }246247 fn as_any_mut(&mut self) -> &mut dyn Any {248 self249 }250}251252#[cfg(feature = "explaining-traces")]253#[derive(Trace)]254pub struct HiDocFormat {255 pub resolver: PathResolver,256 pub max_trace: usize,257}258#[cfg(feature = "explaining-traces")]259impl TraceFormat for HiDocFormat {260 fn write_trace(261 &self,262 out: &mut dyn std::fmt::Write,263 error: &Error,264 ) -> Result<(), std::fmt::Error> {265 struct ResetData {266 loc: Span,267 }268 use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};269270 write!(out, "{}", error.error())?;271 if let ErrorKind::ImportSyntaxError { path, error } = error.error() {272 writeln!(out)?;273 let mut builder = SnippetBuilder::new(path.code());274 builder275 .error(Text::fragment("syntax error", Formatting::default()))276 .range(error.location.range())277 .build();278 let source = builder.build();279 let ansi = source_to_ansi(&source);280 write!(out, "{ansi}")?;281 }282 if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {283 use crate::analyze::DiagLevel;284 let mut builder: Option<SnippetBuilder> = None;285 let mut current_src: Option<&str> = None;286 let flush = |builder: Option<SnippetBuilder>,287 out: &mut dyn std::fmt::Write|288 -> Result<(), std::fmt::Error> {289 if let Some(b) = builder {290 let ansi = source_to_ansi(&b.build());291 write!(out, "\n{}", ansi.trim_end())?;292 }293 Ok(())294 };295 for diag in diagnostics {296 if let Some(span) = &diag.span {297 let src = span.0.code();298 if current_src != Some(src) {299 flush(builder.take(), out)?;300 builder = Some(SnippetBuilder::new(src));301 current_src = Some(src);302 }303 let b = builder.as_mut().unwrap();304 let ab = match diag.level {305 DiagLevel::Error => {306 b.error(Text::fragment(diag.message.clone(), Formatting::default()))307 }308 DiagLevel::Warning => {309 b.warning(Text::fragment(diag.message.clone(), Formatting::default()))310 }311 };312 ab.range(span.range()).build();313 } else {314 flush(builder.take(), out)?;315 current_src = None;316 let prefix = match diag.level {317 DiagLevel::Error => "error",318 DiagLevel::Warning => "warning",319 };320 write!(out, "\n{prefix}: {}", diag.message)?;321 }322 }323 flush(builder, out)?;324 }325 let trace = &error.trace();326 let snippet_builder: RefCell<Option<SnippetBuilder>> = RefCell::new(None);327 let mut last_location: Option<Span> = None;328 let mut flush_builder = |data: Option<ResetData>| {329 use std::fmt::Write;330 let mut out = String::new();331 let location_changed = if let Some(ResetData { loc }) = &data {332 if last_location.as_ref().map(|l| l.0.code()) != Some(loc.0.code()) {333 true334 } else if let (Some(last), new) = (&last_location, loc) {335 336 last.1 > new.1 || last.2 > new.2337 } else {338 false339 }340 } else {341 true342 };343 if location_changed {344 if let Some(builder) = snippet_builder.borrow_mut().take() {345 let rendered = builder.build();346 let ansi = source_to_ansi(&rendered);347 if let Some(loc) = &last_location {348 let _ = writeln!(out, "...at {}", loc.0.source_path());349 }350 let _ = write!(out, "{}", ansi.trim_end());351 }352 last_location = None;353354 if let Some(ResetData { loc }) = data {355 *snippet_builder.borrow_mut() = Some(SnippetBuilder::new(loc.0.code()));356 last_location = Some(loc);357 }358 }359 if out.is_empty() {360 return None;361 }362 Some(out)363 };364 for item in &trace.0 {365 let desc = &item.desc;366 if let Some(source) = &item.location {367 if let Some(flushed) = flush_builder(Some(ResetData {368 loc: source.clone(),369 })) {370 writeln!(out)?;371 write!(out, "{flushed}")?;372 }373 let mut builder = snippet_builder.borrow_mut();374 let builder = builder.as_mut().unwrap();375 builder376 .note(Text::fragment(desc, Formatting::default()))377 .range(source.1 as usize..=(source.2 as usize - 1).max(source.1 as usize))378 .build();379 } else {380 if let Some(flushed) = flush_builder(None) {381 writeln!(out)?;382 write!(out, "{flushed}")?;383 }384 writeln!(out)?;385 write!(out, " {desc}")?;386 }387 }388389 if let Some(flushed) = flush_builder(None) {390 writeln!(out)?;391 write!(out, "{flushed}")?;392 }393 Ok(())394 }395396 fn as_any(&self) -> &dyn Any {397 self398 }399400 fn as_any_mut(&mut self) -> &mut dyn Any {401 self402 }403}