Merge main into fix/script-rethrown-error-place

This commit is contained in:
dotCooCoo
2026-11-11 03:59:08 -08:00
75 changed files with 2351 additions and 146 deletions
+126 -5
View File
@@ -194,7 +194,6 @@ fn script_target_other_app_does_not_route() {
("#target\nvc.log(1);", "#target needs a value"),
("#targetengine \"session\"", "unsupported script directive: #targetengine"),
("#engine \"main\"", "unsupported script directive: #engine"),
("#strict on", "unsupported strict checking: #strict on"),
("#strict maybe", "#strict takes on or off"),
("#frobnicate 1", "unknown script directive: #frobnicate"),
("#include \"x.jsxinc", "unterminated quote in #include"),
@@ -223,12 +222,134 @@ fn script_name_comes_from_the_script_line() {
let _ = std::fs::remove_dir_all(base);
}
/// The script's `ro`, with a read-only `x`.
const READ_ONLY: &str = "var ro = {};\nObject.defineProperty(ro, 'x', {value: 1});\n";
#[test]
fn script_strict_on_is_refused() {
fn strict_on_runs_and_read_only_write_throws() {
let mut s = Session::new();
let e = s.execute("script.run", &json!({"source": "#strict on\nvc.log(1);"})).unwrap_err().to_string();
assert!(e.contains("unsupported strict checking: #strict on"), "{e}");
assert_eq!(s.script_report().unwrap()["kind"], "syntax");
for head in ["#strict on\n", "//@strict on\n", "#strict ON\n", "#strict off\n#strict on\n"] {
let source = format!("{head}{READ_ONLY}vc.log(1);\n ro.x = 2;\nvc.log(2);");
let e = s.execute("script.run", &json!({"source": source})).unwrap_err().to_string();
assert!(e.contains("TypeError: cannot assign to property 'x': it is read-only"), "{head}: {e}");
let report = s.script_report().unwrap();
assert_eq!(report["kind"], "thrown", "{head}");
assert_eq!(report["log"], json!([1]), "{head}: the script ran up to the write");
let line = head.lines().count() as u64 + 4;
assert_eq!((report["line"].as_u64(), report["column"].as_u64()), (Some(line), Some(3)), "{head}: {report}");
}
// The error can be caught, and every kind of assignment is checked.
let source = format!(
"#strict on\n{READ_ONLY}var out = [];\nfunction t(f) {{ try {{ f(); out.push('wrote'); }} catch (e) {{ out.push(e.name); }} }}\nt(function () {{ ro.x = 2; }});\nt(function () {{ ro['x'] += 1; }});\nt(function () {{ ro.x++; }});\nt(function () {{ undefined = 1; }});\nt(function () {{ [].concat.length = 0; }});\nvc.log(out);\nvc.log(ro.x);"
);
let r = s.execute("script.run", &json!({"source": source})).unwrap();
assert_eq!(r["log"], json!([["TypeError", "TypeError", "TypeError", "TypeError", "TypeError"], 1]));
// Without the line, or with `#strict off` last, the write is ignored as before.
for head in ["", "#strict off\n", "#strict on\n#strict off\n"] {
let r = s.execute("script.run", &json!({"source": format!("{head}{READ_ONLY}ro.x = 2;\nvc.log(ro.x);")})).unwrap();
assert_eq!(r["log"], json!([1]), "{head}");
}
}
#[test]
fn strict_on_rejects_getter_only_and_nonextensible_additions() {
let mut s = Session::new();
let source = "#strict on\nvar out = [];\nfunction t(f) { try { f(); out.push('wrote'); } catch (e) { out.push(String(e)); } }\n\
var g = {get v() { return 1; }}, set = {set v(x) { this.got = x; }};\n\
t(function () { g.v = 2; });\n\
t(function () { Object.preventExtensions({}).n = 1; });\n\
t(function () { Object.seal({a: 1}).n = 1; });\n\
t(function () { Object.freeze({a: 1}).a = 2; });\n\
t(function () { vc.extra = 1; });\n\
var open = {};\nt(function () { open.n = 1; });\nt(function () { set.v = 3; });\nt(function () { Object.seal({a: 1}).a = 2; });\n\
vc.log(out);\nvc.log(open.n);\nvc.log(set.got);";
let r = s.execute("script.run", &json!({"source": source})).unwrap();
assert_eq!(
r["log"],
json!([
[
"TypeError: cannot assign to property 'v': it has only a getter",
"TypeError: cannot assign to property 'n': the object is not extensible",
"TypeError: cannot assign to property 'n': the object is not extensible",
"TypeError: cannot assign to property 'a': it is read-only",
"TypeError: cannot assign to property 'extra': the object is not extensible",
"wrote",
"wrote",
"wrote"
],
1,
3
])
);
let r = s.execute("script.run", &json!({"source": source.replacen("#strict on", "#strict off", 1)})).unwrap();
assert_eq!(r["log"][0], json!(["wrote", "wrote", "wrote", "wrote", "wrote", "wrote", "wrote", "wrote"]), "off, nothing is refused");
}
#[test]
fn strict_on_does_not_enable_es5_early_errors() {
let mut s = Session::new();
let source = "#strict on\nwith ({w: 1}) { vc.log(w); }\nfunction f(a, a) { return a; }\nvc.log(f(1, 2));\nvc.log(010);\nundeclared = 4;\nvc.log(undeclared);\n\
vc.log((function () { return typeof this; })());\nvc.log(delete Object.prototype);\nvar eval2 = 5;\nvc.log((function () { arguments = 6; return arguments; })());";
let r = s.execute("script.run", &json!({"source": source})).unwrap();
assert_eq!(r["log"], json!([1, 2, 8, 4, "object", false, 6]));
// 'use strict' still makes the code strict mode code.
let e = s.execute("script.run", &json!({"source": "#strict on\n'use strict';\nundeclared2 = 1;"})).unwrap_err().to_string();
assert!(e.contains("ReferenceError"), "{e}");
let e = s.execute("script.run", &json!({"source": "#strict on\n'use strict';\nwith ({}) {}"})).unwrap_err().to_string();
assert_eq!(s.script_report().unwrap()["kind"], "syntax", "{e}");
}
#[test]
fn strict_on_in_include_controls_expanded_run() {
let base = folder("strict");
write(&base.join("main/on.jsxinc"), "#strict on\nvar inc = 1;");
write(&base.join("main/off.jsxinc"), "//@strict off\n");
write(&base.join("main/writes.jsxinc"), &format!("{READ_ONLY}\n ro.x = 2;\n"));
// An included file's line sets the checks for the script's own code too.
let report = failed(&base, &format!("#include \"on.jsxinc\"\n{READ_ONLY}ro.x = 2;"));
assert!(report["message"].as_str().unwrap().contains("it is read-only"), "{report}");
assert_eq!(report["line"], 4, "{report}");
assert!(report.get("file").is_none(), "{report}");
// A write in an included file is reported at its own line.
let report = failed(&base, "#strict on\n#include \"writes.jsxinc\"");
assert!(report["message"].as_str().unwrap().contains("it is read-only"), "{report}");
assert_eq!((report["line"].as_u64(), report["column"].as_u64()), (Some(4), Some(3)), "{report}");
assert!(report["file"].as_str().unwrap().ends_with("writes.jsxinc"), "{report}");
// The last line of the expanded run wins, wherever it is.
let r = run(&base, &format!("#strict on\n#include \"off.jsxinc\"\n{READ_ONLY}ro.x = 2;\nvc.log(ro.x);")).unwrap();
assert_eq!(r["log"], json!([1]));
let report = failed(&base, &format!("#include \"off.jsxinc\"\n#strict on\n{READ_ONLY}ro.x = 2;"));
assert!(report["message"].as_str().unwrap().contains("it is read-only"), "{report}");
let _ = std::fs::remove_dir_all(base);
}
#[test]
fn strict_mode_in_eval_file_is_scoped() {
let base = folder("strict-eval");
write(
&base.join("main/on.jsx"),
&format!(
"#strict on\n{READ_ONLY}function w(o) {{ o.x = 2; }}\nvar inner;\ntry {{ ro.x = 2; inner = 'wrote'; }} catch (e) {{ inner = e.name; }}\n"
),
);
write(&base.join("main/off.jsx"), &format!("#strict off\n{READ_ONLY}ro.x = 2;\n'off ran';"));
write(&base.join("main/plain.jsx"), &format!("{READ_ONLY}ro.x = 2;\n'plain ran';"));
// A file's own `#strict on` holds for its code, its functions included, and not the caller's.
let r = run(
&base,
"$.evalFile('on.jsx');\nvar after;\ntry { ro.x = 3; after = 'ignored'; } catch (e) { after = e.name; }\nvar called;\ntry { w(ro); called = 'wrote'; } catch (e) { called = e.name; }\nvc.log([inner, after, called]);",
)
.unwrap();
assert_eq!(r["log"], json!([["TypeError", "ignored", "TypeError"]]));
// A file without the line takes the caller's checks, and its own `#strict off` turns them off.
let r = run(&base, "#strict on\nvc.log($.evalFile('off.jsx'));\nvar e1;\ntry { ro.x = 2; } catch (e) { e1 = e.name; }\nvc.log(e1);").unwrap();
assert_eq!(r["log"], json!(["off ran", "TypeError"]), "the caller keeps its own checks");
let report = failed(&base, "#strict on\n$.evalFile('plain.jsx');");
assert!(report["message"].as_str().unwrap().contains("it is read-only"), "{report}");
assert!(report["file"].as_str().unwrap().ends_with("plain.jsx"), "{report}");
assert_eq!(report["line"], 3, "{report}");
assert_eq!(run(&base, "vc.log($.evalFile('plain.jsx'));").unwrap()["log"], json!(["plain ran"]));
let _ = std::fs::remove_dir_all(base);
}
#[test]