From fdc515f5bea165e6cf304890443f0e861026fb56 Mon Sep 17 00:00:00 2001 From: Kristian Myram Date: Tue, 13 Oct 2026 21:58:52 +0200 Subject: [PATCH] #1063: find an undefined name's first use from the failing frame on A ReferenceError's first use was searched across the whole file, so a mention earlier in a comment, a string or another function could report the wrong line. The search now starts at the innermost script frame's line, and covers the whole file only when there is no frame. Follow-up to storytold/effectcraft#1161. Co-Authored-By: Claude Haiku 5.5 --- crates/script/src/runtime.rs | 19 ++++++++++++------- crates/script/src/tests.rs | 12 ++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/script/src/runtime.rs b/crates/script/src/runtime.rs index b94bd80f..ce51c573 100644 --- a/crates/script/src/runtime.rs +++ b/crates/script/src/runtime.rs @@ -563,27 +563,32 @@ fn position(msg: &str, file: &str, code: &str) -> (Option, Option) { let mut it = l[i..].split([':', ')']); Some((it.next()?.parse().ok()?, it.next()?.parse().ok()?)) }; + // The innermost frame in the user's script, or the error's own position. + let frames: Vec<&str> = msg.lines().skip(1).collect(); + let frame = frames.iter().find_map(|l| pick(l)).or_else(|| msg.lines().next().and_then(pick)); // `ReferenceError: name is not defined`: boa places it at the start of the function that reads - // the name, so the first use of the name in the code is the better position (#1063). + // the name, so the name's first use from that frame's line on is the better position (#1063). + // With no frame to start from, the first use anywhere in the code is (#1161). let first = msg.lines().next().unwrap_or(""); let undefined = first.split("ReferenceError: ").nth(1).and_then(|r| r.split(" is not defined").next()); - if let Some(at) = undefined.and_then(|name| first_use(name, code)) { + let start_line = frame.map_or(1, |(line, _)| line); + if let Some(at) = undefined.and_then(|name| first_use(name, code, start_line)) { return (Some(at.0), Some(at.1)); } - let frames: Vec<&str> = msg.lines().skip(1).collect(); - if let Some((a, b)) = frames.iter().find_map(|l| pick(l)).or_else(|| msg.lines().next().and_then(pick)) { + if let Some((a, b)) = frame { return (Some(a), Some(b)); } (None, None) } -/// The first use of `name` as a whole identifier in `code`, as a 1-based line and column. -fn first_use(name: &str, code: &str) -> Option<(u32, u32)> { +/// The first use of `name` as a whole identifier in `code` on or after `start_line` (1-based), as a +/// 1-based line and column. +fn first_use(name: &str, code: &str, start_line: u32) -> Option<(u32, u32)> { if name.is_empty() { return None; } let ident = |c: u8| c.is_ascii_alphanumeric() || c == b'_' || c == b'$'; - for (n, line) in code.lines().enumerate() { + for (n, line) in code.lines().enumerate().skip(start_line.saturating_sub(1) as usize) { let b = line.as_bytes(); let mut from = 0; while let Some(i) = line.get(from..).and_then(|rest| rest.find(name)).map(|i| i + from) { diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 2c821e6a..365c72af 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -187,6 +187,18 @@ fn errors_inside_a_function_wrapper_report_the_failing_line() { assert_eq!(e.line, Some(3), "{e:?}"); } +/// The first use of an undefined name is the one inside the function that reads it: a mention +/// before it, in a comment, a string or another function, doesn't move the report (#1161). +#[test] +fn an_undefined_name_mentioned_earlier_reports_its_use_in_the_reading_function() { + let mut s = session(); + let code = "// nope is mentioned here\nvar text = 'nope';\nfunction other() { return nope; }\n(function () {\n var a = 1;\n nope.call();\n})();\n"; + let o = run_code(&mut s, code, "earlier.jsx"); + let e = o.error.expect("reference error"); + assert!(e.message.contains("nope"), "{e:?}"); + assert_eq!(e.line, Some(6), "{e:?}"); +} + #[test] fn match_names_and_lookups() { let mut s = session();