1#[cfg(feature = "explaining-traces")]2use std::cell::RefCell;3use std::{4 any::Any,5 fmt,6 path::{Component, Path, PathBuf},7};89use jrsonnet_gcmodule::Trace;10use jrsonnet_ir::{CodeLocation, Span};1112use crate::{Error, ResolvePathOwned, analyze::DiagLevel, error::ErrorKind};131415#[derive(Clone, Trace)]16pub enum PathResolver {17 18 FileName,19 20 Absolute,21 22 Relative(PathBuf),23}2425impl PathResolver {26 27 pub fn new_cwd_fallback() -> Self {28 std::env::current_dir().map_or(Self::Absolute, Self::Relative)29 }30 pub fn resolve(&self, from: &Path) -> String {31 match self {32 Self::FileName => from33 .file_name()34 .expect("file name exists")35 .to_string_lossy()36 .into_owned(),37 Self::Absolute => from.to_string_lossy().into_owned(),38 Self::Relative(base) => {39 if from.is_relative() {40 return from.to_string_lossy().into_owned();41 }42 let diff = pathdiff::diff_paths(from, base).expect("base is absolute");43 let parents = diff44 .components()45 .take_while(|c| matches!(c, Component::ParentDir))46 .count();47 let base_depth = base48 .components()49 .filter(|c| matches!(c, Component::Normal(_)))50 .count();51 if parents > 0 && parents >= base_depth {52 return from.to_string_lossy().into_owned();53 }54 diff.to_string_lossy().into_owned()55 }56 }57 }58}596061#[allow(clippy::module_name_repetitions)]62pub trait TraceFormat: Trace {63 fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error>;64 fn format(&self, error: &Error) -> Result<String, fmt::Error> {65 let mut out = String::new();66 self.write_trace(&mut out, error)?;67 Ok(out)68 }69 fn as_any(&self) -> &dyn Any;70 fn as_any_mut(&mut self) -> &mut dyn Any;71}7273fn span_label(resolver: &PathResolver, span: &Span) -> String {74 use std::fmt::Write;75 let mut path = span76 .077 .source_path()78 .path()79 .map_or_else(|| span.0.source_path().to_string(), |p| resolver.resolve(p));80 #[expect(clippy::cast_possible_truncation, reason = "code is limited by 4gb")]81 let len = span.0.code().len() as u32;82 let start = span.1.min(len);83 let end = span.2.min(len);84 let (start_loc, end_loc) = if start == end {85 let [loc] = span.0.map_source_locations(&[start]);86 (loc, loc)87 } else {88 let [s, e] = span.0.map_source_locations(&[start, end]);89 (s, e)90 };91 write!(path, ":").unwrap();92 print_code_location(&mut path, &start_loc, &end_loc).unwrap();93 path94}9596#[cfg(feature = "explaining-traces")]97fn span_render_range(span: &Span) -> Option<std::ops::RangeInclusive<usize>> {98 let len = span.0.code().len();99 if len == 0 {100 return None;101 }102 let max = len - 1;103 let r = span.range();104 Some((*r.start()).min(max)..=(*r.end()).min(max))105}106107fn diag_level_label(level: DiagLevel) -> &'static str {108 match level {109 DiagLevel::Error => "error",110 DiagLevel::Warning => "warning",111 }112}113114fn print_code_location(115 out: &mut impl fmt::Write,116 start: &CodeLocation,117 end: &CodeLocation,118) -> Result<(), fmt::Error> {119 let end_col = end.column.saturating_sub(1).max(start.column);120 if start.line == end.line {121 if start.column == end_col {122 write!(out, "{}:{}", start.line, start.column)?;123 } else {124 write!(out, "{}:{}-{}", start.line, start.column, end_col)?;125 }126 } else {127 write!(128 out,129 "{}:{}-{}:{}",130 start.line, start.column, end.line, end_col131 )?;132 }133 Ok(())134}135136137#[derive(Trace)]138pub struct CompactFormat {139 pub resolver: PathResolver,140 pub max_trace: usize,141 pub padding: usize,142}143impl Default for CompactFormat {144 fn default() -> Self {145 Self {146 resolver: PathResolver::Absolute,147 max_trace: 20,148 padding: 4,149 }150 }151}152153impl TraceFormat for CompactFormat {154 fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error> {155 match error.error() {156 ErrorKind::ImportFileNotFound(from, import) => {157 let from = from158 .path()159 .map_or_else(|| from.to_string(), |path| self.resolver.resolve(path));160 let import = match import {161 ResolvePathOwned::Str(s) => s.clone(),162 ResolvePathOwned::Path(path_buf) => self.resolver.resolve(path_buf),163 };164 write!(out, "import file not found {import} from {from}")?;165 }166 ErrorKind::StaticAnalysisError(_) => {167 write!(out, "static analysis errors")?;168 }169 _ => {170 write!(out, "{}", error.error())?;171 }172 }173174 if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {175 let labels: Vec<Option<String>> = diagnostics176 .iter()177 .map(|d| d.span.as_ref().map(|s| span_label(&self.resolver, s)))178 .collect();179 let align = labels.iter().flatten().map(String::len).max().unwrap_or(0);180 let cont_indent = " ".repeat(self.padding + align + 1);181 for (diag, label) in diagnostics.iter().zip(labels.iter()) {182 writeln!(out)?;183 let level = diag_level_label(diag.level);184 let message = diag.message.replace('\n', &format!("\n{cont_indent}"));185 let label = label.as_deref().unwrap_or("");186 write!(187 out,188 "{:<p$}{label:<w$} {level}: {message}",189 "",190 p = self.padding,191 w = align,192 )?;193 }194 }195196 if let ErrorKind::ImportSyntaxError { error, .. } = error.error() {197 writeln!(out)?;198 let label = span_label(&self.resolver, &error.location);199 write!(out, "{:<p$}{label}", "", p = self.padding)?;200 }201 let file_names = error202 .trace()203 .0204 .iter()205 .map(|el| {206 el.location.as_ref().map(|loc| {207 use std::fmt::Write;208 let mut s = span_label(&self.resolver, loc);209 write!(s, ":").unwrap();210 s211 })212 })213 .collect::<Vec<_>>();214 let align = file_names215 .iter()216 .flatten()217 .map(String::len)218 .max()219 .unwrap_or(0);220 for (el, file) in error.trace().0.iter().zip(file_names) {221 writeln!(out)?;222 if let Some(file) = file {223 write!(224 out,225 "{:<p$}{:<w$} {}",226 "",227 file,228 el.desc,229 p = self.padding,230 w = align231 )?;232 } else {233 write!(out, "{:<p$}{}", "", el.desc, p = self.padding)?;234 }235 }236 Ok(())237 }238239 fn as_any(&self) -> &dyn Any {240 self241 }242243 fn as_any_mut(&mut self) -> &mut dyn Any {244 self245 }246}247248#[derive(Trace)]249pub struct JsFormat {250 pub max_trace: usize,251}252impl TraceFormat for JsFormat {253 fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error> {254 write!(out, "{}", error.error())?;255 for item in &error.trace().0 {256 writeln!(out)?;257 let desc = &item.desc;258 if let Some(source) = &item.location {259 let start_end = source.0.map_source_locations(&[source.1, source.2]);260 let resolved_path = source.0.source_path().path().map_or_else(261 || source.0.source_path().to_string(),262 |r| r.display().to_string(),263 );264265 write!(266 out,267 " at {} ({}:{}:{})",268 desc, resolved_path, start_end[0].line, start_end[0].column,269 )?;270 } else {271 write!(out, " during {desc}")?;272 }273 }274 Ok(())275 }276277 fn as_any(&self) -> &dyn Any {278 self279 }280281 fn as_any_mut(&mut self) -> &mut dyn Any {282 self283 }284}285286#[cfg(feature = "explaining-traces")]287#[derive(Trace)]288pub struct HiDocFormat {289 pub resolver: PathResolver,290 pub max_trace: usize,291}292#[cfg(feature = "explaining-traces")]293impl TraceFormat for HiDocFormat {294 #[allow(clippy::too_many_lines)]295 fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error> {296 struct ResetData {297 loc: Span,298 }299 use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};300301 match error.error() {302 ErrorKind::StaticAnalysisError(_) => write!(out, "static analysis errors")?,303 _ => write!(out, "{}", error.error())?,304 }305 if let ErrorKind::ImportSyntaxError { path, error } = error.error() {306 writeln!(out, "\n...at {}", path.source_path())?;307 if let Some(range) = span_render_range(&error.location) {308 let mut builder = SnippetBuilder::new(path.code());309 builder310 .error(Text::fragment("syntax error", Formatting::default()))311 .range(range)312 .build();313 let ansi = source_to_ansi(&builder.build());314 write!(out, "{}", ansi.trim_end())?;315 }316 }317 if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {318 let mut builder: Option<(SnippetBuilder, Span)> = None;319 let flush = |slot: Option<(SnippetBuilder, Span)>,320 out: &mut dyn fmt::Write|321 -> Result<(), fmt::Error> {322 if let Some((b, anchor)) = slot {323 writeln!(out, "\n...at {}", anchor.0.source_path())?;324 let ansi = source_to_ansi(&b.build());325 write!(out, "{}", ansi.trim_end())?;326 }327 Ok(())328 };329 for diag in diagnostics {330 if let Some(span) = &diag.span {331 let Some(range) = span_render_range(span) else {332 continue;333 };334 let same_src = builder.as_ref().is_some_and(|(_, a)| a.0 == span.0);335 if !same_src {336 flush(builder.take(), out)?;337 builder = Some((SnippetBuilder::new(span.0.code()), span.clone()));338 }339 let b = &mut builder.as_mut().unwrap().0;340 let ab = match diag.level {341 DiagLevel::Error => {342 b.error(Text::fragment(diag.message.clone(), Formatting::default()))343 }344 DiagLevel::Warning => {345 b.warning(Text::fragment(diag.message.clone(), Formatting::default()))346 }347 };348 ab.range(range).build();349 } else {350 flush(builder.take(), out)?;351 let prefix = diag_level_label(diag.level);352 write!(out, "\n{prefix}: {}", diag.message)?;353 }354 }355 flush(builder, out)?;356 }357 let trace = &error.trace();358 let snippet_builder: RefCell<Option<SnippetBuilder>> = RefCell::new(None);359 let mut last_location: Option<Span> = None;360 let mut flush_builder = |data: Option<ResetData>| {361 use std::fmt::Write;362 let mut out = String::new();363 let location_changed = if let Some(ResetData { loc }) = &data {364 if last_location.as_ref().map(|l| l.0.code()) != Some(loc.0.code()) {365 true366 } else if let (Some(last), new) = (&last_location, loc) {367 368 last.1 > new.1 || last.2 > new.2369 } else {370 false371 }372 } else {373 true374 };375 if location_changed {376 if let Some(builder) = snippet_builder.borrow_mut().take() {377 let rendered = builder.build();378 let ansi = source_to_ansi(&rendered);379 if let Some(loc) = &last_location {380 let _ = writeln!(out, "...at {}", loc.0.source_path());381 }382 let _ = write!(out, "{}", ansi.trim_end());383 }384 last_location = None;385386 if let Some(ResetData { loc }) = data {387 *snippet_builder.borrow_mut() = Some(SnippetBuilder::new(loc.0.code()));388 last_location = Some(loc);389 }390 }391 if out.is_empty() {392 return None;393 }394 Some(out)395 };396 for item in &trace.0 {397 let desc = &item.desc;398 if let Some(source) = &item.location {399 if let Some(flushed) = flush_builder(Some(ResetData {400 loc: source.clone(),401 })) {402 writeln!(out)?;403 write!(out, "{flushed}")?;404 }405 let mut builder = snippet_builder.borrow_mut();406 let builder = builder.as_mut().unwrap();407 builder408 .note(Text::fragment(desc, Formatting::default()))409 .range(source.1 as usize..=(source.2 as usize - 1).max(source.1 as usize))410 .build();411 } else {412 if let Some(flushed) = flush_builder(None) {413 writeln!(out)?;414 write!(out, "{flushed}")?;415 }416 writeln!(out)?;417 write!(out, " {desc}")?;418 }419 }420421 if let Some(flushed) = flush_builder(None) {422 writeln!(out)?;423 write!(out, "{flushed}")?;424 }425 Ok(())426 }427428 fn as_any(&self) -> &dyn Any {429 self430 }431432 fn as_any_mut(&mut self) -> &mut dyn Any {433 self434 }435}