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;11#[cfg(feature = "explaining-traces")]12use jrsonnet_ir::Span;1314use crate::{Error, ResolvePathOwned, error::ErrorKind};151617#[derive(Clone, Trace)]18pub enum PathResolver {19 20 FileName,21 22 Absolute,23 24 Relative(PathBuf),25}2627impl PathResolver {28 29 pub fn new_cwd_fallback() -> Self {30 std::env::current_dir().map_or(Self::Absolute, Self::Relative)31 }32 pub fn resolve(&self, from: &Path) -> String {33 match self {34 Self::FileName => from35 .file_name()36 .expect("file name exists")37 .to_string_lossy()38 .into_owned(),39 Self::Absolute => from.to_string_lossy().into_owned(),40 Self::Relative(base) => {41 if from.is_relative() {42 return from.to_string_lossy().into_owned();43 }44 let diff = pathdiff::diff_paths(from, base).expect("base is absolute");45 let parents = diff46 .components()47 .take_while(|c| matches!(c, Component::ParentDir))48 .count();49 let base_depth = base50 .components()51 .filter(|c| matches!(c, Component::Normal(_)))52 .count();53 if parents > 0 && parents >= base_depth {54 return from.to_string_lossy().into_owned();55 }56 diff.to_string_lossy().into_owned()57 }58 }59 }60}616263#[allow(clippy::module_name_repetitions)]64pub trait TraceFormat: Trace {65 fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error>;66 fn format(&self, error: &Error) -> Result<String, fmt::Error> {67 let mut out = String::new();68 self.write_trace(&mut out, error)?;69 Ok(out)70 }71 fn as_any(&self) -> &dyn Any;72 fn as_any_mut(&mut self) -> &mut dyn Any;73}7475fn print_code_location(76 out: &mut impl fmt::Write,77 start: &CodeLocation,78 end: &CodeLocation,79) -> Result<(), fmt::Error> {80 if start.line == end.line {81 if start.column == end.column {82 write!(out, "{}:{}", start.line, start.column)?;83 } else {84 write!(85 out,86 "{}:{}-{}",87 start.line,88 start.column,89 end.column.saturating_sub(1)90 )?;91 }92 } else {93 write!(94 out,95 "{}:{}-{}:{}",96 start.line,97 start.column,98 end.line,99 end.column.saturating_sub(1)100 )?;101 }102 Ok(())103}104105106#[derive(Trace)]107pub struct CompactFormat {108 pub resolver: PathResolver,109 pub max_trace: usize,110 pub padding: usize,111}112impl Default for CompactFormat {113 fn default() -> Self {114 Self {115 resolver: PathResolver::Absolute,116 max_trace: 20,117 padding: 4,118 }119 }120}121122impl TraceFormat for CompactFormat {123 fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error> {124 if let ErrorKind::ImportFileNotFound(from, import) = error.error() {125 let from = from126 .path()127 .map_or_else(|| from.to_string(), |path| self.resolver.resolve(path));128 let import = match import {129 ResolvePathOwned::Str(s) => s.clone(),130 ResolvePathOwned::Path(path_buf) => self.resolver.resolve(path_buf),131 };132 write!(out, "import file not found {import} from {from}")?;133 } else {134 write!(out, "{}", error.error())?;135 }136137 if let ErrorKind::ImportSyntaxError { path, error } = error.error() {138 use std::fmt::Write;139140 writeln!(out)?;141 let mut n = path.source_path().path().map_or_else(142 || path.source_path().to_string(),143 |r| self.resolver.resolve(r),144 );145 let offset = (error.location.1 as usize).min(path.code().len());146 #[expect(clippy::cast_possible_truncation, reason = "code is limited by 4gb")]147 let location = path148 .map_source_locations(&[offset as u32])149 .into_iter()150 .next()151 .unwrap();152153 write!(n, ":").unwrap();154 print_code_location(&mut n, &location, &location).unwrap();155 write!(out, "{:<p$}{n}", "", p = self.padding)?;156 }157 let file_names = error158 .trace()159 .0160 .iter()161 .map(|el| &el.location)162 .map(|location| {163 use std::fmt::Write;164 #[allow(clippy::option_if_let_else)]165 if let Some(location) = location {166 let mut resolved_path = match location.0.source_path().path() {167 Some(r) => self.resolver.resolve(r),168 None => location.0.source_path().to_string(),169 };170 171 let location = location.0.map_source_locations(&[location.1, location.2]);172 write!(resolved_path, ":").unwrap();173 print_code_location(&mut resolved_path, &location[0], &location[1]).unwrap();174 write!(resolved_path, ":").unwrap();175 Some(resolved_path)176 } else {177 None178 }179 })180 .collect::<Vec<_>>();181 let align = file_names182 .iter()183 .flatten()184 .map(String::len)185 .max()186 .unwrap_or(0);187 for (el, file) in error.trace().0.iter().zip(file_names) {188 writeln!(out)?;189 if let Some(file) = file {190 write!(191 out,192 "{:<p$}{:<w$} {}",193 "",194 file,195 el.desc,196 p = self.padding,197 w = align198 )?;199 } else {200 write!(out, "{:<p$}{}", "", el.desc, p = self.padding)?;201 }202 }203 Ok(())204 }205206 fn as_any(&self) -> &dyn Any {207 self208 }209210 fn as_any_mut(&mut self) -> &mut dyn Any {211 self212 }213}214215#[derive(Trace)]216pub struct JsFormat {217 pub max_trace: usize,218}219impl TraceFormat for JsFormat {220 fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error> {221 write!(out, "{}", error.error())?;222 for item in &error.trace().0 {223 writeln!(out)?;224 let desc = &item.desc;225 if let Some(source) = &item.location {226 let start_end = source.0.map_source_locations(&[source.1, source.2]);227 let resolved_path = source.0.source_path().path().map_or_else(228 || source.0.source_path().to_string(),229 |r| r.display().to_string(),230 );231232 write!(233 out,234 " at {} ({}:{}:{})",235 desc, resolved_path, start_end[0].line, start_end[0].column,236 )?;237 } else {238 write!(out, " during {desc}")?;239 }240 }241 Ok(())242 }243244 fn as_any(&self) -> &dyn Any {245 self246 }247248 fn as_any_mut(&mut self) -> &mut dyn Any {249 self250 }251}252253#[cfg(feature = "explaining-traces")]254#[derive(Trace)]255pub struct HiDocFormat {256 pub resolver: PathResolver,257 pub max_trace: usize,258}259#[cfg(feature = "explaining-traces")]260impl TraceFormat for HiDocFormat {261 #[allow(clippy::too_many_lines)]262 fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error> {263 struct ResetData {264 loc: Span,265 }266 use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};267268 write!(out, "{}", error.error())?;269 if let ErrorKind::ImportSyntaxError { path, error } = error.error() {270 writeln!(out)?;271 let mut builder = SnippetBuilder::new(path.code());272 builder273 .error(Text::fragment("syntax error", Formatting::default()))274 .range(error.location.range())275 .build();276 let source = builder.build();277 let ansi = source_to_ansi(&source);278 write!(out, "{ansi}")?;279 }280 if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {281 use crate::analyze::DiagLevel;282 let mut builder: Option<SnippetBuilder> = None;283 let mut current_src: Option<&str> = None;284 let flush = |builder: Option<SnippetBuilder>,285 out: &mut dyn fmt::Write|286 -> Result<(), fmt::Error> {287 if let Some(b) = builder {288 let ansi = source_to_ansi(&b.build());289 write!(out, "\n{}", ansi.trim_end())?;290 }291 Ok(())292 };293 for diag in diagnostics {294 if let Some(span) = &diag.span {295 let src = span.0.code();296 if current_src != Some(src) {297 flush(builder.take(), out)?;298 builder = Some(SnippetBuilder::new(src));299 current_src = Some(src);300 }301 let b = builder.as_mut().unwrap();302 let ab = match diag.level {303 DiagLevel::Error => {304 b.error(Text::fragment(diag.message.clone(), Formatting::default()))305 }306 DiagLevel::Warning => {307 b.warning(Text::fragment(diag.message.clone(), Formatting::default()))308 }309 };310 ab.range(span.range()).build();311 } else {312 flush(builder.take(), out)?;313 current_src = None;314 let prefix = match diag.level {315 DiagLevel::Error => "error",316 DiagLevel::Warning => "warning",317 };318 write!(out, "\n{prefix}: {}", diag.message)?;319 }320 }321 flush(builder, out)?;322 }323 let trace = &error.trace();324 let snippet_builder: RefCell<Option<SnippetBuilder>> = RefCell::new(None);325 let mut last_location: Option<Span> = None;326 let mut flush_builder = |data: Option<ResetData>| {327 use std::fmt::Write;328 let mut out = String::new();329 let location_changed = if let Some(ResetData { loc }) = &data {330 if last_location.as_ref().map(|l| l.0.code()) != Some(loc.0.code()) {331 true332 } else if let (Some(last), new) = (&last_location, loc) {333 334 last.1 > new.1 || last.2 > new.2335 } else {336 false337 }338 } else {339 true340 };341 if location_changed {342 if let Some(builder) = snippet_builder.borrow_mut().take() {343 let rendered = builder.build();344 let ansi = source_to_ansi(&rendered);345 if let Some(loc) = &last_location {346 let _ = writeln!(out, "...at {}", loc.0.source_path());347 }348 let _ = write!(out, "{}", ansi.trim_end());349 }350 last_location = None;351352 if let Some(ResetData { loc }) = data {353 *snippet_builder.borrow_mut() = Some(SnippetBuilder::new(loc.0.code()));354 last_location = Some(loc);355 }356 }357 if out.is_empty() {358 return None;359 }360 Some(out)361 };362 for item in &trace.0 {363 let desc = &item.desc;364 if let Some(source) = &item.location {365 if let Some(flushed) = flush_builder(Some(ResetData {366 loc: source.clone(),367 })) {368 writeln!(out)?;369 write!(out, "{flushed}")?;370 }371 let mut builder = snippet_builder.borrow_mut();372 let builder = builder.as_mut().unwrap();373 builder374 .note(Text::fragment(desc, Formatting::default()))375 .range(source.1 as usize..=(source.2 as usize - 1).max(source.1 as usize))376 .build();377 } else {378 if let Some(flushed) = flush_builder(None) {379 writeln!(out)?;380 write!(out, "{flushed}")?;381 }382 writeln!(out)?;383 write!(out, " {desc}")?;384 }385 }386387 if let Some(flushed) = flush_builder(None) {388 writeln!(out)?;389 write!(out, "{flushed}")?;390 }391 Ok(())392 }393394 fn as_any(&self) -> &dyn Any {395 self396 }397398 fn as_any_mut(&mut self) -> &mut dyn Any {399 self400 }401}