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
before · crates/jrsonnet-ir/src/expr.rs
1use std::{2	fmt::{self, Debug, Display},3	ops::{Deref, RangeInclusive},4};56use jrsonnet_gcmodule::Acyclic;7use jrsonnet_interner::IStr;89use crate::{10	NumValue,11	function::{FunctionSignature, ParamDefault, ParamName, ParamParse},12	source::Source,13};1415#[derive(Debug, PartialEq, Acyclic)]16pub enum FieldName {17	/// {fixed: 2}18	Fixed(IStr),19	/// {["dyn"+"amic"]: 3}20	Dyn(Expr),21}2223#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]24#[repr(u8)]25pub enum Visibility {26	/// :27	Normal,28	/// ::29	Hidden,30	/// :::31	Unhide,32}3334impl Visibility {35	pub fn is_visible(&self) -> bool {36		matches!(self, Self::Normal | Self::Unhide)37	}38}3940#[derive(Debug, PartialEq, Acyclic)]41pub struct AssertStmt {42	pub assertion: Spanned<Expr>,43	pub message: Option<Expr>,44}4546#[derive(Debug, PartialEq, Acyclic)]47pub struct FieldMember {48	pub name: Spanned<FieldName>,49	pub plus: bool,50	pub params: Option<ExprParams>,51	pub visibility: Visibility,52	pub value: Expr,53}5455#[derive(Debug, PartialEq, Acyclic)]56pub enum Member {57	Field(FieldMember),58	BindStmt(BindSpec),59	AssertStmt(AssertStmt),60}6162#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]63pub enum UnaryOpType {64	Plus,65	Minus,66	BitNot,67	Not,68}6970impl Display for UnaryOpType {71	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {72		use UnaryOpType::*;73		write!(74			f,75			"{}",76			match self {77				Plus => "+",78				Minus => "-",79				BitNot => "~",80				Not => "!",81			}82		)83	}84}8586#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]87pub enum BinaryOpType {88	Mul,89	Div,9091	/// Implemented as intrinsic, put here for completeness92	Mod,9394	Add,95	Sub,9697	Lhs,98	Rhs,99100	Lt,101	Gt,102	Lte,103	Gte,104105	BitAnd,106	BitOr,107	BitXor,108109	Eq,110	Neq,111112	And,113	Or,114	#[cfg(feature = "exp-null-coaelse")]115	NullCoaelse,116117	// Equialent to std.objectHasEx(a, b, true)118	In,119}120121impl Display for BinaryOpType {122	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {123		use BinaryOpType::*;124		write!(125			f,126			"{}",127			match self {128				Mul => "*",129				Div => "/",130				Mod => "%",131				Add => "+",132				Sub => "-",133				Lhs => "<<",134				Rhs => ">>",135				Lt => "<",136				Gt => ">",137				Lte => "<=",138				Gte => ">=",139				BitAnd => "&",140				BitOr => "|",141				BitXor => "^",142				Eq => "==",143				Neq => "!=",144				And => "&&",145				Or => "||",146				In => "in",147				#[cfg(feature = "exp-null-coaelse")]148				NullCoaelse => "??",149			}150		)151	}152}153154/// name, default value155#[derive(Debug, PartialEq, Acyclic)]156pub struct ExprParam {157	pub destruct: Destruct,158	pub default: Option<Expr>,159}160161/// Defined function parameters162#[derive(Debug, PartialEq, Acyclic)]163pub struct ExprParams {164	pub exprs: Vec<ExprParam>,165	pub signature: FunctionSignature,166	pub(crate) binds_len: usize,167}168impl ExprParams {169	pub fn len(&self) -> usize {170		self.exprs.len()171	}172	pub fn is_empty(&self) -> bool {173		self.exprs.is_empty()174	}175176	pub fn binds_len(&self) -> usize {177		self.binds_len178	}179	pub fn new(exprs: Vec<ExprParam>) -> Self {180		Self {181			signature: FunctionSignature::new(182				exprs183					.iter()184					.map(|p| {185						ParamParse::new(186							p.destruct.name(),187							ParamDefault::exists(p.default.is_some()),188						)189					})190					.collect(),191			),192			binds_len: exprs.iter().map(|v| v.destruct.binds_len()).sum(),193			exprs,194		}195	}196}197198#[derive(Debug, PartialEq, Acyclic)]199pub struct ArgsDesc {200	pub unnamed: Vec<Expr>,201	pub names: Vec<IStr>,202	pub values: Vec<Expr>,203}204impl ArgsDesc {205	pub fn new(unnamed: Vec<Expr>, names: Vec<IStr>, values: Vec<Expr>) -> Self {206		Self {207			unnamed,208			names,209			values,210		}211	}212}213214#[derive(Debug, PartialEq, Eq, Acyclic)]215pub enum DestructRest {216	/// ...rest217	Keep(IStr),218	/// ...219	Drop,220}221222#[derive(Debug, PartialEq, Acyclic)]223pub enum Destruct {224	Full(Spanned<IStr>),225	#[cfg(feature = "exp-destruct")]226	Skip,227	#[cfg(feature = "exp-destruct")]228	Array {229		start: Vec<Destruct>,230		rest: Option<DestructRest>,231		end: Vec<Destruct>,232	},233	#[cfg(feature = "exp-destruct")]234	Object {235		#[allow(clippy::type_complexity)]236		fields: Vec<(IStr, Option<Destruct>, Option<Spanned<Expr>>)>,237		rest: Option<DestructRest>,238	},239}240impl Destruct {241	/// Name of destructure, used for function parameter names242	pub fn name(&self) -> ParamName {243		match self {244			Self::Full(name) => ParamName::Named(name.value.clone()),245			#[cfg(feature = "exp-destruct")]246			_ => ParamName::Unnamed,247		}248	}249	pub fn binds_len(&self) -> usize {250		#[cfg(feature = "exp-destruct")]251		fn cap_rest(rest: &Option<DestructRest>) -> usize {252			match rest {253				Some(DestructRest::Keep(_)) => 1,254				Some(DestructRest::Drop) => 0,255				None => 0,256			}257		}258		match self {259			Self::Full(_) => 1,260			#[cfg(feature = "exp-destruct")]261			Self::Skip => 0,262			#[cfg(feature = "exp-destruct")]263			Self::Array { start, rest, end } => {264				start.iter().map(Destruct::binds_len).sum::<usize>()265					+ end.iter().map(Destruct::binds_len).sum::<usize>()266					+ cap_rest(rest)267			}268			#[cfg(feature = "exp-destruct")]269			Self::Object { fields, rest } => {270				let mut out = 0;271				for (_, into, _) in fields {272					match into {273						Some(v) => out += v.binds_len(),274						// Field is destructured to default name275						None => out += 1,276					}277				}278				out + cap_rest(rest)279			}280		}281	}282}283284#[derive(Debug, PartialEq, Acyclic)]285pub enum BindSpec {286	Field {287		into: Destruct,288		value: Expr,289	},290	Function {291		name: IStr,292		params: ExprParams,293		value: Expr,294	},295}296impl BindSpec {297	pub fn binds_len(&self) -> usize {298		match self {299			BindSpec::Field { into, .. } => into.binds_len(),300			BindSpec::Function { .. } => 1,301		}302	}303}304305#[derive(Debug, PartialEq, Acyclic)]306pub struct IfSpecData {307	pub span: Span,308	pub cond: Expr,309}310311#[derive(Debug, PartialEq, Acyclic)]312pub struct ForSpecData {313	pub destruct: Destruct,314	pub over: Expr,315}316317#[cfg(feature = "exp-object-iteration")]318#[derive(Debug, PartialEq, Acyclic)]319pub struct ForObjSpecData {320	pub key: IStr,321	pub visibility: Visibility,322	pub value: Destruct,323	pub over: Expr,324}325326#[derive(Debug, PartialEq, Acyclic)]327pub enum CompSpec {328	IfSpec(IfSpecData),329	ForSpec(ForSpecData),330	#[cfg(feature = "exp-object-iteration")]331	ForObjSpec(ForObjSpecData),332}333334#[derive(Debug, PartialEq, Acyclic)]335pub struct ObjComp {336	pub locals: Vec<BindSpec>,337	pub field: Box<FieldMember>,338	pub compspecs: Vec<CompSpec>,339}340341#[derive(Debug, PartialEq, Acyclic)]342pub struct ObjMembers {343	pub locals: Vec<BindSpec>,344	pub asserts: Vec<AssertStmt>,345	pub fields: Vec<FieldMember>,346}347348#[derive(Debug, PartialEq, Acyclic)]349pub enum ObjBody {350	MemberList(ObjMembers),351	ObjComp(ObjComp),352}353354#[derive(Debug, PartialEq, Eq, Clone, Copy, Acyclic)]355pub enum LiteralType {356	This,357	Super,358	Dollar,359	Null,360	True,361	False,362}363364#[derive(Debug, PartialEq, Acyclic)]365pub struct SliceDesc {366	pub start: Option<Spanned<Expr>>,367	pub end: Option<Spanned<Expr>>,368	pub step: Option<Spanned<Expr>>,369}370371#[derive(Debug, PartialEq, Acyclic)]372pub struct AssertExpr {373	pub assert: AssertStmt,374	pub rest: Expr,375}376377#[derive(Debug, PartialEq, Acyclic)]378pub struct BinaryOp {379	pub lhs: Expr,380	pub op: BinaryOpType,381	pub rhs: Expr,382}383384#[derive(Debug, PartialEq, Acyclic, Clone, Copy)]385pub enum ImportKind {386	Normal,387	Str,388	Bin,389}390391#[derive(Debug, PartialEq, Acyclic)]392pub struct IfElse {393	pub cond: IfSpecData,394	pub cond_then: Expr,395	pub cond_else: Option<Expr>,396}397398#[derive(Debug, PartialEq, Acyclic)]399pub struct Slice {400	pub value: Expr,401	pub slice: SliceDesc,402}403404/// Syntax base405#[derive(Debug, PartialEq, Acyclic)]406pub enum Expr {407	Literal(LiteralType),408409	/// String value: "hello"410	Str(IStr),411	/// Number: 1, 2.0, 2e+20412	Num(NumValue),413	/// Variable name: test414	Var(Spanned<IStr>),415416	/// Array of expressions: [1, 2, "Hello"]417	Arr(Vec<Expr>),418	/// Array comprehension:419	/// ```jsonnet420	///  ingredients: [421	///    { kind: kind, qty: 4 / 3 }422	///    for kind in [423	///      'Honey Syrup',424	///      'Lemon Juice',425	///      'Farmers Gin',426	///    ]427	///  ],428	/// ```429	ArrComp(Box<Expr>, Vec<CompSpec>),430431	/// Object: {a: 2}432	Obj(ObjBody),433	/// Object extension: var1 {b: 2}434	ObjExtend(Box<Expr>, ObjBody),435436	/// -2437	UnaryOp(UnaryOpType, Box<Expr>),438	/// 2 - 2439	BinaryOp(Box<BinaryOp>),440	/// assert 2 == 2 : "Math is broken"441	AssertExpr(Box<AssertExpr>),442	/// local a = 2; { b: a }443	LocalExpr(Vec<BindSpec>, Box<Expr>),444445	/// import* "hello"446	Import(Spanned<ImportKind>, Box<Expr>),447	/// error "I'm broken"448	ErrorStmt(Span, Box<Expr>),449	/// a(b, c)450	Apply(Box<Expr>, Spanned<ArgsDesc>, bool),451	/// a[b], a.b, a?.b452	Index {453		indexable: Box<Expr>,454		parts: Vec<IndexPart>,455	},456	/// function(x) x457	Function(ExprParams, Box<Expr>),458	/// if true == false then 1 else 2459	IfElse(Box<IfElse>),460	Slice(Box<Slice>),461}462463#[derive(Debug, PartialEq, Acyclic)]464pub struct IndexPart {465	pub span: Span,466	pub value: Expr,467	#[cfg(feature = "exp-null-coaelse")]468	pub null_coaelse: bool,469}470471/// file, begin offset, end offset472#[derive(Clone, PartialEq, Eq, Acyclic)]473#[repr(C)]474pub struct Span(pub Source, pub u32, pub u32);475impl Span {476	pub fn belongs_to(&self, other: &Span) -> bool {477		other.0 == self.0 && other.1 <= self.1 && other.2 >= self.2478	}479	pub fn range(&self) -> RangeInclusive<usize> {480		let start = self.1;481		let mut end = self.2;482		if end > start {483			// Because it is originally exclusive484			end -= 1;485		}486		start as usize..=end as usize487	}488}489490#[cfg(target_pointer_width = "64")]491static_assertions::assert_eq_size!(Span, (usize, usize));492493impl Debug for Span {494	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {495		write!(f, "{:?}:{:?}-{:?}", self.0, self.1, self.2)496	}497}498499#[derive(Clone, PartialEq, Acyclic)]500pub struct Spanned<T: Acyclic> {501	pub value: T,502	pub span: Span,503}504impl<T: Acyclic> Deref for Spanned<T> {505	type Target = T;506	fn deref(&self) -> &Self::Target {507		&self.value508	}509}510impl<T: Acyclic> Spanned<T> {511	#[inline]512	pub fn new(value: T, span: Span) -> Self {513		Self { value, span }514	}515	pub fn map<U: Acyclic>(self, v: impl FnOnce(T) -> U) -> Spanned<U> {516		Spanned {517			span: self.span,518			value: v(self.value),519		}520	}521	pub fn as_ref<'a>(&'a self) -> Spanned<&'a T>522	where523		&'a T: Acyclic,524	{525		Spanned {526			span: self.span.clone(),527			value: &self.value,528		}529	}530}531532impl<T: Debug + Acyclic> Debug for Spanned<T> {533	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {534		let expr = &**self;535		if f.alternate() {536			write!(f, "{:#?}", expr)?;537		} else {538			write!(f, "{:?}", expr)?;539		}540		write!(f, " from {:?}", self.span)?;541		Ok(())542	}543}
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");