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:
Jarred Sumner
2023-12-16 03:04:40 +01:00
committed by GitHub
parent 9e47ceac87
commit ca89087684
7 changed files with 198 additions and 7 deletions

View File

@@ -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();

View File

@@ -78,6 +78,12 @@ export default [
getter: "getPosition",
cache: true,
},
notes: {
getter: "getNotes",
cache: true,
},
["@@toPrimitive"]: {
fn: "toPrimitive",
length: 1,

View File

@@ -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(),

View File

@@ -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;

View File

@@ -517,7 +517,10 @@ pub const Msg = struct {
for (notes) |*note| {
note.deinit(allocator);
}
allocator.free(notes);
}
msg.notes = null;
}

View File

@@ -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);

View File

@@ -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);
});
});