Make Error column info attribute based

This commit is contained in:
Nikita Popov 2015-04-18 12:51:26 +02:00
parent 33a39fae06
commit 4defbc2174
3 changed files with 128 additions and 130 deletions

View File

@ -5,24 +5,22 @@ namespace PhpParser;
class Error extends \RuntimeException
{
protected $rawMessage;
protected $rawLine;
protected $tokens = array();
protected $tokenIndex;
protected $beginColumnCache;
protected $attributes;
/**
* Creates an Exception signifying a parse error.
*
* @param string $message Error message
* @param int $line Error line in PHP file
* @param array $tokens Array of all tokens in the file that caused the error.
* @param int $tokenIndex Index in $tokens of token where error happened.
* @param string $message Error message
* @param array|int $attributes Attributes of node/token where error occurred
* (or start line of error -- deprecated)
*/
public function __construct($message, $line = -1, array $tokens=array(), $tokenIndex=null) {
public function __construct($message, $attributes = array()) {
$this->rawMessage = (string) $message;
$this->rawLine = (int) $line;
$this->tokens = $tokens;
$this->tokenIndex = $tokenIndex;
if (is_array($attributes)) {
$this->attributes = $attributes;
} else {
$this->attributes = array('startLine' => $attributes);
}
$this->updateMessage();
}
@ -35,6 +33,34 @@ class Error extends \RuntimeException
return $this->rawMessage;
}
/**
* Gets the line the error starts in.
*
* @return int Error start line
*/
public function getStartLine() {
return isset($this->attributes['startLine']) ? $this->attributes['startLine'] : -1;
}
/**
* Gets the line the error ends in.
*
* @return int Error end line
*/
public function getEndLine() {
return isset($this->attributes['endLine']) ? $this->attributes['endLine'] : -1;
}
/**
* Gets the attributes of the node/token the error occurred at.
*
* @return array
*/
public function getAttributes() {
return $this->attributes;
}
/**
* Sets the line of the PHP file the error occurred in.
*
@ -46,120 +72,65 @@ class Error extends \RuntimeException
}
/**
* Gets the error line in the PHP file.
* Sets the line the error starts in.
*
* @return int Error line in the PHP file
* @param int $line Error start line
*/
public function getRawLine() {
return $this->rawLine;
}
/**
* Sets the line of the PHP file the error occurred in.
*
* @param int $line Error line in the PHP file
*/
public function setRawLine($line) {
$this->rawLine = (int) $line;
public function setStartLine($line) {
$this->attributes['startLine'] = (int) $line;
$this->updateMessage();
}
/**
* Checks if valid token-information is available for this error.
*
* Returns whether the error has start and end column information.
*
* For column information enable the startFilePos and endFilePos in the lexer options.
*
* @return bool
*/
public function hasTokenAttributes(){
return is_numeric($this->tokenIndex) && isset($this->tokens[(int)$this->tokenIndex]);
public function hasColumnInfo() {
return isset($this->attributes['startFilePos']) && isset($this->attributes['endFilePos']);
}
/**
* Gets the tokens for the php-file in which this error happened.
* Only works if token-information was provided.
*
* @return array
*/
public function getTokens(){
return $this->tokens;
}
/**
* Get sthe index in tokens in which this error happened.
* Only works if token-information was provided.
*
* Gets the start column (0-based) into the line where the error started.
*
* @param string $code Source code of the file
* @return int
*/
public function getTokenIndex(){
return $this->tokenIndex;
public function getStartColumn($code) {
if (!$this->hasColumnInfo()) {
throw new \RuntimeException('Error does not have column information');
}
return $this->toColumn($code, $this->attributes['startFilePos']);
}
/**
* Gets the first column number in which the error happened.
* Only works if token-information was provided.
*
* Gets the end column (0-based) into the line where the error ended.
*
* @param string $code Source code of the file
* @return int
*/
public function getBeginColumn() {
$beginColumn = null;
if($this->hasTokenAttributes()) {
if(!is_null($this->beginColumnCache)) {
$beginColumn = $this->beginColumnCache;
} else {
$beginColumn = 0;
$tokenIndex = $this->tokenIndex;
for($i=$tokenIndex-1;$i>=0;$i--) {
$tokenText = $this->getTextFromToken($this->tokens[$i]);
$beginColumn += strlen($tokenText);
$newlinePosition = strrpos($tokenText, "\n");
if($newlinePosition !== false){
$beginColumn -= $newlinePosition;
break;
}
}
}
public function getEndColumn($code) {
if (!$this->hasColumnInfo()) {
throw new \RuntimeException('Error does not have column information');
}
return $beginColumn;
return $this->toColumn($code, $this->attributes['endFilePos']);
}
/**
* Gets the last column number in which the error happened.
* Only works if token-information was provided.
*
* @return int
*/
public function getEndColumn(){
$endColumn = null;
if($this->hasTokenAttributes()){
$beginColumn = $this->getBeginColumn();
$token = $this->tokens[(int)$this->tokenIndex];
$tokenText = $this->getTextFromToken($token);
$endColumn = $beginColumn + strlen($tokenText);
private function toColumn($code, $pos) {
if ($pos >= strlen($code)) {
throw new \RuntimeException('Invalid position information');
}
return $endColumn;
}
private function getTextFromToken($token){
$tokenText = $token;
if(is_array($tokenText)){
if(is_int($tokenText[0])){
$tokenText = $tokenText[1];
}else{
$tokenText = $tokenText[0];
}
$lineStartPos = strrpos($code, "\n", $pos - strlen($code));
if (false === $lineStartPos) {
$lineStartPos = -1;
}
return $tokenText;
return $pos - $lineStartPos - 1;
}
/**
@ -168,11 +139,20 @@ class Error extends \RuntimeException
protected function updateMessage() {
$this->message = $this->rawMessage;
if (-1 === $this->rawLine) {
if (-1 === $this->getStartLine()) {
$this->message .= ' on unknown line';
} else {
$this->message .= ' on line ' . $this->rawLine;
$this->message .= ' on line ' . $this->getStartLine();
}
}
/** @deprecated Use getStartLine() instead */
public function getRawLine() {
return $this->getStartLine();
}
/** @deprecated Use setStartLine() instead */
public function setRawLine($line) {
$this->setStartLine($line);
}
}

View File

@ -205,8 +205,8 @@ abstract class ParserAbstract
+ $endAttributes
);
} catch (Error $e) {
if (-1 === $e->getRawLine() && isset($startAttributes['startLine'])) {
$e->setRawLine($startAttributes['startLine']);
if (-1 === $e->getStartLine() && isset($startAttributes['startLine'])) {
$e->setStartLine($startAttributes['startLine']);
}
throw $e;
@ -239,14 +239,9 @@ abstract class ParserAbstract
$expectedString = '';
}
$tokens = $this->lexer->getTokens();
$errorPosition = $this->lexer->getPosition();
throw new Error(
'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString,
$startAttributes['startLine'],
$tokens,
$errorPosition
$startAttributes + $endAttributes
);
}
}

View File

@ -33,32 +33,55 @@ class ErrorTest extends \PHPUnit_Framework_TestCase
$this->assertSame('Some error on unknown line', $error->getMessage());
}
/**
* @depends testConstruct
*/
public function testColumnNumbers() {
/** @dataProvider provideTestColumnInfo */
public function testColumnInfo($code, $startPos, $endPos, $startColumn, $endColumn) {
$error = new Error('Some error', array(
'startFilePos' => $startPos,
'endFilePos' => $endPos,
));
$faultyCode = "<?php \$foo = bar baz; ?>";
$tokens = token_get_all($faultyCode);
$error = new Error('Some error', 1, $tokens, 5);
$this->assertSame(true, $error->hasTokenAttributes());
$this->assertSame(13, $error->getBeginColumn());
$this->assertSame(16, $error->getEndColumn());
$this->assertSame(true, $error->hasColumnInfo());
$this->assertSame($startColumn, $error->getStartColumn($code));
$this->assertSame($endColumn, $error->getEndColumn($code));
}
/**
* @depends testConstruct
*/
public function testTokenInformationMissing(){
public function provideTestColumnInfo() {
return array(
// Error at "bar"
array("<?php foo bar baz", 10, 12, 10, 12),
array("<?php\nfoo bar baz", 10, 12, 4, 6),
array("<?php foo\nbar baz", 10, 12, 0, 2),
array("<?php foo bar\nbaz", 10, 12, 10, 12),
array("<?php\r\nfoo bar baz", 11, 13, 4, 6),
// Error at "baz"
array("<?php foo bar baz", 14, 16, 14, 16),
array("<?php foo bar\nbaz", 14, 16, 0, 2),
// Error at string literal
array("<?php foo 'bar\nbaz' xyz", 10, 18, 10, 3),
array("<?php\nfoo 'bar\nbaz' xyz", 10, 18, 4, 3),
array("<?php foo\n'\nbarbaz\n'\nxyz", 10, 19, 0, 0),
// Error over full string
array("<?php", 0, 4, 0, 4),
array("<?\nphp", 0, 5, 0, 2),
);
}
public function testNoColumnInfo(){
$error = new Error('Some error', 3);
$this->assertSame(false, $error->hasTokenAttributes());
$this->assertSame(null, $error->getBeginColumn());
$this->assertSame(null, $error->getEndColumn());
$this->assertSame(false, $error->hasColumnInfo());
try {
$error->getStartColumn('');
$this->fail('Expected RuntimeException');
} catch (\RuntimeException $e) {
$this->assertEquals('Error does not have column information', $e->getMessage());
}
try {
$error->getEndColumn('');
$this->fail('Expected RuntimeException');
} catch (\RuntimeException $e) {
$this->assertEquals('Error does not have column information', $e->getMessage());
}
}
}