public function equals(LatticeElementInterface $that) { if (!$that instanceof LinkedFlowScope) { return false; } if ($this->optimize() === $that->optimize()) { return true; } // If two flow scopes are in the same function, then they could have two possible function scopes: // 1) the real function scope, or // 2) the BOTTOM scope. // If they have different function scopes, we theoretically should iterate through all the variable in each // scope and compare. However, 99.9% of the time, they're not equal. The other 0.1% of the time, we can just // assume that they are equal. Eventually, it just means that data flow analysis has to propagate the entry // lattice a bit further than it really needs to, but the end result is not affected. if ($this->getFunctionScope() !== $that->getFunctionScope()) { return false; } if ($this->cache === $that->cache) { // If the two flow scopes have the same cache, then we can check equality a lot faster: by just looking at // the "dirty" elements in the cache, and comparing them in both scopes. foreach ($this->cache->dirtySymbols as $name) { if ($this->diffSlots($this->getSlot($name), $that->getSlot($name))) { return false; } } return true; } $myFlowSlots = $this->allFlowSlots(); $otherFlowSlots = $that->allFlowSlots(); foreach ($myFlowSlots as $name => $slot) { if ($this->diffSlots($slot, isset($otherFlowSlots[$name]) ? $otherFlowSlots[$name] : null)) { return false; } unset($otherFlowSlots[$name]); } foreach ($otherFlowSlots as $name => $slot) { if ($this->diffSlots($slot, isset($myFlowSlots[$name]) ? $myFlowSlots[$name] : null)) { return false; } } return true; }
/** * Calculates the out lattices for the different branches. * * Right now, we just treat ON_EX edges like UNCOND edges. If we want to be perfect, we would have to actually join * all the out lattices of this flow with the in lattice, and then make that the out lattice for the ON_EX edge. * However, that would add some extra computation for an edge case. So, the current behavior looks like a "good enough" * approximation. * * @param \PHPParser_Node $source * @param LatticeElementInterface $input * * @return array<LatticeElementInterface> */ protected function branchedFlowThrough($source, LatticeElementInterface $input) { assert($source instanceof \PHPParser_Node); assert($input instanceof LinkedFlowScope); // If we have not walked a path from the entry node to this node, we cannot infer anything // about this scope. So, just skip it for now, we will come back later. if ($input === $this->bottomScope) { $output = $input; } else { $output = $input->createChildFlowScope(); $output = $this->traverse($source, $output); } $condition = $conditionFlowScope = $conditionOutcomes = null; $result = array(); foreach ($this->cfg->getOutEdges($source) as $branchEdge) { $branch = $branchEdge->getType(); $newScope = $output; switch ($branch) { case GraphEdge::TYPE_ON_TRUE: if ($source instanceof \PHPParser_Node_Stmt_Foreach) { $informed = $this->traverse($source->expr, $output->createChildFlowScope()); $exprType = $this->getType($source->expr); if (null !== $source->keyVar && $source->keyVar instanceof \PHPParser_Node_Expr_Variable) { $keyType = null; if ($exprType->isArrayType()) { $keyType = $exprType->toMaybeArrayType()->getKeyType(); } // If we do not have the precise key type available, we can always // assume it to be either a string, or an integer. $this->redeclareSimpleVar($informed, $source->keyVar, $keyType ?: $this->typeRegistry->getNativeType('generic_array_key')); } $valueType = $this->typeRegistry->getNativeType('unknown'); if ($exprType->isArrayType()) { $valueType = $exprType->toMaybeArrayType()->getElementType(); } else { if ($exprType->toMaybeObjectType()) { $valueType = $exprType->toMaybeObjectType()->getTraversableElementType(); } } $source->valueVar->setAttribute('type', $valueType); $this->redeclareSimpleVar($informed, $source->valueVar, $valueType); $newScope = $informed; break; } // FALL THROUGH // FALL THROUGH case GraphEdge::TYPE_ON_FALSE: if (null === $condition) { $condition = NodeUtil::getConditionExpression($source); if (null === $condition && $source instanceof \PHPParser_Node_Stmt_Case && null !== $source->cond) { $condition = $source; // conditionFlowScope is cached from previous iterations of the loop if (null === $conditionFlowScope) { $conditionFlowScope = $this->traverse($source->cond, $output->createChildFlowScope()); } } } if (null !== $condition) { if ($condition instanceof \PHPParser_Node_Expr_BooleanAnd || $condition instanceof \PHPParser_Node_Expr_LogicalAnd || $condition instanceof \PHPParser_Node_Expr_BooleanOr || $condition instanceof \PHPParser_Node_Expr_LogicalOr) { // When handling the short-circuiting binary operators, the outcome scope on true can be // different than the outcome scope on false. // // TODO: // The "right" way to do this is to carry the known outcome all the way through the // recursive traversal, so that we can construct a different flow scope based on the outcome. // However, this would require a bunch of code and a bunch of extra computation for an edge // case. This seems to be a "good enough" approximation. // conditionOutcomes is cached from previous iterations of the loop. if (null === $conditionOutcomes) { $conditionOutcomes = $condition instanceof \PHPParser_Node_Expr_BooleanAnd || $condition instanceof \PHPParser_Node_Expr_LogicalAnd ? $this->traverseAnd($condition, $output->createChildFlowScope()) : $this->traverseOr($condition, $output->createChildFlowScope()); } $newScope = $this->reverseInterpreter->getPreciserScopeKnowingConditionOutcome($condition, $conditionOutcomes->getOutcomeFlowScope($condition, $branch === GraphEdge::TYPE_ON_TRUE), $branch === GraphEdge::TYPE_ON_TRUE); } else { // conditionFlowScope is cached from previous iterations of the loop. if (null === $conditionFlowScope) { // In case of a FOR loop, $condition might be an array of expressions. PHP will execute // all expressions, but only the last one is used to determine the expression outcome. if (is_array($condition)) { $conditionFlowScope = $output->createChildFlowScope(); foreach ($condition as $cond) { $conditionFlowScope = $this->traverse($cond, $conditionFlowScope); } } else { $conditionFlowScope = $this->traverse($condition, $output->createChildFlowScope()); } } // See above comment regarding the handling of FOR loops. $relevantCondition = $condition; if (is_array($condition)) { $relevantCondition = end($condition); } $newScope = $this->reverseInterpreter->getPreciserScopeKnowingConditionOutcome($relevantCondition, $conditionFlowScope, $branch === GraphEdge::TYPE_ON_TRUE); } } break; } if (null === $newScope) { throw new \LogicException('$newScope must not be null for source ' . get_class($source) . ' and branch ' . GraphEdge::getLiteral($branch) . '.'); } $result[] = $newScope->optimize(); } return $result; }
/** * @dataProvider getEqualsTests */ public function testEquals(LatticeElementInterface $a, LatticeElementInterface $b, $expectedOutcome) { $this->assertSame($expectedOutcome, $a->equals($b)); }