mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 15:25:16 +00:00
Scripts: #strict on runs the script with strict write checks instead of refusing it
`#strict on` and `//@strict on` no longer end the run as unsupported. They turn on the checks the dollar-object help page gives for `$.strict` (https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/): an assignment to a read-only property throws a TypeError, and so does a new property on an object that takes none. VectorCraft's own rules: a getter-only property counts as read-only; the objects that take no new properties are the non-extensible ones (preventExtensions, seal, freeze, and the frozen vc and $ objects); the last `#strict` line of the script and its includes sets the whole run, and runs without one are unchanged; eval and Function take the checks of the code that compiles them; a file $.evalFile runs keeps its own line's checks for its code and functions, or takes the caller's without one. The rest of ES5 strict mode stays with 'use strict'. The js crate carries the setting per compiled source (Realm::set_strict_writes, and a strict_writes callback on HostItem::Evaluator), and applies it only to property assignments.
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user