git.delta.rocks / jrsonnet / refs/commits / 8885f2169203

difftreelog

feat show full error range instead of just start

sonopmszLach2026-04-04parent: #75beb61.patch.diff
in: master

4 files changed

modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -15,14 +15,9 @@
 };
 
 #[derive(Debug, Clone)]
-pub struct SyntaxErrorLocation {
-	pub offset: usize,
-}
-
-#[derive(Debug, Clone)]
 pub struct SyntaxError {
 	pub message: String,
-	pub location: SyntaxErrorLocation,
+	pub location: (u32, u32),
 }
 impl fmt::Display for SyntaxError {
 	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -50,7 +50,7 @@
 #[cfg(not(any(feature = "ir-parser", feature = "peg-parser")))]
 compile_error!("at least one of `ir-parser` or `peg-parser` features must be enabled");
 
-pub use error::{SyntaxError, SyntaxErrorLocation};
+pub use error::SyntaxError;
 pub use obj::*;
 pub use rustc_hash;
 use rustc_hash::FxHashMap;
@@ -87,9 +87,7 @@
 	jrsonnet_ir_parser::parse(code, &jrsonnet_ir_parser::ParserSettings { source }).map_err(|e| {
 		SyntaxError {
 			message: e.message,
-			location: SyntaxErrorLocation {
-				offset: e.location.offset,
-			},
+			location: (e.location.0, e.location.1),
 		}
 	})
 }
@@ -107,7 +105,7 @@
 						"expected {}, got {:?}",
 						e.expected,
 						code.chars()
-							.nth(e.location.offset)
+							.nth(e.location.0)
 							.map_or_else(|| "EOF".into(), |c: char| c.to_string())
 					)
 				},
@@ -115,9 +113,7 @@
 			);
 		SyntaxError {
 			message,
-			location: SyntaxErrorLocation {
-				offset: e.location.offset,
-			},
+			location: e.location,
 		}
 	})
 }
modifiedcrates/jrsonnet-evaluator/src/trace/mod.rsdiffbeforeafterboth
after · crates/jrsonnet-evaluator/src/trace/mod.rs
1#[cfg(feature = "explaining-traces")]2use std::cell::RefCell;3use std::{4	any::Any,5	path::{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};1415/// The way paths should be displayed16#[derive(Clone, Trace)]17pub enum PathResolver {18	/// Only filename19	FileName,20	/// Absolute path21	Absolute,22	/// Path relative to base directory23	Relative(PathBuf),24}2526impl PathResolver {27	/// Will return `Self::Relative(cwd)`, or `Self::Absolute` on cwd failure28	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				pathdiff::diff_paths(from, base)44					.expect("base is absolute")45					.to_string_lossy()46					.into_owned()47			}48		}49	}50}5152/// Implements pretty-printing of traces53#[allow(clippy::module_name_repetitions)]54pub trait TraceFormat: Trace {55	fn write_trace(56		&self,57		out: &mut dyn std::fmt::Write,58		error: &Error,59	) -> Result<(), std::fmt::Error>;60	fn format(&self, error: &Error) -> Result<String, std::fmt::Error> {61		let mut out = String::new();62		self.write_trace(&mut out, error)?;63		Ok(out)64	}65	fn as_any(&self) -> &dyn Any;66	fn as_any_mut(&mut self) -> &mut dyn Any;67}6869fn print_code_location(70	out: &mut impl std::fmt::Write,71	start: &CodeLocation,72	end: &CodeLocation,73) -> Result<(), std::fmt::Error> {74	if start.line == end.line {75		if start.column == end.column {76			write!(out, "{}:{}", start.line, end.column.saturating_sub(1))?;77		} else {78			write!(out, "{}:{}-{}", start.line, start.column - 1, end.column)?;79		}80	} else {81		write!(82			out,83			"{}:{}-{}:{}",84			start.line,85			end.column.saturating_sub(1),86			start.line,87			end.column88		)?;89	}90	Ok(())91}9293/// vanilla-like jsonnet formatting94#[derive(Trace)]95pub struct CompactFormat {96	pub resolver: PathResolver,97	pub max_trace: usize,98	pub padding: usize,99}100impl Default for CompactFormat {101	fn default() -> Self {102		Self {103			resolver: PathResolver::Absolute,104			max_trace: 20,105			padding: 4,106		}107	}108}109110impl TraceFormat for CompactFormat {111	fn write_trace(112		&self,113		out: &mut dyn std::fmt::Write,114		error: &Error,115	) -> Result<(), std::fmt::Error> {116		write!(out, "{}", error.error())?;117		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {118			use std::fmt::Write;119120			writeln!(out)?;121			let mut n = path.source_path().path().map_or_else(122				|| path.source_path().to_string(),123				|r| self.resolver.resolve(r),124			);125			let mut offset = error.location.0 as usize;126			let is_eof = if offset >= path.code().len() {127				offset = path.code().len().saturating_sub(1);128				true129			} else {130				false131			};132			let mut location = path133				.map_source_locations(&[offset as u32])134				.into_iter()135				.next()136				.unwrap();137			if is_eof {138				location.column += 1;139			}140141			write!(n, ":").unwrap();142			print_code_location(&mut n, &location, &location).unwrap();143			write!(out, "{:<p$}{n}", "", p = self.padding)?;144		}145		let file_names = error146			.trace()147			.0148			.iter()149			.map(|el| &el.location)150			.map(|location| {151				use std::fmt::Write;152				#[allow(clippy::option_if_let_else)]153				if let Some(location) = location {154					let mut resolved_path = match location.0.source_path().path() {155						Some(r) => self.resolver.resolve(r),156						None => location.0.source_path().to_string(),157					};158					// TODO: Process all trace elements first159					let location = location.0.map_source_locations(&[location.1, location.2]);160					write!(resolved_path, ":").unwrap();161					print_code_location(&mut resolved_path, &location[0], &location[1]).unwrap();162					write!(resolved_path, ":").unwrap();163					Some(resolved_path)164				} else {165					None166				}167			})168			.collect::<Vec<_>>();169		let align = file_names170			.iter()171			.flatten()172			.map(String::len)173			.max()174			.unwrap_or(0);175		for (el, file) in error.trace().0.iter().zip(file_names) {176			writeln!(out)?;177			if let Some(file) = file {178				write!(179					out,180					"{:<p$}{:<w$} {}",181					"",182					file,183					el.desc,184					p = self.padding,185					w = align186				)?;187			} else {188				write!(out, "{:<p$}{}", "", el.desc, p = self.padding,)?;189			}190		}191		Ok(())192	}193194	fn as_any(&self) -> &dyn Any {195		self196	}197198	fn as_any_mut(&mut self) -> &mut dyn Any {199		self200	}201}202203#[derive(Trace)]204pub struct JsFormat {205	pub max_trace: usize,206}207impl TraceFormat for JsFormat {208	fn write_trace(209		&self,210		out: &mut dyn std::fmt::Write,211		error: &Error,212	) -> Result<(), std::fmt::Error> {213		write!(out, "{}", error.error())?;214		for item in &error.trace().0 {215			writeln!(out)?;216			let desc = &item.desc;217			if let Some(source) = &item.location {218				let start_end = source.0.map_source_locations(&[source.1, source.2]);219				let resolved_path = source.0.source_path().path().map_or_else(220					|| source.0.source_path().to_string(),221					|r| r.display().to_string(),222				);223224				write!(225					out,226					"    at {} ({}:{}:{})",227					desc, resolved_path, start_end[0].line, start_end[0].column,228				)?;229			} else {230				write!(out, "    during {desc}")?;231			}232		}233		Ok(())234	}235236	fn as_any(&self) -> &dyn Any {237		self238	}239240	fn as_any_mut(&mut self) -> &mut dyn Any {241		self242	}243}244245#[cfg(feature = "explaining-traces")]246#[derive(Trace)]247pub struct HiDocFormat {248	pub resolver: PathResolver,249	pub max_trace: usize,250}251#[cfg(feature = "explaining-traces")]252impl TraceFormat for HiDocFormat {253	fn write_trace(254		&self,255		out: &mut dyn std::fmt::Write,256		error: &Error,257	) -> Result<(), std::fmt::Error> {258		struct ResetData {259			loc: Span,260		}261		use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};262263		write!(out, "{}", error.error())?;264		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {265			writeln!(out)?;266			let mut offset = error.location;267			// To inclusive range268			if offset.1 > offset.0 {269				offset.1 -= 1;270			}271			let mut builder = SnippetBuilder::new(path.code());272			builder273				.error(Text::fragment("syntax error", Formatting::default()))274				.range(offset.0 as usize..=offset.1 as usize)275				.build();276			let source = builder.build();277			let ansi = source_to_ansi(&source);278			write!(out, "{ansi}")?;279		}280		let trace = &error.trace();281		let snippet_builder: RefCell<Option<SnippetBuilder>> = RefCell::new(None);282		let mut last_location: Option<Span> = None;283		let mut flush_builder = |data: Option<ResetData>| {284			use std::fmt::Write;285			let mut out = String::new();286			let location_changed = if let Some(ResetData { loc }) = &data {287				if last_location.as_ref().map(|l| l.0.code()) != Some(loc.0.code()) {288					true289				} else if let (Some(last), new) = (&last_location, loc) {290					// Reverse condition if traceback291					last.1 > new.1 || last.2 > new.2292				} else {293					false294				}295			} else {296				true297			};298			if location_changed {299				if let Some(builder) = snippet_builder.borrow_mut().take() {300					let rendered = builder.build();301					let ansi = source_to_ansi(&rendered);302					if let Some(loc) = &last_location {303						let _ = writeln!(out, "...at {}", loc.0.source_path());304					}305					let _ = write!(out, "{}", ansi.trim_end());306				}307				last_location = None;308309				if let Some(ResetData { loc }) = data {310					*snippet_builder.borrow_mut() = Some(SnippetBuilder::new(loc.0.code()));311					last_location = Some(loc);312				}313			}314			if out.is_empty() {315				return None;316			}317			Some(out)318		};319		for item in &trace.0 {320			let desc = &item.desc;321			if let Some(source) = &item.location {322				if let Some(flushed) = flush_builder(Some(ResetData {323					loc: source.clone(),324				})) {325					writeln!(out)?;326					write!(out, "{flushed}")?;327				}328				let mut builder = snippet_builder.borrow_mut();329				let builder = builder.as_mut().unwrap();330				builder331					.note(Text::fragment(desc, Formatting::default()))332					.range(source.1 as usize..=(source.2 as usize - 1).max(source.1 as usize))333					.build();334			} else {335				if let Some(flushed) = flush_builder(None) {336					writeln!(out)?;337					write!(out, "{flushed}")?;338				}339				writeln!(out)?;340				write!(out, "   {desc}")?;341			}342		}343344		if let Some(flushed) = flush_builder(None) {345			writeln!(out)?;346			write!(out, "{flushed}")?;347		}348		Ok(())349	}350351	fn as_any(&self) -> &dyn Any {352		self353	}354355	fn as_any_mut(&mut self) -> &mut dyn Any {356		self357	}358}
modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -7,21 +7,16 @@
 	ImportKind, IndexPart, LiteralType, Member, ObjBody, ObjComp, ObjMembers, Slice, SliceDesc,
 	Source, Span, Spanned, UnaryOpType, Visibility, unescape,
 };
-use jrsonnet_lexer::{Lexeme, Lexer, SyntaxKind, T, collect_lexed_str_block};
+use jrsonnet_lexer::{Lexeme, Lexer, Span as LexSpan, SyntaxKind, T, collect_lexed_str_block};
 
 pub struct ParserSettings {
 	pub source: Source,
-}
-
-#[derive(Debug, Clone)]
-pub struct ParseErrorLocation {
-	pub offset: usize,
 }
 
 #[derive(Debug, Clone)]
 pub struct ParseError {
 	pub message: String,
-	pub location: ParseErrorLocation,
+	pub location: LexSpan,
 }
 
 impl std::fmt::Display for ParseError {
@@ -131,9 +126,7 @@
 
 	fn error(&self, message: String) -> ParseError {
 		ParseError {
-			location: ParseErrorLocation {
-				offset: self.span_start() as usize,
-			},
+			location: self.lexemes[self.offset].range,
 			message,
 		}
 	}
@@ -143,9 +136,6 @@
 			return Err(self.error(format!("expected identifier, got {}", self.current_desc())));
 		}
 		let text = self.text();
-		if is_reserved(text) {
-			return Err(self.error(format!("expected identifier, got reserved word '{text}'")));
-		}
 		let s: IStr = text.into();
 		self.eat_any();
 		Ok(s)
@@ -156,23 +146,6 @@
 	}
 }
 
-fn is_reserved(s: &str) -> bool {
-	matches!(
-		s,
-		"assert"
-			| "else" | "error"
-			| "false" | "for"
-			| "function"
-			| "if" | "import"
-			| "importstr"
-			| "importbin"
-			| "in" | "local"
-			| "null" | "tailstrict"
-			| "then" | "self"
-			| "super" | "true"
-	)
-}
-
 fn spanned<T: Acyclic>(
 	p: &mut Parser<'_>,
 	cb: impl FnOnce(&mut Parser<'_>) -> Result<T>,
@@ -1040,9 +1013,7 @@
 		if let Some(desc) = lexeme.kind.error_description() {
 			return Err(ParseError {
 				message: desc.to_owned(),
-				location: ParseErrorLocation {
-					offset: lexeme.range.0 as usize,
-				},
+				location: lexeme.range,
 			});
 		}
 	}