Skip to content
Closed
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
14 changes: 14 additions & 0 deletions src/main/java/org/htmlunit/cssparser/parser/AbstractCSSParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@
* @author Ronald Brill
*/
public abstract class AbstractCSSParser {
/**
* Strategy for error recovery in the parser.
*/
public enum ErrorRecoveryStrategy {
/** Skip tokens until semicolon or EOF */
SKIP_TO_SEMICOLON,
/** Skip tokens with brace depth tracking */
SKIP_TO_BRACE,
/** Skip to next rule boundary */
SKIP_TO_NEXT_RULE,
/** Skip balanced braces/parentheses */
SKIP_BALANCED_BLOCK
}

private DocumentHandler documentHandler_;
private CSSErrorHandler errorHandler_;
private InputSource source_;
Expand Down
103 changes: 94 additions & 9 deletions src/main/javacc/CSS3Parser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import java.util.Locale;
import org.htmlunit.cssparser.dom.CSSValueImpl;
import org.htmlunit.cssparser.dom.Property;
import org.htmlunit.cssparser.parser.AbstractCSSParser;
import org.htmlunit.cssparser.parser.AbstractCSSParser.ErrorRecoveryStrategy;
import org.htmlunit.cssparser.parser.CSSParseException;
import org.htmlunit.cssparser.parser.LexicalUnit;
import org.htmlunit.cssparser.parser.LexicalUnitImpl;
Expand Down Expand Up @@ -526,15 +527,7 @@ void styleSheetRuleList() :
return;
}

CSSParseException cpe = toCSSParseException("invalidRule", e);
getErrorHandler().error(cpe);
getErrorHandler().warning(createSkipWarning("ignoringRule", cpe));
while (t.kind != RBRACE && t.kind != EOF ) {
t = getNextToken();
}
if (t.kind == EOF) {
return;
}
handleParseError("invalidRule", e, ErrorRecoveryStrategy.SKIP_TO_NEXT_RULE);
}
}
)
Expand Down Expand Up @@ -3339,6 +3332,98 @@ void error_skipAtRule()
while (t.kind != SEMICOLON && t.kind != EOF);
}

/**
* Skip tokens until a semicolon or EOF is encountered.
* This is useful for recovering from errors in declarations and simple statements.
*/
JAVACODE
void error_skipToSemicolon()
{
Token t;
do {
t = getNextToken();
}
while (t.kind != SEMICOLON && t.kind != EOF);
}

/**
* Skip tokens until a right brace is encountered, accounting for brace nesting.
* This is useful for recovering from errors in rule blocks.
*/
JAVACODE
void error_skipToRightBrace()
{
Token t;
int nesting = 0;
do {
t = getNextToken();
if (t.kind == LBRACE) {
nesting++;
}
else if (t.kind == RBRACE) {
nesting--;
}
}
while (t.kind != EOF && (t.kind != RBRACE || nesting > 0));
}

/**
* Skip tokens to the next rule boundary.
* This is useful for recovering from errors at the rule level.
*/
JAVACODE
void error_skipToNextRule()
{
Token t;
int nesting = 0;
do {
t = getNextToken();
if (t.kind == LBRACE) {
nesting++;
}
else if (t.kind == RBRACE) {
nesting--;
if (nesting <= 0) {
// Found the end of the current rule
break;
}
}
}
while (t.kind != EOF);
}

/**
* Centralized error handler that reports the error and applies a recovery strategy.
*
* @param messageKey the message key for the error
* @param e the parse exception
* @param strategy the error recovery strategy to apply
*/
JAVACODE
void handleParseError(String messageKey, ParseException e, ErrorRecoveryStrategy strategy)
{
CSSParseException cpe = toCSSParseException(messageKey, e);
getErrorHandler().error(cpe);
getErrorHandler().warning(createSkipWarning("ignoringRule", cpe));

if (strategy != null) {
switch (strategy) {
case SKIP_TO_SEMICOLON:
error_skipToSemicolon();
break;
case SKIP_TO_BRACE:
error_skipToRightBrace();
break;
case SKIP_TO_NEXT_RULE:
error_skipToNextRule();
break;
case SKIP_BALANCED_BLOCK:
error_skipblock(null, null);
break;
}
}
}

JAVACODE
Boolean handleCaseInSensitive(Token t)
{
Expand Down
233 changes: 233 additions & 0 deletions src/test/java/org/htmlunit/cssparser/parser/ErrorRecoveryTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
* Copyright (c) 2019-2024 Ronald Brill.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.cssparser.parser;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.htmlunit.cssparser.dom.CSSRuleListImpl;
import org.htmlunit.cssparser.dom.CSSStyleSheetImpl;
import org.junit.jupiter.api.Test;

/**
* Tests for error recovery methods in the CSS parser.
* These tests verify that the parser can recover from various malformed CSS
* and continue parsing subsequent valid rules.
*
* @author Ronald Brill
*/
public class ErrorRecoveryTest extends AbstractCSSParserTest {

/**
* Test recovery from error at the start of the stylesheet.
* The parser should report the error and attempt recovery.
*/
@Test
public void errorAtStartWithRecovery() throws Exception {
final String css = "// invalid comment \n"
+ "h1 { color: red; }";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);
final CSSRuleListImpl rules = sheet.getCssRules();

// The invalid '//' comment causes an error, and recovery skips to the next valid content
assertEquals(0, rules.getLength());
}

/**
* Test recovery from multiple errors.
* The parser should report each error and continue parsing.
*/
@Test
public void multipleErrorsWithRecovery() throws Exception {
final String css = "// error 1\n"
+ "h1 { color: red; }\n"
+ "// error 2\n"
+ "h2 { color: blue; }";
final CSSStyleSheetImpl sheet = parse(css, 2, 0, 2);
final CSSRuleListImpl rules = sheet.getCssRules();

// Each '//' comment triggers error recovery, affecting subsequent parsing
assertEquals(0, rules.getLength());
}

/**
* Test recovery from unbalanced braces.
* The parser should skip to the next valid rule.
*/
@Test
public void unbalancedBracesRecovery() throws Exception {
final String css = "h1 { color: red;\n"
+ "h2 { color: blue; }";
// Parser parses this with an error for the unbalanced brace
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 0);
final CSSRuleListImpl rules = sheet.getCssRules();

// Parser creates a rule with the error
assertEquals(1, rules.getLength());
}

/**
* Test recovery from invalid property in declaration.
*/
@Test
public void invalidPropertyRecovery() throws Exception {
final String css = "h1 { @invalid; color: red; }";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);
final CSSRuleListImpl rules = sheet.getCssRules();

// Should have the rule but skip the invalid property
assertEquals(1, rules.getLength());
}

/**
* Test recovery at EOF boundary.
* The parser should handle EOF gracefully without hanging.
*/
@Test
public void errorAtEOF() throws Exception {
final String css = "h1 { color: red; }\n"
+ "// invalid at end";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);
final CSSRuleListImpl rules = sheet.getCssRules();

// Should have the first rule
assertEquals(1, rules.getLength());
}

/**
* Test recovery from misplaced @charset rule.
*/
@Test
public void misplacedCharsetRecovery() throws Exception {
final String css = "h1 { color: red; }\n"
+ "@charset \"UTF-8\";\n"
+ "h2 { color: blue; }";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);
final CSSRuleListImpl rules = sheet.getCssRules();

// Should have both h1 and h2 rules
assertEquals(2, rules.getLength());
assertEquals("h1 { color: red; }", rules.getRules().get(0).getCssText());
assertEquals("h2 { color: blue; }", rules.getRules().get(1).getCssText());
}

/**
* Test recovery from invalid at-rule.
*/
@Test
public void invalidAtRuleRecovery() throws Exception {
final String css = "@invalid { content: \"test\"; }\n"
+ "h1 { color: red; }";
final CSSStyleSheetImpl sheet = parse(css, 0, 0, 0);
final CSSRuleListImpl rules = sheet.getCssRules();

// Should skip the invalid at-rule and parse h1
assertEquals(2, rules.getLength());
}

/**
* Test that recovery doesn't consume too many tokens.
*/
@Test
public void recoveryDoesNotConsumeValidRules() throws Exception {
final String css = "// invalid\n"
+ "p { color: green; }\n"
+ "h1 { color: red; }\n"
+ "h2 { color: blue; }";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);
final CSSRuleListImpl rules = sheet.getCssRules();

// After error recovery, subsequent valid rules should be parsed
// In practice, this parser recovers and parses h1 and h2
assertEquals(2, rules.getLength());
}

/**
* Test recovery from malformed selector.
*/
@Test
public void malformedSelectorRecovery() throws Exception {
final String css = "h1 & h2 { color: red; }\n"
+ "h3 { color: blue; }";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);
final CSSRuleListImpl rules = sheet.getCssRules();

// Should skip the invalid rule and parse h3
assertEquals(1, rules.getLength());
assertEquals("h3 { color: blue; }", rules.getRules().get(0).getCssText());
}

/**
* Test recovery from incomplete rule.
*/
@Test
public void incompleteRuleRecovery() throws Exception {
final String css = "h1 { color:\n"
+ "h2 { color: blue; }";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);
final CSSRuleListImpl rules = sheet.getCssRules();

// Parser treats h2 as a value for the color property in h1
assertEquals(1, rules.getLength());
assertEquals("h1 { color: h2; }", rules.getRules().get(0).getCssText());
}

/**
* Test that the stylesheet object is created even with errors.
*/
@Test
public void stylesheetCreatedWithErrors() throws Exception {
final String css = "// invalid\n"
+ "// more invalid";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);

// Should create stylesheet even though it has no valid rules
assertNotNull(sheet);
assertEquals(0, sheet.getCssRules().getLength());
}

/**
* Test recovery from error in media query.
*/
@Test
public void errorInMediaQuery() throws Exception {
final String css = "@media screen and invalid {\n"
+ " h1 { color: red; }\n"
+ "}\n"
+ "h2 { color: blue; }";
final CSSStyleSheetImpl sheet = parse(css, 1, 0, 1);
final CSSRuleListImpl rules = sheet.getCssRules();

// Should skip the invalid media rule and parse h2
assertEquals(1, rules.getLength());
assertEquals("h2 { color: blue; }", rules.getRules().get(0).getCssText());
}

/**
* Test recovery handles balanced braces correctly.
*/
@Test
public void balancedBracesInError() throws Exception {
final String css = "h1 { content: \"{}\"; color: red; }\n"
+ "h2 { color: blue; }";
final CSSStyleSheetImpl sheet = parse(css, 0, 0, 0);
final CSSRuleListImpl rules = sheet.getCssRules();

// Both rules should be valid
assertEquals(2, rules.getLength());
assertEquals("h1 { content: \"{}\"; color: red; }", rules.getRules().get(0).getCssText());
assertEquals("h2 { color: blue; }", rules.getRules().get(1).getCssText());
}
}