Skip to content

Commit 86fe466

Browse files
committed
fix(parser): align private callable diagnostics
1 parent 8ef6aa4 commit 86fe466

4 files changed

Lines changed: 74 additions & 33 deletions

File tree

src/compiler.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4890,10 +4890,6 @@ impl<'source> Parser<'source> {
48904890
if self.array_assignment_pattern_ahead() {
48914891
return self.parse_array_assignment_expression();
48924892
}
4893-
let leading_assignment_pattern_literal = matches!(
4894-
self.current().kind,
4895-
TokenKind::Punctuator(Punctuator::LeftBracket | Punctuator::LeftBrace)
4896-
);
48974893
// QuickJS's `name0` is captured only when the AssignmentExpression
48984894
// starts with the identifier token itself. Parenthesized lvalues are
48994895
// valid References but intentionally do not trigger NamedEvaluation.
@@ -4925,6 +4921,10 @@ impl<'source> Parser<'source> {
49254921
)
49264922
)
49274923
{
4924+
// QuickJS consumes every assignment operator before get_lvalue
4925+
// rejects an optional-chain Reference, so the diagnostic points
4926+
// at the first RHS token.
4927+
self.advance()?;
49284928
return Err(self.syntax_here("invalid assignment left-hand side"));
49294929
}
49304930
let logical = match self.current().kind {
@@ -4940,8 +4940,7 @@ impl<'source> Parser<'source> {
49404940
let infer_name = direct_identifier_name.as_deref() == Some(target.name.as_str());
49414941
return self.parse_logical_identifier_assignment(target, logical, infer_name);
49424942
}
4943-
return self
4944-
.parse_logical_member_assignment(logical, leading_assignment_pattern_literal);
4943+
return self.parse_logical_member_assignment(logical);
49454944
}
49464945

49474946
let assignment_span = self.current().span;
@@ -5145,18 +5144,11 @@ impl<'source> Parser<'source> {
51455144
/// as QuickJS `js_parse_assign_expr2`. The kept member Reference is used
51465145
/// only by the assignment branch; the short-circuit branch removes its
51475146
/// base/key operands with `Nip` and returns the original property value.
5148-
fn parse_logical_member_assignment(
5149-
&mut self,
5150-
logical: LogicalAssignment,
5151-
leading_assignment_pattern_literal: bool,
5152-
) -> Result<(), Error> {
5147+
fn parse_logical_member_assignment(&mut self, logical: LogicalAssignment) -> Result<(), Error> {
51535148
let Some(target) = self.promote_tail_member_get_for_compound()? else {
5154-
// QuickJS consumes a logical-assignment operator after an
5155-
// Array/Object literal before get_lvalue rejects that literal,
5156-
// matching the arithmetic compound-assignment diagnostic path.
5157-
if leading_assignment_pattern_literal {
5158-
self.advance()?;
5159-
}
5149+
// As with every other assignment operator, QuickJS advances to
5150+
// the RHS before get_lvalue rejects a non-Reference left side.
5151+
self.advance()?;
51605152
return Err(self.syntax_here("invalid assignment left-hand side"));
51615153
};
51625154
let lvalue_depth = match &target {
@@ -6822,7 +6814,7 @@ impl<'source> Parser<'source> {
68226814
BytecodeFunctionKind::Generator | BytecodeFunctionKind::AsyncGenerator
68236815
) =>
68246816
{
6825-
return Err(self.syntax_here("unexpected 'yield' keyword"));
6817+
return Err(self.syntax_here("unexpected token in expression: 'yield'"));
68266818
}
68276819
TokenKind::Keyword(keyword)
68286820
if self.current_ir().strict && strict_reserved_identifier(keyword) =>

src/compiler/class.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -488,12 +488,23 @@ impl<'source> Parser<'source> {
488488
let has_line_terminator = quickjs_simple_lookahead_has_line_terminator(
489489
&self.lexer.source()[self.current().span.end.byte_offset..],
490490
);
491+
// QuickJS first treats `async` as an ordinary property name only for
492+
// the five tokens which can immediately finish or continue that
493+
// property. Every other same-line token enters the async-method path;
494+
// a malformed key is then diagnosed at that token by the shared class
495+
// property-name parser. This includes `;`, invalid punctuation, and a
496+
// raw backslash from an attempted escaped `#` token.
491497
Ok(!has_line_terminator
492-
&& (Self::class_property_name_starts(&next.kind)
493-
|| matches!(
494-
next.kind,
495-
TokenKind::Punctuator(Punctuator::Multiply | Punctuator::Semicolon)
496-
)))
498+
&& !matches!(
499+
next.kind,
500+
TokenKind::Punctuator(
501+
Punctuator::Colon
502+
| Punctuator::Comma
503+
| Punctuator::RightBrace
504+
| Punctuator::LeftParen
505+
| Punctuator::Equal
506+
)
507+
))
497508
}
498509

499510
fn class_property_name_starts(kind: &TokenKind<'_>) -> bool {

src/compiler/object_literal.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,7 @@ impl<'source> Parser<'source> {
141141
.to_js_string()?
142142
}
143143
TokenKind::PrivateIdentifier(_) => {
144-
return Err(Error::syntax(
145-
"private identifiers are not valid in object literals",
146-
source_span(token.span),
147-
));
144+
return Err(self.syntax_here("invalid property name"));
148145
}
149146
_ => return Err(self.syntax_here("invalid property name")),
150147
};
@@ -155,6 +152,7 @@ impl<'source> Parser<'source> {
155152
| TokenKind::Keyword(_)
156153
| TokenKind::String(_)
157154
| TokenKind::Number(_)
155+
| TokenKind::PrivateIdentifier(_)
158156
| TokenKind::Punctuator(Punctuator::LeftBracket)
159157
);
160158
let async_prefix_has_line_terminator = method_prefix.as_deref() == Some("async")
@@ -332,10 +330,7 @@ impl<'source> Parser<'source> {
332330
return Ok(ObjectMethodPropertyKey::Computed);
333331
}
334332
TokenKind::PrivateIdentifier(_) => {
335-
return Err(Error::syntax(
336-
"private identifiers are not valid in object literals",
337-
source_span(token.span),
338-
));
333+
return Err(self.syntax_here("invalid property name"));
339334
}
340335
_ => return Err(self.syntax_here("invalid property name")),
341336
};

src/compiler/tests.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,12 @@ fn class_field_early_error_diagnostics_match_quickjs() {
821821
"invalid first character of private name",
822822
24,
823823
),
824+
(
825+
r"class C { async \u0023m() {} }",
826+
"invalid property name",
827+
17,
828+
),
829+
("class C { async @() {} }", "invalid property name", 17),
824830
] {
825831
let error = compile_unlinked_script(source).unwrap_err();
826832
assert_eq!(error.kind(), ErrorKind::Syntax, "{source}");
@@ -897,7 +903,17 @@ fn private_field_early_error_diagnostics_match_quickjs() {
897903

898904
#[test]
899905
fn invalid_assignment_diagnostics_advance_to_rhs_like_quickjs() {
900-
for (source, column) in [("1 = 0", 5), ("f() += 0", 8), ("1 = @", 5)] {
906+
for (source, column) in [
907+
("1 = 0", 5),
908+
("f() += 0", 8),
909+
("1 = @", 5),
910+
("1 &&= 0", 7),
911+
("f() ||= 0", 9),
912+
("a?.b = 0", 8),
913+
("a?.b += 0", 9),
914+
("a?.b ??= 0", 10),
915+
("class C { #x; m(){ #x in {} &&= 0; } }", 33),
916+
] {
901917
let error = compile_unlinked_script(source).unwrap_err();
902918
assert_eq!(error.kind(), ErrorKind::Syntax, "{source}");
903919
assert_eq!(
@@ -1848,6 +1864,14 @@ fn generator_method_frontiers_keep_async_delegation_explicit() {
18481864
.message(),
18491865
"invalid property name"
18501866
);
1867+
let yield_reference = compile_unlinked_script("class C { *#gen() { void yield; } }")
1868+
.expect_err("yield must not parse as an IdentifierReference in a generator method");
1869+
assert_eq!(yield_reference.kind(), ErrorKind::Syntax);
1870+
assert_eq!(
1871+
yield_reference.message(),
1872+
"unexpected token in expression: 'yield'"
1873+
);
1874+
assert_eq!(yield_reference.span().unwrap().start.column, 26);
18511875

18521876
compile_unlinked_script("class C { async *method(){ yield 1; } }")
18531877
.expect("public class async-generator method should use the independent async driver");
@@ -13344,8 +13368,27 @@ fn object_literal_grammar_is_fail_closed_at_remaining_method_frontiers() {
1334413368
compile_unlinked_script("({#private:1})")
1334513369
.unwrap_err()
1334613370
.message(),
13347-
"private identifiers are not valid in object literals"
13371+
"invalid property name"
1334813372
);
13373+
13374+
for (source, column) in [
13375+
("({#private: 1})", 3),
13376+
("({#private() {}})", 3),
13377+
("({*#private() {}})", 4),
13378+
("({get #private() {}})", 7),
13379+
("({set #private(value) {}})", 7),
13380+
("({async #private() {}})", 9),
13381+
("({async *#private() {}})", 10),
13382+
] {
13383+
let error = compile_unlinked_script(source).unwrap_err();
13384+
assert_eq!(error.kind(), ErrorKind::Syntax, "{source:?}");
13385+
assert_eq!(error.message(), "invalid property name", "{source:?}");
13386+
let start = error
13387+
.span()
13388+
.expect("object-literal error lost its span")
13389+
.start;
13390+
assert_eq!((start.line, start.column), (1, column), "{source:?}");
13391+
}
1334913392
}
1335013393

1335113394
#[test]

0 commit comments

Comments
 (0)