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
17 changes: 17 additions & 0 deletions crates/oxc_css_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ pub enum ErrorKind {
UnexpectedLessMixinCall,
UnexpectedSimpleBlock,
TopLevelDeclaration,

// Spec parse errors that CSS Syntax recovers from at EOF / newline. The AST
// and recovery stay conformant; these are recorded in `recoverable_errors()`
// so downstream consumers can tell recovered-from-broken input apart from
// valid CSS. See <https://drafts.csswg.org/css-syntax-3/>.
/// A `{}`-block left open at end of file (§5.4.4); `UnterminatedString`
/// (already defined) covers the EOF-terminated string (§4.3.5).
EofInBlock,
/// A string terminated by a newline instead of its quote — a
/// `<bad-string-token>` (§4.3.5).
BadString,
/// A `(`/`[` group left open at end of file.
UnclosedParen,
}

impl Display for ErrorKind {
Expand Down Expand Up @@ -203,6 +216,10 @@ impl Display for ErrorKind {
Self::UnexpectedLessMixinCall => write!(f, "Less mixin call is disallowed"),
Self::UnexpectedSimpleBlock => write!(f, "simple block is disallowed"),
Self::TopLevelDeclaration => write!(f, "declaration at top level is disallowed"),

Self::EofInBlock => write!(f, "unclosed block before end of file"),
Self::BadString => write!(f, "unterminated string before line break"),
Self::UnclosedParen => write!(f, "unclosed parenthesis before end of file"),
}
}
}
Expand Down
47 changes: 45 additions & 2 deletions crates/oxc_css_parser/src/parser/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,14 @@ impl<'a> Parse<'a> for SimpleBlock<'a> {

// CSS Syntax: EOF closes all open constructs (a parse error, but the
// tree is valid — browsers accept unclosed blocks at EOF). The
// dialects' reference compilers reject them.
// dialects' reference compilers reject them. Recovery is unchanged; the
// parse error is surfaced via `recoverable_errors` so downstream
// consumers can tell it apart from a properly closed block.
if input.syntax == Syntax::Css && matches!(input.cursor.peek()?.token, Token::Eof(..)) {
let end = input.cursor.peek()?.span.start;
input
.recoverable_errors
.push(Error { kind: ErrorKind::EofInBlock, span: Span { start, end: start + 1 } });
return Ok(SimpleBlock { statements, span: Span { start, end } });
}

Expand Down Expand Up @@ -367,15 +372,49 @@ impl<'a> Parser<'a> {
) -> PResult<oxc_allocator::Vec<'a, ComponentValue<'a>>> {
let mut values = self.vec_with_capacity(3);
let mut pairs = Vec::with_capacity(1);
// Span of the outermost currently-open pair (kept in sync with `pairs`
// going empty↔non-empty), so the EOF parse error can point at the
// opener like `SimpleBlock` does, not at the end of file.
let mut outermost_pair_span: Option<Span> = None;
loop {
match &self.cursor.peek()?.token {
Token::Dedent(..) | Token::Linebreak(..) | Token::Eof(..) => break,
Token::Dedent(..) | Token::Linebreak(..) => break,
// CSS Syntax: EOF closes any still-open `(`/`[`/`{` group; recovery
// is unchanged, but record the parse error so downstream consumers
// can tell it apart from a balanced value. Report the outermost
// unclosed opener; a raw-value `{` (e.g. `--x: {` — legal in custom
// properties) is a block, not a paren.
Token::Eof(..) => {
if let (Some(pair), Some(span)) = (pairs.first(), outermost_pair_span) {
let kind = match pair {
crate::util::PairedToken::Brace => ErrorKind::EofInBlock,
_ => ErrorKind::UnclosedParen,
};
self.recoverable_errors.push(Error { kind, span });
}
break;
}
Token::Semicolon(..) if pairs.is_empty() => {
break;
}
Token::LBrace(..) if stop_at_top_level_brace && pairs.is_empty() => {
break;
}
// An unterminated string survives as a preserved token (a parse
// error kept verbatim; CSS Syntax §4.3.5). The tokenizer emits a
// `BadStr` for both recoverable forms; recover the spec's split
// from where the string stopped — at EOF it is a `<string-token>`
// (canonically closable by appending the quote), at a newline a
// `<bad-string-token>`.
Token::BadStr(..) => {
let span = self.cursor.peek()?.span.clone();
let kind = if span.end == self.source.len() {
ErrorKind::UnterminatedString
} else {
ErrorKind::BadString
};
self.recoverable_errors.push(Error { kind, span });
}
// An interpolated string (e.g. `'#{$expr}'` inside
// `filter: progid:...`) must be parsed structurally:
// the tokenizer needs `scan_string_template` to resume
Expand All @@ -386,9 +425,13 @@ impl<'a> Parser<'a> {
continue;
}
token => {
let was_empty = pairs.is_empty();
if !crate::util::track_paired_token(token, &mut pairs) {
break;
}
if was_empty && !pairs.is_empty() {
outermost_pair_span = Some(self.cursor.peek()?.span.clone());
}
}
}
values.push(ComponentValue::TokenWithSpan(self.cursor.bump()?));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/oxc_css_parser/tests/recoverable.rs
---
error: unterminated string before line break
┌─ bad-string-newline.css:2:12
2 │ content: "
│ ^
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/oxc_css_parser/tests/recoverable.rs
---
error: unclosed block before end of file
┌─ unclosed-at-rule-block.css:1:27
1 │ @media (min-width: 500px) {
│ ^
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/oxc_css_parser/tests/recoverable.rs
---
error: unclosed block before end of file
┌─ unclosed-block.css:1:5
1 │ div {
│ ^
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a {
--x: ({
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/oxc_css_parser/tests/recoverable.rs
---
error: unclosed parenthesis before end of file
┌─ unclosed-brace-custom-property.css:2:8
2 │ --x: ({
│ ^

error: unclosed block before end of file
┌─ unclosed-brace-custom-property.css:1:3
1 │ a {
│ ^
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a {
--x: {
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/oxc_css_parser/tests/recoverable.rs
---
error: unclosed block before end of file
┌─ unclosed-brace-in-value.css:2:8
2 │ --x: {
│ ^

error: unclosed block before end of file
┌─ unclosed-brace-in-value.css:1:3
1 │ a {
│ ^
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a {
width: calc(100% - 10px
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/oxc_css_parser/tests/recoverable.rs
---
error: unclosed parenthesis before end of file
┌─ unclosed-paren.css:2:14
2 │ width: calc(100% - 10px
│ ^

error: unclosed block before end of file
┌─ unclosed-paren.css:1:3
1 │ a {
│ ^
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a {
content: "abc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/oxc_css_parser/tests/recoverable.rs
---
error: unterminated string
┌─ unterminated-string-eof.css:2:12
2 │ content: "abc
│ ^^^^

error: unclosed block before end of file
┌─ unterminated-string-eof.css:1:3
1 │ a {
│ ^
5 changes: 3 additions & 2 deletions tasks/conformance/snapshots/less.js.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
suite: less.js
sha: 8ae2cc3bfa79f0718ad6fe5f263a1d6819fe9d5c
files: 459 success: 427 failed: 8 expected: 24
clean: 427 recovered: 0 hard_error: 8 panic: 0
files: 459 success: 426 failed: 9 expected: 24
clean: 426 recovered: 1 hard_error: 8 panic: 0

failures:
ERROR packages/test-data/tests-config/debug/all/linenumbers-all.css expect token `:`, but found `{`
Expand All @@ -12,6 +12,7 @@ ERROR packages/test-data/tests-unit/import/import/invalid-css.less expect
ERROR packages/test-data/tests-unit/property-name-interp/property-name-interp.css CSS rule is expected
ERROR packages/test-data/tests-unit/property-name-interp/property-name-interp.less expect token `(`, but found `{`
ERROR packages/test-data/tests-unit/strings/strings.less expect token `;`, but found `{`
RECOVER packages/test-data/tests-unit/urls/actual.css unterminated string

expected failures (error tests, correct behavior):
ERROR packages/test-data/tests-error/eval/multiple-guards-on-css-selectors.less expect token `;`, but found `<ident>`
Expand Down
13 changes: 11 additions & 2 deletions tasks/conformance/snapshots/sass-spec.snap
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
suite: sass-spec
sha: a2ead9225786d49e91f5cc36755b7713596a2338
files: 26787 success: 26493 failed: 2 expected: 292
clean: 26493 recovered: 0 hard_error: 2 panic: 0
files: 26787 success: 26484 failed: 11 expected: 292
clean: 26484 recovered: 9 hard_error: 2 panic: 0

failures:
RECOVER spec/core_functions/string/unquote.hrx::meaningful_css_characters/output.css unclosed block before end of file
ERROR spec/non_conformant/extend-tests/selector_list.hrx::input.scss expect token `;`, but found `#{`
RECOVER spec/non_conformant/parser/interpolate/27_escaped_double_quotes/01_inline.hrx::output.css unterminated string before line break
RECOVER spec/non_conformant/parser/interpolate/27_escaped_double_quotes/02_variable.hrx::output.css unterminated string before line break
RECOVER spec/non_conformant/parser/interpolate/27_escaped_double_quotes/03_inline_double.hrx::output.css unterminated string before line break
RECOVER spec/non_conformant/parser/interpolate/27_escaped_double_quotes/04_variable_double.hrx::output.css unterminated string before line break
RECOVER spec/non_conformant/parser/interpolate/28_escaped_single_quotes/01_inline.hrx::output.css unterminated string before line break
RECOVER spec/non_conformant/parser/interpolate/28_escaped_single_quotes/02_variable.hrx::output.css unterminated string before line break
RECOVER spec/non_conformant/parser/interpolate/28_escaped_single_quotes/03_inline_double.hrx::output.css unterminated string before line break
RECOVER spec/non_conformant/parser/interpolate/28_escaped_single_quotes/04_variable_double.hrx::output.css unterminated string before line break
ERROR spec/non_conformant/sass/pseudo.hrx::input.sass CSS rule is expected

expected failures (error tests, correct behavior):
Expand Down
8 changes: 4 additions & 4 deletions tasks/conformance/snapshots/summary.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ css-parsing-tests 0 0 0 0 0 0 0
wpt 7 5 0 2 5 0 0 0
webref 0 0 0 0 0 0 0 0
postcss-parser-tests 30 28 2 0 28 1 1 0
sass-spec 26787 26493 2 292 26493 0 2 0
less.js 459 427 8 24 427 0 8 0
sass-spec 26787 26484 11 292 26484 9 2 0
less.js 459 426 9 24 426 1 8 0
------------------------------------------------------------------------
total 27283 26953 12 318 26953 1 11 0
total 27283 26943 22 318 26943 11 11 0

# by syntax
syntax files success failed expected clean recov harderr panic
css 11788 11757 7 24 11757 1 6 0
css 11788 11747 17 24 11747 11 6 0
scss 14750 14519 1 230 14519 0 1 0
sass 439 398 1 40 398 0 1 0
less 306 279 3 24 279 0 3 0
Loading