diff --git a/src/bun.js/BuildMessage.zig b/src/bun.js/BuildMessage.zig index 74bcce2ba9..879b5ab930 100644 --- a/src/bun.js/BuildMessage.zig +++ b/src/bun.js/BuildMessage.zig @@ -27,6 +27,24 @@ pub const BuildMessage = struct { return null; } + pub fn getNotes(this: *BuildMessage, globalThis: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + const notes: []const logger.Data = this.msg.notes orelse &[_]logger.Data{}; + const array = JSC.JSValue.createEmptyArray(globalThis, notes.len); + for (notes, 0..) |note, i| { + const cloned = note.clone(bun.default_allocator) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; + array.putIndex( + globalThis, + @intCast(i), + BuildMessage.create(globalThis, bun.default_allocator, logger.Msg{ .data = cloned, .kind = .note }), + ); + } + + return array; + } + pub fn toStringFn(this: *BuildMessage, globalThis: *JSC.JSGlobalObject) JSC.JSValue { var text = std.fmt.allocPrint(default_allocator, "BuildMessage: {s}", .{this.msg.data.text}) catch { globalThis.throwOutOfMemory(); diff --git a/src/bun.js/resolve_message.classes.ts b/src/bun.js/resolve_message.classes.ts index 5bdd08eac3..db7659019f 100644 --- a/src/bun.js/resolve_message.classes.ts +++ b/src/bun.js/resolve_message.classes.ts @@ -78,6 +78,12 @@ export default [ getter: "getPosition", cache: true, }, + + notes: { + getter: "getNotes", + cache: true, + }, + ["@@toPrimitive"]: { fn: "toPrimitive", length: 1, diff --git a/src/js_lexer.zig b/src/js_lexer.zig index 17c70026a1..852dea69d2 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -165,6 +165,9 @@ fn NewLexer_( number: f64 = 0.0, rescan_close_brace_as_template_token: bool = false, prev_error_loc: logger.Loc = logger.Loc.Empty, + prev_token_was_await_keyword: bool = false, + await_keyword_loc: logger.Loc = logger.Loc.Empty, + fn_or_arrow_start_loc: logger.Loc = logger.Loc.Empty, regex_flags_start: ?u16 = null, allocator: std.mem.Allocator, /// In JavaScript, strings are stored as UTF-16, but nearly every string is ascii. @@ -212,6 +215,9 @@ fn NewLexer_( .string_literal_is_ascii = self.string_literal_is_ascii, .is_ascii_only = self.is_ascii_only, .all_comments = self.all_comments, + .prev_token_was_await_keyword = self.prev_token_was_await_keyword, + .await_keyword_loc = self.await_keyword_loc, + .fn_or_arrow_start_loc = self.fn_or_arrow_start_loc, }; } @@ -269,6 +275,31 @@ fn NewLexer_( // } } + pub fn addRangeErrorWithNotes(self: *LexerType, r: logger.Range, comptime format: []const u8, args: anytype, notes: []const logger.Data) !void { + @setCold(true); + + if (self.is_log_disabled) return; + if (self.prev_error_loc.eql(r.loc)) { + return; + } + + const errorMessage = std.fmt.allocPrint(self.allocator, format, args) catch unreachable; + try self.log.addRangeErrorWithNotes( + &self.source, + r, + errorMessage, + try self.log.msgs.allocator.dupe( + logger.Data, + notes, + ), + ); + self.prev_error_loc = r.loc; + + // if (panic) { + // return Error.ParserError; + // } + } + /// Look ahead at the next n codepoints without advancing the iterator. /// If fewer than n codepoints are available, then return the remainder of the string. fn peek(it: *LexerType, n: usize) string { @@ -1109,6 +1140,7 @@ fn NewLexer_( pub fn next(lexer: *LexerType) !void { lexer.has_newline_before = lexer.end == 0; lexer.has_pure_comment_before = false; + lexer.prev_token_was_await_keyword = false; while (true) { lexer.start = lexer.end; @@ -1819,6 +1851,32 @@ fn NewLexer_( } pub fn expectedString(self: *LexerType, text: string) !void { + if (self.prev_token_was_await_keyword) { + var notes: [1]logger.Data = undefined; + if (!self.fn_or_arrow_start_loc.isEmpty()) { + notes[0] = logger.rangeData( + &self.source, + rangeOfIdentifier( + &self.source, + self.fn_or_arrow_start_loc, + ), + "Consider adding the \"async\" keyword here", + ); + } + + const notes_ptr: []const logger.Data = notes[0..@as( + usize, + @intFromBool(!self.fn_or_arrow_start_loc.isEmpty()), + )]; + + try self.addRangeErrorWithNotes( + self.range(), + "\"await\" can only be used inside an \"async\" function", + .{}, + notes_ptr, + ); + return; + } if (self.source.contents.len != self.start) { try self.addRangeError( self.range(), diff --git a/src/js_parser.zig b/src/js_parser.zig index 509c6cff9d..bbaa62d758 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -2606,6 +2606,7 @@ const AwaitOrYield = enum(u3) { // arrow expressions. const FnOrArrowDataParse = struct { async_range: logger.Range = logger.Range.None, + needs_async_loc: logger.Loc = logger.Loc.Empty, allow_await: AwaitOrYield = AwaitOrYield.allow_ident, allow_yield: AwaitOrYield = AwaitOrYield.allow_ident, allow_super_call: bool = false, @@ -7372,6 +7373,7 @@ fn NewParser_( var scopeIndex = try p.pushScopeForParsePass(js_ast.Scope.Kind.function_args, p.lexer.loc()); var func = try p.parseFn(name, FnOrArrowDataParse{ + .needs_async_loc = loc, .async_range = asyncRange orelse logger.Range.None, .has_async_range = asyncRange != null, .allow_await = if (is_async) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, @@ -7487,6 +7489,9 @@ fn NewParser_( else AwaitOrYield.allow_ident; + // Don't suggest inserting "async" before anything if "await" is found + p.fn_or_arrow_data_parse.needs_async_loc = logger.Loc.Empty; + // If "super()" is allowed in the body, it's allowed in the arguments p.fn_or_arrow_data_parse.allow_super_call = opts.allow_super_call; p.fn_or_arrow_data_parse.allow_super_property = opts.allow_super_property; @@ -11833,6 +11838,7 @@ fn NewParser_( } const func = try p.parseFn(name, FnOrArrowDataParse{ + .needs_async_loc = loc, .async_range = async_range, .allow_await = if (is_async) .allow_expr else .allow_ident, .allow_yield = if (is_generator) .allow_expr else .allow_ident, @@ -12002,7 +12008,9 @@ fn NewParser_( async_range.loc, ) }; _ = p.pushScopeForParsePass(.function_args, async_range.loc) catch unreachable; - var data = FnOrArrowDataParse{}; + var data = FnOrArrowDataParse{ + .needs_async_loc = async_range.loc, + }; var arrow_body = try p.parseArrowBody(args, &data); p.popScope(); return p.newExpr(arrow_body, async_range.loc); @@ -12019,7 +12027,7 @@ fn NewParser_( B.Identifier{ .ref = ref, }, - async_range.loc, + p.lexer.loc(), ) }; try p.lexer.next(); @@ -12028,6 +12036,7 @@ fn NewParser_( var data = FnOrArrowDataParse{ .allow_await = .allow_expr, + .needs_async_loc = args[0].binding.loc, }; var arrowBody = try p.parseArrowBody(args, &data); arrowBody.is_async = true; @@ -12779,6 +12788,7 @@ fn NewParser_( var func = try p.parseFn(null, FnOrArrowDataParse{ .async_range = opts.async_range, + .needs_async_loc = key.loc, .has_async_range = !opts.async_range.isEmpty(), .allow_await = if (opts.is_async) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, .allow_yield = if (opts.is_generator) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, @@ -14053,7 +14063,11 @@ fn NewParser_( return p.newExpr(E.Await{ .value = value }, loc); } }, - else => {}, + .allow_ident => { + p.lexer.prev_token_was_await_keyword = true; + p.lexer.await_keyword_loc = name_range.loc; + p.lexer.fn_or_arrow_start_loc = p.fn_or_arrow_data_parse.needs_async_loc; + }, } }, @@ -14107,9 +14121,10 @@ fn NewParser_( _ = p.pushScopeForParsePass(.function_args, loc) catch unreachable; defer p.popScope(); - var fn_or_arrow_data = FnOrArrowDataParse{}; - const ret = p.newExpr(try p.parseArrowBody(args, &fn_or_arrow_data), loc); - return ret; + var fn_or_arrow_data = FnOrArrowDataParse{ + .needs_async_loc = loc, + }; + return p.newExpr(try p.parseArrowBody(args, &fn_or_arrow_data), loc); } const ref = p.storeNameInRef(name) catch unreachable; diff --git a/src/logger.zig b/src/logger.zig index a34df715c8..134eaa05ee 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -517,7 +517,10 @@ pub const Msg = struct { for (notes) |*note| { note.deinit(allocator); } + + allocator.free(notes); } + msg.notes = null; } diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index d9af8aba0e..49ce961c48 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -2086,7 +2086,7 @@ describe("utimesSync", () => { expect(finalStats.atime).toEqual(prevAccessTime); }); - it.only("works after 2038", () => { + it("works after 2038", () => { const tmp = join(tmpdir(), "utimesSync-test-file-" + Math.random().toString(36).slice(2)); writeFileSync(tmp, "test"); const prevStats = fs.statSync(tmp); diff --git a/test/transpiler/transpiler.test.js b/test/transpiler/transpiler.test.js index 8ce0137c02..05a959c691 100644 --- a/test/transpiler/transpiler.test.js +++ b/test/transpiler/transpiler.test.js @@ -3315,3 +3315,94 @@ console.log("boop"); ); }); }); + +describe("await can only be used inside an async function message", () => { + var transpiler = new Bun.Transpiler({ + logLevel: "debug", + }); + + function assertError(code, hasNote = false) { + try { + transpiler.transformSync(code); + expect.unreachable(); + } catch (e) { + function handle(error) { + expect(error.message).toBe('"await" can only be used inside an "async" function'); + + if (hasNote) { + expect(error.notes).toHaveLength(1); + expect(error.notes[0].message).toBe('Consider adding the "async" keyword here'); + expect(error.notes[0].position.lineText).toContain("foo"); + } else { + expect(error.notes).toHaveLength(0); + } + } + if (e instanceof AggregateError) { + handle(e.errors[0]); + } else { + expect.unreachable(); + } + } + } + it("in object method", () => { + assertError( + `const x = { + foo() { + await bar(); + } + }`, + true, + ); + }); + + it("in class method", () => { + assertError( + `class X { + foo() { + await bar(); + } + }`, + true, + ); + }); + + it("in function statement", () => { + assertError( + `function foo() { + await bar(); + }`, + true, + ); + }); + + it("in function expression", () => { + assertError( + `const foo = function() { + await bar(); + }`, + true, + ); + }); + + it("in arrow function", () => { + assertError( + `const foo = () => { + await bar(); + }`, + false, + ); + }); + + it("in arrow function with block body", () => { + assertError( + `const foo = () => { + await bar(); + }`, + false, + ); + }); + + it("in arrow function with expression body", () => { + assertError(`const foo = () => await bar();`, false); + }); +});