git.delta.rocks / jrsonnet / refs/commits / 40875769025f

difftreelog

refactor better static analysis error display

uklooykrYaroslav Bolyukin2026-05-07parent: #2223f9a.patch.diff
in: master

67 files changed

modifiedcrates/jrsonnet-evaluator/src/analyze.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/analyze.rs
+++ b/crates/jrsonnet-evaluator/src/analyze.rs
@@ -472,7 +472,7 @@
 		match bind {
 			BindSpec::Field { into, .. } => self.alloc_destruct(into),
 			BindSpec::Function { name, .. } => {
-				let (_, id) = self.define_local(name.clone(), None)?;
+				let (_, id) = self.define_local(name.value.clone(), Some(name.span.clone()))?;
 				Some(LDestruct::Full(id))
 			}
 		}
@@ -1337,18 +1337,18 @@
 	stack: &mut AnalysisStack,
 	taint: &mut AnalysisResult,
 ) -> LExpr {
-	if let Expr::Function(params, body) = expr {
-		return analyze_function(Some(name), params, body, stack, taint);
+	if let Expr::Function(span, params, body) = expr {
+		return analyze_function(Some(name), &span, &params, body, stack, taint);
 	}
 	analyze(expr, stack, taint)
 }
 #[allow(clippy::too_many_lines)]
 pub fn analyze(expr: &Expr, stack: &mut AnalysisStack, taint: &mut AnalysisResult) -> LExpr {
 	match expr {
-		Expr::Literal(l) => match l {
+		Expr::Literal(span, l) => match l {
 			LiteralType::This => stack.use_this(taint).map_or_else(
 				|| {
-					stack.report_error("`self` used outside of object", None);
+					stack.report_error("`self` used outside of object", Some(span.clone()));
 					LExpr::BadLocal("self")
 				},
 				LExpr::Slot,
@@ -1357,13 +1357,13 @@
 				if stack.use_super(taint).is_some() {
 					LExpr::Super
 				} else {
-					stack.report_error("`super` used outside of object", None);
+					stack.report_error("`super` used outside of object", Some(span.clone()));
 					LExpr::BadLocal("super")
 				}
 			}
 			LiteralType::Dollar => stack.use_dollar(taint).map_or_else(
 				|| {
-					stack.report_error("`$` used outside of object", None);
+					stack.report_error("`$` used outside of object", Some(span.clone()));
 					LExpr::BadLocal("$")
 				},
 				LExpr::Slot,
@@ -1475,7 +1475,9 @@
 				parts: parts_l,
 			}
 		}
-		Expr::Function(params, body) => analyze_function(None, params, body, stack, taint),
+		Expr::Function(span, params, body) => {
+			analyze_function(None, span, params, body, stack, taint)
+		}
 		Expr::IfElse(ifelse) => {
 			let IfElse {
 				cond,
@@ -1541,15 +1543,22 @@
 ) -> LExpr {
 	match bind {
 		BindSpec::Field {
-			value: Expr::Function(params, value),
+			value: Expr::Function(span, params, value),
 			into: Destruct::Full(name),
-		} => analyze_function(Some(name.value.clone()), params, value, stack, taint),
+		} => analyze_function(Some(name.value.clone()), &span, params, value, stack, taint),
 		BindSpec::Field { value, .. } => analyze(value, stack, taint),
 		BindSpec::Function {
 			params,
 			value,
 			name,
-		} => analyze_function(Some(name.clone()), params, value, stack, taint),
+		} => analyze_function(
+			Some(name.value.clone()),
+			&name.span,
+			params,
+			value,
+			stack,
+			taint,
+		),
 	}
 }
 
@@ -1595,6 +1604,7 @@
 
 fn analyze_function(
 	name: Option<IStr>,
+	span: &Span,
 	params: &ExprParams,
 	body: &Expr,
 	stack: &mut AnalysisStack,
@@ -1648,15 +1658,15 @@
 
 	// function(x) x is an identity function
 	if l_params.len() == 1 && l_params[0].default.is_none() {
-		stack.report_warning(
-			"do not define identity functions manually, use std.id instead",
-			None,
-		);
 		#[allow(irrefutable_let_patterns, reason = "refutable with exp-destruct")]
 		if let LDestruct::Full(param_slot) = &l_params[0].destruct
 			&& let LExpr::Slot(LSlot::Local(s)) = &body_expr
 			&& s == param_slot
 		{
+			stack.report_warning(
+				"do not define identity functions manually, use std.id instead",
+				Some(span.clone()),
+			);
 			return LExpr::IdentityFunction {};
 		}
 	}
@@ -1727,7 +1737,14 @@
 				for (f, name) in fields.iter().zip(field_names) {
 					let value = stack.in_using_closure(|stack| {
 						if let Some(params) = &f.params {
-							analyze_function(name.function_name(), params, &f.value, stack, taint)
+							analyze_function(
+								name.function_name(),
+								&f.name.span,
+								params,
+								&f.value,
+								stack,
+								taint,
+							)
 						} else {
 							analyze(&f.value, stack, taint)
 						}
@@ -1771,7 +1788,14 @@
 			process_local_frame(&comp.locals, stack, taint, |stack, taint| {
 				let value = stack.in_using_closure(|stack| {
 					if let Some(params) = &comp.field.params {
-						analyze_function(None, params, &comp.field.value, stack, taint)
+						analyze_function(
+							None,
+							&comp.field.name.span,
+							params,
+							&comp.field.value,
+							stack,
+							taint,
+						)
 					} else {
 						analyze(&comp.field.value, stack, taint)
 					}
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	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}
modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -77,6 +77,13 @@
 		self.offset += 1;
 	}
 
+	fn eat_any_spanned(&mut self) -> Span {
+		let start = self.span_start();
+		self.eat_any();
+		let end = self.span_end();
+		Span(self.source.clone(), start, end)
+	}
+
 	fn at_eof(&self) -> bool {
 		self.offset >= self.lexemes.len()
 	}
@@ -114,6 +121,12 @@
 		self.eat_any();
 		Ok(())
 	}
+	fn eat_spanned(&mut self, t: SyntaxKind) -> Result<Span> {
+		let start = self.span_start();
+		self.eat(t)?;
+		let end = self.span_end();
+		Ok(Span(self.source.clone(), start, end))
+	}
 
 	fn span_start(&self) -> u32 {
 		if self.at_eof() {
@@ -234,7 +247,7 @@
 	Ok(IStr::from(text))
 }
 
-fn literal(p: &mut Parser<'_>) -> Option<LiteralType> {
+fn literal(p: &mut Parser<'_>) -> Option<(Span, LiteralType)> {
 	let t = match p.peek() {
 		T![self] => LiteralType::This,
 		T![super] => LiteralType::Super,
@@ -244,8 +257,7 @@
 		T![false] => LiteralType::False,
 		_ => return None,
 	};
-	p.eat_any();
-	Some(t)
+	Some((p.eat_any_spanned(), t))
 }
 
 fn assert_stmt(p: &mut Parser<'_>) -> Result<AssertStmt> {
@@ -482,20 +494,20 @@
 			});
 		}
 	}
-	let name_spanned = spanned(p, ident)?;
+	let name = spanned(p, ident)?;
 	if p.try_eat(T!['(']) {
 		let ps = params(p)?;
 		p.eat(T![')'])?;
 		p.eat(T![=])?;
 		Ok(BindSpec::Function {
-			name: name_spanned.value,
+			name,
 			params: ps,
 			value: expr(p)?,
 		})
 	} else {
 		p.eat(T![=])?;
 		Ok(BindSpec::Field {
-			into: Destruct::Full(name_spanned),
+			into: Destruct::Full(name),
 			value: expr(p)?,
 		})
 	}
@@ -676,8 +688,8 @@
 
 #[allow(clippy::too_many_lines)]
 fn expr_basic(p: &mut Parser<'_>) -> Result<Expr> {
-	if let Some(lit) = literal(p) {
-		return Ok(Expr::Literal(lit));
+	if let Some((span, lit)) = literal(p) {
+		return Ok(Expr::Literal(span, lit));
 	}
 
 	match p.peek() {
@@ -755,12 +767,12 @@
 		T![if] => Ok(Expr::IfElse(Box::new(if_else(p)?))),
 
 		T![function] => {
-			p.eat(T![function])?;
+			let span = p.eat_spanned(T![function])?;
 			p.eat(T!['('])?;
 			let ps = params(p)?;
 			p.eat(T![')'])?;
 			let body = expr(p)?;
-			Ok(Expr::Function(ps, Box::new(body)))
+			Ok(Expr::Function(span, ps, Box::new(body)))
 		}
 
 		T![assert] => {
@@ -820,7 +832,7 @@
 	if parts.is_empty() {
 		return;
 	}
-	let old = std::mem::replace(e, Expr::Literal(LiteralType::Null));
+	let old = std::mem::replace(e, Expr::Str(IStr::empty()));
 	*e = Expr::Index {
 		indexable: Box::new(old),
 		parts: std::mem::take(parts),
addedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snap.newdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snap.new
@@ -0,0 +1,81 @@
+---
+source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1110
+expression: "format!(\"{v:#?}\")"
+---
+LocalExpr(
+    [
+        Function {
+            name: "f" from virtual:<test>:6-7,
+            params: ExprParams {
+                exprs: [
+                    ExprParam {
+                        destruct: Full(
+                            "x" from virtual:<test>:8-9,
+                        ),
+                        default: None,
+                    },
+                    ExprParam {
+                        destruct: Full(
+                            "y" from virtual:<test>:11-12,
+                        ),
+                        default: Some(
+                            Num(
+                                1.0,
+                            ),
+                        ),
+                    },
+                ],
+                signature: FunctionSignature(
+                    [
+                        ParamParse {
+                            name: Named(
+                                "x",
+                            ),
+                            default: None,
+                        },
+                        ParamParse {
+                            name: Named(
+                                "y",
+                            ),
+                            default: Exists,
+                        },
+                    ],
+                ),
+                binds_len: 2,
+            },
+            value: BinaryOp(
+                BinaryOp {
+                    lhs: Var(
+                        "x" from virtual:<test>:18-19,
+                    ),
+                    op: Add,
+                    rhs: Var(
+                        "y" from virtual:<test>:22-23,
+                    ),
+                },
+            ),
+        },
+    ],
+    Apply(
+        Var(
+            "f" from virtual:<test>:25-26,
+        ),
+        ArgsDesc {
+            unnamed: [
+                Num(
+                    2.0,
+                ),
+            ],
+            names: [
+                "y",
+            ],
+            values: [
+                Num(
+                    3.0,
+                ),
+            ],
+        } from virtual:<test>:26-34,
+        false,
+    ),
+)
addedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@default_nondefault.jsonnet.snap.newdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@default_nondefault.jsonnet.snap.new
@@ -0,0 +1,56 @@
+---
+source: crates/jrsonnet-ir-parser/src/lib.rs
+assertion_line: 1176
+expression: v
+input_file: crates/jrsonnet-peg-parser/src/tests/default_nondefault.jsonnet
+---
+LocalExpr(
+    [
+        Function {
+            name: "x" from virtual:<test>:6-7,
+            params: ExprParams {
+                exprs: [
+                    ExprParam {
+                        destruct: Full(
+                            "foo" from virtual:<test>:8-11,
+                        ),
+                        default: Some(
+                            Str(
+                                "foo",
+                            ),
+                        ),
+                    },
+                    ExprParam {
+                        destruct: Full(
+                            "bar" from virtual:<test>:21-24,
+                        ),
+                        default: None,
+                    },
+                ],
+                signature: FunctionSignature(
+                    [
+                        ParamParse {
+                            name: Named(
+                                "foo",
+                            ),
+                            default: Exists,
+                        },
+                        ParamParse {
+                            name: Named(
+                                "bar",
+                            ),
+                            default: None,
+                        },
+                    ],
+                ),
+                binds_len: 2,
+            },
+            value: Literal(
+                Null,
+            ),
+        },
+    ],
+    Literal(
+        Null,
+    ),
+)
modifiedcrates/jrsonnet-ir/src/expr.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/expr.rs
+++ b/crates/jrsonnet-ir/src/expr.rs
@@ -288,7 +288,7 @@
 		value: Expr,
 	},
 	Function {
-		name: IStr,
+		name: Spanned<IStr>,
 		params: ExprParams,
 		value: Expr,
 	},
@@ -404,7 +404,7 @@
 /// Syntax base
 #[derive(Debug, PartialEq, Acyclic)]
 pub enum Expr {
-	Literal(LiteralType),
+	Literal(Span, LiteralType),
 
 	/// String value: "hello"
 	Str(IStr),
@@ -454,7 +454,7 @@
 		parts: Vec<IndexPart>,
 	},
 	/// function(x) x
-	Function(ExprParams, Box<Expr>),
+	Function(Span, ExprParams, Box<Expr>),
 	/// if true == false then 1 else 2
 	IfElse(Box<IfElse>),
 	Slice(Box<Slice>),
modifiedcrates/jrsonnet-ir/src/visit.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/visit.rs
+++ b/crates/jrsonnet-ir/src/visit.rs
@@ -178,7 +178,7 @@
 }
 pub fn visit_expr<V: Visitor>(v: &mut V, e: &Expr) {
 	match e {
-		Expr::Literal(_literal_type) => {}
+		Expr::Literal(_span, _literal_type) => {}
 		Expr::Str(_istr) => {}
 		Expr::Num(_num) => {}
 		Expr::Var(_spanned) => {}
@@ -254,7 +254,7 @@
 				v.visit_expr(value);
 			}
 		}
-		Expr::Function(expr_params, expr) => {
+		Expr::Function(_span, expr_params, expr) => {
 			visit_params(v, expr_params);
 			v.visit_expr(expr);
 		}
modifiedcrates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/lib.rs
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -131,7 +131,7 @@
 
 		pub rule bind(s: &ParserSettings) -> BindSpec
 			= into:destruct(s) _ "=" _ value:expr(s) {BindSpec::Field{into, value}}
-			/ name:id() _ "(" _ params:params(s) _ ")" _ "=" _ value:expr(s) {BindSpec::Function{name, params, value}}
+			/ name:spanned(<id()>, s) _ "(" _ params:params(s) _ ")" _ "=" _ value:expr(s) {BindSpec::Function{name, params, value}}
 
 		pub rule assertion(s: &ParserSettings) -> AssertStmt
 			= keyword("assert") _ assertion:spanned(<expr(s)>, s) message:(_ ":" _ e:expr(s) {e})? { AssertStmt{assertion, message} }
@@ -290,14 +290,14 @@
 			}))}
 
 		pub rule literal(s: &ParserSettings) -> Expr
-			= v:(
+			= a:position!() v:(
 				keyword("null") {LiteralType::Null}
 				/ keyword("true") {LiteralType::True}
 				/ keyword("false") {LiteralType::False}
 				/ keyword("self") {LiteralType::This}
 				/ keyword("$") {LiteralType::Dollar}
 				/ keyword("super") {LiteralType::Super}
-			) {Expr::Literal(v)}
+			) b:position!() {Expr::Literal(Span(s.source.clone(), codeidx(a), codeidx(b)), v)}
 
 		rule import_kind() -> ImportKind
 			= keyword("importstr") { ImportKind::Str }
@@ -319,7 +319,7 @@
 			/ local_expr(s)
 			/ if_then_else_expr(s)
 
-			/ keyword("function") _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(params, Box::new(expr))}
+			/ kw:spanned(<keyword("function")>, s) _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(kw.span, params, Box::new(expr))}
 			/ assert:assertion(s) _ ";" _ rest:expr(s) { Expr::AssertExpr(Box::new(AssertExpr{
 				assert, rest
 			})) }
modifiedtests/cpp_test_suite_golden_override/error.03.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.03.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.03.jsonnet.golden
@@ -1,3 +1,3 @@
 runtime error: foo
     error.03.jsonnet:17:21-25: error statement
-    error.03.jsonnet:18:8-8:   field <x> access
\ No newline at end of file
+    error.03.jsonnet:18:8:     field <x> access
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.07.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.07.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.07.jsonnet.golden
@@ -1,4 +1,4 @@
 runtime error: sarcasm
     error.07.jsonnet:18:31-35: error statement
-    error.07.jsonnet:17:33-33: element <3> access
+    error.07.jsonnet:17:33:    element <3> access
     error.07.jsonnet:18:20-53: function <third> call
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.args_commafodder.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.args_commafodder.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.args_commafodder.jsonnet.golden
@@ -1 +1,4 @@
-static analysis errors: undefined local: foo; undefined local: bar; undefined local: qux
\ No newline at end of file
+static analysis errors
+    error.args_commafodder.jsonnet:1:1-3 error: undefined local: foo
+    error.args_commafodder.jsonnet:2:3-5 error: undefined local: bar
+    error.args_commafodder.jsonnet:4:7-9 error: undefined local: qux
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.computed_field_scope.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.computed_field_scope.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.computed_field_scope.jsonnet.golden
@@ -1 +1,3 @@
-static analysis errors: undefined local: x; unused local: x
\ No newline at end of file
+static analysis errors
+    error.computed_field_scope.jsonnet:17:21 error: undefined local: x
+    error.computed_field_scope.jsonnet:17:9  warning: unused local: x
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.field_not_exist.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.field_not_exist.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.field_not_exist.jsonnet.golden
@@ -1,2 +1,2 @@
 no such field: y
-    error.field_not_exist.jsonnet:17:10-10: field <y> access
\ No newline at end of file
+    error.field_not_exist.jsonnet:17:10: field <y> access
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.function_duplicate_param.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.function_duplicate_param.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.function_duplicate_param.jsonnet.golden
@@ -1 +1,3 @@
-static analysis errors: local is already defined in the current frame: x; do not define identity functions manually, use std.id instead
\ No newline at end of file
+static analysis errors
+    error.function_duplicate_param.jsonnet:17:13  error: local is already defined in the current frame: x
+    error.function_duplicate_param.jsonnet:17:1-8 warning: do not define identity functions manually, use std.id instead
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.import_static-check-failure.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.import_static-check-failure.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.import_static-check-failure.jsonnet.golden
@@ -1,2 +1,3 @@
-static analysis errors: undefined local: x
+static analysis errors
+    lib/static_check_failure.jsonnet:2:1 error: undefined local: x
     error.import_static-check-failure.jsonnet:1:1-6: import
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.import_syntax-error.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.import_syntax-error.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.import_syntax-error.jsonnet.golden
@@ -1,4 +1,4 @@
 syntax error: unterminated double-quoted string
-    lib/syntax_error.jsonnet:1:1
-    lib/syntax_error.jsonnet:1:1-2:0:        parse imported
+    lib/syntax_error.jsonnet:1:1-2:1
+    lib/syntax_error.jsonnet:1:1-2:1:        parse imported
     error.import_syntax-error.jsonnet:1:1-6: import
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.overflow.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.overflow.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.overflow.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: invalid number value: non-finite
-    error.overflow.jsonnet:17:1
+    error.overflow.jsonnet:17:1-5
     error.overflow.jsonnet:17:1-5: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.overflow3.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.overflow3.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.overflow3.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: invalid number value: non-finite
-    error.overflow3.jsonnet:17:1
+    error.overflow3.jsonnet:17:1-5
     error.overflow3.jsonnet:17:1-5: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.array_comma.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.array_comma.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.array_comma.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected ']', got number "3"
     error.parse.array_comma.jsonnet:17:7
-    error.parse.array_comma.jsonnet:17:7-7: parse imported
\ No newline at end of file
+    error.parse.array_comma.jsonnet:17:7: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.function_arg_positional_after_named.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.function_arg_positional_after_named.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.function_arg_positional_after_named.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: positional argument after named argument
     error.parse.function_arg_positional_after_named.jsonnet:19:10
-    error.parse.function_arg_positional_after_named.jsonnet:19:10-10: parse imported
\ No newline at end of file
+    error.parse.function_arg_positional_after_named.jsonnet:19:10: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.import_not_literal.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.import_not_literal.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.import_not_literal.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: import path must be a string literal
\ No newline at end of file
+static analysis errors
+    error.parse.import_not_literal.jsonnet:17:1-6 error: import path must be a string literal
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.index_unterminated.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.index_unterminated.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.index_unterminated.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unexpected end of file
     error.parse.index_unterminated.jsonnet:17:3
-    error.parse.index_unterminated.jsonnet:17:3-0:0: parse imported
\ No newline at end of file
+    error.parse.index_unterminated.jsonnet:17:3: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.method_plus.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.method_plus.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.method_plus.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected ':', got '+'
     error.parse.method_plus.jsonnet:17:18
-    error.parse.method_plus.jsonnet:17:18-18: parse imported
\ No newline at end of file
+    error.parse.method_plus.jsonnet:17:18: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.object_comma.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.object_comma.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.object_comma.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected '}', got identifier "z"
     error.parse.object_comma.jsonnet:17:11
-    error.parse.object_comma.jsonnet:17:11-11: parse imported
\ No newline at end of file
+    error.parse.object_comma.jsonnet:17:11: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.object_comprehension_local_clash.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.object_comprehension_local_clash.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.object_comprehension_local_clash.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected '}', got ':'
     error.parse.object_comprehension_local_clash.jsonnet:17:29
-    error.parse.object_comprehension_local_clash.jsonnet:17:29-29: parse imported
\ No newline at end of file
+    error.parse.object_comprehension_local_clash.jsonnet:17:29: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.object_local_clash.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.object_local_clash.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.object_local_clash.jsonnet.golden
@@ -1 +1,3 @@
-static analysis errors: local is already defined in the current frame: x; unused local: x
\ No newline at end of file
+static analysis errors
+    error.parse.object_local_clash.jsonnet:17:21 error: local is already defined in the current frame: x
+    error.parse.object_local_clash.jsonnet:17:9  warning: unused local: x
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.self_in_computed_field.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.self_in_computed_field.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.self_in_computed_field.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected field name, got 'self'
-    error.parse.self_in_computed_field.jsonnet:17:15
+    error.parse.self_in_computed_field.jsonnet:17:15-18
     error.parse.self_in_computed_field.jsonnet:17:15-18: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.static_error_bad_number.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.static_error_bad_number.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.static_error_bad_number.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unexpected '.'
     error.parse.static_error_bad_number.jsonnet:17:1
-    error.parse.static_error_bad_number.jsonnet:17:1-1: parse imported
\ No newline at end of file
+    error.parse.static_error_bad_number.jsonnet:17:1: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.string.invalid_escape.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: invalid string escape
-    error.parse.string.invalid_escape.jsonnet:17:1
+    error.parse.string.invalid_escape.jsonnet:17:1-4
     error.parse.string.invalid_escape.jsonnet:17:1-4: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_non_hex.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_non_hex.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_non_hex.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: invalid string escape
-    error.parse.string.invalid_escape_unicode_non_hex.jsonnet:17:1
+    error.parse.string.invalid_escape_unicode_non_hex.jsonnet:17:1-8
     error.parse.string.invalid_escape_unicode_non_hex.jsonnet:17:1-8: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unterminated double-quoted string
-    error.parse.string.invalid_escape_unicode_short.jsonnet:17:1
-    error.parse.string.invalid_escape_unicode_short.jsonnet:17:1-18:0: parse imported
\ No newline at end of file
+    error.parse.string.invalid_escape_unicode_short.jsonnet:17:1-18:1
+    error.parse.string.invalid_escape_unicode_short.jsonnet:17:1-18:1: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short2.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short2.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short2.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: invalid string escape
-    error.parse.string.invalid_escape_unicode_short2.jsonnet:17:1
+    error.parse.string.invalid_escape_unicode_short2.jsonnet:17:1-7
     error.parse.string.invalid_escape_unicode_short2.jsonnet:17:1-7: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short3.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short3.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.string.invalid_escape_unicode_short3.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unterminated double-quoted string
-    error.parse.string.invalid_escape_unicode_short3.jsonnet:17:1
-    error.parse.string.invalid_escape_unicode_short3.jsonnet:17:1-18:0: parse imported
\ No newline at end of file
+    error.parse.string.invalid_escape_unicode_short3.jsonnet:17:1-18:1
+    error.parse.string.invalid_escape_unicode_short3.jsonnet:17:1-18:1: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.string.unfinished.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.string.unfinished.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.string.unfinished.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unterminated double-quoted string
-    error.parse.string.unfinished.jsonnet:17:1
-    error.parse.string.unfinished.jsonnet:17:1-18:0: parse imported
\ No newline at end of file
+    error.parse.string.unfinished.jsonnet:17:1-18:1
+    error.parse.string.unfinished.jsonnet:17:1-18:1: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.string.unfinished2.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.string.unfinished2.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.string.unfinished2.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unterminated single-quoted string
-    error.parse.string.unfinished2.jsonnet:17:1
-    error.parse.string.unfinished2.jsonnet:17:1-18:0: parse imported
\ No newline at end of file
+    error.parse.string.unfinished2.jsonnet:17:1-18:1
+    error.parse.string.unfinished2.jsonnet:17:1-18:1: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.string_multi_no_newline.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.string_multi_no_newline.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.string_multi_no_newline.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: text block requires new line after |||
-    error.parse.string_multi_no_newline.jsonnet:17:1
-    error.parse.string_multi_no_newline.jsonnet:17:1-18:0: parse imported
\ No newline at end of file
+    error.parse.string_multi_no_newline.jsonnet:17:1-18:1
+    error.parse.string_multi_no_newline.jsonnet:17:1-18:1: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.text_block_bad_whitespace.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.text_block_bad_whitespace.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.text_block_bad_whitespace.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unterminated text block
-    error.parse.text_block_bad_whitespace.jsonnet:17:1
+    error.parse.text_block_bad_whitespace.jsonnet:17:1-20:3
     error.parse.text_block_bad_whitespace.jsonnet:17:1-20:3: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.text_block_eof.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.text_block_eof.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.text_block_eof.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unexpected end of text block
-    error.parse.text_block_eof.jsonnet:17:1
+    error.parse.text_block_eof.jsonnet:17:1-18:6
     error.parse.text_block_eof.jsonnet:17:1-18:6: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.text_block_indent_spaces.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.text_block_indent_spaces.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.text_block_indent_spaces.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unterminated text block
-    error.parse.text_block_indent_spaces.jsonnet:17:1
+    error.parse.text_block_indent_spaces.jsonnet:17:1-20:3
     error.parse.text_block_indent_spaces.jsonnet:17:1-20:3: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.parse.text_block_not_terminated.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.parse.text_block_not_terminated.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.parse.text_block_not_terminated.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unexpected end of text block
-    error.parse.text_block_not_terminated.jsonnet:17:1
-    error.parse.text_block_not_terminated.jsonnet:17:1-19:0: parse imported
\ No newline at end of file
+    error.parse.text_block_not_terminated.jsonnet:17:1-19:1
+    error.parse.text_block_not_terminated.jsonnet:17:1-19:1: parse imported
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.static_error_self.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.static_error_self.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.static_error_self.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: `self` used outside of object
\ No newline at end of file
+static analysis errors
+    error.static_error_self.jsonnet:17:2-5 error: `self` used outside of object
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.static_error_super.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.static_error_super.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.static_error_super.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: `super` used outside of object
\ No newline at end of file
+static analysis errors
+    error.static_error_super.jsonnet:17:2-6 error: `super` used outside of object
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.static_error_var_not_exist.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.static_error_var_not_exist.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.static_error_var_not_exist.jsonnet.golden
@@ -1,2 +1,4 @@
-static analysis errors: undefined local: tmp2
-There is a local with similar name present: tmp; unused local: tmp
\ No newline at end of file
+static analysis errors
+    error.static_error_var_not_exist.jsonnet:17:16-19 error: undefined local: tmp2
+                                                      There is a local with similar name present: tmp
+    error.static_error_var_not_exist.jsonnet:17:7-9   warning: unused local: tmp
\ No newline at end of file
modifiedtests/go_testdata_golden_override/arrcomp5.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/arrcomp5.jsonnet.golden
+++ b/tests/go_testdata_golden_override/arrcomp5.jsonnet.golden
@@ -1 +1,3 @@
-static analysis errors: undefined local: x; unused local: x
\ No newline at end of file
+static analysis errors
+    arrcomp5.jsonnet:1:14 error: undefined local: x
+    arrcomp5.jsonnet:1:25 warning: unused local: x
\ No newline at end of file
modifiedtests/go_testdata_golden_override/arrcomp_if4.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/arrcomp_if4.jsonnet.golden
+++ b/tests/go_testdata_golden_override/arrcomp_if4.jsonnet.golden
@@ -1 +1,3 @@
-static analysis errors: undefined local: y; local could be hoisted to an outer scope: y
\ No newline at end of file
+static analysis errors
+    arrcomp_if4.jsonnet:1:33 error: undefined local: y
+    arrcomp_if4.jsonnet:1:44 warning: local could be hoisted to an outer scope: y
\ No newline at end of file
modifiedtests/go_testdata_golden_override/builtinObjectRemoveKey_super_assert.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/builtinObjectRemoveKey_super_assert.jsonnet.golden
+++ b/tests/go_testdata_golden_override/builtinObjectRemoveKey_super_assert.jsonnet.golden
@@ -1,3 +1,3 @@
 no such field: x
-    builtinObjectRemoveKey_super_assert.jsonnet:2:15-15: field <x> access
+    builtinObjectRemoveKey_super_assert.jsonnet:2:15:    field <x> access
     builtinObjectRemoveKey_super_assert.jsonnet:2:10-15: assertion condition
\ No newline at end of file
modifiedtests/go_testdata_golden_override/dollar_bad.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/dollar_bad.jsonnet.golden
+++ b/tests/go_testdata_golden_override/dollar_bad.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: `$` used outside of object
\ No newline at end of file
+static analysis errors
+    dollar_bad.jsonnet:1:1 error: `$` used outside of object
\ No newline at end of file
modifiedtests/go_testdata_golden_override/error_from_array.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/error_from_array.jsonnet.golden
+++ b/tests/go_testdata_golden_override/error_from_array.jsonnet.golden
@@ -1,3 +1,3 @@
 runtime error: xxx
-    error_from_array.jsonnet:1:2-6:   error statement
-    error_from_array.jsonnet:1:15-15: element <0> access
\ No newline at end of file
+    error_from_array.jsonnet:1:2-6: error statement
+    error_from_array.jsonnet:1:15:  element <0> access
\ No newline at end of file
modifiedtests/go_testdata_golden_override/error_hexnumber.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/error_hexnumber.jsonnet.golden
+++ b/tests/go_testdata_golden_override/error_hexnumber.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected end of file, got identifier "x42"
-    error_hexnumber.jsonnet:1:2
+    error_hexnumber.jsonnet:1:2-4
     error_hexnumber.jsonnet:1:2-4: parse imported
\ No newline at end of file
modifiedtests/go_testdata_golden_override/import_computed.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/import_computed.jsonnet.golden
+++ b/tests/go_testdata_golden_override/import_computed.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: import path must be a string literal
\ No newline at end of file
+static analysis errors
+    import_computed.jsonnet:1:1-6 error: import path must be a string literal
\ No newline at end of file
modifiedtests/go_testdata_golden_override/import_syntax_error.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/import_syntax_error.jsonnet.golden
+++ b/tests/go_testdata_golden_override/import_syntax_error.jsonnet.golden
@@ -1,4 +1,4 @@
 syntax error: unexpected end of file
     syntax_error.jsonnet:1:4
-    syntax_error.jsonnet:1:4-0:0:      parse imported
+    syntax_error.jsonnet:1:4:          parse imported
     import_syntax_error.jsonnet:1:1-6: import
\ No newline at end of file
modifiedtests/go_testdata_golden_override/importbin_computed.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/importbin_computed.jsonnet.golden
+++ b/tests/go_testdata_golden_override/importbin_computed.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: import path must be a string literal
\ No newline at end of file
+static analysis errors
+    importbin_computed.jsonnet:1:1-9 error: import path must be a string literal
\ No newline at end of file
modifiedtests/go_testdata_golden_override/importstr_computed.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/importstr_computed.jsonnet.golden
+++ b/tests/go_testdata_golden_override/importstr_computed.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: import path must be a string literal
\ No newline at end of file
+static analysis errors
+    importstr_computed.jsonnet:1:1-9 error: import path must be a string literal
\ No newline at end of file
modifiedtests/go_testdata_golden_override/insuper4.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/insuper4.jsonnet.golden
+++ b/tests/go_testdata_golden_override/insuper4.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: `super` used outside of object
\ No newline at end of file
+static analysis errors
+    insuper4.jsonnet:1:8-12 error: `super` used outside of object
\ No newline at end of file
modifiedtests/go_testdata_golden_override/insuper6.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/insuper6.jsonnet.golden
+++ b/tests/go_testdata_golden_override/insuper6.jsonnet.golden
@@ -1 +1,2 @@
-static analysis errors: undefined local: undeclared
\ No newline at end of file
+static analysis errors
+    insuper6.jsonnet:1:10-19 error: undefined local: undeclared
\ No newline at end of file
modifiedtests/go_testdata_golden_override/number_leading_zero.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/number_leading_zero.jsonnet.golden
+++ b/tests/go_testdata_golden_override/number_leading_zero.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected end of file, got number "42"
-    number_leading_zero.jsonnet:1:2
+    number_leading_zero.jsonnet:1:2-3
     number_leading_zero.jsonnet:1:2-3: parse imported
\ No newline at end of file
modifiedtests/go_testdata_golden_override/object_comp_assert.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/object_comp_assert.jsonnet.golden
+++ b/tests/go_testdata_golden_override/object_comp_assert.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: asserts are unsupported in object comprehension
     object_comp_assert.jsonnet:1:46
-    object_comp_assert.jsonnet:1:46-46: parse imported
\ No newline at end of file
+    object_comp_assert.jsonnet:1:46: parse imported
\ No newline at end of file
modifiedtests/go_testdata_golden_override/object_comp_illegal.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/object_comp_illegal.jsonnet.golden
+++ b/tests/go_testdata_golden_override/object_comp_illegal.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: missing object comprehension field
     object_comp_illegal.jsonnet:1:34
-    object_comp_illegal.jsonnet:1:34-34: parse imported
\ No newline at end of file
+    object_comp_illegal.jsonnet:1:34: parse imported
\ No newline at end of file
modifiedtests/go_testdata_golden_override/object_invariant11.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/object_invariant11.jsonnet.golden
+++ b/tests/go_testdata_golden_override/object_invariant11.jsonnet.golden
@@ -1,3 +1,3 @@
 assert failed: null
     object_invariant11.jsonnet:1:10-14: assertion failure
-    object_invariant11.jsonnet:1:18-18: field <x> access
\ No newline at end of file
+    object_invariant11.jsonnet:1:18:    field <x> access
\ No newline at end of file
modifiedtests/go_testdata_golden_override/static_error_eof.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/static_error_eof.jsonnet.golden
+++ b/tests/go_testdata_golden_override/static_error_eof.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected ';', got end of file
     static_error_eof.jsonnet:1:12
-    static_error_eof.jsonnet:1:12-0:0: parse imported
\ No newline at end of file
+    static_error_eof.jsonnet:1:12: parse imported
\ No newline at end of file
modifiedtests/go_testdata_golden_override/std.codepoint7.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/std.codepoint7.jsonnet.golden
+++ b/tests/go_testdata_golden_override/std.codepoint7.jsonnet.golden
@@ -1,3 +1,3 @@
 type error: expected char, got string
     argument <str> evaluation
-    std.codepoint7.jsonnet:2:14-0:0: function <builtin_codepoint> call
\ No newline at end of file
+    std.codepoint7.jsonnet:2:14-0:14: function <builtin_codepoint> call
\ No newline at end of file
modifiedtests/go_testdata_golden_override/syntax_error.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/syntax_error.jsonnet.golden
+++ b/tests/go_testdata_golden_override/syntax_error.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: unexpected end of file
     syntax_error.jsonnet:1:4
-    syntax_error.jsonnet:1:4-0:0: parse imported
\ No newline at end of file
+    syntax_error.jsonnet:1:4: parse imported
\ No newline at end of file
modifiedtests/go_testdata_golden_override/unfinished_args.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/unfinished_args.jsonnet.golden
+++ b/tests/go_testdata_golden_override/unfinished_args.jsonnet.golden
@@ -1,3 +1,3 @@
 syntax error: expected ')', got end of file
     unfinished_args.jsonnet:1:17
-    unfinished_args.jsonnet:1:17-0:0: parse imported
\ No newline at end of file
+    unfinished_args.jsonnet:1:17: parse imported
\ No newline at end of file
modifiedtests/go_testdata_golden_override/variable_not_visible.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/variable_not_visible.jsonnet.golden
+++ b/tests/go_testdata_golden_override/variable_not_visible.jsonnet.golden
@@ -1 +1,3 @@
-static analysis errors: undefined local: nested; unused local: x1
\ No newline at end of file
+static analysis errors
+    variable_not_visible.jsonnet:1:44-49 error: undefined local: nested
+    variable_not_visible.jsonnet:1:7-8   warning: unused local: x1
\ No newline at end of file
modifiedtests/tests/snapshots/golden__golden@issue172.jsonnet.snapdiffbeforeafterboth
--- a/tests/tests/snapshots/golden__golden@issue172.jsonnet.snap
+++ b/tests/tests/snapshots/golden__golden@issue172.jsonnet.snap
@@ -3,4 +3,5 @@
 expression: result
 input_file: tests/golden/issue172.jsonnet
 ---
-static analysis errors: undefined local: b
+static analysis errors
+    issue172.jsonnet:1:45 error: undefined local: b
modifiedtests/tests/snapshots/golden__golden@missing_binding.jsonnet.snapdiffbeforeafterboth
--- a/tests/tests/snapshots/golden__golden@missing_binding.jsonnet.snap
+++ b/tests/tests/snapshots/golden__golden@missing_binding.jsonnet.snap
@@ -3,5 +3,6 @@
 expression: result
 input_file: tests/golden/missing_binding.jsonnet
 ---
-static analysis errors: undefined local: sta
-There is a local with similar name present: std
+static analysis errors
+    missing_binding.jsonnet:1:1-3 error: undefined local: sta
+                                  There is a local with similar name present: std
modifiedxtask/src/sourcegen/kinds.rsdiffbeforeafterboth
--- a/xtask/src/sourcegen/kinds.rs
+++ b/xtask/src/sourcegen/kinds.rs
@@ -294,7 +294,7 @@
 		lit("WHITESPACE") => r"[ \t\n\r]+";
 		lit("SINGLE_LINE_SLASH_COMMENT") => r"//[^\r\n]*?(\r\n|\n)?";
 		lit("SINGLE_LINE_HASH_COMMENT") => r"#[^\r\n]*?(\r\n|\n)?";
-		lit("MULTI_LINE_COMMENT") => r"/\*([^*]|\*[^/])*\*/";
+		lit("MULTI_LINE_COMMENT") => r"/\*([^*]|\*+[^*/])*\*+/";
 		error("COMMENT_TOO_SHORT", "comment too short") => r"/\*/";
 		error("COMMENT_UNTERMINATED", "unterminated multi-line comment") =>  r"/\*([^*/]|\*[^/])+";
 		error("NO_OPERATOR", "expected operator");