Enable basic error recovery

Adding only a single recovery rule for now.

The API is now:
 * throwOnError parser option must be disabled.
 * List of Errors is available through $parser->getErrors(). This
   method is available either way.
 * If no recovery is possible $parser->parse() will return null.
   (Obviously only if throwOnError is disabled).
This commit is contained in:
Nikita Popov 2015-04-30 17:37:19 +02:00
parent a35c2a2067
commit 3b7d8e8b5d
9 changed files with 1347 additions and 1280 deletions

View File

@ -215,6 +215,7 @@ statement:
| T_THROW expr ';' { $$ = Stmt\Throw_[$2]; }
| T_GOTO T_STRING ';' { $$ = Stmt\Goto_[$2]; }
| T_STRING ':' { $$ = Stmt\Label[$1]; }
| error { $$ = array(); /* means: no statement */ }
;
catches:

File diff suppressed because it is too large Load Diff

View File

@ -96,7 +96,7 @@ abstract class ParserAbstract
* Creates a parser instance.
*
* @param Lexer $lexer A lexer
* @param array $options Options array. The boolean 'throwOnError' option determined whether an exception should be
* @param array $options Options array. The boolean 'throwOnError' option determines whether an exception should be
* thrown on first error, or if the parser should try to continue parsing the remaining code
* and build a partial AST.
*/
@ -109,7 +109,7 @@ abstract class ParserAbstract
/**
* Get array of errors that occurred during the last parse.
*
* This method may only return multiple errors if the throwOnError option is disabled.
* This method may only return multiple errors if the 'throwOnError' option is disabled.
*
* @return Error[]
*/
@ -122,7 +122,8 @@ abstract class ParserAbstract
*
* @param string $code The source code to parse
*
* @return Node[] Array of statements
* @return Node[]|null Array of statements (or null if the 'throwOnError' option is disabled and the parser was
* unable to recover from an error).
*/
public function parse($code) {
$this->lexer->startLexing($code);
@ -244,7 +245,13 @@ abstract class ParserAbstract
$e->setStartLine($startAttributes['startLine']);
}
$this->errors[] = $e;
if ($this->throwOnError) {
throw $e;
} else {
// Currently can't recover from "special" errors
return null;
}
}
/* Goto - shift nonterminal */
@ -284,7 +291,8 @@ abstract class ParserAbstract
&& $idx < $this->actionTableSize && $this->actionCheck[$idx] == $this->errorSymbol)
) || ($action = $this->action[$idx]) == $this->defaultAction) { // Not totally sure about this
if ($this->stackPos <= 0) {
throw new Error('Could not recover from error');
// Could not recover from error
return null;
}
$state = $stateStack[--$this->stackPos];
//$this->tracePop($state);
@ -296,8 +304,8 @@ abstract class ParserAbstract
case 3:
if ($symbol === 0) {
// Reached EOF
throw new Error('Could not recover from error');
// Reached EOF without recovering from error
return null;
}
//$this->traceDiscard($symbol);
@ -314,6 +322,8 @@ abstract class ParserAbstract
$rule = $state - $this->YYNLSTATES;
}
}
throw new \RuntimeException('Reached end of parser loop');
}
protected function getErrorMessage($symbol, $state) {

View File

@ -30,24 +30,36 @@ class ParserTest extends CodeTestAbstract
/**
* @dataProvider provideTestParseFail
*/
public function testParseFail($name, $code, $expectedMsg) {
public function testParseFail($name, $code, $expected) {
$lexer = new Lexer\Emulative(array('usedAttributes' => array(
'startLine', 'endLine', 'startFilePos', 'endFilePos'
)));
$parser = new Parser($lexer);
$parser = new Parser($lexer, array(
'throwOnError' => false,
));
try {
$parser->parse($code);
$stmts = $parser->parse($code);
$errors = $parser->getErrors();
$this->fail(sprintf('"%s": Expected Error', $name));
} catch (Error $e) {
$output = '';
foreach ($errors as $error) {
$output .= $this->formatErrorMessage($error, $code) . "\n";
}
if (null !== $stmts) {
$dumper = new NodeDumper;
$output .= $dumper->dump($stmts);
}
$this->assertSame($this->canonicalize($expected), $this->canonicalize($output), $name);
}
private function formatErrorMessage(Error $e, $code) {
if ($e->hasColumnInfo()) {
$msg = $e->getRawMessage() . ' from ' . $e->getStartLine() . ':' . $e->getStartColumn($code)
return $e->getRawMessage() . ' from ' . $e->getStartLine() . ':' . $e->getStartColumn($code)
. ' to ' . $e->getEndLine() . ':' . $e->getEndColumn($code);
} else {
$msg = $e->getMessage();
}
$this->assertSame($expectedMsg, $msg, $name);
return $e->getMessage();
}
}

View File

@ -3,7 +3,11 @@ Error positions
<?php foo
-----
Syntax error, unexpected EOF from 1:10 to 1:10
array(
)
-----
<?php foo /* bar */
-----
Syntax error, unexpected EOF from 1:20 to 1:20
array(
)

View File

@ -4,3 +4,5 @@ New without a class
new;
-----
Syntax error, unexpected ';' from 2:4 to 2:4
array(
)

View File

@ -25,8 +25,20 @@ Multiple final modifiers are not allowed on line 1
Cannot use the final modifier on an abstract class member on line 1
-----
<?php abstract final class A { }
// Type in the partial parse could conceivably be any of 0, 16 or 32
-----
Syntax error, unexpected T_FINAL, expecting T_CLASS from 1:16 to 1:20
array(
0: Stmt_Class(
type: 32
name: A
extends: null
implements: array(
)
stmts: array(
)
)
)
-----
<?php class A { abstract $a; }
-----

View File

@ -11,6 +11,8 @@ Cannot use 'PARENT' as class name as it is reserved on line 1
<?php class static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING from 1:13 to 1:18
array(
)
-----
<?php class A extends self {}
-----
@ -23,6 +25,8 @@ Cannot use 'PARENT' as class name as it is reserved from 1:23 to 1:28
<?php class A extends static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING or T_NAMESPACE or T_NS_SEPARATOR from 1:23 to 1:28
array(
)
-----
<?php class A implements self {}
-----
@ -35,6 +39,8 @@ Cannot use 'PARENT' as interface name as it is reserved from 1:26 to 1:31
<?php class A implements static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING or T_NAMESPACE or T_NS_SEPARATOR from 1:26 to 1:31
array(
)
-----
<?php interface self {}
-----
@ -47,6 +53,8 @@ Cannot use 'PARENT' as class name as it is reserved on line 1
<?php interface static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING from 1:17 to 1:22
array(
)
-----
<?php interface A extends self {}
-----
@ -59,3 +67,5 @@ Cannot use 'PARENT' as interface name as it is reserved from 1:27 to 1:32
<?php interface A extends static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING or T_NAMESPACE or T_NS_SEPARATOR from 1:27 to 1:32
array(
)

View File

@ -11,6 +11,8 @@ Cannot use 'PARENT' as namespace name from 1:17 to 1:22
<?php namespace static;
-----
Syntax error, unexpected T_STATIC, expecting T_STRING or T_NS_SEPARATOR or '{' from 1:17 to 1:22
array(
)
-----
<?php use A as self;
-----
@ -23,3 +25,5 @@ Cannot use B as PARENT because 'PARENT' is a special class name on line 1
<?php use C as static;
-----
Syntax error, unexpected T_STATIC, expecting T_STRING from 1:16 to 1:21
array(
)