update v1.0.3.3
This commit is contained in:
129
vendor/jeremeamia/SuperClosure/src/Analyzer/AstAnalyzer.php
vendored
Normal file
129
vendor/jeremeamia/SuperClosure/src/Analyzer/AstAnalyzer.php
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php namespace SuperClosure\Analyzer;
|
||||
|
||||
use SuperClosure\Analyzer\Visitor\ThisDetectorVisitor;
|
||||
use SuperClosure\Exception\ClosureAnalysisException;
|
||||
use SuperClosure\Analyzer\Visitor\ClosureLocatorVisitor;
|
||||
use SuperClosure\Analyzer\Visitor\MagicConstantVisitor;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\PrettyPrinter\Standard as NodePrinter;
|
||||
use PhpParser\Error as ParserError;
|
||||
use PhpParser\NodeVisitor\NameResolver;
|
||||
use PhpParser\Parser as CodeParser;
|
||||
use PhpParser\Lexer\Emulative as EmulativeLexer;
|
||||
|
||||
/**
|
||||
* This is the AST based analyzer.
|
||||
*
|
||||
* We're using reflection and AST-based code parser to analyze a closure and
|
||||
* determine its code and context using the nikic/php-parser library. The AST
|
||||
* based analyzer and has more capabilities than the token analyzer, but is,
|
||||
* unfortunately, about 25 times slower.
|
||||
*/
|
||||
class AstAnalyzer extends ClosureAnalyzer
|
||||
{
|
||||
protected function determineCode(array &$data)
|
||||
{
|
||||
// Find the closure by traversing through a AST of the code.
|
||||
// Note: This also resolves class names to their FQCNs while traversing.
|
||||
$this->locateClosure($data);
|
||||
|
||||
// Make a second pass through the AST, but only through the closure's
|
||||
// nodes, to resolve any magic constants to literal values.
|
||||
$traverser = new NodeTraverser;
|
||||
$traverser->addVisitor(new MagicConstantVisitor($data['location']));
|
||||
$traverser->addVisitor($thisDetector = new ThisDetectorVisitor);
|
||||
$data['ast'] = $traverser->traverse([$data['ast']])[0];
|
||||
$data['hasThis'] = $thisDetector->detected;
|
||||
|
||||
// Bounce the updated AST down to a string representation of the code.
|
||||
$data['code'] = (new NodePrinter)->prettyPrint([$data['ast']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the closure's code and produces an abstract syntax tree (AST).
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @throws ClosureAnalysisException if there is an issue finding the closure
|
||||
*/
|
||||
private function locateClosure(array &$data)
|
||||
{
|
||||
try {
|
||||
$locator = new ClosureLocatorVisitor($data['reflection']);
|
||||
$fileAst = $this->getFileAst($data['reflection']);
|
||||
|
||||
$fileTraverser = new NodeTraverser;
|
||||
$fileTraverser->addVisitor(new NameResolver);
|
||||
$fileTraverser->addVisitor($locator);
|
||||
$fileTraverser->traverse($fileAst);
|
||||
} catch (ParserError $e) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ClosureAnalysisException(
|
||||
'There was an error analyzing the closure code.', 0, $e
|
||||
);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$data['ast'] = $locator->closureNode;
|
||||
if (!$data['ast']) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ClosureAnalysisException(
|
||||
'The closure was not found within the abstract syntax tree.'
|
||||
);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$data['location'] = $locator->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the variables that in the "use" clause of the closure definition.
|
||||
* These are referred to as the "used variables", "static variables", or
|
||||
* "closed upon variables", "context" of the closure.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
protected function determineContext(array &$data)
|
||||
{
|
||||
// Get the variable names defined in the AST
|
||||
$refs = 0;
|
||||
$vars = array_map(function ($node) use (&$refs) {
|
||||
if ($node->byRef) {
|
||||
$refs++;
|
||||
}
|
||||
return $node->var;
|
||||
}, $data['ast']->uses);
|
||||
$data['hasRefs'] = ($refs > 0);
|
||||
|
||||
// Get the variable names and values using reflection
|
||||
$values = $data['reflection']->getStaticVariables();
|
||||
|
||||
// Combine the names and values to create the canonical context.
|
||||
foreach ($vars as $name) {
|
||||
if (isset($values[$name])) {
|
||||
$data['context'][$name] = $values[$name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionFunction $reflection
|
||||
*
|
||||
* @throws ClosureAnalysisException
|
||||
*
|
||||
* @return \PhpParser\Node[]
|
||||
*/
|
||||
private function getFileAst(\ReflectionFunction $reflection)
|
||||
{
|
||||
$fileName = $reflection->getFileName();
|
||||
if (!file_exists($fileName)) {
|
||||
throw new ClosureAnalysisException(
|
||||
"The file containing the closure, \"{$fileName}\" did not exist."
|
||||
);
|
||||
}
|
||||
|
||||
$parser = new CodeParser(new EmulativeLexer);
|
||||
|
||||
return $parser->parse(file_get_contents($fileName));
|
||||
}
|
||||
}
|
||||
62
vendor/jeremeamia/SuperClosure/src/Analyzer/ClosureAnalyzer.php
vendored
Normal file
62
vendor/jeremeamia/SuperClosure/src/Analyzer/ClosureAnalyzer.php
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php namespace SuperClosure\Analyzer;
|
||||
|
||||
use SuperClosure\Exception\ClosureAnalysisException;
|
||||
|
||||
abstract class ClosureAnalyzer
|
||||
{
|
||||
/**
|
||||
* Analyzer a given closure.
|
||||
*
|
||||
* @param \Closure $closure
|
||||
*
|
||||
* @throws ClosureAnalysisException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function analyze(\Closure $closure)
|
||||
{
|
||||
$data = [
|
||||
'reflection' => new \ReflectionFunction($closure),
|
||||
'code' => null,
|
||||
'hasThis' => false,
|
||||
'context' => [],
|
||||
'hasRefs' => false,
|
||||
'binding' => null,
|
||||
'scope' => null,
|
||||
'isStatic' => $this->isClosureStatic($closure),
|
||||
];
|
||||
|
||||
$this->determineCode($data);
|
||||
$this->determineContext($data);
|
||||
$this->determineBinding($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
abstract protected function determineCode(array &$data);
|
||||
|
||||
/**
|
||||
* Returns the variables that are in the "use" clause of the closure.
|
||||
*
|
||||
* These variables are referred to as the "used variables", "static
|
||||
* variables", "closed upon variables", or "context" of the closure.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
abstract protected function determineContext(array &$data);
|
||||
|
||||
private function determineBinding(array &$data)
|
||||
{
|
||||
$data['binding'] = $data['reflection']->getClosureThis();
|
||||
if ($scope = $data['reflection']->getClosureScopeClass()) {
|
||||
$data['scope'] = $scope->getName();
|
||||
}
|
||||
}
|
||||
|
||||
private function isClosureStatic(\Closure $closure)
|
||||
{
|
||||
$rebound = new \ReflectionFunction(@$closure->bindTo(new \stdClass));
|
||||
|
||||
return $rebound->getClosureThis() === null;
|
||||
}
|
||||
}
|
||||
70
vendor/jeremeamia/SuperClosure/src/Analyzer/Token.php
vendored
Normal file
70
vendor/jeremeamia/SuperClosure/src/Analyzer/Token.php
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php namespace SuperClosure\Analyzer;
|
||||
|
||||
/**
|
||||
* A Token object represents and individual token parsed from PHP code.
|
||||
*
|
||||
* Each Token object is a normalized token created from the result of the
|
||||
* `get_token_all()`. function, which is part of PHP's tokenizer.
|
||||
*
|
||||
* @link http://us2.php.net/manual/en/tokens.php
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
/**
|
||||
* @var string The token name. Always null for literal tokens.
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var int|null The token's integer value. Always null for literal tokens.
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @var string The PHP code of the token.
|
||||
*/
|
||||
public $code;
|
||||
|
||||
/**
|
||||
* @var int|null The line number of the token in the original code.
|
||||
*/
|
||||
public $line;
|
||||
|
||||
/**
|
||||
* Constructs a token object.
|
||||
*
|
||||
* @param string $code
|
||||
* @param int|null $value
|
||||
* @param int|null $line
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct($code, $value = null, $line = null)
|
||||
{
|
||||
if (is_array($code)) {
|
||||
list($value, $code, $line) = array_pad($code, 3, null);
|
||||
}
|
||||
|
||||
$this->code = $code;
|
||||
$this->value = $value;
|
||||
$this->line = $line;
|
||||
$this->name = $value ? token_name($value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the token's value/code is equal to the specified value.
|
||||
*
|
||||
* @param mixed $value The value to check.
|
||||
*
|
||||
* @return bool True if the token is equal to the value.
|
||||
*/
|
||||
public function is($value)
|
||||
{
|
||||
return ($this->code === $value || $this->value === $value);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
}
|
||||
118
vendor/jeremeamia/SuperClosure/src/Analyzer/TokenAnalyzer.php
vendored
Normal file
118
vendor/jeremeamia/SuperClosure/src/Analyzer/TokenAnalyzer.php
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php namespace SuperClosure\Analyzer;
|
||||
|
||||
use SuperClosure\Exception\ClosureAnalysisException;
|
||||
|
||||
/**
|
||||
* This is the token based analyzer.
|
||||
*
|
||||
* We're using Uses reflection and tokenization to analyze a closure and
|
||||
* determine its code and context. This is much faster than the AST based
|
||||
* implementation.
|
||||
*/
|
||||
class TokenAnalyzer extends ClosureAnalyzer
|
||||
{
|
||||
public function determineCode(array &$data)
|
||||
{
|
||||
$this->determineTokens($data);
|
||||
$data['code'] = implode('', $data['tokens']);
|
||||
$data['hasThis'] = (strpos($data['code'], '$this') !== false);
|
||||
}
|
||||
|
||||
private function determineTokens(array &$data)
|
||||
{
|
||||
$potential = $this->determinePotentialTokens($data['reflection']);
|
||||
$braceLevel = $index = $step = $insideUse = 0;
|
||||
$data['tokens'] = $data['context'] = [];
|
||||
|
||||
foreach ($potential as $token) {
|
||||
$token = new Token($token);
|
||||
switch ($step) {
|
||||
// Handle tokens before the function declaration.
|
||||
case 0:
|
||||
if ($token->is(T_FUNCTION)) {
|
||||
$data['tokens'][] = $token;
|
||||
$step++;
|
||||
}
|
||||
break;
|
||||
// Handle tokens inside the function signature.
|
||||
case 1:
|
||||
$data['tokens'][] = $token;
|
||||
if ($insideUse) {
|
||||
if ($token->is(T_VARIABLE)) {
|
||||
$varName = trim($token, '$ ');
|
||||
$data['context'][$varName] = null;
|
||||
} elseif ($token->is('&')) {
|
||||
$data['hasRefs'] = true;
|
||||
}
|
||||
} elseif ($token->is(T_USE)) {
|
||||
$insideUse++;
|
||||
}
|
||||
if ($token->is('{')) {
|
||||
$step++;
|
||||
$braceLevel++;
|
||||
}
|
||||
break;
|
||||
// Handle tokens inside the function body.
|
||||
case 2:
|
||||
$data['tokens'][] = $token;
|
||||
if ($token->is('{')) {
|
||||
$braceLevel++;
|
||||
} elseif ($token->is('}')) {
|
||||
$braceLevel--;
|
||||
if ($braceLevel === 0) {
|
||||
$step++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Handle tokens after the function declaration.
|
||||
case 3:
|
||||
if ($token->is(T_FUNCTION)) {
|
||||
throw new ClosureAnalysisException('Multiple closures '
|
||||
. 'were declared on the same line of code. Could '
|
||||
. 'determine which closure was the intended target.'
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function determinePotentialTokens(\ReflectionFunction $reflection)
|
||||
{
|
||||
// Load the file containing the code for the function.
|
||||
$fileName = $reflection->getFileName();
|
||||
if (!is_readable($fileName)) {
|
||||
throw new ClosureAnalysisException(
|
||||
"Cannot read the file containing the closure: \"{$fileName}\"."
|
||||
);
|
||||
}
|
||||
|
||||
$code = '';
|
||||
$file = new \SplFileObject($fileName);
|
||||
$file->seek($reflection->getStartLine() - 1);
|
||||
while ($file->key() < $reflection->getEndLine()) {
|
||||
$code .= $file->current();
|
||||
$file->next();
|
||||
}
|
||||
|
||||
$code = trim($code);
|
||||
if (strpos($code, '<?php') !== 0) {
|
||||
$code = "<?php\n" . $code;
|
||||
}
|
||||
|
||||
return token_get_all($code);
|
||||
}
|
||||
|
||||
protected function determineContext(array &$data)
|
||||
{
|
||||
// Get the values of the variables that are closed upon in "use".
|
||||
$values = $data['reflection']->getStaticVariables();
|
||||
|
||||
// Construct the context by combining the variable names and values.
|
||||
foreach ($data['context'] as $name => &$value) {
|
||||
if (isset($values[$name])) {
|
||||
$value = $values[$name];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
vendor/jeremeamia/SuperClosure/src/Analyzer/Visitor/ClosureLocatorVisitor.php
vendored
Normal file
117
vendor/jeremeamia/SuperClosure/src/Analyzer/Visitor/ClosureLocatorVisitor.php
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php namespace SuperClosure\Analyzer\Visitor;
|
||||
|
||||
use SuperClosure\Exception\ClosureAnalysisException;
|
||||
use PhpParser\Node\Stmt\Namespace_ as NamespaceNode;
|
||||
use PhpParser\Node\Stmt\Trait_ as TraitNode;
|
||||
use PhpParser\Node\Stmt\Class_ as ClassNode;
|
||||
use PhpParser\Node\Expr\Closure as ClosureNode;
|
||||
use PhpParser\Node as AstNode;
|
||||
use PhpParser\NodeVisitorAbstract as NodeVisitor;
|
||||
|
||||
/**
|
||||
* This is a visitor that extends the nikic/php-parser library and looks for a
|
||||
* closure node and its location.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ClosureLocatorVisitor extends NodeVisitor
|
||||
{
|
||||
/**
|
||||
* @var \ReflectionFunction
|
||||
*/
|
||||
private $reflection;
|
||||
|
||||
/**
|
||||
* @var ClosureNode
|
||||
*/
|
||||
public $closureNode;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $location;
|
||||
|
||||
/**
|
||||
* @param \ReflectionFunction $reflection
|
||||
*/
|
||||
public function __construct($reflection)
|
||||
{
|
||||
$this->reflection = $reflection;
|
||||
$this->location = [
|
||||
'class' => null,
|
||||
'directory' => dirname($this->reflection->getFileName()),
|
||||
'file' => $this->reflection->getFileName(),
|
||||
'function' => $this->reflection->getName(),
|
||||
'line' => $this->reflection->getStartLine(),
|
||||
'method' => null,
|
||||
'namespace' => null,
|
||||
'trait' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function enterNode(AstNode $node)
|
||||
{
|
||||
// Determine information about the closure's location
|
||||
if (!$this->closureNode) {
|
||||
if ($node instanceof NamespaceNode) {
|
||||
$namespace = ($node->name && is_array($node->name->parts))
|
||||
? implode('\\', $node->name->parts)
|
||||
: null;
|
||||
$this->location['namespace'] = $namespace;
|
||||
}
|
||||
if ($node instanceof TraitNode) {
|
||||
$this->location['trait'] = $node->name;
|
||||
$this->location['class'] = null;
|
||||
} elseif ($node instanceof ClassNode) {
|
||||
$this->location['class'] = $node->name;
|
||||
$this->location['trait'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Locate the node of the closure
|
||||
if ($node instanceof ClosureNode) {
|
||||
if ($node->getAttribute('startLine') == $this->location['line']) {
|
||||
if ($this->closureNode) {
|
||||
$line = $this->location['file'] . ':' . $node->getAttribute('startLine');
|
||||
throw new ClosureAnalysisException("Two closures were "
|
||||
. "declared on the same line ({$line}) of code. Cannot "
|
||||
. "determine which closure was the intended target.");
|
||||
} else {
|
||||
$this->closureNode = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function leaveNode(AstNode $node)
|
||||
{
|
||||
// Determine information about the closure's location
|
||||
if (!$this->closureNode) {
|
||||
if ($node instanceof NamespaceNode) {
|
||||
$this->location['namespace'] = null;
|
||||
}
|
||||
if ($node instanceof TraitNode) {
|
||||
$this->location['trait'] = null;
|
||||
} elseif ($node instanceof ClassNode) {
|
||||
$this->location['class'] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function afterTraverse(array $nodes)
|
||||
{
|
||||
if ($this->location['class']) {
|
||||
$this->location['class'] = $this->location['namespace'] . '\\' . $this->location['class'];
|
||||
$this->location['method'] = "{$this->location['class']}::{$this->location['function']}";
|
||||
} elseif ($this->location['trait']) {
|
||||
$this->location['trait'] = $this->location['namespace'] . '\\' . $this->location['trait'];
|
||||
$this->location['method'] = "{$this->location['trait']}::{$this->location['function']}";
|
||||
}
|
||||
|
||||
if (!$this->location['class']) {
|
||||
/** @var \ReflectionClass $closureScopeClass */
|
||||
$closureScopeClass = $this->reflection->getClosureScopeClass();
|
||||
$this->location['class'] = $closureScopeClass ? $closureScopeClass->getName() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
vendor/jeremeamia/SuperClosure/src/Analyzer/Visitor/MagicConstantVisitor.php
vendored
Normal file
50
vendor/jeremeamia/SuperClosure/src/Analyzer/Visitor/MagicConstantVisitor.php
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php namespace SuperClosure\Analyzer\Visitor;
|
||||
|
||||
use PhpParser\Node\Scalar\LNumber as NumberNode;
|
||||
use PhpParser\Node\Scalar\String as StringNode;
|
||||
use PhpParser\Node as AstNode;
|
||||
use PhpParser\NodeVisitorAbstract as NodeVisitor;
|
||||
|
||||
/**
|
||||
* This is a visitor that resolves magic constants (e.g., __FILE__) to their
|
||||
* intended values within a closure's AST.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class MagicConstantVisitor extends NodeVisitor
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $location;
|
||||
|
||||
/**
|
||||
* @param array $location
|
||||
*/
|
||||
public function __construct(array $location)
|
||||
{
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
public function leaveNode(AstNode $node)
|
||||
{
|
||||
switch ($node->getType()) {
|
||||
case 'Scalar_MagicConst_Class' :
|
||||
return new StringNode($this->location['class']);
|
||||
case 'Scalar_MagicConst_Dir' :
|
||||
return new StringNode($this->location['directory']);
|
||||
case 'Scalar_MagicConst_File' :
|
||||
return new StringNode($this->location['file']);
|
||||
case 'Scalar_MagicConst_Function' :
|
||||
return new StringNode($this->location['function']);
|
||||
case 'Scalar_MagicConst_Line' :
|
||||
return new NumberNode($node->getAttribute('startLine'));
|
||||
case 'Scalar_MagicConst_Method' :
|
||||
return new StringNode($this->location['method']);
|
||||
case 'Scalar_MagicConst_Namespace' :
|
||||
return new StringNode($this->location['namespace']);
|
||||
case 'Scalar_MagicConst_Trait' :
|
||||
return new StringNode($this->location['trait']);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
vendor/jeremeamia/SuperClosure/src/Analyzer/Visitor/ThisDetectorVisitor.php
vendored
Normal file
27
vendor/jeremeamia/SuperClosure/src/Analyzer/Visitor/ThisDetectorVisitor.php
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php namespace SuperClosure\Analyzer\Visitor;
|
||||
|
||||
use PhpParser\Node as AstNode;
|
||||
use PhpParser\Node\Expr\Variable as VariableNode;
|
||||
use PhpParser\NodeVisitorAbstract as NodeVisitor;
|
||||
|
||||
/**
|
||||
* Detects if the closure's AST contains a $this variable.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ThisDetectorVisitor extends NodeVisitor
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $detected = false;
|
||||
|
||||
public function leaveNode(AstNode $node)
|
||||
{
|
||||
if ($node instanceof VariableNode) {
|
||||
if ($node->name === 'this') {
|
||||
$this->detected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user