diff --git a/src/ast/lib.rs b/src/ast/lib.rs index f4c86093762d..1e1406e213f9 100644 --- a/src/ast/lib.rs +++ b/src/ast/lib.rs @@ -1601,6 +1601,19 @@ impl Log { (self.warnings + self.errors) > 0 } + /// Takes back every message after the first `len`, with its count in `errors` or `warnings`. + #[cold] + #[inline(never)] + pub fn truncate(&mut self, len: usize) { + for msg in self.msgs.drain(len.min(self.msgs.len())..) { + match msg.kind { + Kind::Err => self.errors -= 1, + Kind::Warn => self.warnings -= 1, + _ => {} + } + } + } + pub fn init() -> Log { let level = DEFAULT_LOG_LEVEL.load(); Log { diff --git a/src/bunfig/arguments.rs b/src/bunfig/arguments.rs index 08abed8e9609..1f2777885888 100644 --- a/src/bunfig/arguments.rs +++ b/src/bunfig/arguments.rs @@ -426,14 +426,7 @@ pub fn load_config_above_for_tool( report_bunfig_load_failure_as(cmd, ctx.log, err, Some(&shown)); } // SAFETY: as above. - let log = unsafe { &mut *ctx.log }; - for it in log.msgs.drain(said..) { - match it.kind { - bun_ast::Kind::Err => log.errors -= 1, - bun_ast::Kind::Warn => log.warnings -= 1, - _ => {} - } - } + unsafe { &mut *ctx.log }.truncate(said); return None; } if above == Above::Unwanted { diff --git a/src/js_parser/lexer.rs b/src/js_parser/lexer.rs index 26f624f19c00..fea1c8d8949b 100644 --- a/src/js_parser/lexer.rs +++ b/src/js_parser/lexer.rs @@ -136,7 +136,7 @@ pub struct ScanResult<'a> { /// p.lexer.restore(&snap); /// ``` /// -/// This struct is `Copy` and intentionally excludes `log`, `source`, `arena` +/// This struct is `Copy` and intentionally excludes `source`, `arena` /// (shared/unique borrows that never change across a backtrack) and the three /// growable `Vec` buffers (captured as lengths only — `restore()` truncates). #[derive(Clone, Copy)] @@ -172,6 +172,7 @@ pub struct LexerSnapshot<'a> { // Vec buffer lengths — restore() truncates back to these. pub(crate) all_comments_len: usize, pub(crate) comments_to_preserve_before_len: usize, + pub(crate) log_msgs_len: usize, } /// `'a` is the lifetime of the source contents (arena/source-owned slices like @@ -358,11 +359,12 @@ impl<'a> Lexer<'a> { track_react_suppressions: self.track_react_suppressions, all_comments_len: self.all_comments.len(), comments_to_preserve_before_len: self.comments_to_preserve_before.len(), + log_msgs_len: self.log().msgs.len(), } } /// Rewind to a prior `snapshot()`: copy each scalar field and - /// truncate the Vecs to their snapshotted lengths. `log`/`source`/`arena` + /// truncate the Vecs to their snapshotted lengths. `source`/`arena` /// are left untouched. pub(crate) fn restore(&mut self, original: &LexerSnapshot<'a>) { // Keep this field list in sync with `snapshot()` and the Lexer struct fields. @@ -404,6 +406,19 @@ impl<'a> Lexer<'a> { self.all_comments.truncate(original.all_comments_len); self.comments_to_preserve_before .truncate(original.comments_to_preserve_before_len); + if self.log().msgs.len() > original.log_msgs_len { + self.log().truncate(original.log_msgs_len); + } + } + + /// Whether an error was logged after `snapshot` was taken. + #[inline] + pub(crate) fn logged_error_since(&self, snapshot: &LexerSnapshot<'a>) -> bool { + let msgs = &self.log().msgs; + msgs.len() > snapshot.log_msgs_len + && msgs[snapshot.log_msgs_len..] + .iter() + .any(|msg| msg.kind == bun_ast::Kind::Err) } /// Look ahead at the next n codepoints without advancing the iterator. diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 93aedee2f97c..53fe26e9e705 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -168,9 +168,6 @@ impl<'a> core::ops::DerefMut for NamedImportsType<'a> { pub(crate) struct ParserSnapshot<'a> { lexer: js_lexer::LexerSnapshot<'a>, comments_to_preserve_before: Vec, - log_msgs_len: usize, - log_errors: u32, - log_warnings: u32, allow_in: bool, allow_private_identifiers: bool, has_classic_runtime_warned: bool, @@ -8193,9 +8190,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O /// Everything the parse pass mutates, so that a speculative parse of an /// expression can be undone with [`Self::restore_parser_snapshot`]. The /// lexer-only backtracking in `parse_skip_typescript.rs` only covers - /// types: an expression also declares symbols, pushes scopes, records - /// dynamic imports and logs through `P::log()`, which ignores - /// `lexer.is_log_disabled`. Lists are captured as lengths and truncated + /// types: an expression also declares symbols, pushes scopes and records + /// dynamic imports. Lists are captured as lengths and truncated /// on restore; the attempt's arena allocations just become unreachable. /// The legal comments waiting for the next statement are moved out /// instead: a block body or `import()` drains them, so a length cannot @@ -8203,13 +8199,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O pub(crate) fn parser_snapshot(&mut self) -> ParserSnapshot<'a> { let comments_to_preserve_before = core::mem::take(&mut self.lexer.comments_to_preserve_before); - let log = self.log(); ParserSnapshot { lexer: self.lexer.snapshot(), comments_to_preserve_before, - log_msgs_len: log.msgs.len(), - log_errors: log.errors, - log_warnings: log.warnings, allow_in: self.allow_in, allow_private_identifiers: self.allow_private_identifiers, has_classic_runtime_warned: self.has_classic_runtime_warned, @@ -8239,11 +8231,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.lexer.restore(&snapshot.lexer); self.lexer.comments_to_preserve_before = snapshot.comments_to_preserve_before; - let log = self.log(); - log.msgs.truncate(snapshot.log_msgs_len); - log.errors = snapshot.log_errors; - log.warnings = snapshot.log_warnings; - self.allow_in = snapshot.allow_in; self.allow_private_identifiers = snapshot.allow_private_identifiers; self.has_classic_runtime_warned = snapshot.has_classic_runtime_warned; diff --git a/src/js_parser/parse/parse_skip_typescript.rs b/src/js_parser/parse/parse_skip_typescript.rs index ef1b8d8d917e..1a0cb6dad893 100644 --- a/src/js_parser/parse/parse_skip_typescript.rs +++ b/src/js_parser/parse/parse_skip_typescript.rs @@ -1210,6 +1210,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } if self.lexer.is_contextual_keyword(b"out") { + // As in tsc, "out" is the name unless a word follows it on the same line. + if !flags.contains(TypeParameterFlag::ALLOW_IN_OUT_VARIANCE_ANNOTATIONS) + && !self.word_follows_on_same_line() + { + break; + } + let r = self.lexer.range(); if invalid_modifier_range.len == 0 && !flags.contains(TypeParameterFlag::ALLOW_IN_OUT_VARIANCE_ANNOTATIONS) @@ -1294,6 +1301,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O Ok(result) } + /// Whether an identifier or keyword follows the current token on its line. + #[cold] + #[inline(never)] + fn word_follows_on_same_line(&mut self) -> bool { + self.next_token_matches(|p| { + !p.lexer.has_newline_before && p.lexer.is_identifier_or_keyword() + }) + } + pub(crate) fn skip_type_script_type_stmt( &mut self, opts: &mut ParseStatementOptions, diff --git a/src/js_parser/typescript.rs b/src/js_parser/typescript.rs index 9997da5974ca..d367c71f6ec3 100644 --- a/src/js_parser/typescript.rs +++ b/src/js_parser/typescript.rs @@ -66,6 +66,11 @@ impl<'a, const TS: bool, const SCAN: bool> P<'a, TS, SCAN> { } } + // The attempt that reads these tokens next logs no lexer error. + if p.lexer.logged_error_since(&old_lexer) { + return Err(crate::Error::SyntaxError); + } + // Restore the lexer p.lexer.restore(&old_lexer); Ok(is_ts_arrow_fn) diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 2d68d35449d2..f964ceee31dc 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -994,6 +994,218 @@ describe("Bun.Transpiler", () => { }); }); + describe("the word `out` after `<`", () => { + // The printed code, or the message of every error and warning when there is one. + const run = (code, loader) => { + try { + return transpiler.transformSync(code, loader); + } catch (e) { + return (e instanceof AggregateError ? e.errors : [e]).map(e => e.message); + } + }; + const check = (loader, expected) => { + expect(Object.fromEntries(Object.keys(expected).map(code => [code, run(code, loader)]))).toEqual(expected); + }; + const outModifier = ['The modifier "out" is not valid here']; + const inModifier = ['The modifier "in" is not valid here']; + + it.each(["ts", "tsx"])("a left shift by a variable named out is a shift (%s)", loader => { + // "<<" starts an attempt at type arguments. The attempt fails, and what + // it logged about " d": "x = c << out > d;\n", + "x = c << out >> d": "x = c << out >> d;\n", + "x = a << out.b": "x = a << out.b;\n", + "x = a << out[0]": "x = a << out[0];\n", + "x = a << out()": "x = a << out();\n", + "for (let i = 0; i << out; i++) {}": "for (let i = 0;i << out; i++) {}\n", + // The type parameter list closes before the attempt fails. + "x = a << out in b > c": "x = a << out in b > c;\n", + // More than one attempt reads the same tokens. + "x = a << out << out": "x = a << out << out;\n", + "x = a < b << out as c": "x = a < b << out;\n", + "x = new a < b << out as c": "x = new a < b << out;\n", + "switch (x) { case (a): b << out as c; }": "switch (x) {\n case a:\n b << out;\n}\n", + // The attempt that fails is at an arrow function return type. + "x = a ? (b) : c << out as d": "x = a ? b : c << out;\n", + }); + }); + + it("`` before an expression is a type assertion or a type parameter list", () => { + check("ts", { + "x = y": "x = y;\n", + "x = (y)": "x = y;\n", + "x = (y) => y": "x = (y) => y;\n", + "x = async (y)": "x = async(y);\n", + "x = async (y) => y": "x = async (y) => y;\n", + // "out extends" is a modifier for the attempt at type parameters. The + // attempt fails, and the text is a conditional type. + "x = y": "x = y;\n", + "x = async (y)": "x = async(y);\n", + }); + check("tsx", { + "x = async (y)": "x = async(y);\n", + "x = (y) => y": "x = (y) => y;\n", + "x = async (y) => y": "x = async (y) => y;\n", + "x = async (y)": "x = async(y);\n", + // A JSX element named "out" on the right of "<" closes a type parameter + // list inside an attempt at type arguments that fails later. + "x = a < ": + 'x = a < jsxDEV_7x81h0kn("out", {\n T: true\n}, undefined, false, undefined, this);\n', + }); + }); + + it.each(["ts", "tsx"])("out is the name of a type parameter when no word follows it on its line (%s)", loader => { + check(loader, { + "function f(v: out) {}": "function f(v) {}\n", + "function f(v: out) {}": "function f(v) {}\n", + "function f(v: out) {}": "function f(v) {}\n", + "function f(v: out) {}": "function f(v) {}\n", + "function f(v: out) {}": "function f(v) {}\n", + "function f(v: out) {}": "function f(v) {}\n", + "x = function (v: out) {}": "x = function(v) {};\n", + "class C { m(v: out) {} }": "class C {\n m(v) {}\n}\n", + "x = { m(v: out) {} }": "x = { m(v) {} };\n", + "let x: (v: out) => out": "let x;\n", + "let x: new (v: out) => out": "let x;\n", + "let x: { y(): any }": "let x;\n", + "declare function f(v: out): out": "", + "new Foo<() => void>()": "new Foo;\n", + "foo<() => void>()": "foo();\n", + }); + }); + + it.each(["ts", "tsx"])("a misplaced modifier is still one error (%s)", loader => { + check(loader, { + "function f(v: T) {}": outModifier, + "function f(v: out) {}": outModifier, + "function f() {}": outModifier, + "function f() {}": outModifier, + "function f() {}": outModifier, + "function f() {}": inModifier, + "function f(v: T) {}": inModifier, + // The attempt is kept, so what it logged stays. + "foo<() => void>()": outModifier, + "foo<() => void>()": inModifier, + "x = (a): () => void => 1": inModifier, + "let x: ((a: () => void) => void)": inModifier, + "x = a ? (b): () => void => c : d": outModifier, + "foo<[const: number]>()": ['Unexpected "const"'], + }); + // The name is "out", and the word on the next line does not belong to the list. + expect(run("function f() {}", loader)[0]).toBe('Expected ">" but found "T"'); + }); + + it.each(["ts", "tsx"])("a failed attempt leaves the errors to the parse that follows it (%s)", loader => { + check(loader, { + "x = a << in": ["Unexpected in"], + "x = a << out T > (b)": ['Expected ";" but found "T"', "Unexpected >"], + "x = a < [const: b] > c": ["Unexpected const"], + "function f" but found end of file'], + // An attempt that is kept inside one that fails. + "x = a < ((b: () => void) => void) > c": [...inModifier, "Unexpected )"], + // An error from before the attempt stays. + "function f() {}\nx = c << out as number": inModifier, + "function f() {}\nx = a < [const: b] > c": [...inModifier, "Unexpected const"], + }); + // The attempt that fails is at type parameters before "(". + expect(run("x = y", "ts")).toEqual(["Unexpected in"]); + }); + + it("a rewind of the lexer takes back the warning of the line it reads again", () => { + // Each "-->" line is read by a lookahead or by an attempt that fails, + // and then a second time. Bun.Transpiler throws warnings too. + const warning = ['Treating "-->" as the start of a legacy HTML single-line comment']; + check("ts", { + "x = a < b\n--> c\n": warning, + "x = c\n>y": warning, + "function f c\n>(v: out) {}": warning, + "for (async\n--> c\nin {});": warning, + 'import type from\n--> c\n"./x";': warning, + 'x = a < b > import\n--> c\n("y")': warning, + }); + check("js", { "for (async\n--> c\nin {});": warning }); + check("tsx", { + "var x = <\n--> c\nT,>(a: T) => a;": warning, + // The attempt at type arguments reads "--> t" as a comment. It is the text of the element. + "x = a < \n--> t": + 'x = a < jsxDEV_7x81h0kn("b", {\n children: "--> t"\n}, undefined, false, undefined, this);\n', + }); + // The lookahead for a TSX arrow function logs the error of the escaped + // name. The attempt that reads the name next does not, so the error stays. + check("tsx", { + "x = async <\\u{30}a,>(y) => y": ['Invalid identifier: "0a"'], + "x = <\\u{30}a,>(y) => y": ['Invalid identifier: "0a"'], + }); + }); + + it("every Bun.Transpiler API takes a left shift by out", async () => { + const code = "import a from 'mod';\nexport const r = a << out;\n"; + const imports = [{ kind: "import-statement", path: "mod" }]; + const printed = 'import a from "mod";\nexport const r = a << out;\n'; + expect({ + scanImports: transpiler.scanImports(code, "ts"), + scan: transpiler.scan(code, "ts"), + transformSync: transpiler.transformSync(code, "ts"), + transform: await transpiler.transform(code, "ts"), + }).toEqual({ + scanImports: imports, + scan: { exports: ["r"], imports }, + transformSync: printed, + transform: printed, + }); + }); + + it("bun runs the shift, and bun build warns once about a legacy comment", async () => { + using dir = tempDir("ts-shift-by-out", { + "shift.ts": ` + const c: number = 3, out: number = 2, buf = [2]; + function id(v: out): out { return v; } + console.log(c << out, 1 << out, (c << out) | 1, c << out + 1, buf[0] << out, id(c << out as number)); + const r = c << out + console.log(r) + `, + // The attempt at type arguments after "<" reads the "-->" line, fails, + // and the line is read a second time. + "legacy-comment.ts": "var a = 1, b = 2, x;\nx = a < b\n--> legacy comment\nconsole.log(x);\n", + }); + const [shift, evaluated, built] = await Promise.all( + [ + ["shift.ts"], + // "bun -e" reads plain JavaScript with the TSX loader. + ["-e", "var c = 3, out = 2; console.log(c << out)"], + ["build", "--no-bundle", "legacy-comment.ts"], + ].map(async args => { + await using proc = Bun.spawn({ + cmd: [bunExe(), ...args], + env: { ...bunEnv, NO_COLOR: "1" }, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + }), + ); + expect(shift).toEqual({ stdout: "12 4 13 24 8 12\n12\n", stderr: "", exitCode: 0 }); + expect(evaluated).toEqual({ stdout: "12\n", stderr: "", exitCode: 0 }); + expect({ + stdout: built.stdout, + warnings: built.stderr.split('Treating "-->" as the start of a legacy HTML single-line comment').length - 1, + exitCode: built.exitCode, + }).toEqual({ stdout: "var a = 1, b = 2, x;\nx = a < b;\nconsole.log(x);\n", warnings: 1, exitCode: 0 }); + }); + }); + it.todo("instantiation expressions", async () => { const exp = ts.expectPrinted_; const err = ts.expectParseError;