1
0
mirror of https://github.com/nikic/PHP-Parser.git synced 2025-07-11 01:16:33 +02:00

Compare commits

..

30 Commits

Author SHA1 Message Date
dff239267f Release PHP-Parser 1.3.0 2015-05-02 17:40:40 +02:00
8b64195cf2 Try .17G print if .16G is not enough
This should be enough for all cases, because: A double has 53 bits
of mantissa (including the implicit 1 bit), which is 53*ln(2)/ln(10)
= 15.95 decimal digits. However the leading decimal digit may encode
less than the usual 3.32 bits, which will push this over the edge to
requiring 17 decimal digits.
2015-05-02 11:48:55 +02:00
9caa51b3a5 Add some more tests 2015-05-01 20:18:04 +02:00
5513073a53 Drop Lexer::getPosition()
This was a leftover from the original column info implementation.
2015-05-01 20:18:01 +02:00
659d26c231 Update changelog, fix typos 2015-05-01 19:57:10 +02:00
648800f07f Increase float pretty printing precision
This removes the reliance on the "precision" ini setting.
Furthermore the default of precision=14 is not sufficient.
2015-05-01 19:15:33 +02:00
23bf4f0c13 Add some error documentation 2015-04-30 21:58:45 +02:00
57ac7e39bf Drop name magic from rebuildParser.php
Now using explicitly imported names, instead of magically
prepending Name\.

Also remove trailing whitespace from generated file.
2015-04-30 19:20:38 +02:00
272ab6c8d8 Add some recovery tests 2015-04-30 19:02:51 +02:00
0731b47655 Don't distinguish test-fail / test
With error recovery this is fluid. Using .test for everything.
2015-04-30 17:45:36 +02:00
3b7d8e8b5d 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).
2015-04-30 17:41:57 +02:00
a35c2a2067 Add column info to EOF errors
EOF errors are now located one past the end of the file.
2015-04-27 15:37:41 +02:00
55b2ead967 Make special class name checks case insensitive 2015-04-26 23:13:27 +02:00
e1a0ec3724 Add support for anonymous classes
Has not landed upstream yet, but syntax is unlikely to change.
2015-04-26 23:04:31 +02:00
66896dbde6 Poison attribute initializations in parser
Those should error if they end up being used.
2015-04-26 22:32:18 +02:00
338bc1f8e7 Fix "print" pretty-printing
Precedence was previously ignored.
2015-04-26 11:56:04 +02:00
ab80054e97 Add support for "yield from" 2015-04-26 11:56:02 +02:00
a6d2cd69f8 Clarify attribute assignment code
* Don't assign to attribute stack on reduce - why was that there
in the first place?
* Assign attributes to the position in the stack where the first
token of the production is, instead of one position earlier.
* Add a comment to clarify why we also assign attributes on read,
instead of just on shift.
2015-04-26 11:56:01 +02:00
6996db1e3a Build node attributes inside semantic action methods
Minor performance improvement for parsing, also allows to access
attributes with higher granulity in the parser, though this is not
currently done.

* #n can now be used to access the stack position of a token. $n
  is the same as $this->semStack[#n]. (Post-translate $n will
  actually be the stack position.)
* $attributeStack is now $this->startAttributeStack and
  $endAttributes is now $this->endAttributes.
* Attributes for a node are now computed inside the individual
  reduction methods, instead of being passed as a parameter.
  Accessible through the attributes() macro.
2015-04-26 11:55:59 +02:00
e30c3ac01b Merge pull request from GrahamCampbell/patch-1
Bump version to 1.3
2015-04-19 20:52:51 +02:00
a5477a4eaf Version bump 2015-04-19 17:57:38 +01:00
7a40498cb4 Regenerate parser
Also drop the error() parser macro.
2015-04-19 18:12:56 +02:00
65f1adbe65 Merge primary error recovery code
The grammar does not yet actually implement error recovery.
2015-04-19 18:11:05 +02:00
c8454271e1 Make column info 1-based 2015-04-18 21:20:45 +02:00
62f83a0dc2 Add column info for non-syntax errors where relatively precise
Should it also be added if only rough information is available? E.g.
spanning an entire class?
2015-04-18 13:27:58 +02:00
611fa5c7f1 Test parser position information (syntax errors) 2015-04-18 13:27:57 +02:00
4defbc2174 Make Error column info attribute based 2015-04-18 12:51:26 +02:00
33a39fae06 added column-numbers to syntax errors 2015-04-18 10:14:49 +02:00
e4eab9ec0c Simplify code
The new class couldn't already exist at that point - if it did, the
old name would have been aliased already and the autoloader wouldn't
trigger.
2015-04-03 22:30:30 +02:00
6aaa87f143 Fix PHP 7 alias registration
This fixes the case where the old name is used before the new one
is ever used, e.g. when manually constructing nodes, as opposed to
parsing them.

The previous approach would try to register the alias from OLD to
NEW. This would trigger autoloading on NEW and afterwards it would
register the alias from OLD to NEW. Afterwards the alias registration
which originally triggered the autoload would run, thus redeclaring
the class.

TL;DR aliases suck, closes .
2015-04-03 22:22:39 +02:00
50 changed files with 2951 additions and 1904 deletions

@ -1,8 +1,31 @@
Version 1.2.3-dev
Version 1.3.1-dev
-----------------
Nothing yet.
Version 1.3.0 (2015-05-02)
--------------------------
### Added
* Errors can now store the attributes of the node/token where the error occurred. Previously only the start line was
stored.
* If file positions are enabled in the lexer, errors can now provide column information if it is available. See
[documentation](https://github.com/nikic/PHP-Parser/blob/master/doc/component/Error.markdown#column-information).
* The parser now provides an experimental error recovery mode, which can be enabled by disabling the `throwOnError`
parser option. In this mode the parser will try to construct a partial AST even if the code is not valid PHP. See
[documentation](https://github.com/nikic/PHP-Parser/blob/master/doc/component/Error.markdown#error-recovery).
* Added support for PHP 7 `yield from` expression. It is represented by `Expr\YieldFrom`.
* Added support for PHP 7 anonymous classes. These are represented by ordinary `Stmt\Class_` nodes with the name set to
`null`. Furthermore this implies that `Expr\New_` can now contain a `Stmt\Class_` in its `class` subnode.
### Fixed
* Fixed registration of PHP 7 aliases, for the case where the old name was used before the new name.
* Fixed handling of precedence when pretty-printing `print` expressions.
* Floating point numbers are now pretty-printed with a higher precision.
* Checks for special class names like `self` are now case-insensitive.
Version 1.2.2 (2015-04-03)
--------------------------

@ -10,7 +10,7 @@ ini_set('xdebug.var_display_max_children', -1);
ini_set('xdebug.var_display_max_data', -1);
ini_set('xdebug.var_display_max_depth', -1);
list($operations, $files) = parseArgs($argv);
list($operations, $files, $attributes) = parseArgs($argv);
/* Dump nodes by default */
if (empty($operations)) {
@ -21,7 +21,10 @@ if (empty($files)) {
showHelp("Must specify at least one file.");
}
$parser = new PhpParser\Parser(new PhpParser\Lexer\Emulative);
$lexer = new PhpParser\Lexer\Emulative(array('usedAttributes' => array(
'startLine', 'endLine', 'startFilePos', 'endFilePos'
)));
$parser = new PhpParser\Parser($lexer);
$dumper = new PhpParser\NodeDumper;
$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
$serializer = new PhpParser\Serializer\XML;
@ -45,7 +48,17 @@ foreach ($files as $file) {
try {
$stmts = $parser->parse($code);
} catch (PhpParser\Error $e) {
die("==> Parse Error: {$e->getMessage()}\n");
if ($attributes['with-column-info'] && $e->hasColumnInfo()) {
$startLine = $e->getStartLine();
$endLine = $e->getEndLine();
$startColumn = $e->getStartColumn($code);
$endColumn = $e->getEndColumn($code);
$message .= $e->getRawMessage() . " from $startLine:$startColumn to $endLine:$endColumn";
} else {
$message = $e->getMessage();
}
die($message . "\n");
}
foreach ($operations as $operation) {
@ -81,11 +94,12 @@ The file arguments can also be replaced with a code string:
Operations is a list of the following options (--dump by default):
--dump -d Dump nodes using NodeDumper
--pretty-print -p Pretty print file using PrettyPrinter\Standard
--serialize-xml Serialize nodes using Serializer\XML
--var-dump var_dump() nodes (for exact structure)
--resolve-names -N Resolve names using NodeVisitor\NameResolver
--dump -d Dump nodes using NodeDumper
--pretty-print -p Pretty print file using PrettyPrinter\Standard
--serialize-xml Serialize nodes using Serializer\XML
--var-dump var_dump() nodes (for exact structure)
--resolve-names -N Resolve names using NodeVisitor\NameResolver
--with-column-info -c Show column-numbers for errors (if available)
Example:
@ -101,6 +115,9 @@ OUTPUT
function parseArgs($args) {
$operations = array();
$files = array();
$attributes = array(
'with-column-info' => false,
);
array_shift($args);
$parseOptions = true;
@ -129,6 +146,10 @@ function parseArgs($args) {
case '-N';
$operations[] = 'resolve-names';
break;
case '--with-column-info':
case '-c';
$attributes['with-column-info'] = true;
break;
case '--':
$parseOptions = false;
break;
@ -141,5 +162,5 @@ function parseArgs($args) {
}
}
return array($operations, $files);
return array($operations, $files, $attributes);
}

@ -18,7 +18,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
"dev-master": "1.3-dev"
}
}
}

@ -0,0 +1,77 @@
Error handling
==============
Errors during parsing or analysis are represented using the `PhpParser\Error` exception class. In addition to an error
message, an error can also store additional information about the location the error occurred at.
How much location information is available depends on the origin of the error and how many lexer attributes have been
enabled. At a minimum the start line of the error is usually available.
Column information
------------------
In order to receive information about not only the line, but also the column span an error occurred at, the file
position attributes in the lexer need to be enabled:
```php
$lexer = new PhpParser\Lexer(array(
'usedAttributes' => array('comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'),
));
$parser = new PhpParser\Parser($lexer);
try {
$stmts = $parser->parse($code);
// ...
} catch (PhpParser\Error $e) {
// ...
}
```
Before using column information its availability needs to be checked with `$e->hasColumnInfo()`, as the precise
location of an error cannot always be determined. The methods for retrieving column information also have to be passed
the source code of the parsed file. An example for printing an error:
```php
if ($e->hasColumnInfo()) {
echo $e->getRawMessage() . ' from ' . $e->getStartLine() . ':' . $e->getStartColumn($code)
. ' to ' . $e->getEndLine() . ':' . $e->getEndColumn($code);
} else {
echo $e->getMessage();
}
```
Both line numbers and column numbers are 1-based. EOF errors will be located at the position one past the end of the
file.
Error recovery
--------------
> **EXPERIMENTAL**
By default the parser will throw an exception upon encountering the first error during parsing. An alternative mode is
also supported, in which the parser will remember the error, but try to continue parsing the rest of the source code.
To enable this mode the `throwOnError` parser option needs to be disabled. Any errors that occurred during parsing can
then be retrieved using `$parser->getErrors()`. The `$parser->parse()` method will either return a partial syntax tree
or `null` if recovery fails.
A usage example:
```php
$parser = new PhpParser\Parser(new PhpParser\Lexer, array(
'throwOnError' => false,
));
$stmts = $parser->parse($code);
$errors = $parser->getErrors();
foreach ($errors as $error) {
// $error is an ordinary PhpParser\Error
}
if (null !== $stmts) {
// $stmts is a best-effort partial AST
}
```
The error recovery implementation is experimental -- it currently won't be able to recover from many types of errors.

@ -13,8 +13,7 @@ The `.phpy` file is a normal grammer in `kmyacc` (`yacc`) style, with some trans
applied to it:
* Nodes are created using the syntax `Name[..., ...]`. This is transformed into
`new Node\Name(..., ..., $attributes)`
* `Name::abc` is transformed to `Node\Name::abc`
`new Name(..., ..., attributes())`
* Some function-like constructs are resolved (see `rebuildParser.php` for a list)
* Associative arrays are written as `[key: value, ...]`, which is transformed to
`array('key' => value, ...)`
@ -25,5 +24,5 @@ Building the parser
In order to rebuild the parser, you need [moriyoshi's fork of kmyacc](https://github.com/moriyoshi/kmyacc-forked).
After you compiled/installed it, run the `rebuildParser.php` script.
By default only the `Parser.php` is built. If you want to additionally build `Parser/Debug.php` and `y.output` run the
script with `--debug`. If you want to retain the preprocessed grammar pass `--keep-tmp-grammar`.
By default only the `Parser.php` is built. If you want to additionally emit debug symbols and create `y.output`, run the
script with `--debug`. If you want to retain the preprocessed grammar pass `--keep-tmp-grammar`.

@ -2,11 +2,11 @@
$meta #
#semval($) $this->semValue
#semval($,%t) $this->semValue
#semval(%n) $this->semStack[$this->stackPos-(%l-%n)]
#semval(%n,%t) $this->semStack[$this->stackPos-(%l-%n)]
#include;
#semval(%n) $this->stackPos-(%l-%n)
#semval(%n,%t) $this->stackPos-(%l-%n)
namespace PhpParser;
#include;
/* This is an automatically GENERATED file, which should not be manually edited.
* Instead edit one of the following:
@ -20,7 +20,8 @@ class Parser extends ParserAbstract
protected $actionTableSize = #(YYLAST);
protected $gotoTableSize = #(YYGLAST);
protected $invalidToken = #(YYBADCH);
protected $invalidSymbol = #(YYBADCH);
protected $errorSymbol = #(YYINTERRTOK);
protected $defaultAction = #(YYDEFAULT);
protected $unexpectedTokenRule = #(YYUNEXPECTED);
@ -86,7 +87,7 @@ class Parser extends ParserAbstract
#endif
#reduce
protected function reduceRule%n($attributes) {
protected function reduceRule%n() {
%b
}
#noact

@ -39,10 +39,10 @@ echo 'Building temporary preproprocessed grammar file.', "\n";
$grammarCode = file_get_contents($grammarFile);
$grammarCode = resolveConstants($grammarCode);
$grammarCode = resolveNodes($grammarCode);
$grammarCode = resolveMacros($grammarCode);
$grammarCode = resolveArrays($grammarCode);
$grammarCode = resolveStackAccess($grammarCode);
file_put_contents($tmpGrammarFile, $grammarCode);
@ -52,7 +52,12 @@ echo "Building parser.\n";
$output = trim(shell_exec("$kmyacc $additionalArgs -l -m $skeletonFile $tmpGrammarFile 2>&1"));
echo "Output: \"$output\"\n";
moveFileWithDirCheck($tmpResultFile, $parserResultFile);
$resultCode = file_get_contents($tmpResultFile);
$resultCode = removeTrailingWhitespace($resultCode);
ensureDirExists(dirname($parserResultFile));
file_put_contents($parserResultFile, $resultCode);
unlink($tmpResultFile);
if (!$optionKeepTmpGrammar) {
unlink($tmpGrammarFile);
@ -62,10 +67,6 @@ if (!$optionKeepTmpGrammar) {
/// Preprocessing functions ///
///////////////////////////////
function resolveConstants($code) {
return preg_replace('~[A-Z][a-zA-Z_\\\\]++::~', 'Node\\\\$0', $code);
}
function resolveNodes($code) {
return preg_replace_callback(
'~(?<name>[A-Z][a-zA-Z_\\\\]++)\s*' . PARAMS . '~',
@ -83,7 +84,7 @@ function resolveNodes($code) {
$paramCode .= $param . ', ';
}
return 'new Node\\' . $matches['name'] . '(' . $paramCode . '$attributes)';
return 'new ' . $matches['name'] . '(' . $paramCode . 'attributes())';
},
$code
);
@ -102,10 +103,9 @@ function resolveMacros($code) {
$matches['args']
);
if ('error' == $name) {
assertArgs(1, $args, $name);
return 'throw new Error(' . $args[0] . ')';
if ('attributes' == $name) {
assertArgs(0, $args, $name);
return '$this->startAttributeStack[#1] + $this->endAttributes';
}
if ('init' == $name) {
@ -148,7 +148,7 @@ function resolveMacros($code) {
return 'foreach (' . $args[0] . ' as &$s) { if (is_string($s)) { $s = Node\Scalar\String_::parseEscapeSequences($s, null); } } $s = preg_replace(\'~(\r\n|\n|\r)$~\', \'\', $s); if (\'\' === $s) array_pop(' . $args[0] . ');';
}
throw new Exception(sprintf('Unknown macro "%s"', $name));
return $matches[0];
},
$code
);
@ -191,12 +191,22 @@ function resolveArrays($code) {
);
}
function moveFileWithDirCheck($fromPath, $toPath) {
$dir = dirname($toPath);
function resolveStackAccess($code) {
$code = preg_replace('/\$\d+/', '$this->semStack[$0]', $code);
$code = preg_replace('/#(\d+)/', '$$1', $code);
return $code;
}
function removeTrailingWhitespace($code) {
$lines = explode("\n", $code);
$lines = array_map('rtrim', $lines);
return implode("\n", $lines);
}
function ensureDirExists($dir) {
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
rename($fromPath, $toPath);
}
//////////////////////////////

@ -8,6 +8,7 @@
%left T_LOGICAL_AND
%right T_PRINT
%right T_YIELD
%right T_YIELD_FROM
%left '=' T_PLUS_EQUAL T_MINUS_EQUAL T_MUL_EQUAL T_DIV_EQUAL T_CONCAT_EQUAL T_MOD_EQUAL T_AND_EQUAL T_OR_EQUAL T_XOR_EQUAL T_SL_EQUAL T_SR_EQUAL T_POW_EQUAL
%left '?' ':'
%right T_COALESCE
@ -110,6 +111,13 @@
%token T_NS_SEPARATOR
%token T_ELLIPSIS
%{
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
%}
%%
start:
@ -163,7 +171,7 @@ constant_declaration_list:
;
constant_declaration:
T_STRING '=' static_scalar { $$ = Const_[$1, $3]; }
T_STRING '=' static_scalar { $$ = Node\Const_[$1, $3]; }
;
inner_statement_list:
@ -175,7 +183,8 @@ inner_statement:
statement { $$ = $1; }
| function_declaration_statement { $$ = $1; }
| class_declaration_statement { $$ = $1; }
| T_HALT_COMPILER { error('__HALT_COMPILER() can only be used from the outermost scope'); }
| T_HALT_COMPILER
{ throw new Error('__HALT_COMPILER() can only be used from the outermost scope', attributes()); }
;
statement:
@ -213,6 +222,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:
@ -382,9 +392,9 @@ non_empty_parameter_list:
parameter:
optional_param_type optional_ref optional_ellipsis T_VARIABLE
{ $$ = Param[parseVar($4), null, $1, $2, $3]; }
{ $$ = Node\Param[parseVar($4), null, $1, $2, $3]; }
| optional_param_type optional_ref optional_ellipsis T_VARIABLE '=' static_scalar
{ $$ = Param[parseVar($4), $6, $1, $2, $3]; }
{ $$ = Node\Param[parseVar($4), $6, $1, $2, $3]; }
;
type:
@ -406,7 +416,7 @@ optional_return_type:
argument_list:
'(' ')' { $$ = array(); }
| '(' non_empty_argument_list ')' { $$ = $2; }
| '(' yield_expr ')' { $$ = array(Arg[$2, false, false]); }
| '(' yield_expr ')' { $$ = array(Node\Arg[$2, false, false]); }
;
non_empty_argument_list:
@ -415,9 +425,9 @@ non_empty_argument_list:
;
argument:
expr { $$ = Arg[$1, false, false]; }
| '&' variable { $$ = Arg[$2, true, false]; }
| T_ELLIPSIS expr { $$ = Arg[$2, false, true]; }
expr { $$ = Node\Arg[$1, false, false]; }
| '&' variable { $$ = Node\Arg[$2, true, false]; }
| T_ELLIPSIS expr { $$ = Node\Arg[$2, false, true]; }
;
global_var_list:
@ -615,6 +625,7 @@ expr:
| '`' backticks_expr '`' { $$ = Expr\ShellExec[$2]; }
| T_PRINT expr { $$ = Expr\Print_[$2]; }
| T_YIELD { $$ = Expr\Yield_[null, null]; }
| T_YIELD_FROM expr { $$ = Expr\YieldFrom[$2]; }
| T_FUNCTION optional_ref '(' parameter_list ')' lexical_vars optional_return_type
'{' inner_statement_list '}'
{ $$ = Expr\Closure[[static: false, byRef: $2, params: $4, uses: $6, returnType: $7, stmts: $9]]; }
@ -647,8 +658,14 @@ scalar_dereference:
/* alternative array syntax missing intentionally */
;
anonymous_class:
T_CLASS ctor_arguments extends_from implements_list '{' class_statement_list '}'
{ $$ = array(Stmt\Class_[null, [type: 0, extends: $3, implements: $4, stmts: $6]], $2); }
new_expr:
T_NEW class_name_reference ctor_arguments { $$ = Expr\New_[$2, $3]; }
| T_NEW anonymous_class
{ list($class, $ctorArgs) = $2; $$ = Expr\New_[$class, $ctorArgs]; }
;
lexical_vars:

@ -36,10 +36,12 @@ class Autoloader
static public function autoload($class) {
if (0 === strpos($class, 'PhpParser\\')) {
if (isset(self::$php7AliasesOldToNew[$class])) {
// Old class name was used, register alias to new one (which will
// be autoloaded, if it wasn't yet).
self::registerPhp7Alias(self::$php7AliasesOldToNew[$class], $class);
return;
if (self::$runningOnPhp7) {
return;
}
// Load the new class, alias will be registered afterwards
$class = self::$php7AliasesOldToNew[$class];
}
$fileName = dirname(__DIR__) . '/' . strtr($class, '\\', '/') . '.php';
@ -50,7 +52,9 @@ class Autoloader
if (isset(self::$php7AliasesNewToOld[$class])) {
// New class name was used, register alias for old one, otherwise
// it won't be usable in "instanceof" and other non-autoloading places.
self::registerPhp7Alias($class, self::$php7AliasesNewToOld[$class]);
if (!self::$runningOnPhp7) {
class_alias($class, self::$php7AliasesNewToOld[$class]);
}
}
} else if (0 === strpos($class, 'PHPParser_')) {
if (isset(self::$nonNamespacedAliases[$class])) {
@ -60,14 +64,6 @@ class Autoloader
}
}
private static function registerPhp7Alias($old, $new) {
// Registering these aliases would throw a fatal error on PHP 7,
// we want to avoid that.
if (!self::$runningOnPhp7) {
class_alias($old, $new);
}
}
private static function registerNonNamespacedAliases() {
foreach (self::$nonNamespacedAliases as $old => $new) {
class_alias($new, $old);

@ -5,17 +5,22 @@ namespace PhpParser;
class Error extends \RuntimeException
{
protected $rawMessage;
protected $rawLine;
protected $attributes;
/**
* Creates an Exception signifying a parse error.
*
* @param string $message Error message
* @param int $line Error line in PHP file
* @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) {
public function __construct($message, $attributes = array()) {
$this->rawMessage = (string) $message;
$this->rawLine = (int) $line;
if (is_array($attributes)) {
$this->attributes = $attributes;
} else {
$this->attributes = array('startLine' => $attributes);
}
$this->updateMessage();
}
@ -28,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.
*
@ -39,22 +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;
public function setStartLine($line) {
$this->attributes['startLine'] = (int) $line;
$this->updateMessage();
}
/**
* Sets the line of the PHP file the error occurred in.
* Returns whether the error has start and end column information.
*
* @param int $line Error line in the PHP file
* For column information enable the startFilePos and endFilePos in the lexer options.
*
* @return bool
*/
public function setRawLine($line) {
$this->rawLine = (int) $line;
$this->updateMessage();
public function hasColumnInfo() {
return isset($this->attributes['startFilePos']) && isset($this->attributes['endFilePos']);
}
/**
* Gets the start column (1-based) into the line where the error started.
*
* @param string $code Source code of the file
* @return int
*/
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 end column (1-based) into the line where the error ended.
*
* @param string $code Source code of the file
* @return int
*/
public function getEndColumn($code) {
if (!$this->hasColumnInfo()) {
throw new \RuntimeException('Error does not have column information');
}
return $this->toColumn($code, $this->attributes['endFilePos']);
}
private function toColumn($code, $pos) {
if ($pos > strlen($code)) {
throw new \RuntimeException('Invalid position information');
}
$lineStartPos = strrpos($code, "\n", $pos - strlen($code));
if (false === $lineStartPos) {
$lineStartPos = -1;
}
return $pos - $lineStartPos;
}
/**
@ -63,10 +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);
}
}

@ -123,8 +123,13 @@ class Lexer
$startAttributes = array();
$endAttributes = array();
while (isset($this->tokens[++$this->pos])) {
$token = $this->tokens[$this->pos];
while (1) {
if (isset($this->tokens[++$this->pos])) {
$token = $this->tokens[$this->pos];
} else {
// EOF token with ID 0
$token = "\0";
}
if (isset($this->usedAttributes['startTokenPos'])) {
$startAttributes['startTokenPos'] = $this->pos;
@ -192,10 +197,7 @@ class Lexer
}
}
$startAttributes['startLine'] = $this->line;
// 0 is the EOF token
return 0;
throw new \RuntimeException('Reached end of lexer loop');
}
/**

@ -12,11 +12,12 @@ class Emulative extends \PhpParser\Lexer
protected $newKeywords;
protected $inObjectAccess;
const T_ELLIPSIS = 1001;
const T_POW = 1002;
const T_POW_EQUAL = 1003;
const T_COALESCE = 1004;
const T_SPACESHIP = 1005;
const T_ELLIPSIS = 1001;
const T_POW = 1002;
const T_POW_EQUAL = 1003;
const T_COALESCE = 1004;
const T_SPACESHIP = 1005;
const T_YIELD_FROM = 1006;
const PHP_7_0 = '7.0.0dev';
const PHP_5_6 = '5.6.0rc1';
@ -53,6 +54,7 @@ class Emulative extends \PhpParser\Lexer
}
$this->tokenMap[self::T_COALESCE] = Parser::T_COALESCE;
$this->tokenMap[self::T_SPACESHIP] = Parser::T_SPACESHIP;
$this->tokenMap[self::T_YIELD_FROM] = Parser::T_YIELD_FROM;
if (version_compare(PHP_VERSION, self::PHP_5_6, '>=')) {
return;
@ -91,6 +93,10 @@ class Emulative extends \PhpParser\Lexer
$code = str_replace('??', '~__EMU__COALESCE__~', $code);
$code = str_replace('<=>', '~__EMU__SPACESHIP__~', $code);
$code = preg_replace_callback('(yield[ \n\r\t]+from)', function($matches) {
// Encoding $0 in order to preserve exact whitespace
return '~__EMU__YIELDFROM__' . bin2hex($matches[0]) . '__~';
}, $code);
if (version_compare(PHP_VERSION, self::PHP_5_6, '>=')) {
return $code;
@ -127,8 +133,9 @@ class Emulative extends \PhpParser\Lexer
if ('BINARY' === $matches[1]) {
// the binary number can either be an integer or a double, so return a LNUMBER
// or DNUMBER respectively
$isInt = is_int(bindec($matches[2]));
$replace = array(
array(is_int(bindec($matches[2])) ? T_LNUMBER : T_DNUMBER, $matches[2], $this->tokens[$i + 1][2])
array($isInt ? T_LNUMBER : T_DNUMBER, $matches[2], $this->tokens[$i + 1][2])
);
} else if ('ELLIPSIS' === $matches[1]) {
$replace = array(
@ -150,9 +157,13 @@ class Emulative extends \PhpParser\Lexer
$replace = array(
array(self::T_SPACESHIP, '<=>', $this->tokens[$i + 1][2]),
);
} else if ('YIELDFROM' === $matches[1]) {
$content = $this->hex2bin($matches[2]);
$replace = array(
array(self::T_YIELD_FROM, $content, $this->tokens[$i + 1][2] - substr_count($content, "\n"))
);
} else {
// just ignore all other __EMU__ sequences
continue;
throw new \RuntimeException('Invalid __EMU__ sequence');
}
array_splice($this->tokens, $i, 3, $replace);
@ -188,11 +199,18 @@ class Emulative extends \PhpParser\Lexer
return '??';
} else if ('SPACESHIP' === $matches[1]) {
return '<=>';
} else if ('YIELDFROM' === $matches[1]) {
return $this->hex2bin($matches[2]);
} else {
return $matches[0];
}
}
private function hex2bin($str) {
// TODO Drop when removing support for PHP 5.3
return pack('H*', $str);
}
public function getNextToken(&$value = null, &$startAttributes = null, &$endAttributes = null) {
$token = parent::getNextToken($value, $startAttributes, $endAttributes);
@ -203,11 +221,9 @@ class Emulative extends \PhpParser\Lexer
if (isset($this->newKeywords[strtolower($value)])) {
return $this->newKeywords[strtolower($value)];
}
// keep track of whether we currently are in an object access (after ->)
} elseif (Parser::T_OBJECT_OPERATOR === $token) {
$this->inObjectAccess = true;
} else {
$this->inObjectAccess = false;
// keep track of whether we currently are in an object access (after ->)
$this->inObjectAccess = Parser::T_OBJECT_OPERATOR === $token;
}
return $token;

@ -7,7 +7,7 @@ use PhpParser\Node\Expr;
class New_ extends Expr
{
/** @var Node\Name|Expr Class name */
/** @var Node\Name|Expr|Node\Stmt\Class_ Class name */
public $class;
/** @var Node\Arg[] Arguments */
public $args;
@ -15,9 +15,9 @@ class New_ extends Expr
/**
* Constructs a function call node.
*
* @param Node\Name|Expr $class Class name
* @param Node\Arg[] $args Arguments
* @param array $attributes Additional attributes
* @param Node\Name|Expr|Node\Stmt\Class_ $class Class name (or class node for anonymous classes)
* @param Node\Arg[] $args Arguments
* @param array $attributes Additional attributes
*/
public function __construct($class, array $args = array(), array $attributes = array()) {
parent::__construct(null, $attributes);

@ -0,0 +1,26 @@
<?php
namespace PhpParser\Node\Expr;
use PhpParser\Node\Expr;
class YieldFrom extends Expr
{
/** @var Expr Expression to yield from */
public $expr;
/**
* Constructs an "yield from" node.
*
* @param Expr $expr Expression
* @param array $attributes Additional attributes
*/
public function __construct(Expr $expr, array $attributes = array()) {
parent::__construct(null, $attributes);
$this->expr = $expr;
}
public function getSubNodeNames() {
return array('expr');
}
}

@ -37,7 +37,7 @@ class Param extends NodeAbstract
$this->default = $default;
if ($variadic && null !== $default) {
throw new Error('Variadic parameter cannot have a default value');
throw new Error('Variadic parameter cannot have a default value', $default->getAttributes());
}
}

@ -32,7 +32,7 @@ class Class_ extends ClassLike
/**
* Constructs a class node.
*
* @param string $name Name
* @param string|null $name Name
* @param array $subNodes Array of the following optional subnodes:
* 'type' => 0 : Type
* 'extends' => null : Name of extended class
@ -48,17 +48,23 @@ class Class_ extends ClassLike
$this->implements = isset($subNodes['implements']) ? $subNodes['implements'] : array();
$this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();
if (isset(self::$specialNames[(string) $this->name])) {
if (null !== $this->name && isset(self::$specialNames[strtolower($this->name)])) {
throw new Error(sprintf('Cannot use \'%s\' as class name as it is reserved', $this->name));
}
if (isset(self::$specialNames[(string) $this->extends])) {
throw new Error(sprintf('Cannot use \'%s\' as class name as it is reserved', $this->extends));
if (isset(self::$specialNames[strtolower($this->extends)])) {
throw new Error(
sprintf('Cannot use \'%s\' as class name as it is reserved', $this->extends),
$this->extends->getAttributes()
);
}
foreach ($this->implements as $interface) {
if (isset(self::$specialNames[(string) $interface])) {
throw new Error(sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface));
if (isset(self::$specialNames[strtolower($interface)])) {
throw new Error(
sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
$interface->getAttributes()
);
}
}
}
@ -75,6 +81,10 @@ class Class_ extends ClassLike
return (bool) ($this->type & self::MODIFIER_FINAL);
}
public function isAnonymous() {
return null === $this->name;
}
/**
* @internal
*/

@ -31,13 +31,16 @@ class Interface_ extends ClassLike
$this->extends = isset($subNodes['extends']) ? $subNodes['extends'] : array();
$this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();
if (isset(self::$specialNames[(string) $this->name])) {
if (isset(self::$specialNames[strtolower($this->name)])) {
throw new Error(sprintf('Cannot use \'%s\' as class name as it is reserved', $this->name));
}
foreach ($this->extends as $interface) {
if (isset(self::$specialNames[(string) $interface])) {
throw new Error(sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface));
if (isset(self::$specialNames[strtolower($interface)])) {
throw new Error(
sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
$interface->getAttributes()
);
}
}
}

@ -30,14 +30,17 @@ class Namespace_ extends Node\Stmt
$this->name = $name;
$this->stmts = $stmts;
if (isset(self::$specialNames[(string) $this->name])) {
throw new Error(sprintf('Cannot use \'%s\' as namespace name', $this->name));
if (isset(self::$specialNames[strtolower($this->name)])) {
throw new Error(
sprintf('Cannot use \'%s\' as namespace name', $this->name),
$this->name->getAttributes()
);
}
if (null !== $this->stmts) {
foreach ($this->stmts as $stmt) {
if ($stmt instanceof self) {
throw new Error('Namespace declarations cannot be nested', $stmt->getLine());
throw new Error('Namespace declarations cannot be nested', $stmt->getAttributes());
}
}
}

@ -24,7 +24,7 @@ class UseUse extends Node\Stmt
$alias = $name->getLast();
}
if ('self' == $alias || 'parent' == $alias) {
if ('self' == strtolower($alias) || 'parent' == strtolower($alias)) {
throw new Error(sprintf(
'Cannot use %s as %s because \'%2$s\' is a special class name',
$name, $alias

@ -37,7 +37,9 @@ class NameResolver extends NodeVisitorAbstract
$interface = $this->resolveClassName($interface);
}
$this->addNamespacedName($node);
if (null !== $node->name) {
$this->addNamespacedName($node);
}
} elseif ($node instanceof Stmt\Interface_) {
foreach ($node->extends as &$interface) {
$interface = $this->resolveClassName($interface);

File diff suppressed because it is too large Load Diff

@ -23,6 +23,8 @@ abstract class ParserAbstract
/** @var int Symbol number signifying an invalid token */
protected $invalidSymbol;
/** @var int Symbol number of error recovery token */
protected $errorSymbol;
/** @var int Action number signifying default action */
protected $defaultAction;
/** @var int Rule number signifying that an unexpected token was encountered */
@ -76,18 +78,43 @@ abstract class ParserAbstract
protected $lexer;
/** @var mixed Temporary value containing the result of last semantic action (reduction) */
protected $semValue;
/** @var array Semantic value stack (contains values of tokens and semantic action results) */
protected $semStack;
/** @var int Position in stacks (state stack, semantic value stack, attribute stack) */
protected $stackPos;
/** @var array Semantic value stack (contains values of tokens and semantic action results) */
protected $semStack;
/** @var array[] Start attribute stack */
protected $startAttributeStack;
/** @var array End attributes of last *shifted* token */
protected $endAttributes;
/** @var bool Whether to throw on first error */
protected $throwOnError;
/** @var Error[] Errors collected during last parse */
protected $errors;
/**
* 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.
*/
public function __construct(Lexer $lexer) {
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;
}
/**
@ -95,10 +122,12 @@ 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);
$this->errors = array();
// We start off with no lookahead-token
$symbol = self::SYMBOL_NONE;
@ -106,12 +135,13 @@ abstract class ParserAbstract
// The attributes for a node are taken from the first and last token of the node.
// From the first token only the startAttributes are taken and from the last only
// the endAttributes. Both are merged using the array union operator (+).
$startAttributes = array('startLine' => 1);
$endAttributes = array();
$startAttributes = '*POISON';
$endAttributes = '*POISON';
$this->endAttributes = $endAttributes;
// In order to figure out the attributes for the starting token, we have to keep
// them in a stack
$attributeStack = array($startAttributes);
$this->startAttributeStack = array();
// Start off in the initial state and keep a stack of previous states
$state = 0;
@ -123,6 +153,8 @@ abstract class ParserAbstract
// Current position in the stack(s)
$this->stackPos = 0;
$errorState = 0;
for (;;) {
//$this->traceNewState($state, $symbol);
@ -134,7 +166,7 @@ abstract class ParserAbstract
// The end attributes are fetched into a temporary variable and only set once the token is really
// shifted (not during read). Otherwise you would sometimes get off-by-one errors, when a rule is
// reduced after a token was read but not yet shifted.
$tokenId = $this->lexer->getNextToken($tokenValue, $startAttributes, $nextEndAttributes);
$tokenId = $this->lexer->getNextToken($tokenValue, $startAttributes, $endAttributes);
// map the lexer token id to the internally used symbols
$symbol = $tokenId >= 0 && $tokenId < $this->tokenToSymbolMapSize
@ -148,7 +180,9 @@ abstract class ParserAbstract
));
}
$attributeStack[$this->stackPos] = $startAttributes;
// This is necessary to assign some meaningful attributes to /* empty */ productions. They'll get
// the attributes of the next token, even though they don't contain it themselves.
$this->startAttributeStack[$this->stackPos+1] = $startAttributes;
//$this->traceRead($symbol);
}
@ -171,14 +205,19 @@ abstract class ParserAbstract
//$this->traceShift($symbol);
++$this->stackPos;
$stateStack[$this->stackPos] = $state = $action;
$stateStack[$this->stackPos] = $state = $action;
$this->semStack[$this->stackPos] = $tokenValue;
$attributeStack[$this->stackPos] = $startAttributes;
$endAttributes = $nextEndAttributes;
$this->startAttributeStack[$this->stackPos] = $startAttributes;
$this->endAttributes = $endAttributes;
$symbol = self::SYMBOL_NONE;
if ($action < $this->YYNLSTATES)
if ($errorState) {
--$errorState;
}
if ($action < $this->YYNLSTATES) {
continue;
}
/* $yyn >= YYNLSTATES means shift-and-reduce */
$rule = $action - $this->YYNLSTATES;
@ -200,16 +239,19 @@ abstract class ParserAbstract
//$this->traceReduce($rule);
try {
$this->{'reduceRule' . $rule}(
$attributeStack[$this->stackPos - $this->ruleToLength[$rule]]
+ $endAttributes
);
$this->{'reduceRule' . $rule}();
} 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;
$this->errors[] = $e;
if ($this->throwOnError) {
throw $e;
} else {
// Currently can't recover from "special" errors
return null;
}
}
/* Goto - shift nonterminal */
@ -225,27 +267,72 @@ abstract class ParserAbstract
++$this->stackPos;
$stateStack[$this->stackPos] = $state;
$this->semStack[$this->stackPos] = $this->semValue;
$attributeStack[$this->stackPos] = $startAttributes;
if ($state < $this->YYNLSTATES)
break;
/* >= YYNLSTATES means shift-and-reduce */
$rule = $state - $this->YYNLSTATES;
} else {
/* error */
if ($expected = $this->getExpectedTokens($state)) {
$expectedString = ', expecting ' . implode(' or ', $expected);
} else {
$expectedString = '';
}
switch ($errorState) {
case 0:
$msg = $this->getErrorMessage($symbol, $state);
$error = new Error($msg, $startAttributes + $endAttributes);
$this->errors[] = $error;
if ($this->throwOnError) {
throw $error;
}
// Break missing intentionally
case 1:
case 2:
$errorState = 3;
throw new Error(
'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString,
$startAttributes['startLine']
);
// Pop until error-expecting state uncovered
while (!(
(($idx = $this->actionBase[$state] + $this->errorSymbol) >= 0
&& $idx < $this->actionTableSize && $this->actionCheck[$idx] == $this->errorSymbol)
|| ($state < $this->YY2TBLSTATE
&& ($idx = $this->actionBase[$state + $this->YYNLSTATES] + $this->errorSymbol) >= 0
&& $idx < $this->actionTableSize && $this->actionCheck[$idx] == $this->errorSymbol)
) || ($action = $this->action[$idx]) == $this->defaultAction) { // Not totally sure about this
if ($this->stackPos <= 0) {
// Could not recover from error
return null;
}
$state = $stateStack[--$this->stackPos];
//$this->tracePop($state);
}
//$this->traceShift($this->errorSymbol);
$stateStack[++$this->stackPos] = $state = $action;
break;
case 3:
if ($symbol === 0) {
// Reached EOF without recovering from error
return null;
}
//$this->traceDiscard($symbol);
$symbol = self::SYMBOL_NONE;
break 2;
}
}
if ($state < $this->YYNLSTATES) {
break;
}
/* >= YYNLSTATES means shift-and-reduce */
$rule = $state - $this->YYNLSTATES;
}
}
throw new \RuntimeException('Reached end of parser loop');
}
protected function getErrorMessage($symbol, $state) {
$expectedString = '';
if ($expected = $this->getExpectedTokens($state)) {
$expectedString = ', expecting ' . implode(' or ', $expected);
}
return 'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString;
}
protected function getExpectedTokens($state) {
@ -277,6 +364,7 @@ abstract class ParserAbstract
* Tracing functions used for debugging the parser.
*/
/*
protected function traceNewState($state, $symbol) {
echo '% State ' . $state
. ', Lookahead ' . ($symbol == self::SYMBOL_NONE ? '--none--' : $this->symbolToName[$symbol]) . "\n";
@ -298,6 +386,15 @@ abstract class ParserAbstract
echo '% Reduce by (' . $n . ') ' . $this->productions[$n] . "\n";
}
protected function tracePop($state) {
echo '% Recovering, uncovered state ' . $state . "\n";
}
protected function traceDiscard($symbol) {
echo '% Discard ' . $this->symbolToName[$symbol] . "\n";
}
*/
/*
* Helper functions invoked by semantic actions
*/

@ -96,7 +96,10 @@ class Standard extends PrettyPrinterAbstract
}
public function pScalar_DNumber(Scalar\DNumber $node) {
$stringValue = (string) $node->value;
$stringValue = sprintf('%.16G', $node->value);
if ($node->value !== (double) $stringValue) {
$stringValue = sprintf('%.17G', $node->value);
}
// ensure that number is really printed as float
return preg_match('/^-?[0-9]+$/', $stringValue) ? $stringValue . '.0' : $stringValue;
@ -312,6 +315,14 @@ class Standard extends PrettyPrinterAbstract
return $this->pPrefixOp('Expr_ErrorSuppress', '@', $node->expr);
}
public function pExpr_YieldFrom(Expr\YieldFrom $node) {
return $this->pPrefixOp('Expr_YieldFrom', 'yield from ', $node->expr);
}
public function pExpr_Print(Expr\Print_ $node) {
return $this->pPrefixOp('Expr_Print', 'print ', $node->expr);
}
// Casts
public function pExpr_Cast_Int(Cast\Int_ $node) {
@ -372,10 +383,6 @@ class Standard extends PrettyPrinterAbstract
return 'isset(' . $this->pCommaSeparated($node->vars) . ')';
}
public function pExpr_Print(Expr\Print_ $node) {
return 'print ' . $this->p($node->expr);
}
public function pExpr_Eval(Expr\Eval_ $node) {
return 'eval(' . $this->p($node->expr) . ')';
}
@ -462,6 +469,10 @@ class Standard extends PrettyPrinterAbstract
}
public function pExpr_New(Expr\New_ $node) {
if ($node->class instanceof Stmt\Class_) {
$args = $node->args ? '(' . $this->pCommaSeparated($node->args) . ')' : '';
return 'new ' . $this->pClassCommon($node->class, $args);
}
return 'new ' . $this->p($node->class) . '(' . $this->pCommaSeparated($node->args) . ')';
}
@ -523,11 +534,7 @@ class Standard extends PrettyPrinterAbstract
}
public function pStmt_Class(Stmt\Class_ $node) {
return $this->pModifiers($node->type)
. 'class ' . $node->name
. (null !== $node->extends ? ' extends ' . $this->p($node->extends) : '')
. (!empty($node->implements) ? ' implements ' . $this->pCommaSeparated($node->implements) : '')
. "\n" . '{' . $this->pStmts($node->stmts) . "\n" . '}';
return $this->pClassCommon($node, ' ' . $node->name);
}
public function pStmt_Trait(Stmt\Trait_ $node) {
@ -725,6 +732,14 @@ class Standard extends PrettyPrinterAbstract
return is_string($node) ? $node : $this->p($node);
}
protected function pClassCommon(Stmt\Class_ $node, $afterClassToken) {
return $this->pModifiers($node->type)
. 'class' . $afterClassToken
. (null !== $node->extends ? ' extends ' . $this->p($node->extends) : '')
. (!empty($node->implements) ? ' implements ' . $this->pCommaSeparated($node->implements) : '')
. "\n" . '{' . $this->pStmts($node->stmts) . "\n" . '}';
}
/** @internal */
public function pObjectProperty($node) {
if ($node instanceof Expr) {

@ -66,6 +66,8 @@ abstract class PrettyPrinterAbstract
'Expr_AssignOp_ShiftLeft' => array(160, 1),
'Expr_AssignOp_ShiftRight' => array(160, 1),
'Expr_AssignOp_Pow' => array(160, 1),
'Expr_YieldFrom' => array(165, 1),
'Expr_Print' => array(168, 1),
'Expr_BinaryOp_LogicalAnd' => array(170, -1),
'Expr_BinaryOp_LogicalXor' => array(180, -1),
'Expr_BinaryOp_LogicalOr' => array(190, -1),

@ -5,9 +5,16 @@ namespace PhpParser;
class ErrorTest extends \PHPUnit_Framework_TestCase
{
public function testConstruct() {
$error = new Error('Some error', 10);
$attributes = array(
'startLine' => 10,
'endLine' => 11,
);
$error = new Error('Some error', $attributes);
$this->assertSame('Some error', $error->getRawMessage());
$this->assertSame($attributes, $error->getAttributes());
$this->assertSame(10, $error->getStartLine());
$this->assertSame(11, $error->getEndLine());
$this->assertSame(10, $error->getRawLine());
$this->assertSame('Some error on line 10', $error->getMessage());
@ -19,17 +26,87 @@ class ErrorTest extends \PHPUnit_Framework_TestCase
*/
public function testSetMessageAndLine(Error $error) {
$error->setRawMessage('Some other error');
$error->setRawLine(15);
$this->assertSame('Some other error', $error->getRawMessage());
$this->assertSame(15, $error->getRawLine());
$error->setStartLine(15);
$this->assertSame(15, $error->getStartLine());
$this->assertSame('Some other error on line 15', $error->getMessage());
$error->setRawLine(17);
$this->assertSame(17, $error->getRawLine());
$this->assertSame('Some other error on line 17', $error->getMessage());
}
public function testUnknownLine() {
$error = new Error('Some error');
$this->assertSame(-1, $error->getStartLine());
$this->assertSame(-1, $error->getEndLine());
$this->assertSame(-1, $error->getRawLine());
$this->assertSame('Some error on unknown line', $error->getMessage());
}
}
/** @dataProvider provideTestColumnInfo */
public function testColumnInfo($code, $startPos, $endPos, $startColumn, $endColumn) {
$error = new Error('Some error', array(
'startFilePos' => $startPos,
'endFilePos' => $endPos,
));
$this->assertSame(true, $error->hasColumnInfo());
$this->assertSame($startColumn, $error->getStartColumn($code));
$this->assertSame($endColumn, $error->getEndColumn($code));
}
public function provideTestColumnInfo() {
return array(
// Error at "bar"
array("<?php foo bar baz", 10, 12, 11, 13),
array("<?php\nfoo bar baz", 10, 12, 5, 7),
array("<?php foo\nbar baz", 10, 12, 1, 3),
array("<?php foo bar\nbaz", 10, 12, 11, 13),
array("<?php\r\nfoo bar baz", 11, 13, 5, 7),
// Error at "baz"
array("<?php foo bar baz", 14, 16, 15, 17),
array("<?php foo bar\nbaz", 14, 16, 1, 3),
// Error at string literal
array("<?php foo 'bar\nbaz' xyz", 10, 18, 11, 4),
array("<?php\nfoo 'bar\nbaz' xyz", 10, 18, 5, 4),
array("<?php foo\n'\nbarbaz\n'\nxyz", 10, 19, 1, 1),
// Error over full string
array("<?php", 0, 4, 1, 5),
array("<?\nphp", 0, 5, 1, 3),
);
}
public function testNoColumnInfo() {
$error = new Error('Some error', 3);
$this->assertSame(false, $error->hasColumnInfo());
try {
$error->getStartColumn('');
$this->fail('Expected RuntimeException');
} catch (\RuntimeException $e) {
$this->assertSame('Error does not have column information', $e->getMessage());
}
try {
$error->getEndColumn('');
$this->fail('Expected RuntimeException');
} catch (\RuntimeException $e) {
$this->assertSame('Error does not have column information', $e->getMessage());
}
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Invalid position information
*/
public function testInvalidPosInfo() {
$error = new Error('Some error', array(
'startFilePos' => 10,
'endFilePos' => 11,
));
$error->getStartColumn('code');
}
}

@ -87,6 +87,12 @@ class EmulativeTest extends LexerTest
public function provideTestLexNewFeatures() {
return array(
array('yield from', array(
array(Parser::T_YIELD_FROM, 'yield from'),
)),
array("yield\r\nfrom", array(
array(Parser::T_YIELD_FROM, "yield\r\nfrom"),
)),
array('...', array(
array(Parser::T_ELLIPSIS, '...'),
)),

@ -257,6 +257,7 @@ EOC;
new Stmt\Const_(array(
new Node\Const_('D', new Node\Scalar\String_('E'))
)),
new Expr\New_(new Stmt\Class_(null)),
));
$traverser = new PhpParser\NodeTraverser;
@ -268,10 +269,12 @@ EOC;
$this->assertSame('NS\\B', (string) $stmts[0]->stmts[1]->namespacedName);
$this->assertSame('NS\\C', (string) $stmts[0]->stmts[2]->namespacedName);
$this->assertSame('NS\\D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName);
$this->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[4]->class);
$this->assertSame('A', (string) $stmts[1]->stmts[0]->namespacedName);
$this->assertSame('B', (string) $stmts[1]->stmts[1]->namespacedName);
$this->assertSame('C', (string) $stmts[1]->stmts[2]->namespacedName);
$this->assertSame('D', (string) $stmts[1]->stmts[3]->consts[0]->namespacedName);
$this->assertObjectNotHasAttribute('namespacedName', $stmts[1]->stmts[4]->class);
}
public function testAddTraitNamespacedName() {

@ -11,39 +11,59 @@ class ParserTest extends CodeTestAbstract
/**
* @dataProvider provideTestParse
*/
public function testParse($name, $code, $dump) {
$parser = new Parser(new Lexer\Emulative);
$dumper = new NodeDumper;
public function testParse($name, $code, $expected) {
$lexer = new Lexer\Emulative(array('usedAttributes' => array(
'startLine', 'endLine', 'startFilePos', 'endFilePos'
)));
$parser = new Parser($lexer, array(
'throwOnError' => false,
));
$stmts = $parser->parse($code);
$this->assertSame(
$this->canonicalize($dump),
$this->canonicalize($dumper->dump($stmts)),
$name
);
$errors = $parser->getErrors();
$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);
}
public function provideTestParse() {
return $this->getTests(__DIR__ . '/../code/parser', 'test');
}
/**
* @dataProvider provideTestParseFail
*/
public function testParseFail($name, $code, $msg) {
$parser = new Parser(new Lexer\Emulative);
try {
$parser->parse($code);
$this->fail(sprintf('"%s": Expected Error', $name));
} catch (Error $e) {
$this->assertSame($msg, $e->getMessage(), $name);
private function formatErrorMessage(Error $e, $code) {
if ($e->hasColumnInfo()) {
return $e->getRawMessage() . ' from ' . $e->getStartLine() . ':' . $e->getStartColumn($code)
. ' to ' . $e->getEndLine() . ':' . $e->getEndColumn($code);
} else {
return $e->getMessage();
}
}
public function provideTestParseFail() {
return $this->getTests(__DIR__ . '/../code/parser', 'test-fail');
/**
* @expectedException \PhpParser\Error
* @expectedExceptionMessage Syntax error, unexpected EOF on line 1
*/
public function testParserThrowsSyntaxError() {
$parser = new Parser(new Lexer());
$parser->parse('<?php foo');
}
/**
* @expectedException \PhpParser\Error
* @expectedExceptionMessage Cannot use foo as self because 'self' is a special class name on line 1
*/
public function testParserThrowsSpecialError() {
$parser = new Parser(new Lexer());
$parser->parse('<?php use foo as self;');
}
public function testAttributeAssignment() {

@ -0,0 +1,13 @@
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(
)

@ -0,0 +1,186 @@
Error recovery
-----
<?php
foo()
bar()
baz()
-----
Syntax error, unexpected T_STRING from 4:1 to 4:3
Syntax error, unexpected T_STRING from 5:1 to 5:3
Syntax error, unexpected EOF from 5:6 to 5:6
array(
)
-----
<?php
foo()
bar();
baz();
-----
Syntax error, unexpected T_STRING from 4:1 to 4:3
array(
0: Expr_FuncCall(
name: Name(
parts: array(
0: bar
)
)
args: array(
)
)
1: Expr_FuncCall(
name: Name(
parts: array(
0: baz
)
)
args: array(
)
)
)
-----
<?php
foo();
bar()
baz();
-----
Syntax error, unexpected T_STRING from 5:1 to 5:3
array(
0: Expr_FuncCall(
name: Name(
parts: array(
0: foo
)
)
args: array(
)
)
1: Expr_FuncCall(
name: Name(
parts: array(
0: baz
)
)
args: array(
)
)
)
-----
<?php
abc;
1 + ;
-----
Syntax error, unexpected ';' from 3:5 to 3:5
array(
0: Expr_ConstFetch(
name: Name(
parts: array(
0: abc
)
)
)
)
-----
<?php
function test() {
1 +
}
-----
Syntax error, unexpected '}' from 4:1 to 4:1
array(
0: Stmt_Function(
byRef: false
name: test
params: array(
)
returnType: null
stmts: array(
)
)
)
-----
<?php
$i = 0;
while
$j = 1;
$k = 2;
-----
Syntax error, unexpected T_VARIABLE, expecting '(' from 6:1 to 6:2
array(
0: Expr_Assign(
var: Expr_Variable(
name: i
)
expr: Scalar_LNumber(
value: 0
)
)
1: Expr_Assign(
var: Expr_Variable(
name: j
)
expr: Scalar_LNumber(
value: 1
)
)
2: Expr_Assign(
var: Expr_Variable(
name: k
)
expr: Scalar_LNumber(
value: 2
)
)
)
-----
<?php
$i = 0;
while () {
$j = 1;
}
$k = 2;
// The output here drops the loop - would require Error node to handle this
-----
Syntax error, unexpected ')' from 4:8 to 4:8
array(
0: Expr_Assign(
var: Expr_Variable(
name: i
)
expr: Scalar_LNumber(
value: 0
)
)
1: Expr_Assign(
var: Expr_Variable(
name: j
)
expr: Scalar_LNumber(
value: 1
)
)
2: Expr_Assign(
var: Expr_Variable(
name: k
)
expr: Scalar_LNumber(
value: 2
)
)
)
-----
<?php
// Can't recover this yet, as the '}' for the inner_statement_list
// is always required.
$i = 0;
while (true) {
$i = 1;
$i = 2;
-----
Syntax error, unexpected EOF from 8:12 to 8:12

@ -0,0 +1,8 @@
New without a class
-----
<?php
new;
-----
Syntax error, unexpected ';' from 2:4 to 2:4
array(
)

@ -1,6 +0,0 @@
New without a class
-----
<?php
new;
-----
Syntax error, unexpected ';' on line 2

@ -0,0 +1,194 @@
Anonymous classes
-----
<?php
new class {
public function test() {}
};
new class extends A implements B, C {};
new class() {
public $foo;
};
new class($a, $b) extends A {
use T;
};
class A {
public function test() {
return new class($this) extends A {
const A = 'B';
};
}
}
-----
array(
0: Expr_New(
class: Stmt_Class(
type: 0
name: null
extends: null
implements: array(
)
stmts: array(
0: Stmt_ClassMethod(
type: 1
byRef: false
name: test
params: array(
)
returnType: null
stmts: array(
)
)
)
)
args: array(
)
)
1: Expr_New(
class: Stmt_Class(
type: 0
name: null
extends: Name(
parts: array(
0: A
)
)
implements: array(
0: Name(
parts: array(
0: B
)
)
1: Name(
parts: array(
0: C
)
)
)
stmts: array(
)
)
args: array(
)
)
2: Expr_New(
class: Stmt_Class(
type: 0
name: null
extends: null
implements: array(
)
stmts: array(
0: Stmt_Property(
type: 1
props: array(
0: Stmt_PropertyProperty(
name: foo
default: null
)
)
)
)
)
args: array(
)
)
3: Expr_New(
class: Stmt_Class(
type: 0
name: null
extends: Name(
parts: array(
0: A
)
)
implements: array(
)
stmts: array(
0: Stmt_TraitUse(
traits: array(
0: Name(
parts: array(
0: T
)
)
)
adaptations: array(
)
)
)
)
args: array(
0: Arg(
value: Expr_Variable(
name: a
)
byRef: false
unpack: false
)
1: Arg(
value: Expr_Variable(
name: b
)
byRef: false
unpack: false
)
)
)
4: Stmt_Class(
type: 0
name: A
extends: null
implements: array(
)
stmts: array(
0: Stmt_ClassMethod(
type: 1
byRef: false
name: test
params: array(
)
returnType: null
stmts: array(
0: Stmt_Return(
expr: Expr_New(
class: Stmt_Class(
type: 0
name: null
extends: Name(
parts: array(
0: A
)
)
implements: array(
)
stmts: array(
0: Stmt_ClassConst(
consts: array(
0: Const(
name: A
value: Scalar_String(
value: B
)
)
)
)
)
)
args: array(
0: Arg(
value: Expr_Variable(
name: this
)
byRef: false
unpack: false
)
)
)
)
)
)
)
)
)

@ -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 on line 1
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; }
-----

@ -0,0 +1,71 @@
Invalid class name
-----
<?php class self {}
-----
Cannot use 'self' as class name as it is reserved on line 1
-----
<?php class PARENT {}
-----
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 {}
-----
Cannot use 'self' as class name as it is reserved from 1:23 to 1:26
-----
<?php class A extends PARENT {}
-----
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 {}
-----
Cannot use 'self' as interface name as it is reserved from 1:26 to 1:29
-----
<?php class A implements PARENT {}
-----
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 {}
-----
Cannot use 'self' as class name as it is reserved on line 1
-----
<?php interface PARENT {}
-----
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 {}
-----
Cannot use 'self' as interface name as it is reserved from 1:27 to 1:30
-----
<?php interface A extends PARENT {}
-----
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(
)

@ -1,61 +0,0 @@
Invalid class name
-----
<?php class self {}
-----
Cannot use 'self' as class name as it is reserved on line 1
-----
<?php class parent {}
-----
Cannot use 'parent' as class name as it is reserved on line 1
-----
<?php class static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING on line 1
-----
<?php class A extends self {}
-----
Cannot use 'self' as class name as it is reserved on line 1
-----
<?php class A extends parent {}
-----
Cannot use 'parent' as class name as it is reserved on line 1
-----
<?php class A extends static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING or T_NAMESPACE or T_NS_SEPARATOR on line 1
-----
<?php class A implements self {}
-----
Cannot use 'self' as interface name as it is reserved on line 1
-----
<?php class A implements parent {}
-----
Cannot use 'parent' as interface name as it is reserved on line 1
-----
<?php class A implements static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING or T_NAMESPACE or T_NS_SEPARATOR on line 1
-----
<?php interface self {}
-----
Cannot use 'self' as class name as it is reserved on line 1
-----
<?php interface parent {}
-----
Cannot use 'parent' as class name as it is reserved on line 1
-----
<?php interface static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING on line 1
-----
<?php interface A extends self {}
-----
Cannot use 'self' as interface name as it is reserved on line 1
-----
<?php interface A extends parent {}
-----
Cannot use 'parent' as interface name as it is reserved on line 1
-----
<?php interface A extends static {}
-----
Syntax error, unexpected T_STATIC, expecting T_STRING or T_NAMESPACE or T_NS_SEPARATOR on line 1

@ -1,4 +1,4 @@
Generators (yield expression
Generators (yield expression)
-----
<?php
@ -25,6 +25,10 @@ function gen() {
func(yield $foo);
$foo->func(yield $foo);
new Foo(yield $foo);
yield from $foo;
yield from $foo and yield from $bar;
yield from $foo + $bar;
}
-----
array(
@ -226,6 +230,33 @@ array(
)
)
)
15: Expr_YieldFrom(
expr: Expr_Variable(
name: foo
)
)
16: Expr_BinaryOp_LogicalAnd(
left: Expr_YieldFrom(
expr: Expr_Variable(
name: foo
)
)
right: Expr_YieldFrom(
expr: Expr_Variable(
name: bar
)
)
)
17: Expr_YieldFrom(
expr: Expr_BinaryOp_Plus(
left: Expr_Variable(
name: foo
)
right: Expr_Variable(
name: bar
)
)
)
)
)
)

@ -3,4 +3,4 @@ Invalid variadic function
<?php
function foo(...$foo = []) {}
-----
Variadic parameter cannot have a default value on line 2
Variadic parameter cannot have a default value from 2:24 to 2:25

@ -5,4 +5,4 @@ if (true) {
__halt_compiler();
}
-----
__HALT_COMPILER() can only be used from the outermost scope on line 3
__HALT_COMPILER() can only be used from the outermost scope from 3:5 to 3:19

@ -0,0 +1,29 @@
Invalid namespace names
-----
<?php namespace self;
-----
Cannot use 'self' as namespace name from 1:17 to 1:20
-----
<?php namespace PARENT;
-----
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;
-----
Cannot use A as self because 'self' is a special class name on line 1
-----
<?php use B as PARENT;
-----
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(
)

@ -1,25 +0,0 @@
Invalid namespace names
-----
<?php namespace self;
-----
Cannot use 'self' as namespace name on line 1
-----
<?php namespace parent;
-----
Cannot use 'parent' as namespace name on line 1
-----
<?php namespace static;
-----
Syntax error, unexpected T_STATIC, expecting T_STRING or T_NS_SEPARATOR or '{' on line 1
-----
<?php use A as self;
-----
Cannot use A as self because 'self' is a special class name on line 1
-----
<?php use B as parent;
-----
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 on line 1

@ -7,4 +7,4 @@ namespace A {
}
}
-----
Namespace declarations cannot be nested on line 3
Namespace declarations cannot be nested from 3:5 to 5:5

@ -0,0 +1,27 @@
Anonymous classes
-----
<?php
new class {};
new class extends A implements B, C {};
new class($a) extends A {
private $a;
public function __construct($a) {
$this->a = $a;
}
};
-----
new class
{
};
new class extends A implements B, C
{
};
new class($a) extends A
{
private $a;
public function __construct($a)
{
$this->a = $a;
}
};

@ -37,6 +37,8 @@ FALSE;
1e1000;
1E-100;
1000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
378282246310005.0;
10000000000000002.0;
// strings (normalized to single quoted)
'a';
@ -112,6 +114,8 @@ FALSE;
INF;
1.0E-100;
1.0E+84;
378282246310005.0;
10000000000000002.0;
// strings (normalized to single quoted)
'a';
'a

@ -33,6 +33,11 @@ $a ** $b ** $c;
($a ** $b) ** $c;
-1 ** 2;
yield from $a and yield from $b;
yield from ($a and yield from $b);
print ($a and print $b);
// The following will currently add unnecessary parentheses, because the pretty printer is not aware that assignment
// and incdec only work on variables.
!$a = $b;
@ -62,6 +67,9 @@ $a + $b++;
$a ** $b ** $c;
($a ** $b) ** $c;
-1 ** 2;
yield from $a and yield from $b;
yield from ($a and yield from $b);
print ($a and print $b);
// The following will currently add unnecessary parentheses, because the pretty printer is not aware that assignment
// and incdec only work on variables.
!($a = $b);