package updates

This commit is contained in:
2018-12-19 23:27:43 -06:00
parent aa1da4d4fb
commit 1ffd21369f
1181 changed files with 51194 additions and 11046 deletions

View File

@@ -23,7 +23,7 @@ matrix:
script:
- if [ $TRAVIS_PHP_VERSION = '7.0' ]; then vendor/bin/phpunit --coverage-clover build/logs/clover.xml; else vendor/bin/phpunit; fi
- if [ $TRAVIS_PHP_VERSION = '7.1' ]; then test_old/run-php-src.sh; fi
- if [ $TRAVIS_PHP_VERSION = '7.2' ]; then test_old/run-php-src.sh; fi
after_success:
- if [ $TRAVIS_PHP_VERSION = '7.0' ]; then php vendor/bin/coveralls; fi

View File

@@ -1,8 +1,25 @@
Version 4.0.5-dev
Version 4.1.1-dev
-----------------
Nothing yet.
Version 4.1.0 (2018-10-10)
--------------------------
### Added
* Added support for PHP 7.3 flexible heredoc/nowdoc strings, completing support for PHP 7.3. There
are two caveats for this feature:
* In some rare, pathological cases flexible heredoc/nowdoc strings change the interpretation of
existing doc strings. PHP-Parser will now use the new interpretation.
* Flexible heredoc/nowdoc strings require special support from the lexer. Because this is not
available on PHP versions before 7.3, support has to be emulated. This emulation is not perfect
and some cases which we do not expect to occur in practice (such as flexible doc strings being
nested within each other through abuse of variable-variable interpolation syntax) may not be
recognized correctly.
* Added `DONT_TRAVERSER_CURRENT_AND_CHILDREN` to `NodeTraverser` to skip both traversal of child
nodes, and prevent subsequent visitors from visiting the current node.
Version 4.0.4 (2018-09-18)
--------------------------

View File

@@ -6,7 +6,7 @@ PHP Parser
This is a PHP 5.2 to PHP 7.2 parser written in PHP. Its purpose is to simplify static code analysis and
manipulation.
[**Documentation for version 4.x**][doc_master] (stable; for running on PHP >= 7.0; for parsing PHP 5.2 to PHP 7.2).
[**Documentation for version 4.x**][doc_master] (stable; for running on PHP >= 7.0; for parsing PHP 5.2 to PHP 7.3).
[Documentation for version 3.x][doc_3_x] (unsupported; for running on PHP >= 5.5; for parsing PHP 5.2 to PHP 7.2).

View File

@@ -24,7 +24,7 @@
"bin": ["bin/php-parse"],
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
"dev-master": "4.1-dev"
}
}
}

View File

@@ -791,11 +791,9 @@ common_scalar:
| T_FUNC_C { $$ = Scalar\MagicConst\Function_[]; }
| T_NS_C { $$ = Scalar\MagicConst\Namespace_[]; }
| T_START_HEREDOC T_ENCAPSED_AND_WHITESPACE T_END_HEREDOC
{ $attrs = attributes(); setDocStringAttrs($attrs, $1);
$$ = new Scalar\String_(Scalar\String_::parseDocString($1, $2, false), $attrs); }
{ $$ = $this->parseDocString($1, $2, $3, attributes(), stackAttributes(#3), false); }
| T_START_HEREDOC T_END_HEREDOC
{ $attrs = attributes(); setDocStringAttrs($attrs, $1);
$$ = new Scalar\String_('', $attrs); }
{ $$ = $this->parseDocString($1, '', $2, attributes(), stackAttributes(#2), false); }
;
static_scalar:
@@ -856,8 +854,7 @@ scalar:
{ $attrs = attributes(); $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED;
parseEncapsed($2, '"', true); $$ = new Scalar\Encapsed($2, $attrs); }
| T_START_HEREDOC encaps_list T_END_HEREDOC
{ $attrs = attributes(); setDocStringAttrs($attrs, $1);
parseEncapsedDoc($2, true); $$ = new Scalar\Encapsed($2, $attrs); }
{ $$ = $this->parseDocString($1, $2, $3, attributes(), stackAttributes(#3), true); }
;
static_array_pair_list:

View File

@@ -847,17 +847,14 @@ scalar:
| dereferencable_scalar { $$ = $1; }
| constant { $$ = $1; }
| T_START_HEREDOC T_ENCAPSED_AND_WHITESPACE T_END_HEREDOC
{ $attrs = attributes(); setDocStringAttrs($attrs, $1);
$$ = new Scalar\String_(Scalar\String_::parseDocString($1, $2), $attrs); }
{ $$ = $this->parseDocString($1, $2, $3, attributes(), stackAttributes(#3), true); }
| T_START_HEREDOC T_END_HEREDOC
{ $attrs = attributes(); setDocStringAttrs($attrs, $1);
$$ = new Scalar\String_('', $attrs); }
{ $$ = $this->parseDocString($1, '', $2, attributes(), stackAttributes(#2), true); }
| '"' encaps_list '"'
{ $attrs = attributes(); $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED;
parseEncapsed($2, '"', true); $$ = new Scalar\Encapsed($2, $attrs); }
| T_START_HEREDOC encaps_list T_END_HEREDOC
{ $attrs = attributes(); setDocStringAttrs($attrs, $1);
parseEncapsedDoc($2, true); $$ = new Scalar\Encapsed($2, $attrs); }
{ $$ = $this->parseDocString($1, $2, $3, attributes(), stackAttributes(#3), true); }
;
optional_expr:

View File

@@ -166,15 +166,6 @@ function resolveMacros($code) {
. ' $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, ' . $args[1] . ', ' . $args[2] . '); } }';
}
if ('parseEncapsedDoc' == $name) {
assertArgs(2, $args, $name);
return 'foreach (' . $args[0] . ' as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) {'
. ' $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, ' . $args[1] . '); } }'
. ' $s->value = preg_replace(\'~(\r\n|\n|\r)\z~\', \'\', $s->value);'
. ' if (\'\' === $s->value) array_pop(' . $args[0] . ');';
}
if ('makeNop' == $name) {
assertArgs(3, $args, $name);
@@ -192,15 +183,6 @@ function resolveMacros($code) {
. '? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED)';
}
if ('setDocStringAttrs' == $name) {
assertArgs(2, $args, $name);
return $args[0] . '[\'kind\'] = strpos(' . $args[1] . ', "\'") === false '
. '? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; '
. 'preg_match(\'/\A[bB]?<<<[ \t]*[\\\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\\\'"]?(?:\r\n|\n|\r)\z/\', ' . $args[1] . ', $matches); '
. $args[0] . '[\'docLabel\'] = $matches[1];';
}
if ('prependLeadingComments' == $name) {
assertArgs(1, $args, $name);

View File

@@ -2,7 +2,202 @@
namespace PhpParser\Lexer;
use PhpParser\Error;
use PhpParser\ErrorHandler;
class Emulative extends \PhpParser\Lexer
{
/* No features requiring emulation have been added in PHP > 7.0 */
}
const PHP_7_3 = '7.3.0dev';
/**
* @var array Patches used to reverse changes introduced in the code
*/
private $patches;
public function startLexing(string $code, ErrorHandler $errorHandler = null) {
$this->patches = [];
$preparedCode = $this->prepareCode($code);
if (null === $preparedCode) {
// Nothing to emulate, yay
parent::startLexing($code, $errorHandler);
return;
}
$collector = new ErrorHandler\Collecting();
parent::startLexing($preparedCode, $collector);
$this->fixupTokens();
$errors = $collector->getErrors();
if (!empty($errors)) {
$this->fixupErrors($errors);
foreach ($errors as $error) {
$errorHandler->handleError($error);
}
}
}
/**
* Prepares code for emulation. If nothing has to be emulated null is returned.
*
* @param string $code
* @return null|string
*/
private function prepareCode(string $code) {
if (version_compare(\PHP_VERSION, self::PHP_7_3, '>=')) {
return null;
}
if (strpos($code, '<<<') === false) {
// Definitely doesn't contain heredoc/nowdoc
return null;
}
$flexibleDocStringRegex = <<<'REGEX'
/<<<[ \t]*(['"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\1\r?\n
(?:.*\r?\n)*?
(?<indentation>\h*)\2(?![a-zA-Z_\x80-\xff])(?<separator>(?:;?[\r\n])?)/x
REGEX;
if (!preg_match_all($flexibleDocStringRegex, $code, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
// No heredoc/nowdoc found
return null;
}
// Keep track of how much we need to adjust string offsets due to the modifications we
// already made
$posDelta = 0;
foreach ($matches as $match) {
$indentation = $match['indentation'][0];
$indentationStart = $match['indentation'][1];
$separator = $match['separator'][0];
$separatorStart = $match['separator'][1];
if ($indentation === '' && $separator !== '') {
// Ordinary heredoc/nowdoc
continue;
}
if ($indentation !== '') {
// Remove indentation
$indentationLen = strlen($indentation);
$code = substr_replace($code, '', $indentationStart + $posDelta, $indentationLen);
$this->patches[] = [$indentationStart + $posDelta, 'add', $indentation];
$posDelta -= $indentationLen;
}
if ($separator === '') {
// Insert newline as separator
$code = substr_replace($code, "\n", $separatorStart + $posDelta, 0);
$this->patches[] = [$separatorStart + $posDelta, 'remove', "\n"];
$posDelta += 1;
}
}
if (empty($this->patches)) {
// We did not end up emulating anything
return null;
}
return $code;
}
private function fixupTokens() {
assert(count($this->patches) > 0);
// Load first patch
$patchIdx = 0;
list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
// We use a manual loop over the tokens, because we modify the array on the fly
$pos = 0;
for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) {
$token = $this->tokens[$i];
if (\is_string($token)) {
// We assume that patches don't apply to string tokens
$pos += \strlen($token);
continue;
}
$len = \strlen($token[1]);
$posDelta = 0;
while ($patchPos >= $pos && $patchPos < $pos + $len) {
$patchTextLen = \strlen($patchText);
if ($patchType === 'remove') {
if ($patchPos === $pos && $patchTextLen === $len) {
// Remove token entirely
array_splice($this->tokens, $i, 1, []);
$i--;
$c--;
} else {
// Remove from token string
$this->tokens[$i][1] = substr_replace(
$token[1], '', $patchPos - $pos + $posDelta, $patchTextLen
);
$posDelta -= $patchTextLen;
}
} elseif ($patchType === 'add') {
// Insert into the token string
$this->tokens[$i][1] = substr_replace(
$token[1], $patchText, $patchPos - $pos + $posDelta, 0
);
$posDelta += $patchTextLen;
} else {
assert(false);
}
// Fetch the next patch
$patchIdx++;
if ($patchIdx >= \count($this->patches)) {
// No more patches, we're done
return;
}
list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
// Multiple patches may apply to the same token. Reload the current one to check
// If the new patch applies
$token = $this->tokens[$i];
}
$pos += $len;
}
// A patch did not apply
assert(false);
}
/**
* Fixup line and position information in errors.
*
* @param Error[] $errors
*/
private function fixupErrors(array $errors) {
foreach ($errors as $error) {
$attrs = $error->getAttributes();
$posDelta = 0;
$lineDelta = 0;
foreach ($this->patches as $patch) {
list($patchPos, $patchType, $patchText) = $patch;
if ($patchPos >= $attrs['startFilePos']) {
// No longer relevant
break;
}
if ($patchType === 'add') {
$posDelta += strlen($patchText);
$lineDelta += substr_count($patchText, "\n");
} else {
$posDelta -= strlen($patchText);
$lineDelta -= substr_count($patchText, "\n");
}
}
$attrs['startFilePos'] += $posDelta;
$attrs['endFilePos'] += $posDelta;
$attrs['startLine'] += $lineDelta;
$attrs['endLine'] += $lineDelta;
$error->setAttributes($attrs);
}
}
}

View File

@@ -10,7 +10,7 @@ class StaticCall extends Expr
{
/** @var Node\Name|Expr Class name */
public $class;
/** @var string|Identifier|Expr Method name */
/** @var Identifier|Expr Method name */
public $name;
/** @var Node\Arg[] Arguments */
public $args;

View File

@@ -134,29 +134,6 @@ class String_ extends Scalar
}
throw new Error('Invalid UTF-8 codepoint escape sequence: Codepoint too large');
}
/**
* @internal
*
* Parses a constant doc string.
*
* @param string $startToken Doc string start token content (<<<SMTHG)
* @param string $str String token content
* @param bool $parseUnicodeEscape Whether to parse PHP 7 \u escapes
*
* @return string Parsed string
*/
public static function parseDocString(string $startToken, string $str, bool $parseUnicodeEscape = true) : string {
// strip last newline (thanks tokenizer for sticking it into the string!)
$str = preg_replace('~(\r\n|\n|\r)\z~', '', $str);
// nowdoc string
if (false !== strpos($startToken, '\'')) {
return $str;
}
return self::parseEscapeSequences($str, null, $parseUnicodeEscape);
}
public function getType() : string {
return 'Scalar_String';

View File

@@ -30,6 +30,15 @@ class NodeTraverser implements NodeTraverserInterface
*/
const REMOVE_NODE = 3;
/**
* If NodeVisitor::enterNode() returns DONT_TRAVERSE_CURRENT_AND_CHILDREN, child nodes
* of the current node will not be traversed for any visitors.
*
* For subsequent visitors enterNode() will not be called as well.
* leaveNode() will be invoked for visitors that has enterNode() method invoked.
*/
const DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4;
/** @var NodeVisitor[] Visitors */
protected $visitors = [];
@@ -108,7 +117,9 @@ class NodeTraverser implements NodeTraverserInterface
}
} elseif ($subNode instanceof Node) {
$traverseChildren = true;
foreach ($this->visitors as $visitor) {
$breakVisitorIndex = null;
foreach ($this->visitors as $visitorIndex => $visitor) {
$return = $visitor->enterNode($subNode);
if (null !== $return) {
if ($return instanceof Node) {
@@ -116,6 +127,10 @@ class NodeTraverser implements NodeTraverserInterface
$subNode = $return;
} elseif (self::DONT_TRAVERSE_CHILDREN === $return) {
$traverseChildren = false;
} elseif (self::DONT_TRAVERSE_CURRENT_AND_CHILDREN === $return) {
$traverseChildren = false;
$breakVisitorIndex = $visitorIndex;
break;
} elseif (self::STOP_TRAVERSAL === $return) {
$this->stopTraversal = true;
break 2;
@@ -134,8 +149,9 @@ class NodeTraverser implements NodeTraverserInterface
}
}
foreach ($this->visitors as $visitor) {
foreach ($this->visitors as $visitorIndex => $visitor) {
$return = $visitor->leaveNode($subNode);
if (null !== $return) {
if ($return instanceof Node) {
$this->ensureReplacementReasonable($subNode, $return);
@@ -154,6 +170,10 @@ class NodeTraverser implements NodeTraverserInterface
);
}
}
if ($breakVisitorIndex === $visitorIndex) {
break;
}
}
}
}
@@ -174,7 +194,9 @@ class NodeTraverser implements NodeTraverserInterface
foreach ($nodes as $i => &$node) {
if ($node instanceof Node) {
$traverseChildren = true;
foreach ($this->visitors as $visitor) {
$breakVisitorIndex = null;
foreach ($this->visitors as $visitorIndex => $visitor) {
$return = $visitor->enterNode($node);
if (null !== $return) {
if ($return instanceof Node) {
@@ -182,6 +204,10 @@ class NodeTraverser implements NodeTraverserInterface
$node = $return;
} elseif (self::DONT_TRAVERSE_CHILDREN === $return) {
$traverseChildren = false;
} elseif (self::DONT_TRAVERSE_CURRENT_AND_CHILDREN === $return) {
$traverseChildren = false;
$breakVisitorIndex = $visitorIndex;
break;
} elseif (self::STOP_TRAVERSAL === $return) {
$this->stopTraversal = true;
break 2;
@@ -200,8 +226,9 @@ class NodeTraverser implements NodeTraverserInterface
}
}
foreach ($this->visitors as $visitor) {
foreach ($this->visitors as $visitorIndex => $visitor) {
$return = $visitor->leaveNode($node);
if (null !== $return) {
if ($return instanceof Node) {
$this->ensureReplacementReasonable($node, $return);
@@ -226,6 +253,10 @@ class NodeTraverser implements NodeTraverserInterface
);
}
}
if ($breakVisitorIndex === $visitorIndex) {
break;
}
}
} elseif (\is_array($node)) {
throw new \LogicException('Invalid node structure: Contains nested arrays');

View File

@@ -2264,12 +2264,10 @@ class Php5 extends \PhpParser\ParserAbstract
$this->semValue = new Scalar\MagicConst\Namespace_($this->startAttributeStack[$stackPos-(1-1)] + $this->endAttributes);
},
436 => function ($stackPos) {
$attrs = $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];;
$this->semValue = new Scalar\String_(Scalar\String_::parseDocString($this->semStack[$stackPos-(3-1)], $this->semStack[$stackPos-(3-2)], false), $attrs);
$this->semValue = $this->parseDocString($this->semStack[$stackPos-(3-1)], $this->semStack[$stackPos-(3-2)], $this->semStack[$stackPos-(3-3)], $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes, $this->startAttributeStack[$stackPos-(3-3)] + $this->endAttributeStack[$stackPos-(3-3)], false);
},
437 => function ($stackPos) {
$attrs = $this->startAttributeStack[$stackPos-(2-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$stackPos-(2-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$stackPos-(2-1)], $matches); $attrs['docLabel'] = $matches[1];;
$this->semValue = new Scalar\String_('', $attrs);
$this->semValue = $this->parseDocString($this->semStack[$stackPos-(2-1)], '', $this->semStack[$stackPos-(2-2)], $this->startAttributeStack[$stackPos-(2-1)] + $this->endAttributes, $this->startAttributeStack[$stackPos-(2-2)] + $this->endAttributeStack[$stackPos-(2-2)], false);
},
438 => function ($stackPos) {
$this->semValue = $this->semStack[$stackPos-(1-1)];
@@ -2405,8 +2403,7 @@ class Php5 extends \PhpParser\ParserAbstract
foreach ($this->semStack[$stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '"', true); } }; $this->semValue = new Scalar\Encapsed($this->semStack[$stackPos-(3-2)], $attrs);
},
482 => function ($stackPos) {
$attrs = $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];;
foreach ($this->semStack[$stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, true); } } $s->value = preg_replace('~(\r\n|\n|\r)\z~', '', $s->value); if ('' === $s->value) array_pop($this->semStack[$stackPos-(3-2)]);; $this->semValue = new Scalar\Encapsed($this->semStack[$stackPos-(3-2)], $attrs);
$this->semValue = $this->parseDocString($this->semStack[$stackPos-(3-1)], $this->semStack[$stackPos-(3-2)], $this->semStack[$stackPos-(3-3)], $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes, $this->startAttributeStack[$stackPos-(3-3)] + $this->endAttributeStack[$stackPos-(3-3)], true);
},
483 => function ($stackPos) {
$this->semValue = array();

View File

@@ -2201,20 +2201,17 @@ class Php7 extends \PhpParser\ParserAbstract
$this->semValue = $this->semStack[$stackPos-(1-1)];
},
445 => function ($stackPos) {
$attrs = $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];;
$this->semValue = new Scalar\String_(Scalar\String_::parseDocString($this->semStack[$stackPos-(3-1)], $this->semStack[$stackPos-(3-2)]), $attrs);
$this->semValue = $this->parseDocString($this->semStack[$stackPos-(3-1)], $this->semStack[$stackPos-(3-2)], $this->semStack[$stackPos-(3-3)], $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes, $this->startAttributeStack[$stackPos-(3-3)] + $this->endAttributeStack[$stackPos-(3-3)], true);
},
446 => function ($stackPos) {
$attrs = $this->startAttributeStack[$stackPos-(2-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$stackPos-(2-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$stackPos-(2-1)], $matches); $attrs['docLabel'] = $matches[1];;
$this->semValue = new Scalar\String_('', $attrs);
$this->semValue = $this->parseDocString($this->semStack[$stackPos-(2-1)], '', $this->semStack[$stackPos-(2-2)], $this->startAttributeStack[$stackPos-(2-1)] + $this->endAttributes, $this->startAttributeStack[$stackPos-(2-2)] + $this->endAttributeStack[$stackPos-(2-2)], true);
},
447 => function ($stackPos) {
$attrs = $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED;
foreach ($this->semStack[$stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '"', true); } }; $this->semValue = new Scalar\Encapsed($this->semStack[$stackPos-(3-2)], $attrs);
},
448 => function ($stackPos) {
$attrs = $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];;
foreach ($this->semStack[$stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, true); } } $s->value = preg_replace('~(\r\n|\n|\r)\z~', '', $s->value); if ('' === $s->value) array_pop($this->semStack[$stackPos-(3-2)]);; $this->semValue = new Scalar\Encapsed($this->semStack[$stackPos-(3-2)], $attrs);
$this->semValue = $this->parseDocString($this->semStack[$stackPos-(3-1)], $this->semStack[$stackPos-(3-2)], $this->semStack[$stackPos-(3-3)], $this->startAttributeStack[$stackPos-(3-1)] + $this->endAttributes, $this->startAttributeStack[$stackPos-(3-3)] + $this->endAttributeStack[$stackPos-(3-3)], true);
},
449 => function ($stackPos) {
$this->semValue = null;

View File

@@ -9,6 +9,7 @@ namespace PhpParser;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\Encapsed;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
@@ -710,6 +711,119 @@ abstract class ParserAbstract implements Parser
return new LNumber($num, $attributes);
}
protected function stripIndentation(
string $string, int $indentLen, string $indentChar,
bool $newlineAtStart, bool $newlineAtEnd, array $attributes
) {
if ($indentLen === 0) {
return $string;
}
$start = $newlineAtStart ? '(?:(?<=\n)|\A)' : '(?<=\n)';
$end = $newlineAtEnd ? '(?:(?=[\r\n])|\z)' : '(?=[\r\n])';
$regex = '/' . $start . '([ \t]*)(' . $end . ')?/';
return preg_replace_callback(
$regex,
function ($matches) use ($indentLen, $indentChar, $attributes) {
$prefix = substr($matches[1], 0, $indentLen);
if (false !== strpos($prefix, $indentChar === " " ? "\t" : " ")) {
$this->emitError(new Error(
'Invalid indentation - tabs and spaces cannot be mixed', $attributes
));
} elseif (strlen($prefix) < $indentLen && !isset($matches[2])) {
$this->emitError(new Error(
'Invalid body indentation level ' .
'(expecting an indentation level of at least ' . $indentLen . ')',
$attributes
));
}
return substr($matches[0], strlen($prefix));
},
$string
);
}
protected function parseDocString(
string $startToken, $contents, string $endToken,
array $attributes, array $endTokenAttributes, bool $parseUnicodeEscape
) {
$kind = strpos($startToken, "'") === false
? String_::KIND_HEREDOC : String_::KIND_NOWDOC;
$regex = '/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/';
$result = preg_match($regex, $startToken, $matches);
assert($result === 1);
$label = $matches[1];
$result = preg_match('/\A[ \t]*/', $endToken, $matches);
assert($result === 1);
$indentation = $matches[0];
$attributes['kind'] = $kind;
$attributes['docLabel'] = $label;
$attributes['docIndentation'] = $indentation;
$indentHasSpaces = false !== strpos($indentation, " ");
$indentHasTabs = false !== strpos($indentation, "\t");
if ($indentHasSpaces && $indentHasTabs) {
$this->emitError(new Error(
'Invalid indentation - tabs and spaces cannot be mixed',
$endTokenAttributes
));
// Proceed processing as if this doc string is not indented
$indentation = '';
}
$indentLen = \strlen($indentation);
$indentChar = $indentHasSpaces ? " " : "\t";
if (\is_string($contents)) {
if ($contents === '') {
return new String_('', $attributes);
}
$contents = $this->stripIndentation(
$contents, $indentLen, $indentChar, true, true, $attributes
);
$contents = preg_replace('~(\r\n|\n|\r)\z~', '', $contents);
if ($kind === String_::KIND_HEREDOC) {
$contents = String_::parseEscapeSequences($contents, null, $parseUnicodeEscape);
}
return new String_($contents, $attributes);
} else {
assert(count($contents) > 0);
if (!$contents[0] instanceof Node\Scalar\EncapsedStringPart) {
// If there is no leading encapsed string part, pretend there is an empty one
$this->stripIndentation(
'', $indentLen, $indentChar, true, false, $contents[0]->getAttributes()
);
}
$newContents = [];
foreach ($contents as $i => $part) {
if ($part instanceof Node\Scalar\EncapsedStringPart) {
$isLast = $i === \count($contents) - 1;
$part->value = $this->stripIndentation(
$part->value, $indentLen, $indentChar,
$i === 0, $isLast, $part->getAttributes()
);
$part->value = String_::parseEscapeSequences($part->value, null, $parseUnicodeEscape);
if ($isLast) {
$part->value = preg_replace('~(\r\n|\n|\r)\z~', '', $part->value);
}
if ('' === $part->value) {
continue;
}
}
$newContents[] = $part;
}
return new Encapsed($newContents, $attributes);
}
}
protected function checkModifier($a, $b, $modifierPos) {
// Jumping through some hoops here because verifyModifier() is also used elsewhere
try {

View File

@@ -123,39 +123,31 @@ DOC;
);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Unexpected node of type "Stmt_Echo"
*/
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_Echo"');
$this->createClassBuilder('Test')
->addStmt(new Stmt\Echo_([]))
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Doc comment must be a string or an instance of PhpParser\Comment\Doc
*/
public function testInvalidDocComment() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
$this->createClassBuilder('Test')
->setDocComment(new Comment('Test'));
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Name cannot be empty
*/
public function testEmptyName() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name cannot be empty');
$this->createClassBuilder('Test')
->extend('');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Name must be a string or an instance of Node\Name
*/
public function testInvalidName() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name must be a string or an instance of Node\Name');
$this->createClassBuilder('Test')
->extend(['Foo']);
}

View File

@@ -92,29 +92,23 @@ class FunctionTest extends TestCase
], []), $node);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage void type cannot be nullable
*/
public function testInvalidNullableVoidType() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('void type cannot be nullable');
$this->createFunctionBuilder('test')->setReturnType('?void');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Expected parameter node, got "Name"
*/
public function testInvalidParamError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected parameter node, got "Name"');
$this->createFunctionBuilder('test')
->addParam(new Node\Name('foo'))
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Expected statement or expression node
*/
public function testAddNonStmt() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected statement or expression node');
$this->createFunctionBuilder('test')
->addStmt(new Node\Name('Test'));
}

View File

@@ -79,11 +79,9 @@ class InterfaceTest extends TestCase
]), $node);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Unexpected node of type "Stmt_PropertyProperty"
*/
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_PropertyProperty"');
$this->builder->addStmt(new Stmt\PropertyProperty('invalid'));
}

View File

@@ -135,33 +135,27 @@ class MethodTest extends TestCase
], []), $node);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot add statements to an abstract method
*/
public function testAddStmtToAbstractMethodError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot add statements to an abstract method');
$this->createMethodBuilder('test')
->makeAbstract()
->addStmt(new Print_(new String_('test')))
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot make method with statements abstract
*/
public function testMakeMethodWithStmtsAbstractError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot make method with statements abstract');
$this->createMethodBuilder('test')
->addStmt(new Print_(new String_('test')))
->makeAbstract()
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Expected parameter node, got "Name"
*/
public function testInvalidParamError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected parameter node, got "Name"');
$this->createMethodBuilder('test')
->addParam(new Node\Name('foo'))
;

View File

@@ -129,19 +129,15 @@ class ParamTest extends TestCase
];
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Parameter type cannot be void
*/
public function testVoidTypeError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Parameter type cannot be void');
$this->createParamBuilder('test')->setType('void');
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Type must be a string, or an instance of Name, Identifier or NullableType
*/
public function testInvalidTypeError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Type must be a string, or an instance of Name, Identifier or NullableType');
$this->createParamBuilder('test')->setType(new \stdClass);
}

View File

@@ -37,11 +37,9 @@ class TraitTest extends TestCase
]), $trait);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Unexpected node of type "Stmt_Echo"
*/
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_Echo"');
$this->createTraitBuilder('Test')
->addStmt(new Stmt\Echo_([]))
;

View File

@@ -55,65 +55,53 @@ class TraitUseAdaptationTest extends TestCase
);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot set alias for not alias adaptation buider
*/
public function testAsOnNotAlias() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot set alias for not alias adaptation buider');
$this->createTraitUseAdaptationBuilder('Test', 'foo')
->insteadof('AnotherTrait')
->as('bar')
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot add overwritten traits for not precedence adaptation buider
*/
public function testInsteadofOnNotPrecedence() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot add overwritten traits for not precedence adaptation buider');
$this->createTraitUseAdaptationBuilder('Test', 'foo')
->as('bar')
->insteadof('AnotherTrait')
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Precedence adaptation must have trait
*/
public function testInsteadofWithoutTrait() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Precedence adaptation must have trait');
$this->createTraitUseAdaptationBuilder(null, 'foo')
->insteadof('AnotherTrait')
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot set access modifier for not alias adaptation buider
*/
public function testMakeOnNotAlias() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot set access modifier for not alias adaptation buider');
$this->createTraitUseAdaptationBuilder('Test', 'foo')
->insteadof('AnotherTrait')
->makePublic()
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Multiple access type modifiers are not allowed
*/
public function testMultipleMake() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Multiple access type modifiers are not allowed');
$this->createTraitUseAdaptationBuilder(null, 'foo')
->makePrivate()
->makePublic()
;
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Type of adaptation is not defined
*/
public function testUndefinedType() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Type of adaptation is not defined');
$this->createTraitUseAdaptationBuilder(null, 'foo')
->getNode()
;

View File

@@ -45,11 +45,9 @@ class TraitUseTest extends TestCase
);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Adaptation must have type TraitUseAdaptation
*/
public function testInvalidAdaptationNode() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Adaptation must have type TraitUseAdaptation');
$this->createTraitUseBuilder('Test')
->with(new Stmt\Echo_([]))
;

View File

@@ -69,19 +69,15 @@ class BuilderFactoryTest extends TestCase
);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Expected at least two expressions
*/
public function testConcatOneError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected at least two expressions');
(new BuilderFactory())->concat("a");
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Expected string or Expr
*/
public function testConcatInvalidExpr() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected string or Expr');
(new BuilderFactory())->concat("a", 42);
}
@@ -218,35 +214,27 @@ class BuilderFactoryTest extends TestCase
);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Expected string or instance of Node\Identifier
*/
public function testInvalidIdentifier() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected string or instance of Node\Identifier');
(new BuilderFactory())->classConstFetch('Foo', new Expr\Variable('foo'));
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Expected string or instance of Node\Identifier or Node\Expr
*/
public function testInvalidIdentifierOrExpr() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected string or instance of Node\Identifier or Node\Expr');
(new BuilderFactory())->staticCall('Foo', new Name('bar'));
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Name must be a string or an instance of Node\Name or Node\Expr
*/
public function testInvalidNameOrExpr() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name must be a string or an instance of Node\Name or Node\Expr');
(new BuilderFactory())->funcCall(new Node\Stmt\Return_());
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Variable name must be string or Expr
*/
public function testInvalidVar() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Variable name must be string or Expr');
(new BuilderFactory())->var(new Node\Stmt\Return_());
}

View File

@@ -73,11 +73,9 @@ class ConstExprEvaluatorTest extends TestCase
];
}
/**
* @expectedException \PhpParser\ConstExprEvaluationException
* @expectedExceptionMessage Expression of type Expr_Variable cannot be evaluated
*/
public function testEvaluateFails() {
$this->expectException(ConstExprEvaluationException::class);
$this->expectExceptionMessage('Expression of type Expr_Variable cannot be evaluated');
$evaluator = new ConstExprEvaluator();
$evaluator->evaluateDirectly(new Expr\Variable('a'));
}

View File

@@ -7,11 +7,9 @@ use PHPUnit\Framework\TestCase;
class ThrowingTest extends TestCase
{
/**
* @expectedException \PhpParser\Error
* @expectedExceptionMessage Test
*/
public function testHandleError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('Test');
$errorHandler = new Throwing();
$errorHandler->handleError(new Error('Test'));
}

View File

@@ -94,11 +94,9 @@ class ErrorTest extends TestCase
}
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Invalid position information
*/
public function testInvalidPosInfo() {
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Invalid position information');
$error = new Error('Some error', [
'startFilePos' => 10,
'endFilePos' => 11,

View File

@@ -2,6 +2,7 @@
namespace PhpParser\Lexer;
use PhpParser\ErrorHandler;
use PhpParser\LexerTest;
use PhpParser\Parser\Tokens;
@@ -63,12 +64,11 @@ class EmulativeTest extends LexerTest
$lexer = $this->getLexer();
$lexer->startLexing('<?php ' . $code);
foreach ($expectedTokens as $expectedToken) {
list($expectedTokenType, $expectedTokenText) = $expectedToken;
$this->assertSame($expectedTokenType, $lexer->getNextToken($text));
$this->assertSame($expectedTokenText, $text);
$tokens = [];
while (0 !== $token = $lexer->getNextToken($text)) {
$tokens[] = [$token, $text];
}
$this->assertSame(0, $lexer->getNextToken());
$this->assertSame($expectedTokens, $tokens);
}
/**
@@ -85,6 +85,29 @@ class EmulativeTest extends LexerTest
$this->assertSame(0, $lexer->getNextToken());
}
/**
* @dataProvider provideTestLexNewFeatures
*/
public function testErrorAfterEmulation($code) {
$errorHandler = new ErrorHandler\Collecting;
$lexer = $this->getLexer([]);
$lexer->startLexing('<?php ' . $code . "\0", $errorHandler);
$errors = $errorHandler->getErrors();
$this->assertCount(1, $errors);
$error = $errors[0];
$this->assertSame('Unexpected null byte', $error->getRawMessage());
$attrs = $error->getAttributes();
$expPos = strlen('<?php ' . $code);
$expLine = 1 + substr_count('<?php ' . $code, "\n");
$this->assertSame($expPos, $attrs['startFilePos']);
$this->assertSame($expPos, $attrs['endFilePos']);
$this->assertSame($expLine, $attrs['startLine']);
$this->assertSame($expLine, $attrs['endLine']);
}
public function provideTestLexNewFeatures() {
return [
['yield from', [
@@ -128,6 +151,43 @@ class EmulativeTest extends LexerTest
[Tokens::T_END_HEREDOC, 'NOWDOC'],
[ord(';'), ';'],
]],
// Flexible heredoc/nowdoc
["<<<LABEL\nLABEL,", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_END_HEREDOC, "LABEL"],
[ord(','), ','],
]],
["<<<LABEL\n LABEL,", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_END_HEREDOC, " LABEL"],
[ord(','), ','],
]],
["<<<LABEL\n Foo\n LABEL;", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_ENCAPSED_AND_WHITESPACE, " Foo\n"],
[Tokens::T_END_HEREDOC, " LABEL"],
[ord(';'), ';'],
]],
["<<<A\n A,<<<A\n A,", [
[Tokens::T_START_HEREDOC, "<<<A\n"],
[Tokens::T_END_HEREDOC, " A"],
[ord(','), ','],
[Tokens::T_START_HEREDOC, "<<<A\n"],
[Tokens::T_END_HEREDOC, " A"],
[ord(','), ','],
]],
["<<<LABEL\nLABELNOPE\nLABEL\n", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_ENCAPSED_AND_WHITESPACE, "LABELNOPE\n"],
[Tokens::T_END_HEREDOC, "LABEL"],
]],
// Interpretation changed
["<<<LABEL\n LABEL\nLABEL\n", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_END_HEREDOC, " LABEL"],
[Tokens::T_STRING, "LABEL"],
]],
];
}
}

View File

@@ -235,11 +235,9 @@ class LexerTest extends TestCase
];
}
/**
* @expectedException \PhpParser\Error
* @expectedExceptionMessage __HALT_COMPILER must be followed by "();"
*/
public function testHandleHaltCompilerError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('__HALT_COMPILER must be followed by "();"');
$lexer = $this->getLexer();
$lexer->startLexing('<?php ... __halt_compiler invalid ();');

View File

@@ -51,35 +51,27 @@ class NameTest extends TestCase
$this->assertNull($name->slice(-2, -2));
}
/**
* @expectedException \OutOfBoundsException
* @expectedExceptionMessage Offset 4 is out of bounds
*/
public function testSliceOffsetTooLarge() {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Offset 4 is out of bounds');
(new Name('foo\bar\baz'))->slice(4);
}
/**
* @expectedException \OutOfBoundsException
* @expectedExceptionMessage Offset -4 is out of bounds
*/
public function testSliceOffsetTooSmall() {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Offset -4 is out of bounds');
(new Name('foo\bar\baz'))->slice(-4);
}
/**
* @expectedException \OutOfBoundsException
* @expectedExceptionMessage Length 4 is out of bounds
*/
public function testSliceLengthTooLarge() {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Length 4 is out of bounds');
(new Name('foo\bar\baz'))->slice(0, 4);
}
/**
* @expectedException \OutOfBoundsException
* @expectedExceptionMessage Length -4 is out of bounds
*/
public function testSliceLengthTooSmall() {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Length -4 is out of bounds');
(new Name('foo\bar\baz'))->slice(0, -4);
}
@@ -131,27 +123,21 @@ class NameTest extends TestCase
$this->assertSame('namespace\foo', $name->toCodeString());
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Expected string, array of parts or Name instance
*/
public function testInvalidArg() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Expected string, array of parts or Name instance');
Name::concat('foo', new \stdClass);
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Name cannot be empty
*/
public function testInvalidEmptyString() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Name cannot be empty');
new Name('');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Name cannot be empty
*/
public function testInvalidEmptyArray() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Name cannot be empty');
new Name([]);
}

View File

@@ -98,11 +98,9 @@ OUT;
$this->assertSame($this->canonicalize($expected), $this->canonicalize($dump));
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Can only dump nodes and arrays.
*/
public function testError() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Can only dump nodes and arrays.');
$dumper = new NodeDumper;
$dumper->dump(new \stdClass);
}

View File

@@ -117,11 +117,9 @@ class NodeTraverserTest extends TestCase
);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Invalid node structure: Contains nested arrays
*/
public function testInvalidDeepArray() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Invalid node structure: Contains nested arrays');
$strNode = new String_('Foo');
$stmts = [[[$strNode]]];
@@ -167,6 +165,42 @@ class NodeTraverserTest extends TestCase
$this->assertEquals($stmts, $traverser->traverse($stmts));
}
public function testDontTraverseCurrentAndChildren() {
// print 'str'; -($foo * $foo);
$strNode = new String_('str');
$printNode = new Expr\Print_($strNode);
$varNode = new Expr\Variable('foo');
$mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
$divNode = new Expr\BinaryOp\Div($varNode, $varNode);
$negNode = new Expr\UnaryMinus($mulNode);
$stmts = [$printNode, $negNode];
$visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor1->expects($this->at(1))->method('enterNode')->with($printNode)
->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN));
$visitor1->expects($this->at(2))->method('leaveNode')->with($printNode);
$visitor1->expects($this->at(3))->method('enterNode')->with($negNode);
$visitor2->expects($this->at(1))->method('enterNode')->with($negNode);
$visitor1->expects($this->at(4))->method('enterNode')->with($mulNode)
->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN));
$visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode)->willReturn($divNode);
$visitor1->expects($this->at(6))->method('leaveNode')->with($negNode);
$visitor2->expects($this->at(2))->method('leaveNode')->with($negNode);
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor1);
$traverser->addVisitor($visitor2);
$resultStmts = $traverser->traverse($stmts);
$this->assertInstanceOf(Expr\BinaryOp\Div::class, $resultStmts[1]->expr);
}
public function testStopTraversal() {
$varNode1 = new Expr\Variable('a');
$varNode2 = new Expr\Variable('b');

View File

@@ -13,29 +13,23 @@ abstract class ParserTest extends TestCase
/** @returns Parser */
abstract protected function getParser(Lexer $lexer);
/**
* @expectedException \PhpParser\Error
* @expectedExceptionMessage Syntax error, unexpected EOF on line 1
*/
public function testParserThrowsSyntaxError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('Syntax error, unexpected EOF on line 1');
$parser = $this->getParser(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() {
$this->expectException(Error::class);
$this->expectExceptionMessage('Cannot use foo as self because \'self\' is a special class name on line 1');
$parser = $this->getParser(new Lexer());
$parser->parse('<?php use foo as self;');
}
/**
* @expectedException \PhpParser\Error
* @expectedExceptionMessage Unterminated comment on line 1
*/
public function testParserThrowsLexerError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('Unterminated comment on line 1');
$parser = $this->getParser(new Lexer());
$parser->parse('<?php /*');
}
@@ -109,11 +103,9 @@ EOC;
], $var->getAttributes());
}
/**
* @expectedException \RangeException
* @expectedExceptionMessage The lexer returned an invalid token (id=999, value=foobar)
*/
public function testInvalidToken() {
$this->expectException(\RangeException::class);
$this->expectExceptionMessage('The lexer returned an invalid token (id=999, value=foobar)');
$lexer = new InvalidTokenLexer;
$parser = $this->getParser($lexer);
$parser->parse('dummy');
@@ -123,7 +115,7 @@ EOC;
* @dataProvider provideTestExtraAttributes
*/
public function testExtraAttributes($code, $expectedAttributes) {
$parser = $this->getParser(new Lexer);
$parser = $this->getParser(new Lexer\Emulative);
$stmts = $parser->parse("<?php $code;");
$node = $stmts[0] instanceof Stmt\Expression ? $stmts[0]->expr : $stmts[0];
$attributes = $node->getAttributes();
@@ -152,17 +144,20 @@ EOC;
['"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
['b"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
['B"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
["<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']],
["<<<STR\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']],
["<<<\"STR\"\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']],
["b<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']],
["B<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']],
["<<< \t 'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']],
["<<<'\xff'\n\xff\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => "\xff"]],
["<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']],
["b<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']],
["B<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']],
["<<< \t \"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']],
["<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<<STR\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<<\"STR\"\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["b<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["B<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<< \t 'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<<'\xff'\n\xff\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => "\xff", 'docIndentation' => '']],
["<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["b<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["B<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<< \t \"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<<STR\n STR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => ' ']],
["<<<STR\n\tSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => "\t"]],
["<<<'STR'\n Foo\n STR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => ' ']],
["die", ['kind' => Expr\Exit_::KIND_DIE]],
["die('done')", ['kind' => Expr\Exit_::KIND_DIE]],
["exit", ['kind' => Expr\Exit_::KIND_EXIT]],

View File

@@ -184,11 +184,9 @@ class PrettyPrinterTest extends CodeTestAbstract
];
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot pretty-print AST with Error nodes
*/
public function testPrettyPrintWithError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot pretty-print AST with Error nodes');
$stmts = [new Stmt\Expression(
new Expr\PropertyFetch(new Expr\Variable('a'), new Expr\Error())
)];
@@ -196,11 +194,9 @@ class PrettyPrinterTest extends CodeTestAbstract
$prettyPrinter->prettyPrint($stmts);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot pretty-print AST with Error nodes
*/
public function testPrettyPrintWithErrorInClassConstFetch() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot pretty-print AST with Error nodes');
$stmts = [new Stmt\Expression(
new Expr\ClassConstFetch(new Name('Foo'), new Expr\Error())
)];
@@ -208,11 +204,9 @@ class PrettyPrinterTest extends CodeTestAbstract
$prettyPrinter->prettyPrint($stmts);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Cannot directly print EncapsedStringPart
*/
public function testPrettyPrintEncapsedStringPart() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot directly print EncapsedStringPart');
$expr = new Node\Scalar\EncapsedStringPart('foo');
$prettyPrinter = new PrettyPrinter\Standard;
$prettyPrinter->prettyPrintExpr($expr);

View File

@@ -0,0 +1,359 @@
Flexible heredoc/nowdoc (PHP 7.3)
-----
<?php
$ary = [
<<<FOO
Test
FOO,
<<<'BAR'
Test
BAR,
];
<<<'END'
END;
<<<END
END;
<<<END
@@{ " " }@@
END;
<<<'END'
a
b
c
d
e
END;
<<<END
a
b
$test
d
e
END;
<<<'END'
a
b
c
d
e
END;
<<<END
a\r\n
\ta\n
b\r\n
$test\n
d\r\n
e\n
END;
<<<BAR
$one-
BAR;
<<<BAR
$two -
BAR;
<<<BAR
$three -
BAR;
<<<BAR
$four-$four
BAR;
<<<BAR
$five-$five-
BAR;
<<<BAR
$six-$six-$six
BAR;
<<<BAR
$seven
-
BAR;
<<<BAR
$eight
-
BAR;
<<<BAR
$nine
BAR;
<<<BAR
-
BAR;
<<<BAR
-
BAR;
-----
array(
0: Stmt_Expression(
expr: Expr_Assign(
var: Expr_Variable(
name: ary
)
expr: Expr_Array(
items: array(
0: Expr_ArrayItem(
key: null
value: Scalar_String(
value: Test
)
byRef: false
)
1: Expr_ArrayItem(
key: null
value: Scalar_String(
value: Test
)
byRef: false
)
)
)
)
)
1: Stmt_Expression(
expr: Scalar_String(
value:
)
)
2: Stmt_Expression(
expr: Scalar_String(
value:
)
)
3: Stmt_Expression(
expr: Scalar_String(
value:
)
)
4: Stmt_Expression(
expr: Scalar_String(
value: a
b
c
d
e
)
)
5: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Scalar_EncapsedStringPart(
value: a
b
)
1: Expr_Variable(
name: test
)
2: Scalar_EncapsedStringPart(
value:
d
e
)
)
)
)
6: Stmt_Expression(
expr: Scalar_String(
value:
a
b
c
d
e
)
)
7: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Scalar_EncapsedStringPart(
value: a
@@{ "\t" }@@a
b
)
1: Expr_Variable(
name: test
)
2: Scalar_EncapsedStringPart(
value:
d
e
)
)
)
)
8: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: one
)
1: Scalar_EncapsedStringPart(
value: -
)
)
)
)
9: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: two
)
1: Scalar_EncapsedStringPart(
value: -
)
)
)
)
10: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: three
)
1: Scalar_EncapsedStringPart(
value: -
)
)
)
)
11: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: four
)
1: Scalar_EncapsedStringPart(
value: -
)
2: Expr_Variable(
name: four
)
)
)
)
12: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: five
)
1: Scalar_EncapsedStringPart(
value: -
)
2: Expr_Variable(
name: five
)
3: Scalar_EncapsedStringPart(
value: -
)
)
)
)
13: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: six
)
1: Scalar_EncapsedStringPart(
value: -
)
2: Expr_Variable(
name: six
)
3: Scalar_EncapsedStringPart(
value: -
)
4: Expr_Variable(
name: six
)
)
)
)
14: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: seven
)
1: Scalar_EncapsedStringPart(
value:
-
)
)
)
)
15: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: eight
)
1: Scalar_EncapsedStringPart(
value:
-
)
)
)
)
16: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: nine
)
)
)
)
17: Stmt_Expression(
expr: Scalar_String(
value: -
)
)
18: Stmt_Expression(
expr: Scalar_String(
value: -
)
)
)

View File

@@ -0,0 +1,117 @@
Error conditions for flexible doc strings
-----
<?php
<<<A
@@{ "\t" }@@A;
<<<A
FooBar
@@{ "\t" }@@A;
echo <<<END
@@{ "\t" }@@ X
@@{ "\t\t" }@@END;
echo <<<END
a
b
c
END;
<<<END
\ta
@@{ "\t" }@@END;
<<<TEST
Foo
$var
TEST;
<<<TEST
$var
TEST;
echo <<<END
a
$a
END;
-----
Invalid indentation - tabs and spaces cannot be mixed from 4:1 to 4:3
Invalid indentation - tabs and spaces cannot be mixed from 8:1 to 8:3
Invalid indentation - tabs and spaces cannot be mixed from 10:6 to 12:5
Invalid body indentation level (expecting an indentation level of at least 5) from 14:6 to 18:8
Invalid body indentation level (expecting an indentation level of at least 1) from 20:1 to 22:4
Invalid body indentation level (expecting an indentation level of at least 2) from 25:1 to 26:0
Invalid body indentation level (expecting an indentation level of at least 1) from 30:1 to 30:4
Invalid body indentation level (expecting an indentation level of at least 1) from 34:1 to 35:0
array(
0: Stmt_Expression(
expr: Scalar_String(
value:
)
)
1: Stmt_Expression(
expr: Scalar_String(
value: FooBar
)
)
2: Stmt_Echo(
exprs: array(
0: Scalar_String(
value: X
)
)
)
3: Stmt_Echo(
exprs: array(
0: Scalar_String(
value: a
b
c
)
)
)
4: Stmt_Expression(
expr: Scalar_String(
value: a
)
)
5: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Scalar_EncapsedStringPart(
value: Foo
)
1: Expr_Variable(
name: var
)
)
)
)
6: Stmt_Expression(
expr: Scalar_Encapsed(
parts: array(
0: Expr_Variable(
name: var
)
)
)
)
7: Stmt_Echo(
exprs: array(
0: Scalar_Encapsed(
parts: array(
0: Scalar_EncapsedStringPart(
value: a
)
1: Expr_Variable(
name: a
)
)
)
)
)
)

View File

@@ -1,4 +1,4 @@
wget -q https://github.com/php/php-src/archive/php-7.1.0.tar.gz
wget -q https://github.com/php/php-src/archive/php-7.3.0RC1.tar.gz
mkdir -p ./data/php-src
tar -xzf ./php-7.1.0.tar.gz -C ./data/php-src --strip-components=1
tar -xzf ./php-7.3.0RC1.tar.gz -C ./data/php-src --strip-components=1
php -n test_old/run.php --verbose --no-progress PHP7 ./data/php-src

View File

@@ -59,9 +59,24 @@ $dir = $arguments[1];
switch ($testType) {
case 'Symfony':
$version = 'Php5';
$version = 'Php7';
$fileFilter = function($path) {
return preg_match('~\.php(?:\.cache)?$~', $path) && false === strpos($path, 'skeleton');
if (!preg_match('~\.php$~', $path)) {
return false;
}
if (preg_match('~(?:
# invalid php code
dependency-injection.Tests.Fixtures.xml.xml_with_wrong_ext
# difference in nop statement
| framework-bundle.Resources.views.Form.choice_widget_options\.html
# difference due to INF
| yaml.Tests.InlineTest
)\.php$~x', $path)) {
return false;
}
return true;
};
$codeExtractor = function($file, $code) {
return $code;
@@ -77,26 +92,31 @@ switch ($testType) {
if (preg_match('~(?:
# skeleton files
ext.gmp.tests.001
| ext.skeleton.tests.001
| ext.skeleton.tests.00\d
# multibyte encoded files
| ext.mbstring.tests.zend_multibyte-01
| Zend.tests.multibyte.multibyte_encoding_001
| Zend.tests.multibyte.multibyte_encoding_004
| Zend.tests.multibyte.multibyte_encoding_005
# invalid code due to missing WS after opening tag
| tests.run-test.bug75042-3
# pretty print difference due to INF vs 1e1000
| ext.standard.tests.general_functions.bug27678
| tests.lang.bug24640
| Zend.tests.bug74947
# pretty print differences due to negative LNumbers
| Zend.tests.neg_num_string
| Zend.tests.bug72918
# pretty print difference due to nop statements
| ext.mbstring.tests.htmlent
| ext.standard.tests.file.fread_basic
# its too hard to emulate these on old PHP versions
| Zend.tests.flexible-heredoc-complex-test[1-4]
)\.phpt$~x', $file)) {
return null;
}
if (!preg_match('~--FILE--\s*(.*?)--[A-Z]+--~s', $code, $matches)) {
if (!preg_match('~--FILE--\s*(.*?)\n--[A-Z]+--~s', $code, $matches)) {
return null;
}
if (preg_match('~--EXPECT(?:F|REGEX)?--\s*(?:Parse|Fatal) error~', $code)) {