Skip to content

Commit

Permalink
All parameters passed by reference can create a new variable in outer…
Browse files Browse the repository at this point in the history
… scope
  • Loading branch information
ondrejmirtes committed Jul 19, 2016
1 parent 0cd911a commit 05498a5
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 125 deletions.
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
parameters:
excludes_analyse:
- %rootDir%/tests/*/data/*
ignoreErrors:
- '#PHPUnit_Framework_MockObject_MockObject::method\(\)#'
services:
-
class: PHPStan\Reflection\PhpParser\PhpParserNameClassReflectionExtension
Expand Down
94 changes: 53 additions & 41 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
Expand Down Expand Up @@ -45,25 +46,12 @@
use PHPStan\Broker\Broker;
use PHPStan\Type\ArrayType;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;

class NodeScopeResolver
{

const SPECIAL_FUNCTIONS = [
'preg_match' => [3],
'preg_match_all' => [3],
'preg_replace_callback' => [5],
'preg_replace_callback_array' => [4],
'preg_replace' => [5],
'proc_open' => [3],
'passthru' => [2],
'parse_str' => [2],
'exec' => [2, 3],
'stream_socket_client' => [2, 3],
'openssl_sign' => [2],
];

/** @var \PHPStan\Broker\Broker */
private $broker;

Expand Down Expand Up @@ -119,18 +107,19 @@ public function processNodes(array $nodes, Scope $scope, \Closure $nodeCallback)
continue;
}

if (
$scope->getInFunctionCallName() !== null
&& in_array($scope->getInFunctionCallName(), array_keys(self::SPECIAL_FUNCTIONS), true)
&& $node instanceof Arg
) {
$functionName = $scope->getInFunctionCallName();
$specialArgsPositions = self::SPECIAL_FUNCTIONS[$functionName];
if ($scope->getInFunctionCall() !== null && $node instanceof Arg) {
$functionCall = $scope->getInFunctionCall();
$value = $node->value;
if (in_array($i + 1, $specialArgsPositions, true) && $value instanceof Variable) {
$functionReflection = $this->broker->getFunction($functionName);
$parameters = $functionReflection->getParameters();
$scope = $scope->assignVariable($value->name, $parameters[$i]->getType());

$parameters = $this->findParametersInFunctionCall($functionCall, $scope);

if (
$parameters !== null
&& isset($parameters[$i])
&& $parameters[$i]->isPassedByReference()
&& $value instanceof Variable
) {
$scope = $scope->assignVariable($value->name, new MixedType(true));
}
}

Expand Down Expand Up @@ -179,7 +168,7 @@ private function processNode(\PhpParser\Node $node, Scope $scope, \Closure $node
}
} elseif ($node instanceof \PhpParser\Node\Stmt\Function_) {
$scope = $scope->enterFunction(
$this->broker->getFunction((string) $node->namespacedName)
$this->broker->getFunction($node->namespacedName, $scope)
);
} elseif ($node instanceof \PhpParser\Node\Stmt\ClassMethod) {
if ($scope->getClass() !== null) {
Expand Down Expand Up @@ -251,8 +240,8 @@ private function processNode(\PhpParser\Node $node, Scope $scope, \Closure $node
foreach ($node->stmts as $statement) {
$scope = $this->lookForAssigns($scope, $statement);
}
} elseif ($node instanceof FuncCall && $node->name instanceof Name) {
$scope = $scope->enterFunctionCall((string) $node->name);
} elseif ($node instanceof FuncCall || $node instanceof MethodCall) {
$scope = $scope->enterFunctionCall($node);
} elseif ($node instanceof Array_) {
foreach ($node->items as $item) {
$scope = $this->lookForAssigns($scope, $item->value);
Expand Down Expand Up @@ -388,19 +377,17 @@ private function lookForAssigns(Scope $scope, \PhpParser\Node $node): Scope
$scope = $this->lookForAssigns($scope, $argument);
}

if ($node instanceof FuncCall && $node->name instanceof Name) {
if (in_array((string) $node->name, array_keys(self::SPECIAL_FUNCTIONS), true)) {
$functionName = (string) $node->name;
$newVariablePositions = self::SPECIAL_FUNCTIONS[$functionName];
foreach ($newVariablePositions as $newVariablePosition) {
if (count($node->args) >= $newVariablePosition) {
$arg = $node->args[$newVariablePosition - 1]->value;
if ($arg instanceof Variable) {
$functionReflection = $this->broker->getFunction($functionName);
$parameters = $functionReflection->getParameters();
$scope = $scope->assignVariable($arg->name, $parameters[$newVariablePosition - 1]->getType());
}
}
$parameters = $this->findParametersInFunctionCall($node, $scope);

if ($parameters !== null) {
foreach ($parameters as $i => $parameter) {
if (!isset($node->args[$i]) || !$parameter->isPassedByReference()) {
continue;
}

$arg = $node->args[$i]->value;
if ($arg instanceof Variable && is_string($arg->name)) {
$scope = $scope->assignVariable($arg->name, new MixedType(true));
}
}
}
Expand Down Expand Up @@ -647,4 +634,29 @@ private function hasStatementEarlyTermination(Node $statement, Scope $scope): bo
return false;
}

/**
* @param \PhpParser\Node\Expr $functionCall
* @param \PHPStan\Analyser\Scope $scope
* @return null|\ReflectionParameter[]
*/
private function findParametersInFunctionCall(Expr $functionCall, Scope $scope)
{
if ($functionCall instanceof FuncCall && $functionCall->name instanceof Name) {
if ($this->broker->hasFunction($functionCall->name, $scope)) {
return $this->broker->getFunction($functionCall->name, $scope)->getNativeReflection()->getParameters();
}
} elseif ($functionCall instanceof MethodCall && is_string($functionCall->name)) {
$type = $scope->getType($functionCall->var);
if ($type->getClass() !== null && $this->broker->hasClass($type->getClass())) {
$classReflection = $this->broker->getClass($type->getClass())->getNativeReflection();
$methodName = $functionCall->name;
if ($classReflection->hasMethod($methodName)) {
return $classReflection->getMethod($methodName)->getParameters();
}
}
}

return null;
}

}
48 changes: 26 additions & 22 deletions src/Analyser/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Analyser;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Cast\Bool_;
use PhpParser\Node\Expr\Cast\Double;
Expand Down Expand Up @@ -91,9 +92,9 @@ class Scope
private $anonymousClass;

/**
* @var string|null
* @var \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|null
*/
private $inFunctionCallName;
private $inFunctionCall;

/**
* @var \PHPStan\Type\Type[]
Expand All @@ -116,7 +117,7 @@ public function __construct(
array $variablesTypes = [],
bool $inClosureBind = false,
ClassReflection $anonymousClass = null,
string $inFunctionCallName = null,
Expr $inFunctionCall = null,
array $moreSpecificTypes = [],
array $currentlyAssignedVariables = []
)
Expand All @@ -143,7 +144,7 @@ public function __construct(
$this->variableTypes = $variablesTypes;
$this->inClosureBind = $inClosureBind;
$this->anonymousClass = $anonymousClass;
$this->inFunctionCallName = $inFunctionCallName;
$this->inFunctionCall = $inFunctionCall;
$this->moreSpecificTypes = $moreSpecificTypes;
$this->currentlyAssignedVariables = $currentlyAssignedVariables;
}
Expand Down Expand Up @@ -230,11 +231,11 @@ public function getAnonymousClass(): ClassReflection
}

/**
* @return string|null
* @return \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|null
*/
public function getInFunctionCallName()
public function getInFunctionCall()
{
return $this->inFunctionCallName;
return $this->inFunctionCall;
}

public function getType(Node $node): Type
Expand Down Expand Up @@ -451,12 +452,11 @@ public function getType(Node $node): Type
}

if ($node instanceof FuncCall && $node->name instanceof Name) {
$functionName = (string) $node->name;
if (!$this->broker->hasFunction($functionName)) {
if (!$this->broker->hasFunction($node->name, $this)) {
return new MixedType(true);
}

return $this->broker->getFunction($functionName)->getReturnType();
return $this->broker->getFunction($node->name, $this)->getReturnType();
}

return new MixedType(false);
Expand Down Expand Up @@ -532,7 +532,7 @@ public function enterClosureBind(): self
$this->getVariableTypes(),
true,
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName(),
$this->getInFunctionCall(),
$this->moreSpecificTypes
);
}
Expand All @@ -552,7 +552,7 @@ public function enterAnonymousClass(ClassReflection $anonymousClass): self
],
$this->isInClosureBind(),
$anonymousClass,
$this->getInFunctionCallName()
$this->getInFunctionCall()
);
}

Expand Down Expand Up @@ -622,7 +622,7 @@ public function enterAnonymousFunction(array $parameters, array $uses): self
$variableTypes,
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName()
$this->getInFunctionCall()
);
}

Expand Down Expand Up @@ -671,7 +671,11 @@ public function enterCatch(string $exceptionClassName, string $variableName): se
);
}

public function enterFunctionCall(string $functionName): self
/**
* @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall $functionCall
* @return self
*/
public function enterFunctionCall($functionCall): self
{
return new self(
$this->broker,
Expand All @@ -684,7 +688,7 @@ public function enterFunctionCall(string $functionName): self
$this->getVariableTypes(),
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$functionName,
$functionCall,
$this->moreSpecificTypes
);
}
Expand All @@ -705,7 +709,7 @@ public function enterVariableAssign(string $variableName): self
$this->getVariableTypes(),
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName(),
$this->getInFunctionCall(),
$this->moreSpecificTypes,
$currentlyAssignedVariables
);
Expand Down Expand Up @@ -737,7 +741,7 @@ public function assignVariable(
$variableTypes,
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName(),
$this->getInFunctionCall(),
$this->moreSpecificTypes
);
}
Expand All @@ -759,7 +763,7 @@ public function unsetVariable(string $variableName): self
$variableTypes,
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName(),
$this->getInFunctionCall(),
$this->moreSpecificTypes
);
}
Expand Down Expand Up @@ -788,7 +792,7 @@ public function intersectVariables(Scope $otherScope): self
$intersectedVariableTypes,
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName(),
$this->getInFunctionCall(),
$this->moreSpecificTypes
);
}
Expand All @@ -811,7 +815,7 @@ public function addVariables(Scope $otherScope): self
$variableTypes,
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName(),
$this->getInFunctionCall(),
$this->moreSpecificTypes
);
}
Expand All @@ -835,7 +839,7 @@ public function specifyObjectType(Node $expr, string $className): self
$variableTypes,
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName(),
$this->getInFunctionCall(),
$this->moreSpecificTypes
);
}
Expand Down Expand Up @@ -874,7 +878,7 @@ private function addMoreSpecificTypes(array $types): self
$this->getVariableTypes(),
$this->isInClosureBind(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCallName(),
$this->getInFunctionCall(),
$moreSpecificTypes
);
}
Expand Down
34 changes: 29 additions & 5 deletions src/Broker/Broker.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Broker;

use PHPStan\Analyser\Scope;
use PHPStan\Reflection\BrokerAwareClassReflectionExtension;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionReflectionFactory;
Expand Down Expand Up @@ -110,10 +111,11 @@ public function hasClass(string $className): bool
}
}

public function getFunction(string $functionName): \PHPStan\Reflection\FunctionReflection
public function getFunction(\PhpParser\Node\Name $nameNode, Scope $scope): \PHPStan\Reflection\FunctionReflection
{
if (!$this->hasFunction($functionName)) {
throw new \PHPStan\Broker\FunctionNotFoundException($functionName);
$functionName = $this->resolveFunctionName($nameNode, $scope);
if ($functionName === null) {
throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode);
}

$lowerCasedFunctionName = strtolower($functionName);
Expand All @@ -124,9 +126,31 @@ public function getFunction(string $functionName): \PHPStan\Reflection\FunctionR
return $this->functionReflections[$lowerCasedFunctionName];
}

public function hasFunction(string $functionName): bool
public function hasFunction(\PhpParser\Node\Name $nameNode, Scope $scope): bool
{
return function_exists($functionName);
return $this->resolveFunctionName($nameNode, $scope) !== null;
}

/**
* @param \PhpParser\Node\Name $nameNode
* @param \PHPStan\Analyser\Scope $scope
* @return string|null
*/
public function resolveFunctionName(\PhpParser\Node\Name $nameNode, Scope $scope)
{
$name = (string) $nameNode;
if ($scope->getNamespace() !== null && !$nameNode->isFullyQualified()) {
$namespacedName = sprintf('%s\\%s', $scope->getNamespace(), $name);
if (function_exists($namespacedName)) {
return $namespacedName;
}
}

if (function_exists($name)) {
return $name;
}

return null;
}

}

0 comments on commit 05498a5

Please sign in to comment.