Skip to content

Commit

Permalink
[Php56] Remove parent attribute usage on UndefinedVariableResolver (#…
Browse files Browse the repository at this point in the history
…4147)

* [Php56] Remove parent attribute usage on UndefinedVariableResolver

* clean up
  • Loading branch information
samsonasik committed Jun 10, 2023
1 parent 5ba083c commit 6758a5e
Showing 1 changed file with 82 additions and 116 deletions.
198 changes: 82 additions & 116 deletions rules/Php56/NodeAnalyzer/UndefinedVariableResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\AssignOp\Coalesce as AssignOpCoalesce;
use PhpParser\Node\Expr\AssignRef;
use PhpParser\Node\Expr\BinaryOp\Coalesce;
Expand All @@ -23,7 +23,6 @@
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\Unset_;
use PhpParser\NodeTraverser;
use PHPStan\Analyser\Scope;
Expand Down Expand Up @@ -67,17 +66,21 @@ public function resolve(ClassMethod | Function_ | Closure $node): array
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}

$checkedVariables = $this->resolveCheckedVariables($node, $checkedVariables);
if ($node instanceof Case_) {
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}

if (! $node instanceof Variable) {
return null;
}

$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
if (! $parentNode instanceof Node) {
if ($node->getAttribute(AttributeKey::IS_BEING_ASSIGNED) === true) {
return null;
}

$variableName = (string) $this->nodeNameResolver->getName($node);
if ($this->shouldSkipVariable($node, $variableName, $checkedVariables, $parentNode)) {
if ($this->shouldSkipVariable($node, $variableName, $checkedVariables)) {
return null;
}

Expand All @@ -94,78 +97,105 @@ public function resolve(ClassMethod | Function_ | Closure $node): array
return array_unique($undefinedVariables);
}

private function hasVariableTypeOrCurrentStmtUnreachable(Variable $variable, ?string $variableName): bool
/**
* @param string[] $checkedVariables
* @return string[]
*/
private function resolveCheckedVariables(Node $node, array $checkedVariables): array
{
if (! is_string($variableName)) {
return true;
if ($node instanceof Empty_ && $node->expr instanceof Variable) {
$checkedVariables[] = (string) $this->nodeNameResolver->getName($node->expr);
return $checkedVariables;
}

// defined 100 %
/** @var Scope $scope */
$scope = $variable->getAttribute(AttributeKey::SCOPE);
if ($scope->hasVariableType($variableName)->yes()) {
return true;
if ($node instanceof Isset_ || $node instanceof Unset_) {
return $this->resolveCheckedVariablesFromIssetOrUnset($node, $checkedVariables);
}

$currentStmt = $this->betterNodeFinder->resolveCurrentStatement($variable);
return $currentStmt instanceof Stmt && $currentStmt->getAttribute(AttributeKey::IS_UNREACHABLE) === true;
}

private function shouldSkipWithParent(Node $parentNode): bool
{
if (in_array($parentNode::class, [Unset_::class, UnsetCast::class, Isset_::class, Empty_::class], true)) {
return true;
if ($node instanceof UnsetCast && $node->expr instanceof Variable) {
$checkedVariables[] = (string) $this->nodeNameResolver->getName($node->expr);
return $checkedVariables;
}

// when parent Node origNode is null, it means parent Node just reprinted, so it can't be verified
// so skip it
return ! $parentNode->getAttribute(AttributeKey::ORIGINAL_NODE) instanceof Node;
}
if ($node instanceof Coalesce && $node->left instanceof Variable) {
$checkedVariables[] = (string) $this->nodeNameResolver->getName($node->left);
return $checkedVariables;
}

private function isAsCoalesceLeftOrAssignOpCoalesceVar(Node $parentNode, Variable $variable): bool
{
if ($parentNode instanceof Coalesce && $parentNode->left === $variable) {
return true;
if ($node instanceof AssignOpCoalesce && $node->var instanceof Variable) {
$checkedVariables[] = (string) $this->nodeNameResolver->getName($node->var);
return $checkedVariables;
}

if (! $parentNode instanceof AssignOpCoalesce) {
return false;
if ($node instanceof AssignRef && $node->var instanceof Variable) {
$checkedVariables[] = (string) $this->nodeNameResolver->getName($node->var);
}

return $parentNode->var === $variable;
return $this->resolveCheckedVariablesFromArrayOrList($node, $checkedVariables);
}

private function isAssign(Node $parentNode): bool
/**
* @param string[] $checkedVariables
* @return string[]
*/
private function resolveCheckedVariablesFromIssetOrUnset(Isset_|Unset_ $node, array $checkedVariables): array
{
return in_array($parentNode::class, [Assign::class, AssignRef::class], true);
foreach ($node->vars as $expr) {
if ($expr instanceof Variable) {
$checkedVariables[] = (string) $this->nodeNameResolver->getName($expr);
}
}

return $checkedVariables;
}

/**
* @param string[] $checkedVariables
* @return string[]
*/
private function shouldSkipVariable(
Variable $variable,
string $variableName,
array &$checkedVariables,
Node $parentNode
): bool {
if ($this->isAsCoalesceLeftOrAssignOpCoalesceVar($parentNode, $variable)) {
return true;
private function resolveCheckedVariablesFromArrayOrList(Node $node, array $checkedVariables): array
{
if (! $node instanceof Array_ && ! $node instanceof List_) {
return $checkedVariables;
}

if ($this->isAssign($parentNode)) {
return true;
foreach ($node->items as $item) {
if (! $item instanceof ArrayItem) {
continue;
}

if (! $item->value instanceof Variable) {
continue;
}

$checkedVariables[] = (string) $this->nodeNameResolver->getName($item->value);
}

if ($this->shouldSkipWithParent($parentNode)) {
return $checkedVariables;
}

private function hasVariableTypeOrCurrentStmtUnreachable(Variable $variable, ?string $variableName): bool
{
if (! is_string($variableName)) {
return true;
}

// list() = | [$values] = defines variables as null
if ($this->isListAssign($parentNode)) {
// defined 100 %
/** @var Scope $scope */
$scope = $variable->getAttribute(AttributeKey::SCOPE);
if ($scope->hasVariableType($variableName)->yes()) {
return true;
}

$currentStmt = $this->betterNodeFinder->resolveCurrentStatement($variable);
return $currentStmt instanceof Stmt && $currentStmt->getAttribute(AttributeKey::IS_UNREACHABLE) === true;
}

/**
* @param string[] $checkedVariables
*/
private function shouldSkipVariable(Variable $variable, string $variableName, array &$checkedVariables): bool
{
$variableName = $this->nodeNameResolver->getName($variable);

// skip $this, as probably in outer scope
Expand All @@ -185,32 +215,12 @@ private function shouldSkipVariable(
return true;
}

if (in_array($variableName, $checkedVariables, true)) {
return true;
}

$checkedVariables[] = $variableName;
if ($this->hasPreviousCheckedWithIsset($variable)) {
return true;
}
$checkedVariables = array_filter(
$checkedVariables,
static fn (string $variableName): bool => $variableName !== ''
);

if ($this->hasPreviousCheckedWithEmpty($variable)) {
return true;
}

return $this->isAfterSwitchCaseWithParentCase($variable);
}

private function isAfterSwitchCaseWithParentCase(Variable $variable): bool
{
$previousSwitch = $this->betterNodeFinder->findFirstPreviousOfTypes($variable, [Switch_::class]);

if (! $previousSwitch instanceof Switch_) {
return false;
}

$parentNode = $previousSwitch->getAttribute(AttributeKey::PARENT_NODE);
return $parentNode instanceof Case_;
return in_array($variableName, $checkedVariables, true);
}

private function isDifferentWithOriginalNodeOrNoScope(Variable $variable): bool
Expand All @@ -223,48 +233,4 @@ private function isDifferentWithOriginalNodeOrNoScope(Variable $variable): bool
$nodeScope = $variable->getAttribute(AttributeKey::SCOPE);
return ! $nodeScope instanceof Scope;
}

private function hasPreviousCheckedWithIsset(Variable $variable): bool
{
return (bool) $this->betterNodeFinder->findFirstPrevious($variable, function (Node $subNode) use (
$variable
): bool {
if (! $subNode instanceof Isset_) {
return false;
}

$vars = $subNode->vars;
foreach ($vars as $var) {
if ($this->nodeComparator->areNodesEqual($variable, $var)) {
return true;
}
}

return false;
});
}

private function hasPreviousCheckedWithEmpty(Variable $variable): bool
{
return (bool) $this->betterNodeFinder->findFirstPrevious($variable, function (Node $subNode) use (
$variable
): bool {
if (! $subNode instanceof Empty_) {
return false;
}

$subNodeExpr = $subNode->expr;
return $this->nodeComparator->areNodesEqual($subNodeExpr, $variable);
});
}

private function isListAssign(Node $node): bool
{
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
if ($parentNode instanceof List_) {
return true;
}

return $parentNode instanceof Array_;
}
}

0 comments on commit 6758a5e

Please sign in to comment.