mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
fed8ff8753
Closes out remaining spec compliance gaps in the lexer, Regex vs Division cases, `>>` cases, + many more. Fully passes on Test262, All Typescript tests, Oxc tests, SWC tests, VSCode (codebase), And 1_000 machine hours of differential fuzzing; Confidence is high that it's either 'correct' or very close. This PR specifically needs AI Disclosure in both finding and fixing the cases now because the bitwise logic is complex; Something that I'll start addressing once 'correctness' is locked in (and performance). --------- Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
910 lines
38 KiB
Rust
910 lines
38 KiB
Rust
//! Diagnostic coverage for every code class. Each positive test has a
|
|
//! `*_never_flagged` counterpart: valid input must emit nothing.
|
|
#![cfg(target_endian = "little")]
|
|
#![expect(
|
|
clippy::cast_possible_truncation,
|
|
clippy::unreadable_literal,
|
|
reason = "test helpers: lengths fit u32; the fuzz PRNG uses raw constants"
|
|
)]
|
|
|
|
use oxc_lexer::{Diagnostic, PAD, TokenKind, default_options, diag_code, lex_utf8};
|
|
|
|
fn diags(code: &str) -> Vec<Diagnostic> {
|
|
let mut buf = code.as_bytes().to_vec();
|
|
let len = buf.len() as u32;
|
|
buf.resize(buf.len() + PAD, 0); // lexer over-reads up to PAD bytes past `len`
|
|
let (res, _arena) = lex_utf8(&buf, len, default_options());
|
|
res.diagnostics().to_vec()
|
|
}
|
|
|
|
fn codes(code: &str) -> Vec<u16> {
|
|
diags(code).iter().map(|d| d.code).collect()
|
|
}
|
|
|
|
fn codes_jsx(code: &str) -> Vec<u16> {
|
|
let mut buf = code.as_bytes().to_vec();
|
|
let len = buf.len() as u32;
|
|
buf.resize(buf.len() + PAD, 0);
|
|
let mut opts = default_options();
|
|
opts.jsx = true;
|
|
let (res, _arena) = lex_utf8(&buf, len, opts);
|
|
res.diagnostics().iter().map(|d| d.code).collect()
|
|
}
|
|
|
|
fn codes_tsx(code: &str) -> Vec<u16> {
|
|
let mut buf = code.as_bytes().to_vec();
|
|
let len = buf.len() as u32;
|
|
buf.resize(buf.len() + PAD, 0);
|
|
let mut opts = default_options();
|
|
opts.jsx = true;
|
|
opts.ts = true;
|
|
let (res, _arena) = lex_utf8(&buf, len, opts);
|
|
res.diagnostics().iter().map(|d| d.code).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn unterminated_string() {
|
|
// opener at 4 to EOF: span (4, 4)
|
|
let d = diags("x = 'abc");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!(d[0].code, diag_code::UNTERMINATED_STRING);
|
|
assert_eq!((d[0].off, d[0].len), (4, 4));
|
|
}
|
|
|
|
#[test]
|
|
fn unterminated_template() {
|
|
assert_eq!(codes("x = `abc"), vec![diag_code::UNTERMINATED_TEMPLATE]);
|
|
assert_eq!(codes("x = `a${b}c"), vec![diag_code::UNTERMINATED_TEMPLATE]);
|
|
// An open substitution at EOF is a parser-level error; oxc_parser's
|
|
// lexer is also silent here.
|
|
assert!(codes("x = `a${b").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn unterminated_block_comment() {
|
|
assert_eq!(codes("x /* abc"), vec![diag_code::UNTERMINATED_BLOCK_COMMENT]);
|
|
}
|
|
|
|
#[test]
|
|
fn unterminated_regex() {
|
|
assert_eq!(codes("x = /abc"), vec![diag_code::UNTERMINATED_REGEXP]);
|
|
}
|
|
|
|
#[test]
|
|
fn unterminated_in_jsx_path() {
|
|
// carve_jsx must detect the same unterminated literals as carve
|
|
assert_eq!(codes_jsx("const x = 'abc"), vec![diag_code::UNTERMINATED_STRING]);
|
|
assert_eq!(codes_jsx("const x = `abc"), vec![diag_code::UNTERMINATED_TEMPLATE]);
|
|
assert_eq!(codes_jsx("const x = 1 /* abc"), vec![diag_code::UNTERMINATED_BLOCK_COMMENT]);
|
|
assert_eq!(codes_jsx("const x = /abc"), vec![diag_code::UNTERMINATED_REGEXP]);
|
|
assert!(codes_jsx("const el = <div className='x'>hi</div>;").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn numeric_separator_and_empty_radix() {
|
|
use diag_code as D;
|
|
assert_eq!(codes("x = 1_000_"), vec![D::INVALID_NUMERIC_SEPARATOR]); // trailing
|
|
assert_eq!(codes("x = 1__2"), vec![D::INVALID_NUMERIC_SEPARATOR]); // double
|
|
assert_eq!(codes("x = 0x_1"), vec![D::INVALID_NUMERIC_SEPARATOR]); // after prefix
|
|
assert_eq!(codes("x = 0xAB_"), vec![D::INVALID_NUMERIC_SEPARATOR]); // trailing in hex
|
|
assert_eq!(codes("x = 0x;"), vec![D::INVALID_NUMERIC_LITERAL]); // empty radix
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_octal_like_decimal() {
|
|
use diag_code as D;
|
|
// Leading `0` + digit: no separators, no bigint suffix, and an exponent
|
|
// only as lowercase `e` after an 8/9 (oxc_parser's read_legacy_octal
|
|
// quirk).
|
|
assert_eq!(codes("x = 0_0;"), vec![D::INVALID_NUMERIC_SEPARATOR]);
|
|
assert_eq!(codes("x = 00_0;"), vec![D::INVALID_NUMERIC_SEPARATOR]);
|
|
assert_eq!(codes("x = 08_0;"), vec![D::INVALID_NUMERIC_SEPARATOR]);
|
|
assert_eq!(codes("x = 00n;"), vec![D::INVALID_BIGINT]);
|
|
assert_eq!(codes("x = 08n;"), vec![D::INVALID_BIGINT]);
|
|
assert_eq!(codes("x = 0008n;"), vec![D::INVALID_BIGINT]);
|
|
assert_eq!(codes("x = 00e1;"), vec![D::INVALID_NUMERIC_LITERAL]);
|
|
assert_eq!(codes("x = 08E1;"), vec![D::INVALID_NUMERIC_LITERAL]);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_numbers_never_flagged() {
|
|
for n in [
|
|
"123",
|
|
"1_000",
|
|
"1_000_000",
|
|
"3.14",
|
|
"1e10",
|
|
"1E3",
|
|
"1e1_0",
|
|
"1.0e-1_0",
|
|
"0xFF",
|
|
"0xDEAD_BEEF",
|
|
"0o17",
|
|
"0b1010",
|
|
"123n",
|
|
"1_000n",
|
|
"0xFFn",
|
|
".5",
|
|
"5.",
|
|
"0.0",
|
|
"0",
|
|
// legacy-octal-like decimals that are valid in sloppy mode
|
|
"00",
|
|
"0123",
|
|
"08",
|
|
"09",
|
|
"0008",
|
|
"08.5",
|
|
"09e1",
|
|
"08e-1",
|
|
] {
|
|
assert!(codes(&format!("x = {n};")).is_empty(), "false positive on valid `{n}`");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn numeric_adjacency() {
|
|
use diag_code as D;
|
|
// misplaced bigint suffix: span = the trailing `n`
|
|
let d = diags("x = 1.5n;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!(d[0].code, D::INVALID_BIGINT);
|
|
assert_eq!((d[0].off, d[0].len), (7, 1));
|
|
assert_eq!(codes("x = 1e3n;"), vec![D::INVALID_BIGINT]);
|
|
// digit invalid for the radix: span = that digit only (a digit is not an
|
|
// identifier start, so the parser's run stops after one char)
|
|
let d = diags("x = 0b12;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!(d[0].code, D::INVALID_NUMERIC_LITERAL);
|
|
assert_eq!((d[0].off, d[0].len), (7, 1));
|
|
assert_eq!(codes("x = 0o18;"), vec![D::INVALID_NUMERIC_LITERAL]);
|
|
// number followed by an identifier: span covers the ident run
|
|
let d = diags("x = 3in y;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!(d[0].code, D::INVALID_NUMERIC_LITERAL);
|
|
assert_eq!((d[0].off, d[0].len), (5, 2)); // `in`
|
|
let d = diags("x = 123abc;");
|
|
assert_eq!((d[0].off, d[0].len), (7, 3)); // `abc`
|
|
assert_eq!(codes("x = 0xFFg;"), vec![D::INVALID_NUMERIC_LITERAL]);
|
|
// coalesce is shared by all four modes, so the jsx path fires too
|
|
assert_eq!(codes_jsx("x = 1.5n;"), vec![D::INVALID_BIGINT]);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_numeric_adjacency_never_flagged() {
|
|
for src in [
|
|
"x = 0x1Fn;", // hex bigint: `n` consumed by the scanner
|
|
"x = 123n;", // decimal bigint
|
|
"x = 1_000n;", // bigint with separators
|
|
"x = 0b101;", // valid binary
|
|
"x = 1..toString();", // number `1.` then `.` property access — dot is legal
|
|
"x = 1.e3;", // exponent directly after the dot
|
|
"x = 1.5.toString();", // float then member access
|
|
"let abc123def = 1;", // digits inside an identifier never glue as numbers
|
|
"x = x1;", // trailing digit in identifier
|
|
"x = .5;", // dot-led float
|
|
"x = 5.;", // trailing-dot float
|
|
"for (var i in x) {}", // spaced `in` operator
|
|
"x = 1 in y;", // spaced `in` after number
|
|
"x = a instanceof b;",
|
|
"t = `${1}n`;", // template text after a substitution
|
|
"s = '1.5n';", // string interior
|
|
"c = /* 1.5n */ 1;", // comment interior
|
|
"r = /1.5n/;", // regex interior
|
|
"x = 1\u{a0}+ 2;", // NBSP after number: Unicode WHITESPACE, not ident
|
|
"x = 1\u{2028}y = 2;", // LS after number: a line terminator, valid
|
|
"x = 1\u{3000}+ 2;", // ideographic space after number
|
|
] {
|
|
assert!(codes(src).is_empty(), "false positive on `{src}`");
|
|
}
|
|
// JSX text and expression containers must not glue text as numbers.
|
|
assert!(codes_jsx("const el = <div>1.5n</div>;").is_empty());
|
|
assert!(codes_jsx("const el = <div a=\"3in\">{1.5}</div>;").is_empty());
|
|
// The content-blind TSX type-arg skip must not leak numeric diagnostics
|
|
// from digit-letter adjacency inside a string type-arg.
|
|
assert!(codes_tsx("const el = <Foo<\"3px\"> x={1}/>;").is_empty());
|
|
assert!(codes_tsx("const el = <Foo<1.5, \"2xl\"> y/>;").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn regex_flags() {
|
|
use diag_code as D;
|
|
assert_eq!(codes("r = /x/q;"), vec![D::INVALID_REGEXP_FLAG]); // unknown flag
|
|
assert_eq!(codes("r = /x/gg;"), vec![D::DUPLICATE_REGEXP_FLAG]); // duplicate
|
|
assert!(codes("r = /x/gimsuydv;").is_empty()); // all valid flags, once each
|
|
assert!(codes("r = /x/;").is_empty()); // no flags
|
|
}
|
|
|
|
#[test]
|
|
fn unexpected_character() {
|
|
use diag_code as D;
|
|
assert_eq!(codes("a\u{1}b"), vec![D::UNEXPECTED_CHARACTER]);
|
|
assert_eq!(codes("x = 1\u{7} 2"), vec![D::UNEXPECTED_CHARACTER]);
|
|
assert_eq!(codes("\u{1}\u{2}"), vec![D::UNEXPECTED_CHARACTER, D::UNEXPECTED_CHARACTER]);
|
|
}
|
|
|
|
#[test]
|
|
fn line_terminator_in_string() {
|
|
use diag_code as D;
|
|
assert_eq!(codes("x = 'a\nb';"), vec![D::LINE_TERMINATOR_IN_STRING]);
|
|
assert_eq!(codes("x = 'a\r\nb';"), vec![D::LINE_TERMINATOR_IN_STRING]);
|
|
// escaped line terminators are legal continuations, CRLF as one sequence
|
|
assert!(codes("x = 'a\\\nb';").is_empty());
|
|
assert!(codes("x = 'a\\\rb';").is_empty());
|
|
assert!(codes("x = 'a\\\r\nb';").is_empty());
|
|
// escaped backslash followed by a raw terminator is invalid
|
|
assert_eq!(codes("x = 'a\\\\\r\nb';"), vec![D::LINE_TERMINATOR_IN_STRING]);
|
|
|
|
// Span parity: opener through the first unescaped terminator; for CRLF
|
|
// only the CR (the parser consumes one char, then reports).
|
|
let d = diags("x = 'a\nb';");
|
|
assert_eq!((d[0].off, d[0].len), (4, 3));
|
|
let d = diags("x = 'a\r\nb';");
|
|
assert_eq!((d[0].off, d[0].len), (4, 3));
|
|
let d = diags("x = 'a\\\\\r\nb';");
|
|
assert_eq!((d[0].off, d[0].len), (4, 5));
|
|
}
|
|
|
|
#[test]
|
|
fn line_terminator_in_regexp() {
|
|
use diag_code as D;
|
|
// Our token still runs to the closing `/`; the diagnostic reproduces the
|
|
// parser's span, opener to just past the first terminator.
|
|
let d = diags("x = /a\nb/;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!(d[0].code, D::LINE_TERMINATOR_IN_REGEXP);
|
|
assert_eq!((d[0].off, d[0].len), (4, 3)); // `/a<LF>`
|
|
let d = diags("x = /a\r\nb/;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!(d[0].code, D::LINE_TERMINATOR_IN_REGEXP);
|
|
assert_eq!((d[0].off, d[0].len), (4, 3)); // `/a<CR>`: one diag, span past the CR
|
|
// escaped terminators and terminators inside `[...]` are also invalid
|
|
assert_eq!(codes("x = /a\\\nb/;"), vec![D::LINE_TERMINATOR_IN_REGEXP]);
|
|
assert_eq!(codes("x = /a\\\r\nb/;"), vec![D::LINE_TERMINATOR_IN_REGEXP]);
|
|
// backslash at EOF: the escape peek must not read pad bytes and
|
|
// fabricate a terminator
|
|
assert_eq!(codes("x = /a\\"), vec![D::UNTERMINATED_REGEXP]);
|
|
assert_eq!(codes("x = /a[\n]b/;"), vec![D::LINE_TERMINATOR_IN_REGEXP]);
|
|
// newline then EOF: the parser stops at the terminator first
|
|
assert_eq!(codes("x = /abc\n"), vec![D::LINE_TERMINATOR_IN_REGEXP]);
|
|
assert_eq!(codes_jsx("x = /a\nb/;"), vec![D::LINE_TERMINATOR_IN_REGEXP]);
|
|
assert!(codes("x = /a\\nb/g;").is_empty()); // `\n` as two chars
|
|
assert!(codes("x = a / b\nc / d;").is_empty()); // division across lines
|
|
assert!(codes("x = /a[b-z]+/;").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_unicode_escape_in_string() {
|
|
use diag_code as D;
|
|
// Spans run from the backslash to just past the last byte the parser's
|
|
// escape scanner consumed. (`x = ` puts the `\` at offset 5.)
|
|
let d = diags(r#"x = "\u{110000}";"#); // out of range: span `\u{110000` (excl `}`)
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 9, D::INVALID_UNICODE_ESCAPE));
|
|
let d = diags(r#"x = "\uZZZZ";"#); // bad hex: span `\u`
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 2, D::INVALID_UNICODE_ESCAPE));
|
|
let d = diags(r#"x = "\u1ZZZ";"#); // one leading valid digit: span `\u1`
|
|
assert_eq!((d[0].off, d[0].len), (5, 3));
|
|
let d = diags(r#"x = "\xGG";"#); // bad hex \x: span `\x`
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 2, D::INVALID_UNICODE_ESCAPE));
|
|
let d = diags(r#"x = "\x4G";"#); // partial hex: span `\x4`
|
|
assert_eq!((d[0].off, d[0].len), (5, 3));
|
|
let d = diags(r#"x = "\u{}";"#); // empty braces: span `\u{`
|
|
assert_eq!((d[0].off, d[0].len), (5, 3));
|
|
let d = diags(r#"x = "\u{12";"#); // unterminated braces: `\u{12`
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len), (5, 5));
|
|
// the parser stops at the digit that crosses 0x10FFFF
|
|
let d = diags(r#"x = "\u{1100000}";"#);
|
|
assert_eq!((d[0].off, d[0].len), (5, 9)); // `\u{110000`
|
|
// u32 wrap-around: `\u{100000041}` wraps back into range
|
|
assert_eq!(codes(r#"x = "\u{100000041}";"#), vec![D::INVALID_UNICODE_ESCAPE]);
|
|
let d = diags(r#"x = "\u{0041ZZZ}";"#); // non-hex break, in-range value
|
|
assert_eq!((d[0].off, d[0].len), (5, 7)); // `\u{0041`
|
|
let d = diags(r#"x = "\u004";"#); // truncated at the closing quote
|
|
assert_eq!((d[0].off, d[0].len), (5, 5)); // `\u004`
|
|
// one error per bad escape, anchored at each backslash
|
|
assert_eq!(codes(r#"x = "\xG \xG";"#).len(), 2);
|
|
let d = diags(r#"x = "\uD800\uZZZZ";"#); // 2nd escape bad
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len), (11, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn valid_escapes_never_flagged() {
|
|
for s in [
|
|
r#""\u0041""#,
|
|
r#""\u{1F600}""#,
|
|
r#""\u{0}""#,
|
|
r#""\u{10FFFF}""#, // boundary: strictly-greater check
|
|
r#""\u{0000000041}""#, // leading zeros never cross the limit
|
|
r#""\z""#, // NonEscapeCharacter — valid, cooks to `z`
|
|
r#""\\n""#, // escaped backslash + literal n
|
|
r#""\0""#,
|
|
r#""\01""#, // legacy octal: parser/strict-mode territory
|
|
r#""\7""#,
|
|
r#""\8""#, // NonOctalDecimalEscape: parser territory
|
|
r#""\uD800""#, // lone surrogate: valid in strings
|
|
r#""\uD83D\uDE00""#, // surrogate-pair escapes (two valid escapes)
|
|
r#""😀""#, // raw multi-byte content, no escapes
|
|
r#""\x41""#,
|
|
] {
|
|
assert!(codes(&format!("x = {s};")).is_empty(), "false positive on `{s}`");
|
|
}
|
|
assert!(codes("x = 'a\\\nb';").is_empty()); // LF line continuation
|
|
assert!(codes("x = 'a\\\r\nb';").is_empty()); // CRLF line continuation
|
|
}
|
|
|
|
#[test]
|
|
fn template_invalid_escapes_stay_silent() {
|
|
// An invalid escape in a tagged template is legal, and tagged-ness is
|
|
// parser context; oxc_parser's lexer is also silent here.
|
|
assert!(codes(r"t = `\uZZZZ`;").is_empty());
|
|
assert!(codes(r"t = `\u{110000}`;").is_empty());
|
|
assert!(codes(r"t = `\xGG`;").is_empty());
|
|
assert!(codes(r"t = tag`\uZZ`;").is_empty());
|
|
assert!(codes(r"t = `a${b}\uZZ`;").is_empty()); // middle/tail segments
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_unicode_escape_jsx_path() {
|
|
use diag_code as D;
|
|
assert_eq!(codes_jsx(r#"const x = "\uZZZZ";"#), vec![D::INVALID_UNICODE_ESCAPE]);
|
|
// JSX attribute strings have no escapes — the value is verbatim source.
|
|
assert!(codes_jsx(r#"const el = <div a="\uZZ">x</div>;"#).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn valid_input_emits_nothing() {
|
|
assert!(codes("let x = 'abc';").is_empty());
|
|
assert!(codes("const t = `a${b}c`; /* ok */ // ok\nlet r = /x/g;").is_empty());
|
|
assert!(codes("function f(){ return 1 / 2; }").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_unicode_character() {
|
|
use diag_code as D;
|
|
// Chars that are neither whitespace nor ID_Start/ID_Continue at code
|
|
// level: U+2E2F vertical tilde, U+180E Mongolian vowel separator, emoji.
|
|
let d = diags("var x\u{2E2F} = 1;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 3, D::UNEXPECTED_CHARACTER));
|
|
assert_eq!(codes("var \u{180E}x = 1;"), vec![D::UNEXPECTED_CHARACTER]);
|
|
assert_eq!(codes("let a = \u{1F600};"), vec![D::UNEXPECTED_CHARACTER]);
|
|
// A raw U+FFFD at code level is oxc_parser's binary-file error, distinct
|
|
// from opt-in UTF-8 validation.
|
|
assert_eq!(codes("var \u{FFFD} = 1;"), vec![D::INVALID_UTF8]);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_unicode_never_flagged() {
|
|
// identifier chars, whitespace, and anything inside literal content
|
|
for src in [
|
|
// spellchecker:disable-next-line
|
|
"var caf\u{E9} = 1;", // é: ID_Continue
|
|
"var \u{4F60}\u{597D} = 1;", // CJK idents
|
|
"let \u{3C0} = 3.14;", // π
|
|
"x\u{00A0}= 1;", // NBSP whitespace
|
|
"let s = '\u{1F600}\u{2E2F}\u{180E}';", // anything in a string
|
|
"let t = `\u{1F600} ${x} \u{2E2F}`;", // anything in a template
|
|
"// \u{2E2F}\u{1F600}\nlet u = 1;", // anything in a comment
|
|
"/* \u{180E} */ let v = 1;", // block comment
|
|
"x = /\u{1F600}/u;", // anything in a regex body
|
|
] {
|
|
assert!(codes(src).is_empty(), "false positive on valid {src:?}");
|
|
}
|
|
}
|
|
|
|
fn diags_bytes(code: &[u8]) -> Vec<Diagnostic> {
|
|
let mut buf = code.to_vec();
|
|
let len = buf.len() as u32;
|
|
buf.resize(buf.len() + PAD, 0);
|
|
let mut opts = default_options();
|
|
opts.validate_utf8 = true;
|
|
let (res, _arena) = lex_utf8(&buf, len, opts);
|
|
res.diagnostics().to_vec()
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_utf8() {
|
|
use diag_code as D;
|
|
// span = the maximal invalid subpart; one diagnostic per file
|
|
let d = diags_bytes(b"x = \xFF;"); // lone invalid byte
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (4, 1, D::INVALID_UTF8));
|
|
assert_eq!(diags_bytes(b"x = \xC3;")[0].code, D::INVALID_UTF8); // truncated 2-byte
|
|
assert_eq!(diags_bytes(b"x = \xC3").len(), 1); // truncated at EOF
|
|
assert_eq!(diags_bytes(b"x = \x80;")[0].code, D::INVALID_UTF8); // stray continuation
|
|
// Overlong / surrogate / beyond-U+10FFFF: byte 2 is range-checked per
|
|
// lead, so the invalid subpart is the lead alone.
|
|
let d = diags_bytes(b"x = '\xE0\x80\x80';");
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 1, D::INVALID_UTF8));
|
|
let d = diags_bytes(b"x = '\xED\xA0\x80';");
|
|
assert_eq!((d[0].off, d[0].len), (5, 1));
|
|
let d = diags_bytes(b"x = '\xF4\x90\x80\x80';");
|
|
assert_eq!((d[0].off, d[0].len), (5, 1));
|
|
// truncated 4-byte at EOF: lead + two range-valid continuations
|
|
let d = diags_bytes(b"x = \xF0\x9F\x98");
|
|
assert_eq!((d[0].off, d[0].len), (4, 3));
|
|
assert_eq!(diags_bytes(b"x = \xFF + \xFF;").len(), 1); // first subpart only
|
|
// context-free: fires inside a string body too
|
|
let d = diags_bytes(b"x = 'a\xFFb';");
|
|
assert_eq!((d[0].off, d[0].code), (6, D::INVALID_UTF8));
|
|
}
|
|
|
|
#[test]
|
|
fn valid_utf8_never_flagged() {
|
|
let codes_v =
|
|
|s: &str| -> Vec<u16> { diags_bytes(s.as_bytes()).iter().map(|d| d.code).collect() };
|
|
for src in [
|
|
// spellchecker:disable-next-line
|
|
"let caf\u{e9} = 1;", // 2-byte
|
|
"x = '\u{1F600}';", // 4-byte emoji in string
|
|
"a\u{a0}b", // NBSP (ws-reclass path preserved)
|
|
"let \u{4e2d}\u{6587} = 1;", // CJK identifier (3-byte)
|
|
"x\u{2028}y = 1;", // LS
|
|
"s = '\u{FEFF}';", // ZWNBSP mid-file
|
|
] {
|
|
assert!(
|
|
!codes_v(src).contains(&diag_code::INVALID_UTF8),
|
|
"false positive on valid UTF-8 `{src}`"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn utf8_fuzz_against_std() {
|
|
use diag_code as D;
|
|
// Deterministic LCG fuzz against std::str::from_utf8: a diag iff std
|
|
// errors, offset == valid_up_to(), len == error_len() when std has one.
|
|
let mut s: u64 = 0x243F_6A88_85A3_08D3;
|
|
for _ in 0..4000 {
|
|
let mut buf = vec![b' ']; // keep offset 0 clear of hashbang/BOM handling
|
|
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
|
let len = 3 + (s >> 59) as usize;
|
|
for _ in 0..len {
|
|
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
|
let r = (s >> 33) as u8;
|
|
buf.push(if r & 1 == 0 { 0x40 + (r >> 3) } else { r });
|
|
}
|
|
let d = diags_bytes(&buf);
|
|
let first6 = d.iter().find(|d| d.code == D::INVALID_UTF8);
|
|
match core::str::from_utf8(&buf) {
|
|
Ok(_) => assert!(first6.is_none(), "false positive on valid UTF-8 {buf:?}"),
|
|
Err(e) => {
|
|
let d6 = first6.unwrap_or_else(|| panic!("missed invalid UTF-8 in {buf:?}"));
|
|
assert_eq!(d6.off as usize, e.valid_up_to(), "offset mismatch on {buf:?}");
|
|
if let Some(l) = e.error_len() {
|
|
assert_eq!(d6.len as usize, l, "subpart len mismatch on {buf:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_identifier_escape_bare() {
|
|
use diag_code as D;
|
|
// bare `\`: span = the one char after it, backslash excluded
|
|
let d = diags(r"x = \A");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 1, D::INVALID_IDENTIFIER_ESCAPE));
|
|
// backslash at EOF: empty span at n
|
|
let d = diags(r"x = \");
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 0, D::INVALID_IDENTIFIER_ESCAPE));
|
|
// control bytes keep their own code
|
|
assert_eq!(codes("a\u{1}b"), vec![D::UNEXPECTED_CHARACTER]);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_identifier_escape_payload() {
|
|
use diag_code as D;
|
|
// spans start after the backslash, end where the parser scanner stopped
|
|
let d = diags(r"var \uZZ = 1");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 1, D::INVALID_IDENTIFIER_ESCAPE));
|
|
let d = diags(r"var \u004 = 1"); // 3 leading valid hex
|
|
assert_eq!((d[0].off, d[0].len), (5, 4));
|
|
let d = diags(r"var \u{} = 1"); // no digits; closing brace NOT consumed
|
|
assert_eq!((d[0].off, d[0].len), (5, 2));
|
|
let d = diags(r"var \u{110000} = 1"); // stops right after the crossing digit
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len), (5, 8));
|
|
let d = diags(r"var \u{FFFFFFFFFF} = 1"); // ditto, incremental check
|
|
assert_eq!((d[0].off, d[0].len), (5, 8));
|
|
let d = diags(r"var \u{12 = 1"); // missing closing brace
|
|
assert_eq!((d[0].off, d[0].len), (5, 4));
|
|
// surrogates: lone (both forms) and pairs are invalid in identifiers
|
|
let d = diags(r"var \uD800 = 1");
|
|
assert_eq!((d[0].off, d[0].len), (5, 5));
|
|
let d = diags(r"var \u{D800} = 1");
|
|
assert_eq!((d[0].off, d[0].len), (5, 7));
|
|
// a well-formed pair: one diagnostic spanning both escapes
|
|
let src = ["var ", r"\uD800", r"\uDC00", " = 1"].concat();
|
|
let d = diags(&src);
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len), (5, 11));
|
|
// high + invalid low: the parser rewinds — two independent diagnostics
|
|
let src = ["var ", r"\uD800", r"\uD800", " = 1"].concat();
|
|
let d = diags(&src);
|
|
assert_eq!(d.len(), 2);
|
|
assert_eq!((d[0].off, d[0].len), (5, 5));
|
|
assert_eq!((d[1].off, d[1].len), (11, 5));
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_identifier_escape_jsx_path() {
|
|
use diag_code as D;
|
|
assert!(codes_jsx(r"const \u{110000} = 1").contains(&D::INVALID_IDENTIFIER_ESCAPE));
|
|
assert!(codes_jsx(r"const el = <div>a\b</div>;").is_empty()); // JSX text
|
|
}
|
|
|
|
#[test]
|
|
fn valid_identifier_escapes_never_flagged() {
|
|
use diag_code as D;
|
|
for src in [
|
|
r"let \u0041 = 1;", // 4-digit form
|
|
r"let \u{41} = 1;", // brace form
|
|
r"let \u{10FFFF} = 1;", // boundary code point (ident-char check deferred)
|
|
r"a\u0041b", // mid-identifier escape (misc_post merge)
|
|
r"n\u0065w", // keyword lookalike via escape
|
|
r"class A{ #\u0061 = 1 }", // private name with escape
|
|
r"x = '\n';", // string escapes: not this class
|
|
r"r = /\d+/g;", // regex escapes: interiors carved
|
|
r"// \A", // comment interior
|
|
"t = `a\nb`;", // template interior
|
|
] {
|
|
assert!(!codes(src).contains(&D::INVALID_IDENTIFIER_ESCAPE), "false positive on `{src}`");
|
|
}
|
|
// Out-of-range escape in a STRING is code 7's business, never code 8.
|
|
assert!(!codes(r"s = '\u{110000}';").contains(&D::INVALID_IDENTIFIER_ESCAPE));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_exponent() {
|
|
use diag_code as D;
|
|
// `1e` / `1e+`: the marker and sign were consumed but no digits followed
|
|
let d = diags("x = 1e;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (4, 2, D::INVALID_NUMERIC_LITERAL));
|
|
let d = diags("x = 1e+;");
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (4, 3, D::INVALID_NUMERIC_LITERAL));
|
|
assert_eq!(codes("x = .5e;"), vec![D::INVALID_NUMERIC_LITERAL]);
|
|
assert_eq!(codes("x = 0e;"), vec![D::INVALID_NUMERIC_LITERAL]);
|
|
assert_eq!(codes("x = 08e;"), vec![D::INVALID_NUMERIC_LITERAL]);
|
|
// `1en`: empty exponent and misplaced bigint suffix both fire — unlike
|
|
// the parser, we don't abort at the first error
|
|
let cs = codes("x = 1en;");
|
|
assert!(cs.contains(&D::INVALID_NUMERIC_LITERAL));
|
|
assert!(cs.contains(&D::INVALID_BIGINT));
|
|
for n in ["1e3", "1E+5", "1e-1", "1.e3", "1e1_0", ".5e2", "0e0", "0xe", "0xEn"] {
|
|
assert!(codes(&format!("x = {n};")).is_empty(), "false positive on `{n}`");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_ident_after_number() {
|
|
use diag_code as D;
|
|
// same adjacency error as ASCII; span = the identifier-start run
|
|
let d = diags("x = 1π;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 2, D::INVALID_NUMERIC_LITERAL));
|
|
let d = diags("x = 123abcπ;");
|
|
assert_eq!((d[0].off, d[0].len), (7, 5)); // `abc` + 2-byte pi
|
|
assert_eq!(codes("x = 1.5π;"), vec![D::INVALID_NUMERIC_LITERAL]);
|
|
// unicode whitespace after a number stays clean
|
|
assert!(codes("x = 1\u{a0}+ 2;").is_empty());
|
|
assert!(codes("x = 1\u{2028}y = 2;").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn escaped_char_not_identifier() {
|
|
use diag_code as D;
|
|
// A well-formed escape decoding to a non-identifier char: empty span
|
|
// right after the escape (the bridge decodes backward for the message).
|
|
let d = diags(r"var \u0020 = 1");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (10, 0, D::UNEXPECTED_CHARACTER));
|
|
let d = diags(r"var \u{30}x = 1");
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (10, 0, D::UNEXPECTED_CHARACTER));
|
|
// mid-identifier escapes use is_identifier_part: digits fine there,
|
|
assert!(codes(r"let a\u{30}b = 1;").is_empty());
|
|
// whitespace not
|
|
assert_eq!(codes(r"let a\u0020b = 1;"), vec![D::UNEXPECTED_CHARACTER]);
|
|
for src in [r"let \u0041 = 1;", r"let \u{24} = 1;", r"let a\u{5F}b = 1;"] {
|
|
assert!(codes(src).is_empty(), "false positive on `{src}`");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn line_separator_in_regexp() {
|
|
use diag_code as D;
|
|
// LS/PS are LineTerminators: invalid in regex bodies, raw or escaped;
|
|
// span ends past the whole 3-byte char
|
|
let d = diags("x = /a\u{2028}b/;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (4, 5, D::LINE_TERMINATOR_IN_REGEXP));
|
|
assert_eq!(codes("x = /a\u{2029}b/;"), vec![D::LINE_TERMINATOR_IN_REGEXP]);
|
|
let d = diags("x = /a\\\u{2028}b/;"); // escaped LS: still invalid
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len), (4, 6));
|
|
assert!(codes("x = /a\u{2014}b/;").is_empty()); // other E2-led chars are fine
|
|
assert!(codes("x = /a\u{2713}b/u;").is_empty());
|
|
assert!(codes(r"x = /a\u2028b/;").is_empty()); // escape text, no raw LS
|
|
assert!(codes("s = '\u{2028}';").is_empty()); // legal in strings/templates
|
|
assert!(codes("t = `\u{2028}`;").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn misplaced_hash() {
|
|
use diag_code as D;
|
|
// A `#` that opens neither a hashbang nor a private name: the parser
|
|
// consumes the char after `#` and reports on it.
|
|
let d = diags("\n#!/bin/sh");
|
|
let h = d.iter().find(|x| x.code == D::UNEXPECTED_CHARACTER).unwrap();
|
|
assert_eq!((h.off, h.len), (2, 1)); // later garbage tokens may add diags
|
|
let d = diags("x = #;");
|
|
assert_eq!(d.len(), 1);
|
|
assert_eq!((d[0].off, d[0].len, d[0].code), (5, 1, D::UNEXPECTED_CHARACTER));
|
|
let d = diags("x = #"); // `#` at EOF: empty span at n
|
|
assert_eq!((d[0].off, d[0].len), (5, 0));
|
|
assert!(codes("#!/usr/bin/env node\nlet x = 1;").is_empty());
|
|
assert!(codes("class A { #x = 1; m() { return this.#x; } }").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn template_cooked_invalid_marker() {
|
|
// A NotEscapeSequence makes the cooked value None; legality depends on
|
|
// tagged-ness, so the span carries a marker bit, not a diagnostic.
|
|
let get = |code: &str| -> Vec<bool> {
|
|
let mut buf = code.as_bytes().to_vec();
|
|
let len = buf.len() as u32;
|
|
buf.resize(buf.len() + PAD, 0);
|
|
let (res, arena) = lex_utf8(&buf, len, default_options());
|
|
res.templates(&arena).iter().map(|t| t.cooked_invalid()).collect()
|
|
};
|
|
assert_eq!(get(r"t = `\uZZ`;"), vec![true]); // bad unicode escape
|
|
assert_eq!(get(r"t = `\xG`;"), vec![true]); // bad hex escape
|
|
assert_eq!(get(r"t = `\u{110000}`;"), vec![true]); // out of range
|
|
assert_eq!(get(r"t = `\101`;"), vec![true]); // octal
|
|
assert_eq!(get(r"t = `\8`;"), vec![true]); // NonOctalDecimalEscape
|
|
assert_eq!(get(r"t = `\n`;"), vec![false]); // ordinary escape
|
|
assert_eq!(get(r"t = `\0`;"), vec![false]); // NUL, no digit after
|
|
assert_eq!(get("t = `abc`;"), vec![false]); // no escapes
|
|
assert_eq!(get(r"t = `a${b}c`;"), vec![false, false]); // head + tail
|
|
// still zero diagnostics, matching oxc_parser's silent lexer
|
|
assert!(codes(r"t = `\101`;").is_empty());
|
|
assert!(codes(r"t = `\uZZ`;").is_empty());
|
|
// strings are untouched by the marker path
|
|
assert!(codes(r#"s = "\8";"#).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn unterminated_jsx_element() {
|
|
assert_eq!(codes_tsx("var a = <T>(x: T) => x;"), vec![diag_code::UNTERMINATED_JSX_ELEMENT]);
|
|
assert_eq!(codes_tsx("f(<T>(x: T) => x);"), vec![diag_code::UNTERMINATED_JSX_ELEMENT]);
|
|
}
|
|
|
|
#[test]
|
|
fn generic_function_types_never_flagged() {
|
|
for code in [
|
|
"let a: <T>(x: T) => T = null!;",
|
|
"const r = f<{ m: <T>(x: T) => T }>(0);",
|
|
"let h = <T,>(x: T) => x;",
|
|
"const e = <T>(x)</T>;",
|
|
] {
|
|
assert!(codes_tsx(code).is_empty(), "{code:?}");
|
|
}
|
|
}
|
|
|
|
fn sorted_codes_tsx(code: &str) -> Vec<u16> {
|
|
let mut v = codes_tsx(code);
|
|
v.sort_unstable();
|
|
v
|
|
}
|
|
|
|
#[test]
|
|
fn unterminated_jsx() {
|
|
assert_eq!(codes_tsx("const a = <div>"), vec![diag_code::UNTERMINATED_JSX_ELEMENT]);
|
|
assert_eq!(codes_tsx("const a = <div>hi"), vec![diag_code::UNTERMINATED_JSX_ELEMENT]);
|
|
assert_eq!(codes_tsx("const a = <div><b>x</b>"), vec![diag_code::UNTERMINATED_JSX_ELEMENT]);
|
|
assert_eq!(codes_tsx("const a = <div attr"), vec![diag_code::UNTERMINATED_JSX_TAG]);
|
|
assert_eq!(codes_tsx("const a = <"), Vec::<u16>::new());
|
|
assert_eq!(codes_tsx("const a = <div>{x"), vec![diag_code::UNTERMINATED_JSX_CONTAINER]);
|
|
assert_eq!(codes_tsx("const a = <div>{x}</div"), vec![diag_code::UNTERMINATED_JSX_TAG]);
|
|
let mut want = vec![diag_code::UNTERMINATED_STRING, diag_code::UNTERMINATED_JSX_TAG];
|
|
want.sort_unstable();
|
|
assert_eq!(sorted_codes_tsx("const a = <div attr=\"x"), want);
|
|
assert_eq!(codes_tsx("const a = <div></span>;"), vec![diag_code::JSX_CLOSING_TAG_MISMATCH]);
|
|
assert_eq!(codes_tsx("const a = <></div>;"), vec![diag_code::JSX_CLOSING_TAG_MISMATCH]);
|
|
assert_eq!(codes_tsx("const a = <div></>;"), vec![diag_code::JSX_CLOSING_TAG_MISMATCH]);
|
|
assert_eq!(codes_tsx("const a = <A.B></A.C>;"), vec![diag_code::JSX_CLOSING_TAG_MISMATCH]);
|
|
assert_eq!(codes_jsx("const a = <div>{x"), vec![diag_code::UNTERMINATED_JSX_CONTAINER]);
|
|
assert_eq!(codes_jsx("const a = <div>{x}"), vec![diag_code::UNTERMINATED_JSX_ELEMENT]);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_jsx_never_flagged() {
|
|
for code in [
|
|
"const a = <div>hi</div>;",
|
|
"const a = <a b=\"x\ny\" c='z'/>;",
|
|
"const a = <></>;",
|
|
"const a = <A.B></A.B>;",
|
|
"const a = <a-b></a-b>;",
|
|
"const a = <a:b></a:b>;",
|
|
"const a = <div>{x}</div>;",
|
|
"const a = <div>{/* c */}</div>;",
|
|
"let u = <T>(a)</ /*b*/ T>;",
|
|
"let u = <T>(a)</T\n>;",
|
|
"const a = <div>\n <span>{1}</span>\n</div>;",
|
|
"const a = <Foo<Bar> x={1} />;",
|
|
"const a = <div>{cond ? <b/> : <i>{`t${x}`}</i>}</div>;",
|
|
"const a = <div>{[1, 2].map(n => <b key={n}>{n}</b>)}</div>;",
|
|
"const r = f<{ m: <T>(x: `a${T}b`) => Array<T> }>(0);\nconst z = <T>hi</T>;",
|
|
"const r = f<{ m: <T>(x: \"(\") => Array<T> }>(0);\nconst z = <T>hi</T>;",
|
|
] {
|
|
assert!(codes_tsx(code).is_empty(), "{code:?}: {:?}", codes_tsx(code));
|
|
}
|
|
for code in ["const a = <div>hi</div>;", "const a = <></>;", "const a = <a b=\"x\ny\"/>;"] {
|
|
assert!(codes_jsx(code).is_empty(), "{code:?}: {:?}", codes_jsx(code));
|
|
}
|
|
}
|
|
|
|
fn diags_jsx(code: &str) -> Vec<Diagnostic> {
|
|
let mut buf = code.as_bytes().to_vec();
|
|
let len = buf.len() as u32;
|
|
buf.resize(buf.len() + PAD, 0);
|
|
let mut opts = default_options();
|
|
opts.jsx = true;
|
|
let (res, _arena) = lex_utf8(&buf, len, opts);
|
|
res.diagnostics().to_vec()
|
|
}
|
|
|
|
#[test]
|
|
fn bare_gt_or_rbrace_in_jsx_text() {
|
|
let d = diags_jsx("let x = <div>></div>;");
|
|
assert_eq!(d.len(), 1, "{d:?}");
|
|
assert_eq!((d[0].code, d[0].off, d[0].len), (diag_code::JSX_TEXT_INVALID_CHARACTER, 13, 1));
|
|
let d = diags_jsx("let x = <div>}</div>;");
|
|
assert_eq!(d.len(), 1, "{d:?}");
|
|
assert_eq!((d[0].code, d[0].off, d[0].len), (diag_code::JSX_TEXT_INVALID_CHARACTER, 13, 1));
|
|
assert_eq!(codes_tsx("let x = <div>a>b</div>;"), vec![diag_code::JSX_TEXT_INVALID_CHARACTER]);
|
|
for code in
|
|
["let x = <div>{'>'}</div>;", "let x = <div>a > b</div>;", "let x = <div>{x}</div>;"]
|
|
{
|
|
assert!(codes_tsx(code).is_empty(), "{code:?}: {:?}", codes_tsx(code));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn jsx_closing_names_compare_across_trivia() {
|
|
for code in [
|
|
"x = <A.B.C.D>foo</A . B . C.D>;",
|
|
"x = <A.B>t</A\n.B>;",
|
|
"x = <A.B>t</A./*c*/B>;",
|
|
"x = <a:b>t</a : b>;",
|
|
"x = <a>t< b>u</ b ></a>;",
|
|
"x = <a--b>t</a--b>;",
|
|
] {
|
|
assert!(codes_tsx(code).is_empty(), "{code:?}: {:?}", codes_tsx(code));
|
|
}
|
|
assert_eq!(codes_tsx("x = <A.B>t</A.C>;"), vec![diag_code::JSX_CLOSING_TAG_MISMATCH]);
|
|
assert_eq!(codes_tsx("x = <a-b>t</a-c>;"), vec![diag_code::JSX_CLOSING_TAG_MISMATCH]);
|
|
assert_eq!(codes_tsx("x = <A.B>t</A>;"), vec![diag_code::JSX_CLOSING_TAG_MISMATCH]);
|
|
}
|
|
|
|
#[test]
|
|
fn id_continue_only_character_cannot_start_an_identifier() {
|
|
let d = diags("\u{203f} = 10;");
|
|
assert_eq!(d.len(), 1, "{d:?}");
|
|
assert_eq!((d[0].code, d[0].off, d[0].len), (diag_code::UNEXPECTED_CHARACTER, 0, 3));
|
|
let d = diags("var \u{200c} = [];");
|
|
assert_eq!(d.len(), 1, "{d:?}");
|
|
assert_eq!((d[0].code, d[0].off, d[0].len), (diag_code::UNEXPECTED_CHARACTER, 4, 3));
|
|
assert_eq!(codes("var \u{200d} = [];"), vec![diag_code::UNEXPECTED_CHARACTER]);
|
|
assert_eq!(codes("class C { #\u{203f} = 1 }"), vec![diag_code::UNEXPECTED_CHARACTER]);
|
|
for code in ["a\u{203f} = 10;", "a\u{200c}b = 1;", "var x = \"\u{203f}\";", "x = a.\u{e9};"] {
|
|
assert!(codes(code).is_empty(), "{code:?}: {:?}", codes(code));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_octal_adjacency_diagnostics() {
|
|
assert_eq!(codes("x = 010e5;"), vec![diag_code::INVALID_NUMERIC_LITERAL]);
|
|
assert_eq!(codes("x = 010n;"), vec![diag_code::INVALID_BIGINT]);
|
|
assert_eq!(codes("x = 08n;"), vec![diag_code::INVALID_BIGINT]);
|
|
for code in ["x = 010.5;", "x = 010.toString();", "x = 08.5;", "x = 09e1;"] {
|
|
assert!(codes(code).is_empty(), "{code:?}: {:?}", codes(code));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_jsx_shapes_never_flagged() {
|
|
for code in [
|
|
"const a = <Foo prop=<Bar><Baz /></Bar> />;",
|
|
"const a = <Foo key=<T></T>>{x}\n <b />\n</Foo>;",
|
|
"const a = <Foo key=<T/> other=\"x\">{x}</Foo>;",
|
|
"x = <a b=<>t</> />;",
|
|
"const a = <div>\n x<br />\n < br />\n y\n</div>;",
|
|
"export const foo = function* <T>() {};",
|
|
"const h = async function* <T>() {};",
|
|
"type X = { <T>(x: T): U; }",
|
|
"interface X { <T>(x: T): U; new <T>(x: T): U; }",
|
|
"x = [function (): T {}\n< y];",
|
|
] {
|
|
assert!(codes_tsx(code).is_empty(), "{code:?}: {:?}", codes_tsx(code));
|
|
}
|
|
for code in ["<App foo=<div>bar</div> />;", "x = <a b=<c>{d}</c>>{e}<f/></a>;", "x = <a--b/>;"]
|
|
{
|
|
assert!(codes_jsx(code).is_empty(), "{code:?}: {:?}", codes_jsx(code));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn value_lanes_hold_exactly_one_entry_per_value_token() {
|
|
for (code, ts, numbers, atoms) in [
|
|
("0 #a 0 #b 0 #c 0 #d 0 #e 0 #f 0 #g 0 #h 0 #i", false, 9, 0),
|
|
(
|
|
"class C { #e: number = 0
|
|
#f: string = 1
|
|
#g = 2
|
|
#h = 3
|
|
#i = 4
|
|
#j = 5
|
|
#k = 6
|
|
#l = 7
|
|
#m = 8 }",
|
|
true,
|
|
9,
|
|
0,
|
|
),
|
|
("1n 's' 2n 's' 3n 's' 4n 's' 5n 's' 6n 's' 7n 's' 8n 's' 9n 's'", false, 9, 0),
|
|
(
|
|
"\\u0061b 0 \\u0061b 0 \\u0061b 0 \\u0061b 0 \\u0061b 0 \\u0061b 0 \\u0061b 0 \\u0061b 0 \\u0061b 0",
|
|
false,
|
|
9,
|
|
9,
|
|
),
|
|
(
|
|
"#\\u0061b x #\\u0061b x #\\u0061b x #\\u0061b x #\\u0061b x #\\u0061b x #\\u0061b x #\\u0061b x #\\u0061b x",
|
|
false,
|
|
0,
|
|
9,
|
|
),
|
|
] {
|
|
let mut buf = code.as_bytes().to_vec();
|
|
let len = buf.len() as u32;
|
|
buf.resize(buf.len() + PAD, 0);
|
|
let mut opts = default_options();
|
|
opts.ts = ts;
|
|
let (res, _arena) = lex_utf8(&buf, len, opts);
|
|
assert_eq!(res.numbers_count, numbers, "numbers lane for {code:?}");
|
|
assert_eq!(res.atoms_count, atoms, "atoms lane for {code:?}");
|
|
assert_eq!(res.diagnostic_count, 0, "diagnostics for {code:?}: {:?}", res.diagnostics());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn block_comment_star_plus_slash_is_not_a_terminator() {
|
|
for (code, tokens) in [
|
|
("x = /* a *+/ b */ 1;", 4),
|
|
("/* *+/ */ x", 1),
|
|
("x = /**+/**/ 1;", 4),
|
|
(
|
|
"/*
|
|
*+/ still comment
|
|
*/ y",
|
|
1,
|
|
),
|
|
] {
|
|
let mut buf = code.as_bytes().to_vec();
|
|
let len = buf.len() as u32;
|
|
buf.resize(buf.len() + PAD, 0);
|
|
let (res, arena) = lex_utf8(&buf, len, default_options());
|
|
let kinds = res.tok_kinds(&arena);
|
|
let non_trivia = kinds.iter().filter(|k| !k.is_trivia() && **k != TokenKind::Eof).count();
|
|
assert_eq!(non_trivia, tokens, "{code:?}: {kinds:?}");
|
|
assert_eq!(res.diagnostic_count, 0, "{code:?}: {:?}", res.diagnostics());
|
|
}
|
|
}
|