git.delta.rocks / jrsonnet / refs/heads / master

difftreelog

source

crates/jrsonnet-evaluator/src/trace/mod.rs11.3 KiBsourcehistory
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};1314/// The way paths should be displayed15#[derive(Clone, Trace)]16pub enum PathResolver {17	/// Only filename18	FileName,19	/// Absolute path20	Absolute,21	/// Path relative to base directory22	Relative(PathBuf),23}2425impl PathResolver {26	/// Will return `Self::Relative(cwd)`, or `Self::Absolute` on cwd failure27	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}5960/// Implements pretty-printing of traces61#[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}135136/// vanilla-like jsonnet formatting137#[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					// Reverse condition if traceback368					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}