mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
feat: Make using await inside a non-async function have a helpful error message (#7690)
* Update fs.test.ts * Make using `await` inside a non-async function have a good error --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -78,6 +78,12 @@ export default [
|
||||
getter: "getPosition",
|
||||
cache: true,
|
||||
},
|
||||
|
||||
notes: {
|
||||
getter: "getNotes",
|
||||
cache: true,
|
||||
},
|
||||
|
||||
["@@toPrimitive"]: {
|
||||
fn: "toPrimitive",
|
||||
length: 1,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -517,7 +517,10 @@ pub const Msg = struct {
|
||||
for (notes) |*note| {
|
||||
note.deinit(allocator);
|
||||
}
|
||||
|
||||
allocator.free(notes);
|
||||
}
|
||||
|
||||
msg.notes = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user