diff --git a/.changeset/gracefully-handle-stalled-parser.md b/.changeset/gracefully-handle-stalled-parser.md new file mode 100644 index 000000000000..335d4af18235 --- /dev/null +++ b/.changeset/gracefully-handle-stalled-parser.md @@ -0,0 +1,8 @@ +--- +"@biomejs/biome": patch +--- +Our JavaScript parser can now gracefully handle situations where we detect the +parser to have stalled, such as in +[#4622](https://github.com/biomejs/biome/issues/4622). This means we don't fail +with an assertion anymore, but invalid code can trigger a graceful diagnostic +in such cases. diff --git a/crates/biome_js_parser/src/syntax/pattern.rs b/crates/biome_js_parser/src/syntax/pattern.rs index fc4ae027abf2..b6ca498757cd 100644 --- a/crates/biome_js_parser/src/syntax/pattern.rs +++ b/crates/biome_js_parser/src/syntax/pattern.rs @@ -156,7 +156,11 @@ pub(crate) trait ParseObjectPattern { let mut progress = ParserProgress::default(); while !p.at(T!['}']) { - progress.assert_progressing(p); + if !progress.has_progressed(p) { + let diagnostic = Self::expected_property_pattern_error(p, p.cur_range()); + p.error(diagnostic); + break; + } if p.at(T![,]) { // missing element diff --git a/crates/biome_js_parser/tests/js_test_suite/error/issue4622.ts b/crates/biome_js_parser/tests/js_test_suite/error/issue4622.ts new file mode 100644 index 000000000000..bde1dd36f254 --- /dev/null +++ b/crates/biome_js_parser/tests/js_test_suite/error/issue4622.ts @@ -0,0 +1,3 @@ +r={ += +): \ No newline at end of file diff --git a/crates/biome_js_parser/tests/js_test_suite/error/issue4622.ts.snap b/crates/biome_js_parser/tests/js_test_suite/error/issue4622.ts.snap new file mode 100644 index 000000000000..23df286a5f37 --- /dev/null +++ b/crates/biome_js_parser/tests/js_test_suite/error/issue4622.ts.snap @@ -0,0 +1,140 @@ +--- +source: crates/biome_js_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```ts +r={ += +): +``` + + +## AST + +``` +JsModule { + bom_token: missing (optional), + interpreter_token: missing (optional), + directives: JsDirectiveList [], + items: JsModuleItemList [ + JsExpressionStatement { + expression: JsAssignmentExpression { + left: JsIdentifierAssignment { + name_token: IDENT@0..1 "r" [] [], + }, + operator_token: EQ@1..2 "=" [] [], + right: JsAssignmentExpression { + left: JsObjectAssignmentPattern { + l_curly_token: L_CURLY@2..3 "{" [] [], + properties: JsObjectAssignmentPatternPropertyList [ + JsObjectAssignmentPatternShorthandProperty { + identifier: missing (required), + init: JsInitializerClause { + eq_token: EQ@3..5 "=" [Newline("\n")] [], + expression: missing (required), + }, + }, + missing separator, + JsObjectAssignmentPatternProperty { + member: missing (required), + colon_token: missing (required), + pattern: missing (required), + init: missing (optional), + }, + ], + r_curly_token: missing (required), + }, + operator_token: missing (required), + right: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + JsBogusStatement { + items: [ + R_PAREN@5..7 ")" [Newline("\n")] [], + COLON@7..8 ":" [] [], + ], + }, + ], + eof_token: EOF@8..8 "" [] [], +} +``` + +## CST + +``` +0: JS_MODULE@0..8 + 0: (empty) + 1: (empty) + 2: JS_DIRECTIVE_LIST@0..0 + 3: JS_MODULE_ITEM_LIST@0..8 + 0: JS_EXPRESSION_STATEMENT@0..5 + 0: JS_ASSIGNMENT_EXPRESSION@0..5 + 0: JS_IDENTIFIER_ASSIGNMENT@0..1 + 0: IDENT@0..1 "r" [] [] + 1: EQ@1..2 "=" [] [] + 2: JS_ASSIGNMENT_EXPRESSION@2..5 + 0: JS_OBJECT_ASSIGNMENT_PATTERN@2..5 + 0: L_CURLY@2..3 "{" [] [] + 1: JS_OBJECT_ASSIGNMENT_PATTERN_PROPERTY_LIST@3..5 + 0: JS_OBJECT_ASSIGNMENT_PATTERN_SHORTHAND_PROPERTY@3..5 + 0: (empty) + 1: JS_INITIALIZER_CLAUSE@3..5 + 0: EQ@3..5 "=" [Newline("\n")] [] + 1: (empty) + 1: (empty) + 2: JS_OBJECT_ASSIGNMENT_PATTERN_PROPERTY@5..5 + 0: (empty) + 1: (empty) + 2: (empty) + 3: (empty) + 2: (empty) + 1: (empty) + 2: (empty) + 1: (empty) + 1: JS_BOGUS_STATEMENT@5..8 + 0: R_PAREN@5..7 ")" [Newline("\n")] [] + 1: COLON@7..8 ":" [] [] + 4: EOF@8..8 "" [] [] + +``` + +## Diagnostics + +``` +issue4622.ts:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an identifier but instead found '='. + + 1 │ r={ + > 2 │ = + │ ^ + 3 │ ): + + i Expected an identifier here. + + 1 │ r={ + > 2 │ = + │ ^ + 3 │ ): + +issue4622.ts:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression, or an assignment but instead found ')'. + + 1 │ r={ + 2 │ = + > 3 │ ): + │ ^ + + i Expected an expression, or an assignment here. + + 1 │ r={ + 2 │ = + > 3 │ ): + │ ^ + +``` diff --git a/crates/biome_parser/src/lib.rs b/crates/biome_parser/src/lib.rs index 0e79ee868151..4603dc8721d1 100644 --- a/crates/biome_parser/src/lib.rs +++ b/crates/biome_parser/src/lib.rs @@ -510,16 +510,21 @@ pub trait Parser: Sized { pub struct ParserProgress(Option); impl ParserProgress { - /// Returns true if the current parser position is passed this position + /// Returns true if the current parser position is passed this position, + /// and updates the progress. #[inline] - pub fn has_progressed

(&self, p: &P) -> bool + pub fn has_progressed

(&mut self, p: &P) -> bool where P: Parser, { - match self.0 { + let has_progressed = match self.0 { None => true, Some(pos) => pos < p.source().position(), - } + }; + + self.0 = Some(p.source().position()); + + has_progressed } /// Asserts that the parsing is still making progress. @@ -539,8 +544,6 @@ impl ParserProgress { p.cur(), p.cur_range(), ); - - self.0 = Some(p.source().position()); } }