diff --git a/lib/PhpParser/ErrorHandler.php b/lib/PhpParser/ErrorHandler.php new file mode 100644 index 0000000..fa2c2f8 --- /dev/null +++ b/lib/PhpParser/ErrorHandler.php @@ -0,0 +1,13 @@ +errors[] = $error; + } + + /** + * Get collected errors. + * + * @return Error[] + */ + public function getErrors() { + return $this->errors; + } + + /** + * Check whether there are any errors. + * + * @return bool + */ + public function hasErrors() { + return !empty($this->errors); + } + + /** + * Reset/clear collected errors. + */ + public function clearErrors() { + $this->errors = []; + } +} \ No newline at end of file diff --git a/lib/PhpParser/ErrorHandler/Throwing.php b/lib/PhpParser/ErrorHandler/Throwing.php new file mode 100644 index 0000000..c5a76dd --- /dev/null +++ b/lib/PhpParser/ErrorHandler/Throwing.php @@ -0,0 +1,18 @@ +code = $code; // keep the code around for __halt_compiler() handling $this->pos = -1; $this->line = 1; $this->filePos = 0; - $this->errors = []; // If inline HTML occurs without preceding code, treat it as if it had a leading newline. // This ensures proper composability, because having a newline is the "safe" assumption. @@ -70,7 +74,7 @@ class Lexer $this->resetErrors(); $this->tokens = @token_get_all($code); - $this->handleErrors(); + $this->handleErrors($errorHandler); if (false !== $scream) { ini_set('xdebug.scream', $scream); @@ -88,7 +92,7 @@ class Lexer } } - private function handleInvalidCharacterRange($start, $end, $line) { + private function handleInvalidCharacterRange($start, $end, $line, ErrorHandler $errorHandler) { for ($i = $start; $i < $end; $i++) { $chr = $this->code[$i]; if ($chr === 'b' || $chr === 'B') { @@ -105,12 +109,12 @@ class Lexer ); } - $this->errors[] = new Error($errorMsg, [ + $errorHandler->handleError(new Error($errorMsg, [ 'startLine' => $line, 'endLine' => $line, 'startFilePos' => $i, 'endFilePos' => $i, - ]); + ])); } } @@ -132,7 +136,7 @@ class Lexer && false === strpos($error['message'], 'Undefined variable'); } - protected function handleErrors() { + protected function handleErrors(ErrorHandler $errorHandler) { if (!$this->errorMayHaveOccurred()) { return; } @@ -151,7 +155,8 @@ class Lexer if (substr($this->code, $filePos, $tokenLen) !== $tokenValue) { // Something is missing, must be an invalid character $nextFilePos = strpos($this->code, $tokenValue, $filePos); - $this->handleInvalidCharacterRange($filePos, $nextFilePos, $line); + $this->handleInvalidCharacterRange( + $filePos, $nextFilePos, $line, $errorHandler); $filePos = $nextFilePos; } @@ -163,19 +168,20 @@ class Lexer if (substr($this->code, $filePos, 2) === '/*') { // Unlike PHP, HHVM will drop unterminated comments entirely $comment = substr($this->code, $filePos); - $this->errors[] = new Error('Unterminated comment', [ + $errorHandler->handleError(new Error('Unterminated comment', [ 'startLine' => $line, 'endLine' => $line + substr_count($comment, "\n"), 'startFilePos' => $filePos, 'endFilePos' => $filePos + \strlen($comment), - ]); + ])); // Emulate the PHP behavior $isDocComment = isset($comment[3]) && $comment[3] === '*'; $this->tokens[] = [$isDocComment ? T_DOC_COMMENT : T_COMMENT, $comment, $line]; } else { // Invalid characters at the end of the input - $this->handleInvalidCharacterRange($filePos, \strlen($this->code), $line); + $this->handleInvalidCharacterRange( + $filePos, \strlen($this->code), $line, $errorHandler); } return; } @@ -183,12 +189,12 @@ class Lexer // Check for unterminated comment $lastToken = $this->tokens[count($this->tokens) - 1]; if ($this->isUnterminatedComment($lastToken)) { - $this->errors[] = new Error('Unterminated comment', [ + $errorHandler->handleError(new Error('Unterminated comment', [ 'startLine' => $line - substr_count($lastToken[1], "\n"), 'endLine' => $line, 'startFilePos' => $filePos - \strlen($lastToken[1]), 'endFilePos' => $filePos, - ]); + ])); } } @@ -302,15 +308,6 @@ class Lexer return $this->tokens; } - /** - * Returns errors that occurred during lexing. - * - * @return Error[] Array of lexer errors - */ - public function getErrors() { - return $this->errors; - } - /** * Handles __halt_compiler() by returning the text after it. * diff --git a/lib/PhpParser/Lexer/Emulative.php b/lib/PhpParser/Lexer/Emulative.php index 03ca7d0..739a328 100644 --- a/lib/PhpParser/Lexer/Emulative.php +++ b/lib/PhpParser/Lexer/Emulative.php @@ -2,6 +2,7 @@ namespace PhpParser\Lexer; +use PhpParser\ErrorHandler; use PhpParser\Parser\Tokens; class Emulative extends \PhpParser\Lexer @@ -50,10 +51,10 @@ class Emulative extends \PhpParser\Lexer $this->tokenMap[self::T_POW_EQUAL] = Tokens::T_POW_EQUAL; } - public function startLexing($code) { + public function startLexing($code, ErrorHandler $errorHandler = null) { $this->inObjectAccess = false; - parent::startLexing($code); + parent::startLexing($code, $errorHandler); if ($this->requiresEmulation($code)) { $this->emulateTokens(); } diff --git a/lib/PhpParser/NodeTraverser.php b/lib/PhpParser/NodeTraverser.php index 6a21aed..3b63776 100644 --- a/lib/PhpParser/NodeTraverser.php +++ b/lib/PhpParser/NodeTraverser.php @@ -4,9 +4,7 @@ namespace PhpParser; class NodeTraverser implements NodeTraverserInterface { - /** - * @var NodeVisitor[] Visitors - */ + /** @var NodeVisitor[] Visitors */ protected $visitors; /** diff --git a/lib/PhpParser/NodeVisitor/NameResolver.php b/lib/PhpParser/NodeVisitor/NameResolver.php index f853ba7..02ec1c8 100644 --- a/lib/PhpParser/NodeVisitor/NameResolver.php +++ b/lib/PhpParser/NodeVisitor/NameResolver.php @@ -2,6 +2,7 @@ namespace PhpParser\NodeVisitor; +use PhpParser\ErrorHandler; use PhpParser\NodeVisitorAbstract; use PhpParser\Error; use PhpParser\Node; @@ -18,6 +19,18 @@ class NameResolver extends NodeVisitorAbstract /** @var array Map of format [aliasType => [aliasName => originalName]] */ protected $aliases; + /** @var ErrorHandler Error handler */ + protected $errorHandler; + + /** + * Constructs a name resolution visitor. + * + * @param ErrorHandler|null $errorHandler Error handler + */ + public function __construct(ErrorHandler $errorHandler = null) { + $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing; + } + public function beforeTraverse(array $nodes) { $this->resetState(); } @@ -132,13 +145,14 @@ class NameResolver extends NodeVisitorAbstract Stmt\Use_::TYPE_CONSTANT => 'const ', ); - throw new Error( + $this->errorHandler->handleError(new Error( sprintf( 'Cannot use %s%s as %s because the name is already in use', $typeStringMap[$type], $name, $use->alias ), - $use->getLine() - ); + $use->getAttributes() + )); + return; } $this->aliases[$type][$aliasName] = $name; @@ -160,10 +174,10 @@ class NameResolver extends NodeVisitorAbstract // don't resolve special class names if (in_array(strtolower($name->toString()), array('self', 'parent', 'static'))) { if (!$name->isUnqualified()) { - throw new Error( + $this->errorHandler->handleError(new Error( sprintf("'\\%s' is an invalid class name", $name->toString()), - $name->getLine() - ); + $name->getAttributes() + )); } return $name; diff --git a/lib/PhpParser/Parser.php b/lib/PhpParser/Parser.php index 67913c9..fb7dcad 100644 --- a/lib/PhpParser/Parser.php +++ b/lib/PhpParser/Parser.php @@ -7,18 +7,11 @@ interface Parser { * Parses PHP code into a node tree. * * @param string $code The source code to parse + * @param ErrorHandler|null $errorHandler Error handler to use for lexer/parser errors, defaults + * to ErrorHandler\Throwing. * * @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); - - /** - * Get array of errors that occurred during the last parse. - * - * This method may only return multiple errors if the 'throwOnError' option is disabled. - * - * @return Error[] - */ - public function getErrors(); + public function parse($code, ErrorHandler $errorHandler = null); } diff --git a/lib/PhpParser/Parser/Multiple.php b/lib/PhpParser/Parser/Multiple.php index 95446e2..25296a4 100644 --- a/lib/PhpParser/Parser/Multiple.php +++ b/lib/PhpParser/Parser/Multiple.php @@ -3,61 +3,52 @@ namespace PhpParser\Parser; use PhpParser\Error; +use PhpParser\ErrorHandler; use PhpParser\Parser; class Multiple implements Parser { /** @var Parser[] List of parsers to try, in order of preference */ private $parsers; - /** @var Error[] Errors collected during last parse */ - private $errors; /** * Create a parser which will try multiple parsers in an order of preference. * * Parsers will be invoked in the order they're provided to the constructor. If one of the - * parsers runs without errors, it's output is returned. Otherwise the errors (and - * PhpParser\Error exception) of the first parser are used. + * parsers runs without throwing, it's output is returned. Otherwise the exception that the + * first parser generated is thrown. * * @param Parser[] $parsers */ public function __construct(array $parsers) { $this->parsers = $parsers; - $this->errors = []; } - public function parse($code) { - list($firstStmts, $firstErrors, $firstError) = $this->tryParse($this->parsers[0], $code); - if ($firstErrors === []) { - $this->errors = []; + public function parse($code, ErrorHandler $errorHandler = null) { + if (null === $errorHandler) { + $errorHandler = new ErrorHandler\Throwing; + } + + list($firstStmts, $firstError) = $this->tryParse($this->parsers[0], $errorHandler, $code); + if ($firstError === null) { return $firstStmts; } for ($i = 1, $c = count($this->parsers); $i < $c; ++$i) { - list($stmts, $errors) = $this->tryParse($this->parsers[$i], $code); - if ($errors === []) { - $this->errors = []; + list($stmts, $error) = $this->tryParse($this->parsers[$i], $errorHandler, $code); + if ($error === null) { return $stmts; } } - $this->errors = $firstErrors; - if ($firstError) { - throw $firstError; - } - return $firstStmts; + throw $firstError; } - public function getErrors() { - return $this->errors; - } - - private function tryParse(Parser $parser, $code) { + private function tryParse(Parser $parser, ErrorHandler $errorHandler, $code) { $stmts = null; $error = null; try { - $stmts = $parser->parse($code); + $stmts = $parser->parse($code, $errorHandler); } catch (Error $error) {} - $errors = $parser->getErrors(); - return [$stmts, $errors, $error]; + return [$stmts, $error]; } } diff --git a/lib/PhpParser/ParserAbstract.php b/lib/PhpParser/ParserAbstract.php index c018c1d..cf4a2ed 100644 --- a/lib/PhpParser/ParserAbstract.php +++ b/lib/PhpParser/ParserAbstract.php @@ -103,8 +103,8 @@ abstract class ParserAbstract implements Parser /** @var array Start attributes of last *read* token */ protected $lookaheadStartAttributes; - /** @var bool Whether to throw on first error */ - protected $throwOnError; + /** @var ErrorHandler Error handler */ + protected $errorHandler; /** @var Error[] Errors collected during last parse */ protected $errors; /** @var int Error state, used to avoid error floods */ @@ -114,42 +114,36 @@ abstract class ParserAbstract implements Parser * Creates a parser instance. * * @param Lexer $lexer A lexer - * @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. + * @param array $options Options array. Currently no options are supported. */ public function __construct(Lexer $lexer, array $options = array()) { $this->lexer = $lexer; $this->errors = array(); - $this->throwOnError = isset($options['throwOnError']) ? $options['throwOnError'] : true; - } - /** - * Get array of errors that occurred during the last parse. - * - * This method may only return multiple errors if the 'throwOnError' option is disabled. - * - * @return Error[] - */ - public function getErrors() { - return $this->errors; + if (isset($options['throwOnError'])) { + throw new \LogicException( + '"throwOnError" is no longer supported, use "errorHandler" instead'); + } } /** * Parses PHP code into a node tree. * + * If a non-throwing error handler is used, the parser will continue parsing after an error + * occurred and attempt to build a partial AST. + * * @param string $code The source code to parse + * @param ErrorHandler|null $errorHandler Error handler to use for lexer/parser errors, defaults + * to ErrorHandler\Throwing. * * @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) { - // Initialize the lexer and inherit lexing errors - $this->lexer->startLexing($code); - $this->errors = $this->lexer->getErrors(); - if ($this->throwOnError && !empty($this->errors)) { - throw $this->errors[0]; - } + public function parse($code, ErrorHandler $errorHandler = null) { + $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing; + + // Initialize the lexer + $this->lexer->startLexing($code, $this->errorHandler); // We start off with no lookahead-token $symbol = self::SYMBOL_NONE; @@ -346,10 +340,7 @@ abstract class ParserAbstract implements Parser } protected function emitError(Error $error) { - $this->errors[] = $error; - if ($this->throwOnError) { - throw $error; - } + $this->errorHandler->handleError($error); } protected function getErrorMessage($symbol, $state) { diff --git a/test/PhpParser/CodeParsingTest.php b/test/PhpParser/CodeParsingTest.php index d17507b..904f92c 100644 --- a/test/PhpParser/CodeParsingTest.php +++ b/test/PhpParser/CodeParsingTest.php @@ -15,12 +15,8 @@ class CodeParsingTest extends CodeTestAbstract $lexer = new Lexer\Emulative(array('usedAttributes' => array( 'startLine', 'endLine', 'startFilePos', 'endFilePos', 'comments' ))); - $parser5 = new Parser\Php5($lexer, array( - 'throwOnError' => false, - )); - $parser7 = new Parser\Php7($lexer, array( - 'throwOnError' => false, - )); + $parser5 = new Parser\Php5($lexer); + $parser7 = new Parser\Php7($lexer); $output5 = $this->getParseOutput($parser5, $code); $output7 = $this->getParseOutput($parser7, $code); @@ -38,11 +34,11 @@ class CodeParsingTest extends CodeTestAbstract } private function getParseOutput(Parser $parser, $code) { - $stmts = $parser->parse($code); - $errors = $parser->getErrors(); + $errors = new ErrorHandler\Collecting; + $stmts = $parser->parse($code, $errors); $output = ''; - foreach ($errors as $error) { + foreach ($errors->getErrors() as $error) { $output .= $this->formatErrorMessage($error, $code) . "\n"; } diff --git a/test/PhpParser/ErrorHandler/CollectingTest.php b/test/PhpParser/ErrorHandler/CollectingTest.php new file mode 100644 index 0000000..3742981 --- /dev/null +++ b/test/PhpParser/ErrorHandler/CollectingTest.php @@ -0,0 +1,22 @@ +assertFalse($errorHandler->hasErrors()); + $this->assertEmpty($errorHandler->getErrors()); + + $errorHandler->handleError($e1 = new Error('Test 1')); + $errorHandler->handleError($e2 = new Error('Test 2')); + $this->assertTrue($errorHandler->hasErrors()); + $this->assertSame([$e1, $e2], $errorHandler->getErrors()); + + $errorHandler->clearErrors(); + $this->assertFalse($errorHandler->hasErrors()); + $this->assertEmpty($errorHandler->getErrors()); + } +} \ No newline at end of file diff --git a/test/PhpParser/ErrorHandler/ThrowingTest.php b/test/PhpParser/ErrorHandler/ThrowingTest.php new file mode 100644 index 0000000..d7df7c2 --- /dev/null +++ b/test/PhpParser/ErrorHandler/ThrowingTest.php @@ -0,0 +1,16 @@ +handleError(new Error('Test')); + } +} \ No newline at end of file diff --git a/test/PhpParser/LexerTest.php b/test/PhpParser/LexerTest.php index f81757f..d2f570e 100644 --- a/test/PhpParser/LexerTest.php +++ b/test/PhpParser/LexerTest.php @@ -19,11 +19,12 @@ class LexerTest extends \PHPUnit_Framework_TestCase $this->markTestSkipped('HHVM does not throw warnings from token_get_all()'); } + $errorHandler = new ErrorHandler\Collecting(); $lexer = $this->getLexer(['usedAttributes' => [ 'comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos' ]]); - $lexer->startLexing($code); - $errors = $lexer->getErrors(); + $lexer->startLexing($code, $errorHandler); + $errors = $errorHandler->getErrors(); $this->assertSame(count($messages), count($errors)); for ($i = 0; $i < count($messages); $i++) { diff --git a/test/PhpParser/Parser/MultipleTest.php b/test/PhpParser/Parser/MultipleTest.php index e2722c3..2394467 100644 --- a/test/PhpParser/Parser/MultipleTest.php +++ b/test/PhpParser/Parser/MultipleTest.php @@ -30,7 +30,6 @@ class MultipleTest extends ParserTest { /** @dataProvider provideTestParse */ public function testParse($code, Multiple $parser, $expected) { $this->assertEquals($expected, $parser->parse($code)); - $this->assertSame([], $parser->getErrors()); } public function provideTestParse() { @@ -92,22 +91,4 @@ class MultipleTest extends ParserTest { $parser = new Multiple([$parserA, $parserB]); $parser->parse('dummy'); } - - public function testGetErrors() { - $errorsA = [new Error('A1'), new Error('A2')]; - $parserA = $this->getMockBuilder('PhpParser\Parser')->getMock(); - $parserA->expects($this->at(0))->method('parse'); - $parserA->expects($this->at(1)) - ->method('getErrors')->will($this->returnValue($errorsA)); - - $errorsB = [new Error('B1'), new Error('B2')]; - $parserB = $this->getMockBuilder('PhpParser\Parser')->getMock(); - $parserB->expects($this->at(0))->method('parse'); - $parserB->expects($this->at(1)) - ->method('getErrors')->will($this->returnValue($errorsB)); - - $parser = new Multiple([$parserA, $parserB]); - $parser->parse('dummy'); - $this->assertSame($errorsA, $parser->getErrors()); - } } \ No newline at end of file