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
--- a/crates/jrsonnet-evaluator/src/trace/mod.rs
+++ b/crates/jrsonnet-evaluator/src/trace/mod.rs
@@ -7,11 +7,9 @@
 };
 
 use jrsonnet_gcmodule::Trace;
-use jrsonnet_ir::CodeLocation;
-#[cfg(feature = "explaining-traces")]
-use jrsonnet_ir::Span;
+use jrsonnet_ir::{CodeLocation, Span};
 
-use crate::{Error, ResolvePathOwned, error::ErrorKind};
+use crate::{Error, ResolvePathOwned, analyze::DiagLevel, error::ErrorKind};
 
 /// The way paths should be displayed
 #[derive(Clone, Trace)]
@@ -72,31 +70,64 @@
 	fn as_any_mut(&mut self) -> &mut dyn Any;
 }
 
+fn span_label(resolver: &PathResolver, span: &Span) -> String {
+	use std::fmt::Write;
+	let mut path = span
+		.0
+		.source_path()
+		.path()
+		.map_or_else(|| span.0.source_path().to_string(), |p| resolver.resolve(p));
+	#[expect(clippy::cast_possible_truncation, reason = "code is limited by 4gb")]
+	let len = span.0.code().len() as u32;
+	let start = span.1.min(len);
+	let end = span.2.min(len);
+	let (start_loc, end_loc) = if start == end {
+		let [loc] = span.0.map_source_locations(&[start]);
+		(loc, loc)
+	} else {
+		let [s, e] = span.0.map_source_locations(&[start, end]);
+		(s, e)
+	};
+	write!(path, ":").unwrap();
+	print_code_location(&mut path, &start_loc, &end_loc).unwrap();
+	path
+}
+
+#[cfg(feature = "explaining-traces")]
+fn span_render_range(span: &Span) -> Option<std::ops::RangeInclusive<usize>> {
+	let len = span.0.code().len();
+	if len == 0 {
+		return None;
+	}
+	let max = len - 1;
+	let r = span.range();
+	Some((*r.start()).min(max)..=(*r.end()).min(max))
+}
+
+fn diag_level_label(level: DiagLevel) -> &'static str {
+	match level {
+		DiagLevel::Error => "error",
+		DiagLevel::Warning => "warning",
+	}
+}
+
 fn print_code_location(
 	out: &mut impl fmt::Write,
 	start: &CodeLocation,
 	end: &CodeLocation,
 ) -> Result<(), fmt::Error> {
+	let end_col = end.column.saturating_sub(1).max(start.column);
 	if start.line == end.line {
-		if start.column == end.column {
+		if start.column == end_col {
 			write!(out, "{}:{}", start.line, start.column)?;
 		} else {
-			write!(
-				out,
-				"{}:{}-{}",
-				start.line,
-				start.column,
-				end.column.saturating_sub(1)
-			)?;
+			write!(out, "{}:{}-{}", start.line, start.column, end_col)?;
 		}
 	} else {
 		write!(
 			out,
 			"{}:{}-{}:{}",
-			start.line,
-			start.column,
-			end.line,
-			end.column.saturating_sub(1)
+			start.line, start.column, end.line, end_col
 		)?;
 	}
 	Ok(())
@@ -121,61 +152,63 @@
 
 impl TraceFormat for CompactFormat {
 	fn write_trace(&self, out: &mut dyn fmt::Write, error: &Error) -> Result<(), fmt::Error> {
-		if let ErrorKind::ImportFileNotFound(from, import) = error.error() {
-			let from = from
-				.path()
-				.map_or_else(|| from.to_string(), |path| self.resolver.resolve(path));
-			let import = match import {
-				ResolvePathOwned::Str(s) => s.clone(),
-				ResolvePathOwned::Path(path_buf) => self.resolver.resolve(path_buf),
-			};
-			write!(out, "import file not found {import} from {from}")?;
-		} else {
-			write!(out, "{}", error.error())?;
+		match error.error() {
+			ErrorKind::ImportFileNotFound(from, import) => {
+				let from = from
+					.path()
+					.map_or_else(|| from.to_string(), |path| self.resolver.resolve(path));
+				let import = match import {
+					ResolvePathOwned::Str(s) => s.clone(),
+					ResolvePathOwned::Path(path_buf) => self.resolver.resolve(path_buf),
+				};
+				write!(out, "import file not found {import} from {from}")?;
+			}
+			ErrorKind::StaticAnalysisError(_) => {
+				write!(out, "static analysis errors")?;
+			}
+			_ => {
+				write!(out, "{}", error.error())?;
+			}
 		}
 
-		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {
-			use std::fmt::Write;
+		if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {
+			let labels: Vec<Option<String>> = diagnostics
+				.iter()
+				.map(|d| d.span.as_ref().map(|s| span_label(&self.resolver, s)))
+				.collect();
+			let align = labels.iter().flatten().map(String::len).max().unwrap_or(0);
+			let cont_indent = " ".repeat(self.padding + align + 1);
+			for (diag, label) in diagnostics.iter().zip(labels.iter()) {
+				writeln!(out)?;
+				let level = diag_level_label(diag.level);
+				let message = diag.message.replace('\n', &format!("\n{cont_indent}"));
+				let label = label.as_deref().unwrap_or("");
+				write!(
+					out,
+					"{:<p$}{label:<w$} {level}: {message}",
+					"",
+					p = self.padding,
+					w = align,
+				)?;
+			}
+		}
 
+		if let ErrorKind::ImportSyntaxError { error, .. } = error.error() {
 			writeln!(out)?;
-			let mut n = path.source_path().path().map_or_else(
-				|| path.source_path().to_string(),
-				|r| self.resolver.resolve(r),
-			);
-			let offset = (error.location.1 as usize).min(path.code().len());
-			#[expect(clippy::cast_possible_truncation, reason = "code is limited by 4gb")]
-			let location = path
-				.map_source_locations(&[offset as u32])
-				.into_iter()
-				.next()
-				.unwrap();
-
-			write!(n, ":").unwrap();
-			print_code_location(&mut n, &location, &location).unwrap();
-			write!(out, "{:<p$}{n}", "", p = self.padding)?;
+			let label = span_label(&self.resolver, &error.location);
+			write!(out, "{:<p$}{label}", "", p = self.padding)?;
 		}
 		let file_names = error
 			.trace()
 			.0
 			.iter()
-			.map(|el| &el.location)
-			.map(|location| {
-				use std::fmt::Write;
-				#[allow(clippy::option_if_let_else)]
-				if let Some(location) = location {
-					let mut resolved_path = match location.0.source_path().path() {
-						Some(r) => self.resolver.resolve(r),
-						None => location.0.source_path().to_string(),
-					};
-					// TODO: Process all trace elements first
-					let location = location.0.map_source_locations(&[location.1, location.2]);
-					write!(resolved_path, ":").unwrap();
-					print_code_location(&mut resolved_path, &location[0], &location[1]).unwrap();
-					write!(resolved_path, ":").unwrap();
-					Some(resolved_path)
-				} else {
-					None
-				}
+			.map(|el| {
+				el.location.as_ref().map(|loc| {
+					use std::fmt::Write;
+					let mut s = span_label(&self.resolver, loc);
+					write!(s, ":").unwrap();
+					s
+				})
 			})
 			.collect::<Vec<_>>();
 		let align = file_names
@@ -265,40 +298,45 @@
 		}
 		use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};
 
-		write!(out, "{}", error.error())?;
+		match error.error() {
+			ErrorKind::StaticAnalysisError(_) => write!(out, "static analysis errors")?,
+			_ => write!(out, "{}", error.error())?,
+		}
 		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {
-			writeln!(out)?;
-			let mut builder = SnippetBuilder::new(path.code());
-			builder
-				.error(Text::fragment("syntax error", Formatting::default()))
-				.range(error.location.range())
-				.build();
-			let source = builder.build();
-			let ansi = source_to_ansi(&source);
-			write!(out, "{ansi}")?;
+			writeln!(out, "\n...at {}", path.source_path())?;
+			if let Some(range) = span_render_range(&error.location) {
+				let mut builder = SnippetBuilder::new(path.code());
+				builder
+					.error(Text::fragment("syntax error", Formatting::default()))
+					.range(range)
+					.build();
+				let ansi = source_to_ansi(&builder.build());
+				write!(out, "{}", ansi.trim_end())?;
+			}
 		}
 		if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {
-			use crate::analyze::DiagLevel;
-			let mut builder: Option<SnippetBuilder> = None;
-			let mut current_src: Option<&str> = None;
-			let flush = |builder: Option<SnippetBuilder>,
+			let mut builder: Option<(SnippetBuilder, Span)> = None;
+			let flush = |slot: Option<(SnippetBuilder, Span)>,
 			             out: &mut dyn fmt::Write|
 			 -> Result<(), fmt::Error> {
-				if let Some(b) = builder {
+				if let Some((b, anchor)) = slot {
+					writeln!(out, "\n...at {}", anchor.0.source_path())?;
 					let ansi = source_to_ansi(&b.build());
-					write!(out, "\n{}", ansi.trim_end())?;
+					write!(out, "{}", ansi.trim_end())?;
 				}
 				Ok(())
 			};
 			for diag in diagnostics {
 				if let Some(span) = &diag.span {
-					let src = span.0.code();
-					if current_src != Some(src) {
+					let Some(range) = span_render_range(span) else {
+						continue;
+					};
+					let same_src = builder.as_ref().is_some_and(|(_, a)| a.0 == span.0);
+					if !same_src {
 						flush(builder.take(), out)?;
-						builder = Some(SnippetBuilder::new(src));
-						current_src = Some(src);
+						builder = Some((SnippetBuilder::new(span.0.code()), span.clone()));
 					}
-					let b = builder.as_mut().unwrap();
+					let b = &mut builder.as_mut().unwrap().0;
 					let ab = match diag.level {
 						DiagLevel::Error => {
 							b.error(Text::fragment(diag.message.clone(), Formatting::default()))
@@ -307,14 +345,10 @@
 							b.warning(Text::fragment(diag.message.clone(), Formatting::default()))
 						}
 					};
-					ab.range(span.range()).build();
+					ab.range(range).build();
 				} else {
 					flush(builder.take(), out)?;
-					current_src = None;
-					let prefix = match diag.level {
-						DiagLevel::Error => "error",
-						DiagLevel::Warning => "warning",
-					};
+					let prefix = diag_level_label(diag.level);
 					write!(out, "\n{prefix}: {}", diag.message)?;
 				}
 			}
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
after · crates/jrsonnet-peg-parser/src/lib.rs
1use jrsonnet_gcmodule::Acyclic;2use jrsonnet_ir::{3	ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BindSpec, CompSpec, Destruct, DestructRest, Expr,4	ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr, IfElse, IfSpecData,5	ImportKind, IndexPart, LiteralType, Member, NumValue, ObjBody, ObjComp, ObjMembers, Slice,6	SliceDesc, Source, Span, Spanned, Visibility, unescape,7};8use peg::parser;910pub struct ParserSettings {11	pub source: Source,12}1314macro_rules! expr_bin {15	($a:ident $op:ident $b:ident) => {16		Expr::BinaryOp(Box::new(BinaryOp {17			lhs: $a,18			op: $op,19			rhs: $b,20		}))21	};22}23macro_rules! expr_un {24	($op:ident $a:ident) => {25		Expr::UnaryOp($op, Box::new($a))26	};27}2829parser! {30	pub grammar jsonnet_parser() for str {31		use peg::ParseLiteral;3233		rule eof() = quiet!{![_]} / expected!("<eof>")34		rule eol() = "\n" / eof()3536		/// Standard C-like comments37		rule comment()38			= "//" (!eol()[_])* eol()39			/ "/*" (!("*/")[_])* "*/"40			/ "#" (!eol()[_])* eol()4142		rule single_whitespace() = quiet!{([' ' | '\r' | '\n' | '\t'] / comment())} / expected!("<whitespace>")43		rule _() = quiet!{([' ' | '\r' | '\n' | '\t']+) / comment()}* / expected!("<whitespace>")4445		/// For comma-delimited elements46		rule comma() = quiet!{_ "," _} / expected!("<comma>")47		rule alpha() -> char = c:$(['_' | 'a'..='z' | 'A'..='Z']) {c.chars().next().unwrap()}48		rule digit() -> char = d:$(['0'..='9']) {d.chars().next().unwrap()}49		rule end_of_ident() = !['0'..='9' | '_' | 'a'..='z' | 'A'..='Z']50		/// Sequence of digits51		rule uint_str() -> &'input str = a:$(digit()+ ("_" digit()+)*) { a }52		/// Number in scientific notation format53		rule number() -> f64 = quiet!{a:$(uint_str() ("." uint_str())? (['e'|'E'] (s:['+'|'-'])? uint_str())?) {? a.replace('_',"").parse().map_err(|_| "<number>") }} / expected!("<number>")5455		/// Reserved word followed by any non-alphanumberic56		rule reserved() = ("assert" / "else" / "error" / "false" / "for" / "function" / "if" / "import" / "importstr" / "importbin" / "in" / "local" / "null" / "tailstrict" / "then" / "self" / "super" / "true") end_of_ident()57		rule id() -> IStr = v:$(quiet!{ !reserved() alpha() (alpha() / digit())*} / expected!("<identifier>")) { v.into() }5859		rule keyword(id: &'static str) -> ()60			= #{|input, pos| input.parse_string_literal(pos, id)} end_of_ident()6162		pub rule param(s: &ParserSettings) -> ExprParam = destruct:destruct(s) default:(_ "=" _ default:expr(s){default})? { ExprParam { destruct, default } }63		pub rule params(s: &ParserSettings) -> ExprParams64			= params:param(s) ** comma() comma()? { ExprParams::new(params) }65			/ { ExprParams::new(Vec::new()) }6667		pub rule arg(s: &ParserSettings) -> (Option<IStr>, Expr)68			= name:(quiet! { (s:id() _ "=" !['='] _ {s})? } / expected!("<argument name>")) expr:expr(s) {(name, expr)}6970		pub rule args(s: &ParserSettings) -> ArgsDesc71			= args:arg(s)**comma() comma()? {?72				let unnamed_count = args.iter().take_while(|(n, _)| n.is_none()).count();73				let mut unnamed = Vec::with_capacity(unnamed_count);74				let mut names = Vec::with_capacity(args.len() - unnamed_count);75				let mut values = Vec::with_capacity(args.len() - unnamed_count);76				let mut named_started = false;77				for (name, value) in args {78					if let Some(name) = name {79						named_started = true;80						names.push(name);81						values.push(value);82					} else {83						if named_started {84							return Err("<named argument>")85						}86						unnamed.push(value);87					}88				}89				Ok(ArgsDesc{unnamed, names, values})90			}9192		pub rule destruct_rest() -> DestructRest93			= "..." into:(_ into:id() {into})? {into.map_or_else(|| DestructRest::Drop, DestructRest::Keep)}94		pub rule destruct_array(s: &ParserSettings) -> Destruct95			= "[" _ start:destruct(s)**comma() rest:(96				comma() _ rest:destruct_rest()? end:(97					comma() end:destruct(s)**comma() (_ comma())? {end}98					/ comma()? {Vec::new()}99				) {(rest, end)}100				/ comma()? {(None, Vec::new())}101			) _ "]" {?102				#[cfg(feature = "exp-destruct")] return Ok(Destruct::Array {103					start,104					rest: rest.0,105					end: rest.1,106				});107				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")108			}109		pub rule destruct_object(s: &ParserSettings) -> Destruct110			= "{" _111				fields:(name:id() into:(_ ":" _ into:destruct(s) {into})? default:(_ "=" _ v:spanned(<expr(s)>, s) {v})? {(name, into, default)})**comma()112				rest:(113					comma() rest:destruct_rest()? {rest}114					/ comma()? {None}115				)116			_ "}" {?117				#[cfg(feature = "exp-destruct")] return Ok(Destruct::Object {118					fields,119					rest,120				});121				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")122			}123		pub rule destruct(s: &ParserSettings) -> Destruct124			= v:spanned(<id()>, s) {Destruct::Full(v)}125			/ "?" {?126				#[cfg(feature = "exp-destruct")] return Ok(Destruct::Skip);127				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")128			}129			/ arr:destruct_array(s) {arr}130			/ obj:destruct_object(s) {obj}131132		pub rule bind(s: &ParserSettings) -> BindSpec133			= into:destruct(s) _ "=" _ value:expr(s) {BindSpec::Field{into, value}}134			/ name:spanned(<id()>, s) _ "(" _ params:params(s) _ ")" _ "=" _ value:expr(s) {BindSpec::Function{name, params, value}}135136		pub rule assertion(s: &ParserSettings) -> AssertStmt137			= keyword("assert") _ assertion:spanned(<expr(s)>, s) message:(_ ":" _ e:expr(s) {e})? { AssertStmt{assertion, message} }138139		pub rule whole_line() -> &'input str140			= str:$((!['\n'][_])* "\n") {str}141		pub rule string_block() -> String142			= "|||" chomped:"-"? (!['\n']single_whitespace())* "\n"143			empty_lines:$(['\n']*)144			prefix:[' ' | '\t']+ first_line:whole_line()145			lines:("\n" {"\n"} / [' ' | '\t']*<{prefix.len()}> s:whole_line() {s})*146			[' ' | '\t']*<, {prefix.len() - 1}> "|||"147			{148				let mut l = empty_lines.to_owned();149				l.push_str(first_line);150				l.extend(lines);151				if chomped.is_some() {152					debug_assert!(l.ends_with('\n'));153					l.truncate(l.len() - 1);154				}155				l156			}157158		rule hex_char()159			= quiet! { ['0'..='9' | 'a'..='f' | 'A'..='F'] } / expected!("<hex char>")160161		rule string_char(c: rule<()>)162			= (!['\\']!c()[_])+163			/ "\\\\"164			/ "\\u" hex_char() hex_char() hex_char() hex_char()165			/ "\\x" hex_char() hex_char()166			/ ['\\'] (quiet! { ['b' | 'f' | 'n' | 'r' | 't' | '"' | '\''] } / expected!("<escape character>"))167		pub rule string() -> String168			= ['"'] str:$(string_char(<"\"">)*) ['"'] {? unescape::unescape(str).ok_or("<escaped string>")}169			/ ['\''] str:$(string_char(<"\'">)*) ['\''] {? unescape::unescape(str).ok_or("<escaped string>")}170			/ quiet!{ "@'" str:$(("''" / (!['\''][_]))*) "'" {str.replace("''", "'")}171			/ "@\"" str:$(("\"\"" / (!['"'][_]))*) "\"" {str.replace("\"\"", "\"")}172			/ string_block() } / expected!("<string>")173174		pub rule field_name(s: &ParserSettings) -> FieldName175			= name:id() {FieldName::Fixed(name)}176			/ name:string() {FieldName::Fixed(name.into())}177			/ "[" _ expr:expr(s) _ "]" {FieldName::Dyn(expr)}178		pub rule visibility() -> Visibility179			= ":::" {Visibility::Unhide}180			/ "::" {Visibility::Hidden}181			/ ":" {Visibility::Normal}182		pub rule field(s: &ParserSettings) -> FieldMember183			= name:spanned(<field_name(s)>, s) _ plus:"+"? _ visibility:visibility() _ value:expr(s) {FieldMember{184				name,185				plus: plus.is_some(),186				params: None,187				visibility,188				value,189			}}190			/ name:spanned(<field_name(s)>, s) _ "(" _ params:params(s) _ ")" _ visibility:visibility() _ value:expr(s) {FieldMember{191				name,192				plus: false,193				params: Some(params),194				visibility,195				value,196			}}197		pub rule obj_local(s: &ParserSettings) -> BindSpec198			= keyword("local") _ bind:bind(s) {bind}199		pub rule member(s: &ParserSettings) -> Member200			= bind:obj_local(s) {Member::BindStmt(bind)}201			/ assertion:assertion(s) {Member::AssertStmt(assertion)}202			/ field:field(s) {Member::Field(field)}203		pub rule objinside(s: &ParserSettings) -> ObjBody204			=  members:(member(s) ** comma()) comma()? _ compspecs:compspecs(s)? {?205				Ok(if let Some(compspecs) = compspecs {206					let mut locals = Vec::new();207					let mut field = None;208					for member in members {209						match member {210							Member::Field(field_member) => if field.replace(field_member).is_some() {211								return Err("<object comprehension can only contain one field>")212							},213							Member::BindStmt(bind_spec) => locals.push(bind_spec),214							Member::AssertStmt(assert_stmt) => return Err("<asserts are unsupported in object comprehension>"),215						}216					}217					ObjBody::ObjComp(ObjComp {218						locals,219						field: Box::new(field.ok_or("<missing object comprehension field>")?),220						compspecs221					})222				} else {223					let mut locals = Vec::new();224					let mut asserts = Vec::new();225					let mut fields = Vec::new();226					for member in members {227						match member {228							Member::Field(field_member) => fields.push(field_member),229							Member::BindStmt(bind_spec) => locals.push(bind_spec),230							Member::AssertStmt(assert_stmt) => asserts.push(assert_stmt),231						}232					}233					ObjBody::MemberList(ObjMembers {234						locals,235						asserts,236						fields237					})238				})239			}240		pub rule ifspec(s: &ParserSettings) -> IfSpecData241			= i:spanned(<keyword("if")>, s) _ cond:expr(s) {IfSpecData { span: i.span, cond }}242		pub rule forspec(s: &ParserSettings) -> ForSpecData243			= keyword("for") _ destruct:destruct(s) _ keyword("in") _ over:expr(s) { ForSpecData { destruct, over } }244		rule ensure_object_iteration()245			= "" {?246				#[cfg(not(feature = "exp-object-iteration"))] return Err("!!!experimental object iteration was not enabled");247				#[cfg(feature = "exp-object-iteration")] Ok(())248			}249		pub rule forobjspec(s: &ParserSettings) -> CompSpec250			= ensure_object_iteration() keyword("for") _ "[" _ key:id() _ "]" _ vis:visibility() _ value:destruct(s) _ keyword("in") _ over:expr(s) {251				#[cfg(feature = "exp-object-iteration")] return CompSpec::ForObjSpec(jrsonnet_ir::ForObjSpecData { key, visibility: vis, value, over });252				#[cfg(not(feature = "exp-object-iteration"))] unreachable!("ensure_object_iteration will fail if feature is not enabled")253			}254		rule compspec(s: &ParserSettings) -> CompSpec255			= i:ifspec(s) { CompSpec::IfSpec(i) } / f:forobjspec(s) { f } / f:forspec(s) {CompSpec::ForSpec(f)}256		pub rule compspecs(s: &ParserSettings) -> Vec<CompSpec>257			= specs:compspec(s) ++ _ {?258				if matches!(specs[0], CompSpec::IfSpec(_)) {259					return Err("<first compspec should be for>")260				}261				Ok(specs)262			}263		pub rule local_expr(s: &ParserSettings) -> Expr264			= keyword("local") _ binds:bind(s) ** comma() (_ ",")? _ ";" _ expr:expr(s) { Expr::LocalExpr(binds, Box::new(expr)) }265		pub rule string_expr(s: &ParserSettings) -> Expr266			= s:string() {Expr::Str(s.into())}267		pub rule obj_expr(s: &ParserSettings) -> Expr268			= "{" _ body:objinside(s) _ "}" {Expr::Obj(body)}269		pub rule array_expr(s: &ParserSettings) -> Expr270			= "[" _ elems:(expr(s) ** comma()) _ comma()? "]" {Expr::Arr(elems)}271		pub rule array_comp_expr(s: &ParserSettings) -> Expr272			= "[" _ expr:expr(s) _ comma()? _ specs:(r: compspecs(s) _ {r}) "]" {273				Expr::ArrComp(Box::new(expr), specs)274			}275		pub rule number_expr(s: &ParserSettings) -> Expr276			= n:number() {? NumValue::new(n).map_or_else(|| Err("!!!numbers are finite"), |n| Ok(Expr::Num(n)))}277278		rule spanned<T: Acyclic>(x: rule<T>, s: &ParserSettings) -> Spanned<T>279			= a:position!() n:x() b:position!() { Spanned::new(n, Span(s.source.clone(), codeidx(a), codeidx(b))) }280281		pub rule var_expr(s: &ParserSettings) -> Expr282			= n:spanned(<id()>, s) { Expr::Var(n) }283		pub rule id_loc(s: &ParserSettings) -> Spanned<Expr>284			= a:position!() n:id() b:position!() { Spanned::new(Expr::Str(n), Span(s.source.clone(), codeidx(a), codeidx(b))) }285		pub rule if_then_else_expr(s: &ParserSettings) -> Expr286			= cond:ifspec(s) _ keyword("then") _ cond_then:expr(s) cond_else:(_ keyword("else") _ e:expr(s) {e})? {Expr::IfElse(Box::new(IfElse{287				cond,288				cond_then,289				cond_else,290			}))}291292		pub rule literal(s: &ParserSettings) -> Expr293			= a:position!() v:(294				keyword("null") {LiteralType::Null}295				/ keyword("true") {LiteralType::True}296				/ keyword("false") {LiteralType::False}297				/ keyword("self") {LiteralType::This}298				/ keyword("$") {LiteralType::Dollar}299				/ keyword("super") {LiteralType::Super}300			) b:position!() {Expr::Literal(Span(s.source.clone(), codeidx(a), codeidx(b)), v)}301302		rule import_kind() -> ImportKind303			= keyword("importstr") { ImportKind::Str }304			/ keyword("importbin") { ImportKind::Bin }305			/ keyword("import") { ImportKind::Normal }306307		pub rule expr_basic(s: &ParserSettings) -> Expr308			= literal(s)309310			/ string_expr(s) / number_expr(s)311			/ array_expr(s)312			/ obj_expr(s)313			/ array_expr(s)314			/ array_comp_expr(s)315316			/ kind:spanned(<import_kind()>, s) _ path:expr(s) {Expr::Import(kind, Box::new(path))}317318			/ var_expr(s)319			/ local_expr(s)320			/ if_then_else_expr(s)321322			/ kw:spanned(<keyword("function")>, s) _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(kw.span, params, Box::new(expr))}323			/ assert:assertion(s) _ ";" _ rest:expr(s) { Expr::AssertExpr(Box::new(AssertExpr{324				assert, rest325			})) }326327			/ err_kw:spanned(<keyword("error")>, s) _ expr:expr(s) { Expr::ErrorStmt(err_kw.span, Box::new(expr)) }328329		rule slice_part(s: &ParserSettings) -> Option<Spanned<Expr>>330			= _ e:(e:spanned(<expr(s)>, s) _{e})? {e}331		pub rule slice_desc(s: &ParserSettings) -> SliceDesc332			= start:slice_part(s) ":" pair:(end:slice_part(s) step:(":" e:slice_part(s){e})? {(end, step.flatten())})? {333				let (end, step) = if let Some((end, step)) = pair {334					(end, step)335				}else{336					(None, None)337				};338339				SliceDesc { start, end, step }340			}341342		rule binop(x: rule<()>) -> ()343			= quiet!{ x() } / expected!("<binary op>")344		rule unaryop(x: rule<()>) -> ()345			= quiet!{ x() } / expected!("<unary op>")346347		rule ensure_null_coaelse()348			= "" {?349				#[cfg(not(feature = "exp-null-coaelse"))] return Err("!!!experimental null coaelscing was not enabled");350				#[cfg(feature = "exp-null-coaelse")] Ok(())351			}352		use jrsonnet_ir::BinaryOpType::*;353		use jrsonnet_ir::UnaryOpType::*;354		rule expr(s: &ParserSettings) -> Expr355			= precedence! {356				a:(@) _ binop(<"||">) _ b:@ {expr_bin!(a Or b)}357				a:(@) _ binop(<"??">) _ ensure_null_coaelse() b:@ {358					#[cfg(feature = "exp-null-coaelse")] return expr_bin!(a NullCoaelse b);359					unreachable!("ensure_null_coaelse will fail if feature is not enabled")360				}361				--362				a:(@) _ binop(<"&&">) _ b:@ {expr_bin!(a And b)}363				--364				a:(@) _ binop(<"|">) _ b:@ {expr_bin!(a BitOr b)}365				--366				a:@ _ binop(<"^">) _ b:(@) {expr_bin!(a BitXor b)}367				--368				a:(@) _ binop(<"&">) _ b:@ {expr_bin!(a BitAnd b)}369				--370				a:(@) _ binop(<"==">) _ b:@ {expr_bin!(a Eq b)}371				a:(@) _ binop(<"!=">) _ b:@ {expr_bin!(a Neq b)}372				--373				a:(@) _ binop(<"<">) _ b:@ {expr_bin!(a Lt b)}374				a:(@) _ binop(<">">) _ b:@ {expr_bin!(a Gt b)}375				a:(@) _ binop(<"<=">) _ b:@ {expr_bin!(a Lte b)}376				a:(@) _ binop(<">=">) _ b:@ {expr_bin!(a Gte b)}377				a:(@) _ binop(<keyword("in")>) _ b:@ {expr_bin!(a In b)}378				--379				a:(@) _ binop(<"<<">) _ b:@ {expr_bin!(a Lhs b)}380				a:(@) _ binop(<">>">) _ b:@ {expr_bin!(a Rhs b)}381				--382				a:(@) _ binop(<"+">) _ b:@ {expr_bin!(a Add b)}383				a:(@) _ binop(<"-">) _ b:@ {expr_bin!(a Sub b)}384				--385				a:(@) _ binop(<"*">) _ b:@ {expr_bin!(a Mul b)}386				a:(@) _ binop(<"/">) _ b:@ {expr_bin!(a Div b)}387				a:(@) _ binop(<"%">) _ b:@ {expr_bin!(a Mod b)}388				--389						unaryop(<"+">) _ b:@ {expr_un!(Plus b)}390						unaryop(<"-">) _ b:@ {expr_un!(Minus b)}391						unaryop(<"!">) _ b:@ {expr_un!(Not b)}392						unaryop(<"~">) _ b:@ {expr_un!(BitNot b)}393				--394				value:(@) _ "[" _ slice:slice_desc(s) _ "]" {Expr::Slice(Box::new(Slice{value, slice}))}395				indexable:(@) _ parts:index_part(s)+ {Expr::Index{indexable: Box::new(indexable), parts}}396				a:(@) _ args:spanned(<"(" _ a:args(s) _ ")" {a}>, s) ts:(_ keyword("tailstrict"))? {Expr::Apply(Box::new(a), args, ts.is_some())}397				a:(@) _ "{" _ body:objinside(s) _ "}" {Expr::ObjExtend(Box::new(a), body)}398				--399				e:expr_basic(s) {e}400				"(" _ e:expr(s) _ ")" {e}401			}402		pub rule index_part(s: &ParserSettings) -> IndexPart403		= n:("?" _ ensure_null_coaelse())? "." _ value:id_loc(s) {IndexPart {404			span: value.span,405			value: value.value,406			#[cfg(feature = "exp-null-coaelse")]407			null_coaelse: n.is_some(),408		}}409		/ n:("?" _ "." _ ensure_null_coaelse())? value:spanned(<"[" _ v:expr(s) _ "]" {v}>, s) {IndexPart {410			span: value.span,411			value: value.value,412			#[cfg(feature = "exp-null-coaelse")]413			null_coaelse: n.is_some(),414		}}415416		pub rule jsonnet(s: &ParserSettings) -> Expr = _ e:expr(s) _ {e}417	}418}419420fn codeidx(i: usize) -> u32 {421	u32::try_from(i).expect("code has 4g hard limit")422}423424pub type ParseError = peg::error::ParseError<peg::str::LineCol>;425pub fn parse(str: &str, settings: &ParserSettings) -> Result<Expr, ParseError> {426	jsonnet_parser::jsonnet(str, settings)427}428/// Used for importstr values429pub fn string_to_expr(str: IStr, settings: &ParserSettings) -> Spanned<Expr> {430	let len = str.len();431	Spanned::new(432		Expr::Str(str),433		Span(settings.source.clone(), 0, codeidx(len)),434	)435}436437#[cfg(test)]438mod tests {439	#[test]440	#[cfg(not(feature = "exp-null-coaelse"))]441	fn snapshots() {442		use std::fs;443444		use insta::{assert_snapshot, glob};445		use jrsonnet_ir::{IStr, Source};446447		use crate::{ParserSettings, parse};448449		glob!("tests/*.jsonnet", |path| {450			let input = fs::read_to_string(path).expect("read test file");451			let v = parse(452				&input,453				&ParserSettings {454					source: Source::new_virtual("<test>".into(), IStr::empty()),455				},456			)457			.unwrap();458			let v = format!("{v:#?}");459			assert_snapshot!(v);460		});461	}462}
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");