Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fix-js-parser-9548.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#9548](https://github.com/biomejs/biome/issues/9548): Biome now parses conditional expressions whose consequent is an arrow function returning a parenthesized object expression.
35 changes: 31 additions & 4 deletions crates/biome_js_parser/src/syntax/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ enum ExpressionContextFlag {
AllowObjectExpression = 1 << 1,
InDecorator = 1 << 2,
AllowTSTypeAssertion = 1 << 3,
InConditionalConsequent = 1 << 4,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
Expand Down Expand Up @@ -80,6 +81,10 @@ impl ExpressionContextFlags {
const ALLOW_TS_TYPE_ASSERTION: Self =
Self(make_bitflags!(ExpressionContextFlag::{AllowTSTypeAssertion}));

/// If `true`, the parser is inside the consequent of a conditional expression.
const IN_CONDITIONAL_CONSEQUENT: Self =
Self(make_bitflags!(ExpressionContextFlag::{InConditionalConsequent}));

pub fn contains(&self, other: impl Into<Self>) -> bool {
self.0.contains(other.into().0)
}
Expand Down Expand Up @@ -124,6 +129,13 @@ impl ExpressionContext {
self.and(ExpressionContextFlags::ALLOW_TS_TYPE_ASSERTION, allowed)
}

pub(crate) fn and_in_conditional_consequent(self, in_conditional_consequent: bool) -> Self {
self.and(
ExpressionContextFlags::IN_CONDITIONAL_CONSEQUENT,
in_conditional_consequent,
)
}

/// Returns true if object expressions or object patterns are valid in this context
pub(crate) fn is_object_expression_allowed(&self) -> bool {
self.0
Expand All @@ -140,6 +152,11 @@ impl ExpressionContext {
self.0.contains(ExpressionContextFlags::IN_DECORATOR)
}

pub(crate) fn is_in_conditional_consequent(&self) -> bool {
self.0
.contains(ExpressionContextFlags::IN_CONDITIONAL_CONSEQUENT)
}

/// Adds the `flag` if `set` is `true`, otherwise removes the `flag`
fn and(self, flag: ExpressionContextFlags, set: bool) -> Self {
Self(if set { self.0 | flag } else { self.0 - flag })
Expand Down Expand Up @@ -275,7 +292,7 @@ pub(crate) fn parse_assignment_expression_or_higher(
p: &mut JsParser,
context: ExpressionContext,
) -> ParsedSyntax {
let arrow_expression = parse_arrow_function_expression(p);
let arrow_expression = parse_arrow_function_expression(p, context);

if arrow_expression.is_present() {
return arrow_expression;
Expand All @@ -302,6 +319,13 @@ fn parse_assignment_expression_or_higher_base(
.and_then(|target| parse_assign_expr_recursive(p, target, checkpoint, context))
}

pub(crate) fn parse_assignment_expression_or_higher_no_arrow(
p: &mut JsParser,
context: ExpressionContext,
) -> ParsedSyntax {
parse_assignment_expression_or_higher_base(p, context)
}

// test js assign_expr
// foo += bar = b ??= 3;
// foo -= bar;
Expand Down Expand Up @@ -463,8 +487,11 @@ pub(super) fn parse_conditional_expr(p: &mut JsParser, context: ExpressionContex
let m = marker.precede(p);
p.bump(T![?]);

parse_conditional_expr_consequent(p, ExpressionContext::default())
.or_add_diagnostic(p, js_parse_error::expected_expression_assignment);
parse_conditional_expr_consequent(
p,
ExpressionContext::default().and_in_conditional_consequent(true),
)
.or_add_diagnostic(p, js_parse_error::expected_expression_assignment);

p.expect(T![:]);

Expand All @@ -486,7 +513,7 @@ pub(super) fn parse_conditional_expr(p: &mut JsParser, context: ExpressionContex
fn parse_conditional_expr_consequent(p: &mut JsParser, context: ExpressionContext) -> ParsedSyntax {
let checkpoint = p.checkpoint();

let arrow_expression = parse_arrow_function_expression(p);
let arrow_expression = parse_arrow_function_expression(p, context);
if arrow_expression.is_present() && p.at(T![:]) {
return arrow_expression;
}
Expand Down
54 changes: 41 additions & 13 deletions crates/biome_js_parser/src/syntax/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::syntax::class::{
};
use crate::syntax::expr::{
ExpressionContext, is_nth_at_identifier, parse_assignment_expression_or_higher,
parse_assignment_expression_or_higher_no_arrow,
};
use crate::syntax::js_parse_error;
use crate::syntax::js_parse_error::{
Expand Down Expand Up @@ -523,9 +524,12 @@ impl Ambiguity {
}
}

pub(crate) fn parse_arrow_function_expression(p: &mut JsParser) -> ParsedSyntax {
parse_parenthesized_arrow_function_expression(p)
.or_else(|| parse_arrow_function_with_single_parameter(p))
pub(crate) fn parse_arrow_function_expression(
p: &mut JsParser,
context: ExpressionContext,
) -> ParsedSyntax {
parse_parenthesized_arrow_function_expression(p, context)
.or_else(|| parse_arrow_function_with_single_parameter(p, context))
}

/// Tries to parse the header of a parenthesized arrow function expression.
Expand Down Expand Up @@ -608,7 +612,10 @@ fn try_parse_parenthesized_arrow_function_head(
// test ts ts_arrow_function_type_parameters
// let a = <A, B extends A, C = string>(a: A, b: B, c: C) => "hello";
// let b = async <A, B>(a: A, b: B): Promise<string> => "hello";
fn parse_possible_parenthesized_arrow_function_expression(p: &mut JsParser) -> ParsedSyntax {
fn parse_possible_parenthesized_arrow_function_expression(
p: &mut JsParser,
context: ExpressionContext,
) -> ParsedSyntax {
let start_pos = p.cur_range().start();

// Test if we already tried to parse this position as an arrow function and failed.
Expand All @@ -621,7 +628,8 @@ fn parse_possible_parenthesized_arrow_function_expression(p: &mut JsParser) -> P
try_parse_parenthesized_arrow_function_head(p, Ambiguity::Disallowed)
}) {
Ok((m, flags)) => {
parse_arrow_body(p, flags).or_add_diagnostic(p, js_parse_error::expected_arrow_body);
parse_arrow_body(p, flags, context)
.or_add_diagnostic(p, js_parse_error::expected_arrow_body);

Present(m.complete(p, JS_ARROW_FUNCTION_EXPRESSION))
}
Expand All @@ -636,16 +644,22 @@ fn parse_possible_parenthesized_arrow_function_expression(p: &mut JsParser) -> P
}
}

fn parse_parenthesized_arrow_function_expression(p: &mut JsParser) -> ParsedSyntax {
fn parse_parenthesized_arrow_function_expression(
p: &mut JsParser,
context: ExpressionContext,
) -> ParsedSyntax {
let is_parenthesized = is_parenthesized_arrow_function_expression(p);
match is_parenthesized {
IsParenthesizedArrowFunctionExpression::True => {
let (m, flags) = try_parse_parenthesized_arrow_function_head(p, Ambiguity::Allowed).expect("'CompletedMarker' because function should never return 'Err' if called with 'Ambiguity::Allowed'.");
parse_arrow_body(p, flags).or_add_diagnostic(p, js_parse_error::expected_arrow_body);
let (m, flags) =
try_parse_parenthesized_arrow_function_head(p, Ambiguity::Allowed)
.expect("'CompletedMarker' because function should never return 'Err' if called with 'Ambiguity::Allowed'.");
parse_arrow_body(p, flags, context)
.or_add_diagnostic(p, js_parse_error::expected_arrow_body);
Present(m.complete(p, JS_ARROW_FUNCTION_EXPRESSION))
}
IsParenthesizedArrowFunctionExpression::Unknown => {
parse_possible_parenthesized_arrow_function_expression(p)
parse_possible_parenthesized_arrow_function_expression(p, context)
}
IsParenthesizedArrowFunctionExpression::False => Absent,
}
Expand Down Expand Up @@ -840,7 +854,10 @@ fn arrow_function_parameter_flags(p: &JsParser, mut flags: SignatureFlags) -> Si
// await => {}
// baz =>
// {}
fn parse_arrow_function_with_single_parameter(p: &mut JsParser) -> ParsedSyntax {
fn parse_arrow_function_with_single_parameter(
p: &mut JsParser,
context: ExpressionContext,
) -> ParsedSyntax {
if !is_arrow_function_with_single_parameter(p) {
return Absent;
}
Expand All @@ -863,7 +880,7 @@ fn parse_arrow_function_with_single_parameter(p: &mut JsParser) -> ParsedSyntax
.expect("Expected function parameter to be present as guaranteed by is_arrow_function_with_simple_parameter");

p.bump(T![=>]);
parse_arrow_body(p, flags).or_add_diagnostic(p, js_parse_error::expected_arrow_body);
parse_arrow_body(p, flags, context).or_add_diagnostic(p, js_parse_error::expected_arrow_body);

Present(m.complete(p, JS_ARROW_FUNCTION_EXPRESSION))
}
Expand All @@ -885,7 +902,11 @@ fn is_arrow_function_with_single_parameter(p: &mut JsParser) -> bool {
}
}

fn parse_arrow_body(p: &mut JsParser, mut flags: SignatureFlags) -> ParsedSyntax {
fn parse_arrow_body(
p: &mut JsParser,
mut flags: SignatureFlags,
context: ExpressionContext,
) -> ParsedSyntax {
// test js arrow_in_constructor
// class A {
// constructor() {
Expand All @@ -901,7 +922,14 @@ fn parse_arrow_body(p: &mut JsParser, mut flags: SignatureFlags) -> ParsedSyntax
parse_function_body(p, flags)
} else {
p.with_state(EnterFunction(flags), |p| {
parse_assignment_expression_or_higher(p, ExpressionContext::default())
if context.is_in_conditional_consequent()
&& matches!(p.cur(), T!['('])
&& matches!(p.nth(1), T!['{'] | T!['['])
{
parse_assignment_expression_or_higher_no_arrow(p, context)
} else {
parse_assignment_expression_or_higher(p, context)
}
})
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const slotFn = isFirstMount
? i => ({ [CONTENT_SLOT]: i })
: i => wrapSlotExpr(newExprs[i]);
Loading
Loading