Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Variable collector #224

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Reflection/ReflectionFunctionAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter;
use PhpParser\PrettyPrinterAbstract;
use SuperClosure\Analyzer\AstAnalyzer;
use BetterReflection\Util\Visitor\VariableCollectionVisitor;

abstract class ReflectionFunctionAbstract implements \Reflector
{
Expand Down Expand Up @@ -619,4 +620,13 @@ public function getReturnStatementsAst()

return $visitor->getReturnNodes();
}

public function getVariables()
{
$nodeTraverser = new NodeTraverser();
$nodeTraverser->addVisitor($visitor = new VariableCollectionVisitor($this->getDeclaringClass(), $this->reflector));
$nodeTraverser->traverse([$this->getNode()]);

return $visitor->getVariables();
}
}
2 changes: 2 additions & 0 deletions src/Reflection/ReflectionMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use BetterReflection\Reflector\Reflector;
use PhpParser\Node\Stmt\ClassMethod as MethodNode;
use PhpParser\NodeTraverser;
use BetterReflection\Util\Visitor\VariableCollectionVisitor;

class ReflectionMethod extends ReflectionFunctionAbstract
{
Expand Down
81 changes: 81 additions & 0 deletions src/Reflection/ReflectionVariable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace BetterReflection\Reflection;

use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types;

class ReflectionVariable
{
/**
* @var string
*/
private $name;

/**
* @var int
*/
private $declaredAt;

/**
* @var $type
*/
private $type;

/**
* @param string $name
* @param int $declaredAt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure about just having the line number.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, ideally there would be the range of (characters?) that the variable is available for, but the PHP parser just gives us line numbers ..

* @param string $type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't say much - need some sort of more restrictive hint here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its actually a ReflectionType ...

* @return ReflectionType
*/
public static function createFromName($name, ReflectionType $type = null, $declaredAt)
{
$reflectionType = new self();
$reflectionType->name = $name;
$reflectionType->declaredAt = $declaredAt;
$reflectionType->type = $type;
return $reflectionType;
}

public function getName()
{
return $this->name;
}

public function getDeclaredAt()
{
return $this->declaredAt;
}

/**
* Get a PhpDocumentor type object for this type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the type mutated during execution? Two variables with same name, but different type and declaration line, then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We return all declarations, so you could get two with the same name, but covering different parts of the code.

*
* @return Type
*/
public function getTypeObject()
{
return $this->type;
}

/**
* Checks if it is a built-in type (i.e., it's not an object...)
*
* @see http://php.net/manual/en/reflectiontype.isbuiltin.php
* @return bool
*/
public function isBuiltin()
{
return (!$this->type instanceof Types\Object_);
}

/**
* Convert this string type to a string
*
* @see https://github.com/php/php-src/blob/master/ext/reflection/php_reflection.c#L2993
* @return string
*/
public function __toString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't add this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it useful for testing, but mainly I added it to be consistent with the other Reflection* classes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless it actually is required API, I wouldn't add it. __toString() is often abused, so let's not give users more rope to hang themselves :)

Copy link
Contributor Author

@dantleech dantleech Dec 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, will remove it. I can only guess that the __toString functionality in the other classes is useful for dumping a representation of a class as a whole, and that Variables would not be part of the representation /cc @asgrim

{
return sprintf('@var $%s (%s): %s', $this->name, $this->type, $this->declaredAt);
}
}
273 changes: 273 additions & 0 deletions src/Util/Visitor/VariableCollectionVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<?php

namespace BetterReflection\Util\Visitor;

use PhpParser\NodeVisitorAbstract;
use BetterReflection\Reflection\ReflectionVariable;
use BetterReflection\TypesFinder\FindTypeFromAst;
use BetterReflection\Reflection\ReflectionType;
use BetterReflection\Reflection\ReflectionClass;
use PhpParser\Node;
use PhpParser\Node\Expr;
use BetterReflection\Reflector\Reflector;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\Types as DocType;

class VariableCollectionVisitor extends NodeVisitorAbstract
{
/**
* @var ReflectionClass
*/
private $reflection;

/**
* @var Reflector
*/
private $reflector;

/**
* @var array
*/
private $variables = [];

/**
* @var array
*/
private $methodParamTypes = [];

/**
* Construct with the reflection class for the AST that we are traversing
* and a reflector instance to resolve types from other classes.
*/
public function __construct(ReflectionClass $reflection, Reflector $reflector)
{
$this->reflector = $reflector;
$this->reflection = $reflection;
}

/**
* {@inheritdoc}
*
* Just in case this visitor is invoked again, reset the
* variables before traversal starts.
*/
public function beforeTraverse(array $nodes)
{
$this->variables = [];
}

/**
* {@inheritdoc}
*
* Currently we care about two types of variables:
*
* 1. Parameters that are passed to class methods.
* 2. Newly declared variables.
*/
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\ClassMethod) {
$this->processClassMethod($node);
return;
}

if ($node instanceof Expr\Assign) {
$this->processAssignation($node);
return;
}
}

/**
* Return all the variables which were discovered in the AST.
*
* @return ReflectionVariable[]
*/
public function getVariables()
{
return $this->variables;
}

private function processClassMethod(Node\Stmt\ClassMethod $node)
{
// reset when we enter the class method scope
$this->methodParamTypes = [];

foreach ($node->params as $param) {
$reflParam = $this->reflection->getMethod($node->name)->getParameter($param->name);
$type = $reflParam->getType();

// if the type is null, then try and guess the type from the docblock, if
// multiple types are available, then return the first.
if (null === $type) {
$types = $reflParam->getDocBlockTypes();
$type = count($types) ? ReflectionType::createFromType(reset($types), false) : null;
}

$this->methodParamTypes[$param->name] = $type;
$this->variables[] = ReflectionVariable::createFromName($param->name, $type, $param->getAttribute('startLine'));
}
}

private function processAssignation(Expr\Assign $node)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just processAssign

{
// assignment is not directly to a vaiable, so just ignore it
// rather than trying to recreate state.
if (false === $node->var instanceof Expr\Variable) {
return;
}

$type = $this->typeFromExpression($node->expr);
$this->variables[] = ReflectionVariable::createFromName($node->var->name, $type, $node->getAttribute('startLine'));
}

private function typeFromExpression(Node $expr)
{
$type = null;

// new object, just get the type and we are good.
if ($expr instanceof Expr\New_) {
return $this->createType($expr->class);
}

// if this is a property fetch we must resolve the call
// chain to determine the type.
if ($expr instanceof Expr\PropertyFetch) {
$exprs = $this->flattenFetchChain($expr);

return $this->resolvePropertyFetchChain($exprs);
}

// if it is a method call then we recurse to resolve the type.
if ($expr instanceof Expr\MethodCall) {
$type = $this->typeFromExpression($expr->var);
$reflection = $this->reflector->reflect($type);
$method = $reflection->getMethod($expr->name);

return $method->getReturnType();
}

if ($expr instanceof Expr\Variable) {
if ($expr->name === 'this') {
$type = ReflectionType::createFromType(new Object_(new Fqsen('\\'.$this->reflection->getName())), false);

return $type;
}

if (isset($this->methodParamTypes[$expr->name])) {
return $this->methodParamTypes[$expr->name];
}
}

// if this is a function call, try and instantiate the runtime native
// \ReflectionFunction, if it is not internal then ignore it as we
// cannot guarantee that we are running in the same process as the code
// we are analyzing.
//
// TODO: This is no positive test case for this ... which PHP internal functions
// actually have a return type??
if ($expr instanceof Expr\FuncCall) {
$func = (string) $expr->name;
$reflection = new \ReflectionFunction($func);
Copy link
Contributor Author

@dantleech dantleech Nov 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty hacky - wonder if we should simply not support functions I guess we should at least try and use BR\ReflecitonFunction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can stop at the return type declared on the function btw - no need to introspect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, this is a function_call(), how else would I determine the return type? But anyway, not tested yet, so I have no idea what would happen here.


// do not try and find out return type for non-internal functions
if (false === $reflection->isInternal()) {
return;
}

// in the case that no return type was provided, just return
// "mixed".
return $reflection->getReturnType() ?: ReflectionType::createFromType(new DocType\Mixed(), false);
}

if ($expr instanceof Expr\ArrayDimFetch) {
return ReflectionType::createFromType(new DocType\Mixed(), false);
}

if ($expr instanceof Expr\Array_) {
return ReflectionType::createFromType(new DocType\Array_(), false);
}

if ($expr instanceof Node\Scalar) {
switch (get_class($expr)) {
case Node\Scalar\DNumber::class:
return ReflectionType::createFromType(new DocType\Float_(), false);

case Node\Scalar\LNumber::class:
return ReflectionType::createFromType(new DocType\Integer(), false);

case Node\Scalar\String_::class:
return ReflectionType::createFromType(new DocType\String_(), false);

case Node\Scalar\Encapsed::class:
case Node\Scalar\MagicConst::class:
case Node\Scalar\EncapsedStringPart::class:
// TODO: ???
return;
default:
throw new \RuntimeException(sprintf(
'Do not know scalar type "%s"', get_class($expr)
));
}
}

throw new \RuntimeException(sprintf(
'Could not determine type from expression for node of type "%s"',
get_class($expr)
));
}

private function resolvePropertyFetchChain(array $exprs)
{
$reflection = $this->reflection;

foreach ($exprs as $expr) {
// TODO: This seems very wrong ...
if (false === $expr instanceof Expr\PropertyFetch) {
continue;
}

// resolve the type of the property from its docblock
$prop = $reflection->getProperty($expr->name);
$types = $prop->getDocBlockTypes();
$type = reset($types);

// set the new reflection to the type of the property
$reflection = $this->reflector->reflect($type->getFqsen());
}

return $reflection->getName();
}

private function flattenFetchChain(Expr\PropertyFetch $node)
{
$exprs = [];

// if this the child of this node is also a property fetch node then
// recurse, otherwise just add the child to the chain and return.
if ($node->var instanceof Expr\PropertyFetch) {
$exprs = array_merge($exprs, $this->flattenFetchChain($node->var));
} else {
$exprs[] = $node->var;
}

$exprs[] = $node;

return $exprs;
}

private function createType($type)
{
$typeHint = (new FindTypeFromAst())->__invoke(
$type,
$this->reflection->getLocatedSource(),
$this->reflection->getNamespaceName()
);

if (null === $typeHint) {
return;
}

return ReflectionType::createFromType($typeHint, false);
}
}
Loading