git.delta.rocks / jrsonnet / refs/commits / 2223f9ab687d

difftreelog

source

crates/jrsonnet-evaluator/src/trace/mod.rs10.2 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;11#[cfg(feature = "explaining-traces")]12use jrsonnet_ir::Span;1314use crate::{Error, ResolvePathOwned, error::ErrorKind};1516/// The way paths should be displayed17#[derive(Clone, Trace)]18pub enum PathResolver {19	/// Only filename20	FileName,21	/// Absolute path22	Absolute,23	/// Path relative to base directory24	Relative(PathBuf),25}2627impl PathResolver {28	/// Will return `Self::Relative(cwd)`, or `Self::Absolute` on cwd failure29	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}6162/// Implements pretty-printing of traces63#[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}104105/// vanilla-like jsonnet formatting106#[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					// TODO: Process all trace elements first171					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					// Reverse condition if traceback334					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}