Files
vectorcraft/crates/js/src/tests.rs
T

808 lines
30 KiB
Rust

use proptest::prelude::*;
use crate::{
BinaryOp, Expr, ExprId, ForInTarget, FunctionKind, Options, Program, PropertyValue, Stmt, StmtId, SyntaxError, TOO_DEEP, TokenKind, parse,
tokenize,
};
fn ok(src: &str) -> Program {
match parse(src, &Options::default()) {
Ok(p) => p,
Err(e) => panic!("{src:?} should parse: {e}"),
}
}
fn err(src: &str) -> SyntaxError {
match parse(src, &Options::default()) {
Ok(_) => panic!("{src:?} should not parse"),
Err(e) => e,
}
}
fn strict_err(src: &str) -> SyntaxError {
let full = format!("'use strict';\n{src}");
match parse(&full, &Options::default()) {
Ok(_) => panic!("{src:?} should not parse in strict mode code"),
Err(e) => e,
}
}
fn with_preprocessor() -> Options {
Options { preprocessor_lines: true, ..Options::default() }
}
/// The expression of the program's only (or first) statement.
fn first_expr(p: &Program) -> ExprId {
match p.ast.stmt(p.body[0]) {
Some(Stmt::Expr(e)) => *e,
other => panic!("not an expression statement: {other:?}"),
}
}
fn expr(p: &Program, id: ExprId) -> &Expr {
p.ast.expr(id).unwrap()
}
fn stmt(p: &Program, id: StmtId) -> &Stmt {
p.ast.stmt(id).unwrap()
}
fn utf16(s: &str) -> Vec<u16> {
s.encode_utf16().collect()
}
// ---- expressions ----
#[test]
fn binary_operators_follow_precedence_and_associate_left() {
let p = ok("a + b * c - d;");
let Expr::Binary { op: BinaryOp::Sub, left, right } = expr(&p, first_expr(&p)) else { panic!() };
assert!(matches!(expr(&p, *right), Expr::Ident(n) if n == "d"));
let Expr::Binary { op: BinaryOp::Add, right: mul, .. } = expr(&p, *left) else { panic!() };
assert!(matches!(expr(&p, *mul), Expr::Binary { op: BinaryOp::Mul, .. }));
}
#[test]
fn logical_operators_bind_looser_than_comparisons() {
let p = ok("a || b && c == d;");
let Expr::Logical { right, .. } = expr(&p, first_expr(&p)) else { panic!() };
let Expr::Logical { right: eq, .. } = expr(&p, *right) else { panic!() };
assert!(matches!(expr(&p, *eq), Expr::Binary { op: BinaryOp::Eq, .. }));
}
#[test]
fn assignment_associates_right() {
let p = ok("a = b += c;");
let Expr::Assign { op: None, value, .. } = expr(&p, first_expr(&p)) else { panic!() };
assert!(matches!(expr(&p, *value), Expr::Assign { op: Some(BinaryOp::Add), .. }));
}
#[test]
fn new_takes_the_first_argument_list_after_its_callee() {
let p = ok("new a.b(1).c(2);");
let Expr::Call { callee, args } = expr(&p, first_expr(&p)) else { panic!() };
assert_eq!(args.len(), 1);
let Expr::Member { object, property } = expr(&p, *callee) else { panic!() };
assert_eq!(property.name, "c");
let Expr::New { callee, args } = expr(&p, *object) else { panic!() };
assert_eq!(args.len(), 1);
assert!(matches!(expr(&p, *callee), Expr::Member { .. }));
let p = ok("new new X()();");
let Expr::New { callee, .. } = expr(&p, first_expr(&p)) else { panic!() };
assert!(matches!(expr(&p, *callee), Expr::New { .. }));
let p = ok("new X;");
assert!(matches!(expr(&p, first_expr(&p)), Expr::New { args, .. } if args.is_empty()));
}
#[test]
fn conditional_and_sequence_expressions() {
let p = ok("a ? b : c ? d : e, f;");
let Expr::Sequence(list) = expr(&p, first_expr(&p)) else { panic!() };
assert_eq!(list.len(), 2);
let Expr::Conditional { alternate, .. } = expr(&p, list[0]) else { panic!() };
assert!(matches!(expr(&p, *alternate), Expr::Conditional { .. }));
}
#[test]
fn array_literals_keep_holes() {
let p = ok("[1, , 3, ];");
let Expr::Array(elements) = expr(&p, first_expr(&p)) else { panic!() };
assert_eq!(elements.len(), 3);
assert!(elements[1].is_none());
let p = ok("[,];");
assert!(matches!(expr(&p, first_expr(&p)), Expr::Array(e) if e.len() == 1));
}
#[test]
fn object_literals_with_accessors_and_keyword_names() {
let p = ok("({a: 1, 'b': 2, 3: 3, if: 4, get: 5, set: 6, get x() { return 1; }, set x(v) {}, });");
let Expr::Paren(inner) = expr(&p, first_expr(&p)) else { panic!() };
let Expr::Object(props) = expr(&p, *inner) else { panic!() };
assert_eq!(props.len(), 8);
assert!(matches!(props[6].value, PropertyValue::Get(_)));
assert!(matches!(props[7].value, PropertyValue::Set(_)));
let PropertyValue::Get(getter) = props[6].value else { panic!() };
assert_eq!(p.ast.function(getter).unwrap().kind, FunctionKind::Getter);
}
#[test]
fn accessors_take_the_right_number_of_parameters() {
assert!(err("({get a(x) {}});").message.contains("getter"));
assert!(err("({set a() {}});").message.contains("setter"));
assert!(err("({set a(x, y) {}});").message.contains("setter"));
}
#[test]
fn property_names_may_repeat() {
for src in ["({a: 1, a: 2});", "({a: 1, 'a': 2});", "({get a() {}, set a(v) {}});", "({a: 1, get a() {}});", "({get a() {}, get a() {}});"] {
ok(src);
ok(&format!("'use strict'; {src}"));
}
}
#[test]
fn escaped_get_and_set_do_not_start_accessors() {
assert!(err("({g\\u0065t a() {}});").message.contains("expected ':'"));
assert!(err("({\\u0073et a(v) {}});").message.contains("expected ':'"));
ok("({g\\u0065t: 1});");
}
#[test]
fn member_names_may_be_reserved_words() {
ok("a.if.var.function = b.true;");
ok("a.\\u0076ar = 1;");
}
// ---- statements ----
#[test]
fn else_if_chains_nest_as_alternates() {
let p = ok("if (a) x; else if (b) y; else z;");
let Stmt::If { alternate: Some(alt), .. } = stmt(&p, p.body[0]) else { panic!() };
let Stmt::If { alternate: Some(last), .. } = stmt(&p, *alt) else { panic!() };
assert!(matches!(stmt(&p, *last), Stmt::Expr(_)));
}
#[test]
fn for_statements_of_every_form() {
ok("for (;;) break;");
ok("for (var i = 0, n = a.length; i < n; i++) {}");
ok("for (i = 0; i < 10; i++) continue;");
ok("for (var k in o) {}");
ok("for (k in o) {}");
ok("for (a.b in o) {}");
ok("for (var x = 1 in o) {}");
ok("for (var i = (a in b); i; ) break;");
let p = ok("for (var k in o) {}");
assert!(matches!(stmt(&p, p.body[0]), Stmt::ForIn { target: ForInTarget::Var(_), .. }));
assert!(err("for (a + b in o) {}").message.contains("for-in"));
assert!(err("for (var a, b in o) {}").message.contains("expected ';'"));
}
#[test]
fn switch_try_with_labels_and_debugger() {
ok("switch (x) { case 1: a(); break; default: b(); case 2: }");
ok("try { a(); } catch (e) { b(); } finally { c(); }");
ok("try {} finally {}");
ok("with (o) { a = 1; }");
ok("outer: for (;;) { inner: for (;;) { continue outer; } }");
ok("a: b: while (1) { continue a; }");
ok("a: { break a; }");
ok("debugger;");
ok("do x(); while (a) y();");
assert!(err("switch (x) { default: default: }").message.contains("default"));
assert!(err("try {}").message.contains("catch or finally"));
}
#[test]
fn break_and_continue_need_a_target() {
assert!(err("break;").message.contains("break outside"));
assert!(err("continue;").message.contains("continue outside"));
assert!(err("switch (x) { case 1: continue; }").message.contains("continue outside"));
assert!(err("for (;;) { break nowhere; }").message.contains("undefined label"));
assert!(err("a: { continue a; }").message.contains("does not name a loop"));
assert!(err("a: while (1) { function f() { break a; } }").message.contains("undefined label"));
assert!(err("while (1) { function f() { break; } }").message.contains("break outside"));
assert!(err("a: a: ;").message.contains("duplicate label"));
assert!(err("a: { a: ; }").message.contains("duplicate label"));
ok("a: ; a: ;");
}
#[test]
fn return_only_inside_functions() {
assert!(err("return 1;").message.contains("return outside"));
ok("function f() { return; }");
ok("(function () { return 1 })();");
}
#[test]
fn function_declarations_in_blocks_and_branches() {
ok("{ function f() {} }");
ok("if (a) function f() {} else function g() {}");
ok("switch (a) { case 1: function f() {} }");
ok("l: function f() {}");
ok("{ l: m: function f() {} }");
ok("'use strict'; { function f() {} }");
assert!(strict_err("if (a) function f() {}").message.contains("functions can only be declared"));
assert!(strict_err("l: function f() {}").message.contains("functions can only be declared"));
for body in ["while (a) function f() {}", "for (;;) function f() {}", "do function f() {} while (a)", "with (a) function f() {}"] {
assert!(err(body).message.contains("cannot stand here"), "{body}");
}
assert!(err("if (a) l: function f() {}").message.contains("cannot stand here"));
assert!(err("while (a) l: function f() {}").message.contains("cannot stand here"));
}
// ---- automatic semicolon insertion ----
#[test]
fn semicolons_are_inserted_at_line_breaks_braces_and_the_end() {
ok("a = 1\nb = 2");
ok("function f() { return 1 }");
ok("a = 1");
let e = err("a = 1 b = 2");
assert_eq!((e.line, e.column), (1, 7));
}
#[test]
fn restricted_productions_end_at_a_line_break() {
let p = ok("function f() { return\n1 }");
let Stmt::FunctionDecl(f) = stmt(&p, p.body[0]) else { panic!() };
let body = &p.ast.function(*f).unwrap().body;
assert_eq!(body.len(), 2);
assert!(matches!(stmt(&p, body[0]), Stmt::Return(None)));
let p = ok("a\n++b");
assert_eq!(p.body.len(), 2);
assert!(matches!(expr(&p, first_expr(&p)), Expr::Ident(_)));
ok("for (;;) { break\nfoo }");
assert!(err("throw\nnew Error()").message.contains("line break after throw"));
}
#[test]
fn no_semicolon_is_inserted_inside_a_for_header() {
assert!(err("for (a\nb\n) {}").message.contains("expected ';'"));
}
// ---- the division and regular expression goals ----
#[test]
fn slash_divides_after_a_value_and_starts_a_regexp_elsewhere() {
let p = ok("a / b / c;");
assert!(matches!(expr(&p, first_expr(&p)), Expr::Binary { op: BinaryOp::Div, .. }));
let p = ok("x = /a\\/b[/]c/gi;");
let Expr::Assign { value, .. } = expr(&p, first_expr(&p)) else { panic!() };
let Expr::RegExp { pattern, flags } = expr(&p, *value) else { panic!() };
assert_eq!(*pattern, utf16("a\\/b[/]c"));
assert_eq!(flags, "gi");
ok("if (x) /re/.test(y);");
ok("{} /re/.test(y);");
ok("x = y /= 2;");
ok("x = /=a/;");
ok("return_ = typeof /x/;");
ok("a(/x/, /y/);");
// After a line break a `/` still divides when it can: there is no semicolon to insert.
let p = ok("a = b\n/c/g");
assert_eq!(p.body.len(), 1);
}
#[test]
fn tokenize_reads_each_slash_as_the_grammar_decides() {
let tokens = tokenize("a = b / c; d = /e/g.exec(f);", &Options::default()).unwrap();
let kinds: Vec<&TokenKind> = tokens.iter().map(|t| &t.kind).collect();
assert!(kinds.iter().any(|k| matches!(k, TokenKind::RegExp { flags, .. } if flags == "g")));
assert_eq!(kinds.iter().filter(|k| matches!(k, TokenKind::RegExp { .. })).count(), 1);
assert!(matches!(kinds.last(), Some(TokenKind::Eof)));
}
// ---- literals ----
fn number(src: &str) -> f64 {
let p = ok(src);
let Expr::Number(n) = expr(&p, first_expr(&p)) else { panic!("{src}") };
*n
}
fn string(src: &str) -> Vec<u16> {
let p = ok(src);
let Expr::String(s) = expr(&p, first_expr(&p)) else { panic!("{src}") };
s.clone()
}
#[test]
fn numeric_literals() {
assert_eq!(number("0;"), 0.0);
assert_eq!(number("12.5e-1;"), 1.25);
assert_eq!(number(".5;"), 0.5);
assert_eq!(number("5.;"), 5.0);
assert_eq!(number("1E3;"), 1000.0);
assert_eq!(number("0x1F;"), 31.0);
assert_eq!(number("0XfF;"), 255.0);
assert_eq!(number("0x20000000000001;"), 9007199254740992.0);
// 2^136 + 2^83 + 1 and 2^150 + 2^97 + 1: a set bit past 128 bits rounds the half-way case up.
assert_eq!(number(&format!("0x1{}8{}1;", "0".repeat(13), "0".repeat(19))), 2f64.powi(136) + 2f64.powi(84));
assert_eq!(number(&format!("01{}2{}1;", "0".repeat(17), "0".repeat(31))), 2f64.powi(150) + 2f64.powi(98));
assert_eq!(number("017;"), 15.0);
assert_eq!(number("08;"), 8.0);
assert_eq!(number("09.5;"), 9.5);
assert_eq!(number("1e400;"), f64::INFINITY);
assert!(err("3in []").message.contains("immediately after numeric literal"));
assert!(err("0x;").message.contains("hexadecimal"));
assert!(err("1.e").message.contains("immediately after"));
assert!(strict_err("017;").message.contains("octal literals"));
assert!(strict_err("08;").message.contains("octal literals"));
assert!(strict_err("({017: 1});").message.contains("octal"));
}
#[test]
fn string_literals_and_escapes() {
assert_eq!(string(r#""a\tb\n\\\"\'";"#), utf16("a\tb\n\\\"'"));
assert_eq!(string(r#"'\x41é€';"#), utf16("A\u{e9}\u{20ac}"));
assert_eq!(string("'a\\\nb';"), utf16("ab"));
assert_eq!(string("'a\\\r\nb';"), utf16("ab"));
assert_eq!(string(r"'\0';"), vec![0]);
assert_eq!(string(r"'\101\7\08';"), vec![0x41, 7, 0, u16::from(b'8')]);
assert_eq!(string(r"'\q\8';"), utf16("q8"));
assert_eq!(string("'\u{2028}';"), vec![0x2028]);
assert_eq!(string("'\u{1F600}';"), utf16("\u{1F600}"));
assert!(err("'abc").message.contains("unterminated string"));
assert!(err("'a\nb'").message.contains("unterminated string"));
assert!(err(r"'\x4';").message.contains("hexadecimal escape"));
assert!(err(r"'\u12';").message.contains("Unicode escape"));
assert_eq!(string(r"'\u{41}\u{1F600}\u{0000E9}';"), utf16("A\u{1F600}\u{e9}"));
for bad in [r"'\u{}';", r"'\u{110000}';", r"'\u{41';", r"'\u{4g}';"] {
assert!(err(bad).message.contains("Unicode escape"), "{bad}");
}
assert!(strict_err(r"'\101';").message.contains("octal escape"));
assert!(strict_err(r"'\8';").message.contains("octal escape"));
ok(r"'use strict'; '\0';");
}
#[test]
fn identifiers_with_unicode_and_escapes() {
let p = ok("var caf\\u00e9 = 1, \u{3b1}\u{3b2} = 2, $_x1 = 3, a\u{300} = 4;");
let Stmt::Var(decls) = stmt(&p, p.body[0]) else { panic!() };
let names: Vec<&str> = decls.iter().map(|d| d.name.name.as_str()).collect();
assert_eq!(names, ["caf\u{e9}", "\u{3b1}\u{3b2}", "$_x1", "a\u{300}"]);
let p = ok("\\u{61}b;");
assert!(matches!(expr(&p, first_expr(&p)), Expr::Ident(n) if n == "ab"));
assert!(err("var \\u0076ar = 1;").message.contains("escaped characters"));
assert!(err("var a\\u0020b;").message.contains("identifier escape"));
assert!(err("var \\x41;").message.contains("escape"));
assert!(err("var a = 1 @ 2;").message.contains("unexpected character '@'"));
assert!(err("var \u{2e2f};").message.contains("unexpected character"));
assert!(err("var a\u{2e2f};").message.contains("unexpected character"));
}
#[test]
fn identifier_characters_follow_their_unicode_categories() {
// Letters (Lu, Ll, Lt, Lm, Lo, Nl) start an identifier; marks (Mn, Mc), decimal digits (Nd)
// and connector punctuation (Pc) may follow.
for good in [
"var \u{995}\u{9cd}\u{9b7} = 1;",
"var \u{b95}\u{bcd}\u{b95} = 1;",
"var a\u{9be} = 1;",
"var x\u{966} = 1;",
"var a\u{203f}b = 1;",
"var \u{2b0} = 1;",
"var \u{2160} = 1;",
"var \u{1c5} = 1;",
"var \u{5d0}\u{5b7} = 1;",
] {
ok(good);
}
// Other numbers (No) are not identifier characters, and a mark cannot start an identifier.
for bad in ["var a\u{b2} = 1;", "var x\u{bd} = 1;", "var \u{9be}a = 1;", "var \u{300}a = 1;", "var \u{966} = 1;"] {
assert!(err(bad).message.contains("unexpected character"), "{bad:?}");
}
assert!(parse("var \\u09BEa = 1;", &Options::default()).is_err());
assert!(parse("var a\\u00B2 = 1;", &Options::default()).is_err());
}
#[test]
fn comments_and_line_terminators() {
let p = ok("a /* one\ntwo */ b");
assert_eq!(p.body.len(), 2, "a comment with a line break separates statements");
assert!(err("a /* one two */ b").message.contains("unexpected identifier 'b'"));
assert!(err("/* open").message.contains("unterminated comment"));
ok("// only a comment");
ok("a\u{2028}b\u{2029}c\r\nd\re");
ok("\u{feff}var\u{a0}a\u{3000}=\u{b}1\u{c};");
}
// ---- regular expression literals ----
#[test]
fn regular_expression_flags_and_patterns_are_checked() {
for good in [
"/a|b*c+?d{2}e{1,}f{1,3}?/",
"/[a-z\\d-]/",
"/[\\d-z]/",
"/]/",
"/{/",
"/a{,}/",
"/x{a}/",
"/(?:a)(?=b)(?!c)/",
"/(?=a)*/",
"/\\1(a)/",
"/[]/",
"/[^]/",
"/\\cJ\\c/",
"/[\\b-\\n]/",
"/a/gim",
] {
ok(&format!("x = {good};"));
}
for (bad, message) in [
("/(/", "unterminated group"),
("/a)/", "unmatched"),
("/?/", "nothing to repeat"),
("/a**/", "nothing to repeat"),
("/^*/", "nothing to repeat"),
("/{1}/", "nothing to repeat"),
("/a{2,1}/", "out of order"),
("/[z-a]/", "out of order"),
("/(?<a>x)/", "invalid group"),
("/[a/", "unterminated regular expression"),
("/a\\/", "unterminated regular expression"),
("/a\n/", "unterminated regular expression"),
("/a/gg", "flags"),
("/a/x", "flags"),
("/a/\\u0067", "flags"),
] {
let e = err(&format!("x = {bad};"));
assert!(e.message.contains(message), "{bad}: {}", e.message);
assert_eq!((e.line, e.column), (1, 5), "{bad}");
}
}
// ---- strict mode ----
#[test]
fn use_strict_makes_the_rest_strict_mode_code() {
assert!(ok("'use strict'; a;").strict);
assert!(ok("\"use strict\"\na;").strict);
assert!(!ok("'use\\x20strict'; a;").strict, "a directive with an escape is not \"use strict\"");
assert!(!ok("('use strict'); a;").strict);
assert!(!ok("a; 'use strict';").strict);
assert!(ok("'other'; 'use strict'; a;").strict);
assert!(!ok("'use strict' + 1;").strict);
let p = ok("function f() { 'use strict'; } function g() {}");
let funcs = &p.ast.functions;
assert!(funcs[0].strict && !funcs[1].strict && !p.strict);
assert!(parse("with (a) {}", &Options { strict: true, ..Options::default() }).is_err());
}
#[test]
fn strict_mode_early_errors() {
assert!(strict_err("with (a) {}").message.contains("with statements"));
assert!(strict_err("delete a;").message.contains("delete"));
assert!(strict_err("delete ((a));").message.contains("delete"));
ok("'use strict'; delete a.b; delete a[0];");
for target in ["eval = 1;", "arguments++;", "--eval;", "arguments += 1;", "(eval) = 1;", "for (eval in o) {}"] {
assert!(strict_err(target).message.contains("cannot assign"), "{target}");
}
for binding in ["var eval;", "function arguments() {}", "function f(eval) {}", "try {} catch (arguments) {}", "(function eval() {});"] {
assert!(strict_err(binding).message.contains("cannot be declared"), "{binding}");
}
for word in ["implements", "interface", "let", "package", "private", "protected", "public", "static", "yield"] {
ok(&format!("var {word} = 1;"));
assert!(strict_err(&format!("var {word} = 1;")).message.contains("reserved word"), "{word}");
assert!(strict_err(&format!("{word};")).message.contains("reserved word"), "{word}");
}
assert!(strict_err("function f(a, a) {}").message.contains("duplicate parameter"));
ok("function f(a, a) {}");
let e = strict_err("for (var x = 1 in o) {}");
assert!(e.message.contains("initializer"), "{}", e.message);
assert_eq!((e.line, e.column), (2, 16));
ok("'use strict'; for (var x in o) {}");
}
#[test]
fn a_function_made_strict_by_its_own_prologue_checks_its_name_and_parameters() {
let e = err("function f(a, a) { 'use strict'; }");
assert!(e.message.contains("duplicate parameter"));
assert_eq!((e.line, e.column), (1, 15));
assert!(err("function eval() { 'use strict'; }").message.contains("cannot be declared"));
assert!(err("function f(arguments) { 'use strict'; }").message.contains("cannot be declared"));
assert!(err("function static() { 'use strict'; }").message.contains("reserved word"));
assert!(err("function f() { '\\01'; 'use strict'; }").message.contains("octal escape"));
assert!(err("'\\01'; 'use strict';").message.contains("octal escape"));
assert!(err("function f() { 'use strict'; 010 }").message.contains("octal literals"));
assert!(err("function f() { 'use strict'; var public; }").message.contains("reserved word"));
}
// ---- assignment targets ----
#[test]
fn invalid_assignment_targets() {
for bad in ["1 = 2;", "a + b = c;", "(a, b) = 1;", "a = 1 = 2;", "++1;", "1++;", "--a.b++;", "this = 1;", "'a' += 1;", "(a = 1) = 2;"] {
let e = err(bad);
assert!(e.message.contains("invalid"), "{bad}: {}", e.message);
}
ok("(a) = 1; a.b = 1; a[0] += 2; a.b++; --a[1]; (a.b) = c;");
// Assigning to a call's result fails at run time outside strict mode code, and is a
// SyntaxError in it.
ok("f() = 1; f()++; for (f() in o) {}");
for call in ["f() = 1;", "f() += 1;", "++f();", "f()--;", "(f()) = 1;", "for (f() in o) {}"] {
assert!(strict_err(call).message.contains("invalid") || strict_err(call).message.contains("for-in"), "{call}");
}
}
// ---- preprocessor lines ----
#[test]
fn preprocessor_lines_are_read_as_directives() {
let src = "#target vectorcraft\n #include \"lib/a.jsxinc\"\n//@includepath \"~/x;~/y\"\n#strict on\n#unknown x\nvar a = 1;\n#targetengine main";
let p = parse(src, &with_preprocessor()).unwrap();
let names: Vec<(&str, u32, &str)> = p.directives.iter().map(|d| (d.name.as_str(), d.line, d.value.as_str())).collect();
assert_eq!(
names,
[
("target", 1, "vectorcraft"),
("include", 2, "\"lib/a.jsxinc\""),
("includepath", 3, "\"~/x;~/y\""),
("strict", 4, "on"),
("unknown", 5, "x"),
("targetengine", 7, "main"),
]
);
assert_eq!(p.directives[2].form, "//@");
assert!(p.directives[4].known.is_none());
assert_eq!(p.body.len(), 1);
assert!(parse(src, &Options::default()).is_err(), "without preprocessor lines a # is an error");
}
#[test]
fn a_hash_away_from_the_start_of_a_line_is_an_error() {
let e = parse("var a = 1; #include x", &with_preprocessor()).unwrap_err();
assert!(e.message.contains("unexpected character '#'"));
assert_eq!((e.line, e.column), (1, 12));
assert!(parse("/* c */ #include x", &with_preprocessor()).is_err());
// A comment naming no directive stays a comment.
assert!(parse("//@other\n", &with_preprocessor()).unwrap().directives.is_empty());
}
#[test]
fn a_directive_between_tokens_keeps_its_place_in_the_token_list() {
let tokens = tokenize("a\n#include x\nb", &with_preprocessor()).unwrap();
let kinds: Vec<String> = tokens
.iter()
.map(|t| match &t.kind {
TokenKind::Ident(n) => n.clone(),
TokenKind::Directive(d) => format!("#{}", d.name),
TokenKind::Eof => "eof".into(),
other => format!("{other:?}"),
})
.collect();
assert_eq!(kinds, ["a", "#include", "b", "eof"]);
assert!(tokens[2].newline_before);
}
// ---- positions ----
#[test]
fn errors_carry_line_and_column() {
let e = err("var a = 1;\nvar b = ];");
assert_eq!((e.line, e.column, e.offset), (2, 9, 19));
assert_eq!(e.to_string(), "SyntaxError: unexpected token ']' (line 2, column 9)");
let e = err("a\r\nb\r\n 'x");
assert_eq!((e.line, e.column), (3, 3));
let e = err("a\u{2028} )");
assert_eq!((e.line, e.column), (2, 3));
let e = err("'\u{1F600}' )");
assert_eq!((e.line, e.column), (1, 6), "columns count UTF-16 code units");
let e = err("x = 'a\\\nb' )");
assert_eq!((e.line, e.column), (2, 4), "a line continuation in a string counts its line");
}
#[test]
fn line_count_and_spans() {
let p = ok("a;\r\nb;\nc;");
assert_eq!(p.line_count(), 3);
assert_eq!(ok("a;\n").line_count(), 1);
assert_eq!(ok("").line_count(), 0);
let span = p.ast.stmt_span(p.body[2]);
assert_eq!(p.line_column(span.start), (3, 1));
assert_eq!((span.start, span.end), (7, 9));
}
// ---- nesting and limits ----
#[test]
fn deep_nesting_gives_a_syntax_error_not_a_stack_overflow() {
const N: usize = 100_000;
let cases = [
format!("{}1{}", "(".repeat(N), ")".repeat(N)),
format!("{}1{}", "[".repeat(N), "]".repeat(N)),
format!("x = {}{}", "{a:".repeat(N), "}".repeat(N)),
format!("{}{}", "{".repeat(N), "}".repeat(N)),
format!("{}{}", "function f(){".repeat(N), "}".repeat(N)),
format!("{}1", "a ? b : ".repeat(N)),
format!("{}x{}", "f(".repeat(N), ")".repeat(N)),
format!("{}x", "if (a) ".repeat(N)),
];
for src in &cases {
let e = err(src);
assert_eq!(e.message, TOO_DEEP, "{}", &src[..20]);
assert_eq!(e.line, 1);
assert!(e.column > 1);
}
let e = err(&format!("x = /{}a{}/;", "(".repeat(N), ")".repeat(N)));
assert!(e.message.contains(TOO_DEEP));
}
#[test]
fn nesting_up_to_the_limit_parses() {
let depth = 50;
ok(&format!("{}1{};", "(".repeat(depth), ")".repeat(depth)));
ok(&format!("{}{}", "{".repeat(depth), "}".repeat(depth)));
ok(&format!("x = {}{};", "[".repeat(depth), "]".repeat(depth)));
}
#[test]
fn long_flat_runs_do_not_nest() {
const N: usize = 100_000;
ok(&format!("x = 1{};", " + 1".repeat(N)));
ok(&format!("x = {}y;", "!".repeat(N)));
ok(&format!("{}x = 1;", "a = ".repeat(N)));
ok(&format!("if (a) b;{} else c;", " else if (a) b;".repeat(N)));
ok(&format!("{}x;", (0..N).map(|i| format!("l{i}: ")).collect::<String>()));
ok(&format!("x{};", ".y".repeat(N)));
ok(&format!("x{};", "()".repeat(N)));
ok(&"a;\n".repeat(N));
}
#[test]
fn long_label_chains_and_parameter_lists_are_checked_in_linear_time() {
// Each check looks a name up in a hash set or map. Comparing every name with every earlier
// one would take about 2 * 10^10 steps at this size.
const N: usize = 200_000;
let labels: String = (0..N).map(|i| format!("l{i}: ")).collect();
let body: String = (0..N).map(|i| format!("m{i}: break l{i}; ")).collect();
ok(&format!("{labels}for (;;) {{ {body}continue l0; }}"));
assert!(err(&format!("{labels}l0: x;")).message.contains("duplicate label"));
assert!(err(&format!("{labels}for (;;) {{ m: break l{N}; }}")).message.contains("undefined label"));
let params = (0..N).map(|i| format!("p{i}")).collect::<Vec<_>>().join(", ");
ok(&format!("function f({params}) {{ 'use strict'; }}"));
assert!(err(&format!("function f({params}, p0) {{ 'use strict'; }}")).message.contains("duplicate parameter"));
}
#[test]
fn nesting_to_the_default_limit_fits_a_small_stack() {
// The deepest nesting each production allows, parsed on a thread with a 1 MiB stack (the
// stack a wasm build or a secondary thread may have).
let n = crate::DEFAULT_MAX_DEPTH;
let cases = [
format!("x = {}1{};", "(".repeat(n), ")".repeat(n)),
format!("x = {}1{};", "[".repeat(n), "]".repeat(n)),
format!("x = {}1{};", "{a:".repeat(n), "}".repeat(n)),
format!("{}{}", "function f(){".repeat(n), "}".repeat(n)),
format!("{}x;", "if (a) ".repeat(n)),
format!("x = {}1;", "a ? b : ".repeat(n)),
format!("{}x{};", "f(".repeat(n), ")".repeat(n)),
format!("{}x{};", "a[".repeat(n), "]".repeat(n)),
];
let handle = std::thread::Builder::new()
.stack_size(1 << 20)
.spawn(move || cases.iter().map(|src| parse(src, &Options::default()).map(|_| ()).map_err(|e| e.message)).collect::<Vec<_>>())
.unwrap();
for result in handle.join().unwrap() {
// Each either parses or stops at the limit; neither overflows the stack.
assert!(result.is_ok() || result == Err(TOO_DEEP.to_string()), "{result:?}");
}
}
#[test]
fn sources_over_the_length_limit_are_refused() {
let options = Options { max_source_units: 10, ..Options::default() };
assert_eq!(parse("a = 1;", &options).map(|_| ()), Ok(()));
assert_eq!(parse("a = 1 + 2 + 3;", &options).unwrap_err().message, crate::TOO_LONG);
assert_eq!(parse("'\u{e9}\u{e9}\u{e9}\u{e9}';", &options).map(|_| ()), Ok(()), "the limit counts UTF-16 code units");
}
// ---- robustness ----
/// Fragments of the language, put together at random so the parser reaches its deeper states.
pub(crate) const FRAGMENTS: &[&str] = &[
"var ",
"function ",
"f",
"(",
")",
"{",
"}",
"[",
"]",
";",
",",
"=",
"+",
"-",
"/",
"/=",
"*",
"?",
":",
"!",
"++",
"--",
"if",
"else",
"for",
"in",
"while",
"do",
"return",
"break",
"continue",
"new",
"delete",
"typeof",
"this",
"with",
"switch",
"case",
"default",
"try",
"catch",
"finally",
"throw",
"get",
"set",
"'s'",
"\"\\x4",
"1",
"0x",
"017",
".5",
"1e",
"/re/g",
"a:",
"\n",
" ",
"'use strict';",
"#include x\n",
"//c\n",
"/*",
"*/",
"\\u0061",
"\u{2028}",
"eval",
"arguments",
".",
"x",
];
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn arbitrary_text_never_panics(src in ".{0,200}") {
let _ = parse(&src, &Options::default());
let _ = parse(&src, &Options { strict: true, preprocessor_lines: true, ..Options::default() });
}
#[test]
fn arbitrary_fragments_never_panic(parts in proptest::collection::vec(0..FRAGMENTS.len(), 0..80)) {
let src: String = parts.iter().map(|&i| FRAGMENTS[i]).collect();
for options in [Options::default(), Options { strict: true, preprocessor_lines: true, max_depth: 8, ..Options::default() }] {
match parse(&src, &options) {
Ok(p) => {
for (_, span) in &p.ast.exprs {
prop_assert!(span.start <= span.end && span.end as usize <= src.encode_utf16().count());
}
}
Err(e) => prop_assert!(e.line >= 1 && e.column >= 1),
}
}
}
#[test]
fn arbitrary_utf16_never_panics(units in proptest::collection::vec(any::<u16>(), 0..120)) {
let _ = crate::parse_utf16(&units, &Options::default());
}
}