Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/ast/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 1 addition & 8 deletions src/bunfig/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 17 additions & 2 deletions src/js_parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
Comment thread
robobun marked this conversation as resolved.
}

/// 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.
Expand Down
17 changes: 2 additions & 15 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<js_ast::G::Comment>,
log_msgs_len: usize,
log_errors: u32,
log_warnings: u32,
allow_in: bool,
allow_private_identifiers: bool,
has_classic_runtime_warned: bool,
Expand Down Expand Up @@ -8193,23 +8190,18 @@ 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
Comment thread
robobun marked this conversation as resolved.
/// 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
/// restore them.
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,
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/js_parser/parse/parse_skip_typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
robobun marked this conversation as resolved.
&& !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)
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/js_parser/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
212 changes: 212 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<out" must not stay.
check(loader, {
"c << out": "c << out;\n",
"x = c << out as number": "x = c << out;\n",
"x = c << out in d": "x = c << out in d;\n",
"x = c << out instanceof d": "x = c << out instanceof d;\n",
"x = c << out satisfies number": "x = c << out;\n",
"x = c << out\nfoo()": "x = c << out;\nfoo();\n",
"x = a << out, b": "x = a << out, b;\n",
"x = a << out ? 1 : 2": "x = a << out ? 1 : 2;\n",
"x = c << out < d": "x = c << out < d;\n",
"x = c << out > 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("`<out>` before an expression is a type assertion or a type parameter list", () => {
check("ts", {
"x = <out>y": "x = y;\n",
"x = <out>(y)": "x = y;\n",
"x = <out>(y) => y": "x = (y) => y;\n",
"x = async <out>(y)": "x = async(y);\n",
"x = async <out>(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 = <out extends string ? 1 : 2>y": "x = y;\n",
"x = async <out extends string ? 1 : 2>(y)": "x = async(y);\n",
});
check("tsx", {
"x = async <out>(y)": "x = async(y);\n",
"x = <out,>(y) => y": "x = (y) => y;\n",
"x = async <out,>(y) => y": "x = async (y) => y;\n",
"x = async <out extends string ? 1 : 2>(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 < <out T></out>":
'x = a < jsxDEV_7x81h0kn("out", {\n T: true\n}, undefined, false, undefined, this);\n',
});
});
Comment thread
robobun marked this conversation as resolved.

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<out>(v: out) {}": "function f(v) {}\n",
"function f<out, T>(v: out) {}": "function f(v) {}\n",
"function f<T, out>(v: out) {}": "function f(v) {}\n",
"function f<out = number>(v: out) {}": "function f(v) {}\n",
"function f<const out>(v: out) {}": "function f(v) {}\n",
"function f<out\n>(v: out) {}": "function f(v) {}\n",
"x = function <out>(v: out) {}": "x = function(v) {};\n",
"class C { m<out>(v: out) {} }": "class C {\n m(v) {}\n}\n",
"x = { m<out>(v: out) {} }": "x = { m(v) {} };\n",
"let x: <out>(v: out) => out": "let x;\n",
"let x: new <out>(v: out) => out": "let x;\n",
"let x: { y<out>(): any }": "let x;\n",
"declare function f<out>(v: out): out": "",
"new Foo<<out>() => void>()": "new Foo;\n",
"foo<<out>() => void>()": "foo();\n",
});
});

it.each(["ts", "tsx"])("a misplaced modifier is still one error (%s)", loader => {
check(loader, {
"function f<out T>(v: T) {}": outModifier,
"function f<out extends number>(v: out) {}": outModifier,
"function f<out in T>() {}": outModifier,
"function f<out const T>() {}": outModifier,
"function f<out out>() {}": outModifier,
"function f<in out>() {}": inModifier,
"function f<in T>(v: T) {}": inModifier,
// The attempt is kept, so what it logged stays.
"foo<<out T>() => void>()": outModifier,
"foo<<in T>() => void>()": inModifier,
"x = (a): <in T>() => void => 1": inModifier,
"let x: ((a: <in T>() => void) => void)": inModifier,
"x = a ? (b): <out T>() => 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<out\nT>() {}", 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<out": ['Expected ">" but found end of file'],
// An attempt that is kept inside one that fails.
"x = a < ((b: <in T>() => void) => void) > c": [...inModifier, "Unexpected )"],
// An error from before the attempt stays.
"function f<in T>() {}\nx = c << out as number": inModifier,
"function f<in T>() {}\nx = a < [const: b] > c": [...inModifier, "Unexpected const"],
});
// The attempt that fails is at type parameters before "(".
expect(run("x = <in T>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 = <T\n--> c\n>y": warning,
"function f<out\n--> 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 < <b>\n--> t</b>":
'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<out>(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;
Expand Down
Loading