Skip to content

Commit ebce6e7

Browse files
committed
fix(json): preserve parse diagnostic locations
1 parent 31184f9 commit ebce6e7

2 files changed

Lines changed: 79 additions & 3 deletions

File tree

src/runtime/intrinsics/json/parse.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,27 @@ impl Runtime {
144144
};
145145
match parser.parse_document() {
146146
Ok(value) => Ok(NativeConversion::Value(value)),
147-
Err(JsonParseFailure::Syntax(failure)) => Ok(NativeConversion::Throw(
148-
self.new_native_error(realm, NativeErrorKind::Syntax, &failure.message)?,
149-
)),
147+
Err(JsonParseFailure::Syntax(failure)) => {
148+
// `js_json_parse` passes the synthetic filename `<input>` to
149+
// `JS_ParseJSON3`. Its `js_parse_error_v` path constructs the
150+
// SyntaxError without a backtrace, then prepends that exact
151+
// token location before the active native/bytecode frames.
152+
let position = parser.source_location(failure.offset)?;
153+
let exception = self.new_native_error_without_backtrace_from_error(
154+
realm,
155+
NativeErrorKind::Syntax,
156+
&Error::new(ErrorKind::Syntax, failure.message),
157+
)?;
158+
self.ensure_error_backtrace(
159+
&exception,
160+
false,
161+
Some(ExplicitBacktraceLocation {
162+
filename: JsString::from_static("<input>"),
163+
position,
164+
}),
165+
)?;
166+
Ok(NativeConversion::Throw(exception))
167+
}
150168
Err(JsonParseFailure::Runtime(error)) => Err(error),
151169
}
152170
}

src/runtime/intrinsics/json/tests.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,64 @@ fn json_module_parser_reports_pinned_quickjs_token_locations() {
234234
}
235235
}
236236

237+
#[test]
238+
fn json_parse_prepends_pinned_input_location_to_the_active_backtrace() {
239+
let runtime = Runtime::new();
240+
let mut context = runtime.new_context();
241+
let result = context
242+
.eval_with_filename(
243+
r#"
244+
(function authored() {
245+
try {
246+
JSON.parse('\n" \\x"');
247+
} catch (error) {
248+
return [
249+
error.name,
250+
error.message,
251+
error.fileName,
252+
error.lineNumber,
253+
error.columnNumber,
254+
error.stack
255+
];
256+
}
257+
})()
258+
"#,
259+
"json-callsite.js",
260+
)
261+
.unwrap();
262+
let Value::Object(result) = result else {
263+
panic!("JSON.parse diagnostic probe did not return its result array");
264+
};
265+
266+
for (index, expected) in [
267+
Value::String(JsString::from_static("SyntaxError")),
268+
Value::String(JsString::from_static("Bad escaped character")),
269+
Value::String(JsString::from_static("<input>")),
270+
Value::Int(2),
271+
Value::Int(5),
272+
]
273+
.into_iter()
274+
.enumerate()
275+
{
276+
let key = runtime.intern_property_key(&index.to_string()).unwrap();
277+
assert_eq!(context.get_property(&result, &key).unwrap(), expected);
278+
}
279+
280+
let stack_key = runtime.intern_property_key("5").unwrap();
281+
let Value::String(stack) = context.get_property(&result, &stack_key).unwrap() else {
282+
panic!("JSON.parse SyntaxError stack was not a string");
283+
};
284+
let stack = stack.to_string();
285+
assert!(
286+
stack.starts_with(" at <input>:2:5\n at parse (native)\n"),
287+
"JSON.parse stack lost its pinned synthetic source frame: {stack:?}",
288+
);
289+
assert!(
290+
stack.contains(" at authored (json-callsite.js:"),
291+
"JSON.parse stack lost its authored caller: {stack:?}",
292+
);
293+
}
294+
237295
#[test]
238296
fn quickjs_extended_json_module_parser_reports_pinned_negative_boundaries() {
239297
let cases = [

0 commit comments

Comments
 (0)