public function serialize(ControlFlowGraph $graph) { $dot = "digraph G {\n"; $this->ids = new \SplObjectStorage(); $this->idCount = 0; $dot .= " " . $this->getId($graph->getImplicitReturn()) . " [shape=box,label=\"implicit return\",style=filled]\n"; $entryPoint = $graph->getEntryPoint(); $nodes = $graph->getNodes(); foreach ($nodes as $astNode) { $node = $nodes[$astNode]; $id = $this->getId($node); $dot .= sprintf(' %s [shape=box,label="%s"', $id, str_replace('"', '\\"', NodeUtil::getStringRepr($astNode))); if ($node === $entryPoint) { $dot .= ",style=filled"; } $dot .= "]\n"; foreach ($node->getOutEdges() as $edge) { $dot .= sprintf(" %s -> %s", $id, $this->getId($edge->getDest())); switch ($edge->getType()) { case GraphEdge::TYPE_ON_FALSE: $dot .= ' [label="false"]'; break; case GraphEdge::TYPE_ON_TRUE: $dot .= ' [label="true"]'; break; case GraphEdge::TYPE_ON_EX: $dot .= ' [label="exception"]'; break; } $dot .= "\n"; } } $dot .= "}"; return $dot; }
public function visit(NodeTraversal $t, \PHPParser_Node $node, \PHPParser_Node $parent = null) { // Consider only non-dynamic variables in this pass. if (!$node instanceof \PHPParser_Node_Expr_Variable || !is_string($node->name)) { return; } // We ignore PHP globals. if (NodeUtil::isSuperGlobal($node)) { return; } // Ignore variables which are defined here. if ($parent instanceof \PHPParser_Node_Expr_Assign && $parent->var === $node) { return; } if (!$t->getScope()->isDeclared($node->name)) { if ($bestName = CheckForTyposPass::getMostSimilarName($node->name, $t->getScope()->getVarNames())) { $this->phpFile->addComment($node->getLine(), Comment::error('typos.mispelled_variable_name', 'The variable ``%offending_variable_name%`` does not exist. Did you mean ``%closest_variable_name%``?', array('offending_variable_name' => '$' . $node->name, 'closest_variable_name' => '$' . $bestName))); } else { $this->phpFile->addComment($node->getLine(), Comment::warning('usage.undeclared_variable', 'The variable ``%variable_name%`` does not exist. Did you forget to declare it?', array('variable_name' => '$' . $node->name))); } return; } if ($parent instanceof \PHPParser_Node_Expr_ArrayDimFetch && $node->getAttribute('array_initializing_variable') && $parent->var === $node) { $this->phpFile->addComment($node->getLine(), Comment::warning('usage.non_initialized_array', '``%array_fetch_expr%`` was never initialized. Although not strictly required by PHP, it is generally a good practice to add ``%array_fetch_expr% = array();`` before regardless.', array('array_fetch_expr' => self::$prettyPrinter->prettyPrintExpr($node)))); } }
protected function analyzeStream() { $this->newContent = ''; $getNameType = $this->registry->createUnionType(array($this->registry->getClassOrCreate('ReflectionZendExtension'), $this->registry->getClassOrCreate('ReflectionExtension'), $this->registry->getClassOrCreate('ReflectionFunction'), $this->registry->getClassOrCreate('ReflectionParameter'), $this->registry->getClassOrCreate('ReflectionClass'), $reflectionMethodType = $this->registry->getClassOrCreate('ReflectionMethod'))); $reflectionPropertyType = $this->registry->getClassOrCreate('ReflectionProperty'); $lastNode = null; while ($this->stream->moveNext()) { switch (true) { case $this->stream->node instanceof \PHPParser_Node_Expr_MethodCall && $lastNode !== $this->stream->node: if (NodeUtil::isCallToMethod($this->stream->node, 'getName') && null !== ($type = $this->stream->node->var->getAttribute('type')) && $this->isSubtype($type, $getNameType)) { $this->newContent .= '->name'; $this->stream->skipUntil('END_OF_CALL'); break; } if (NodeUtil::isCallToMethod($this->stream->node, 'getDeclaringClass') && null !== ($type = $this->stream->node->var->getAttribute('type')) && ($this->isSubtype($type, $reflectionMethodType) || $this->isSubtype($type, $reflectionPropertyType)) && ($parent = $this->stream->node->getAttribute('parent')) && NodeUtil::isCallToMethod($parent, 'getName')) { $this->newContent .= '->class'; $this->stream->skipUntil('END_OF_CALL'); $this->stream->skipUntil('END_OF_CALL'); break; } $this->newContent .= $this->stream->token->getContent(); break; default: $this->newContent .= $this->stream->token->getContent(); } $lastNode = $this->stream->node; } }
public function visit(NodeTraversal $t, \PHPParser_Node $n, \PHPParser_Node $parent = null) { if ($n instanceof \PHPParser_Node_Stmt_Property) { $containerNode = NodeUtil::getContainer($n)->get(); $class = $this->typeRegistry->getClassByNode($containerNode); if ($class->isInterface()) { $this->phpFile->addComment($n->getLine(), Comment::error('basic_semantics.property_on_interface', 'In PHP, declaring a method on an interface is not yet allowed.')); } } if (NodeUtil::isMethodContainer($n)) { $class = $this->typeRegistry->getClassByNode($n); if ($class instanceof Clazz && !$class->isAbstract()) { $abstractMethods = array(); foreach ($class->getMethods() as $method) { assert($method instanceof ClassMethod); /** @var $method ClassMethod */ if ($method->isAbstract()) { $abstractMethods[] = $method->getMethod()->getName(); } } if (!empty($abstractMethods)) { switch (count($abstractMethods)) { case 1: $this->phpFile->addComment($n->getLine(), Comment::error('basic_semantics.abstract_method_on_non_abstract_class', 'There is one abstract method ``%method%`` in this class; you could implement it, or declare this class as abstract.', array('method' => reset($abstractMethods)))); break; default: $this->phpFile->addComment($n->getLine(), Comment::error('basic_semantics.abstract_methods_on_non_abstract_class', 'There is at least one abstract method in this class. Maybe declare it as abstract, or implement the remaining methods: %methods%', array('methods' => implode(', ', $abstractMethods)))); } } } } }
public function shouldTraverse(NodeTraversal $traversal, \PHPParser_Node $node, \PHPParser_Node $parent = null) { if (NodeUtil::isScopeCreator($node) && null !== $parent) { if ($node instanceof \PHPParser_Node_Expr_Closure) { $node->setAttribute('type', $this->typeRegistry->getClassOrCreate('Closure')); } return false; } return true; }
public function visit(NodeTraversal $t, \PHPParser_Node $node, \PHPParser_Node $parent = null) { if (!NodeUtil::isCallLike($node)) { return; } if (null === ($function = $this->typeRegistry->getCalledFunctionByNode($node))) { return; } foreach ($function->getParameters() as $param) { $index = $param->getIndex(); if (!isset($node->args[$index])) { continue; } $node->args[$index]->setAttribute('param_expects_ref', $param->isPassedByRef()); } }
private function getThisType(\PHPParser_Node $node, Scope $parentScope = null) { $parent = $node; while (null !== $parent) { // As of PHP 5.4, closures inherit the this type of their parent scope. Since there is currently no way to // configure the PHP version that we build against, we will simply always infer the ThisType. Other passes, // can then perform checks whether ``$this`` may be accessed, allowing us to produce better error messages. if ($parent instanceof \PHPParser_Node_Expr_Closure) { if (null === $parentScope) { return null; } return $parentScope->getTypeOfThis(); } if (NodeUtil::isMethodContainer($parent)) { $name = implode("\\", $parent->namespacedName->parts); return $this->typeRegistry->getClassOrCreate($name); } $parent = $parent->getAttribute('parent'); } return null; }
public function visit(NodeTraversal $t, \PHPParser_Node $node, \PHPParser_Node $parent = null) { if (!NodeUtil::isCallLike($node)) { return; } if (null === ($function = $this->typeRegistry->getCalledFunctionByNode($node))) { return; } foreach ($function->getParameters() as $param) { if (!$param->isPassedByRef()) { continue; } $index = $param->getIndex(); if (!isset($node->args[$index])) { continue; } $arg = $node->args[$index]; if (!NodeUtil::canBePassedByRef($arg->value)) { $this->phpFile->addComment($arg->getLine(), Comment::error('usage.non_referencable_arg', '``%argument%`` cannot be passed to ``%function_name%()`` as the parameter ``$%parameter_name%`` expects a reference.', array('argument' => self::$prettyPrinter->prettyPrintExpr($arg->value), 'function_name' => $function->getName(), 'parameter_name' => $param->getName()))); } } }
/** * Computes the GEN and KILL set. * * @param boolean $conditional */ private function computeGenKill(\PHPParser_Node $node, BitSet $gen, BitSet $kill, $conditional) { switch (true) { case $node instanceof \PHPParser_Node_Expr_Closure: foreach ($node->uses as $use) { assert($use instanceof \PHPParser_Node_Expr_ClosureUse); $this->addToSetIfLocal($use->var, $gen); } return; case $node instanceof BlockNode: case NodeUtil::isScopeCreator($node): return; case $node instanceof \PHPParser_Node_Stmt_While: case $node instanceof \PHPParser_Node_Stmt_Do: case $node instanceof \PHPParser_Node_Stmt_If: $this->computeGenKill($node->cond, $gen, $kill, $conditional); return; case $node instanceof \PHPParser_Node_Stmt_Foreach: if (null !== $node->keyVar) { if ($node->keyVar instanceof \PHPParser_Node_Expr_Variable) { $this->addToSetIfLocal($node->keyVar, $kill); $this->addToSetIfLocal($node->keyVar, $gen); } else { $this->computeGenKill($node->keyVar, $gen, $kill, $conditional); } } if ($node->valueVar instanceof \PHPParser_Node_Expr_Variable) { $this->addToSetIfLocal($node->valueVar, $kill); $this->addToSetIfLocal($node->valueVar, $gen); } else { $this->computeGenKill($node->valueVar, $gen, $kill, $conditional); } $this->computeGenKill($node->expr, $gen, $kill, $conditional); return; case $node instanceof \PHPParser_Node_Stmt_For: foreach ($node->cond as $cond) { $this->computeGenKill($cond, $gen, $kill, $conditional); } return; case $node instanceof \PHPParser_Node_Expr_BooleanAnd: case $node instanceof \PHPParser_Node_Expr_BooleanOr: $this->computeGenKill($node->left, $gen, $kill, $conditional); // May short circuit. $this->computeGenKill($node->right, $gen, $kill, true); return; case $node instanceof \PHPParser_Node_Expr_Ternary: $this->computeGenKill($node->cond, $gen, $kill, $conditional); // Assume both sides are conditional. if (null !== $node->if) { $this->computeGenKill($node->if, $gen, $kill, true); } $this->computeGenKill($node->else, $gen, $kill, true); return; case $node instanceof \PHPParser_Node_Param: $this->markAllParametersEscaped(); return; case $node instanceof \PHPParser_Node_Expr_Variable: if (!is_string($node->name)) { $this->addAllToSetIfLocal($gen); return; } $this->addToSetIfLocal($node->name, $gen); return; case $node instanceof \PHPParser_Node_Expr_Include: $this->computeGenKill($node->expr, $gen, $kill, $conditional); if ($node->type === \PHPParser_Node_Expr_Include::TYPE_INCLUDE || $node->type === \PHPParser_Node_Expr_Include::TYPE_REQUIRE) { $this->addAllToSetIfLocal($gen); } return; case NodeUtil::isMaybeFunctionCall($node, 'get_defined_vars'): $this->addAllToSetIfLocal($gen); break; case NodeUtil::isMaybeFunctionCall($node, 'compact'): foreach ($node->args as $arg) { $this->computeGenKill($arg, $gen, $kill, $conditional); } $varNames = array(); $isDynamic = false; foreach ($node->args as $arg) { if ($arg->value instanceof \PHPParser_Node_Scalar_String) { $varNames[] = $arg->value->value; continue; } if ($arg->value instanceof \PHPParser_Node_Expr_Array) { foreach ($arg->value->items as $item) { assert($item instanceof \PHPParser_Node_Expr_ArrayItem); if ($item->value instanceof \PHPParser_Node_Scalar_String) { $varNames[] = $item->value->value; continue; } $isDynamic = true; break 2; } continue; } $isDynamic = true; break; } $this->addAllToSetIfLocal($gen, $isDynamic ? null : $varNames); return; case $node instanceof \PHPParser_Node_Expr_AssignList: foreach ($node->vars as $var) { if (!$var instanceof \PHPParser_Node_Expr_Variable) { continue; } if (!$conditional) { $this->addToSetIfLocal($var->name, $kill); } $this->addToSetIfLocal($var->name, $gen); } $this->computeGenKill($node->expr, $gen, $kill, $conditional); return; // AssignList is already handled in the previous CASE block. // AssignList is already handled in the previous CASE block. case NodeUtil::isAssignmentOp($node) && $node->var instanceof \PHPParser_Node_Expr_Variable: if ($node->var->name instanceof \PHPParser_Node_Expr) { $this->computeGenKill($node->var->name, $gen, $kill, $conditional); } if (!$conditional) { $this->addToSetIfLocal($node->var->name, $kill); } if (!$node instanceof \PHPParser_Node_Expr_Assign) { // Assignments such as a += 1 reads a first. $this->addToSetIfLocal($node->var->name, $gen); } $this->computeGenKill($node->expr, $gen, $kill, $conditional); return; default: foreach ($node as $subNode) { if (is_array($subNode)) { foreach ($subNode as $aSubNode) { if (!$aSubNode instanceof \PHPParser_Node) { continue; } $this->computeGenKill($aSubNode, $gen, $kill, $conditional); } continue; } else { if (!$subNode instanceof \PHPParser_Node) { continue; } } $this->computeGenKill($subNode, $gen, $kill, $conditional); } } }
private function addToDefIfLocal($name, \PHPParser_Node $node = null, \PHPParser_Node $rValue = null, DefinitionLattice $definitions) { $var = $this->scope->getVar($name); if (null === $var) { return; } // Invalidate other definitions if they depended on this variable as it has been changed. foreach ($definitions as $otherVar) { if (null === ($otherDef = $definitions[$otherVar])) { continue; } if ($otherDef->dependsOn($var)) { $definitions[$otherVar] = null; } } // The node can be null if we are dealing with a conditional definition. For conditional // definitions, this analysis cannot do much. if (null === $node) { $definitions[$var] = null; return; } $definition = new Definition($node, $rValue); if (null !== $rValue) { NodeTraversal::traverseWithCallback($rValue, new PreOrderCallback(function ($t, \PHPParser_Node $node) use($definition) { if (NodeUtil::isScopeCreator($node)) { return false; } if (!NodeUtil::isName($node)) { return; } if (null === ($var = $this->scope->getVar($node->name))) { if (null !== $this->logger) { $this->logger->debug(sprintf('Could not find variable "%s" in the current scope. ' . 'This could imply an error in SyntacticScopeCreator.', $node->name)); } // We simply assume that the variable was declared so that we can properly // invalidate the definition if it is changed. For this analysis, it does not // matter whether it actually exists. A later pass will add a warning anyway. $var = $this->scope->declareVar($node->name); } $definition->addDependentVar($var); })); } $definitions[$var] = $definition; }
private function narrowMaybeConditionScope(\PHPParser_Node $expr, LinkedFlowScope $blindScope, $condition) { // In contrast to a non-short-circuiting condition scope, for may-be short-circuiting scopes, // we make the simplification to not narrow down the expression, and the defining expression // by always using the defining expression in case the variable is of boolean type and never // if it has a different type. if (NodeUtil::isName($expr) && null !== ($defExpr = $expr->getAttribute('defining_expr')) && null !== ($exprType = $expr->getAttribute('type')) && $exprType->isBooleanType()) { return $this->firstPreciserScopeKnowingConditionOutcome($defExpr, $blindScope, $condition); } return $this->firstPreciserScopeKnowingConditionOutcome($expr, $blindScope, $condition); }
private function enterNode(\PHPParser_Node $node) { if (NodeUtil::isMethodContainer($node)) { $this->commentParser->setCurrentClassName(implode("\\", $node->namespacedName->parts)); $this->commentParser->setImportedNamespaces($this->importedNamespaces); $this->classParser->setImportedNamespaces($this->importedNamespaces); $class = $this->classParser->parse($node); $this->classFiles[$class] = $this->phpFile; if ($this->typeRegistry->hasClass($class->getName(), TypeRegistry::LOOKUP_NO_CACHE)) { $this->analyzer->logger->warning(sprintf('The class "%s" has been defined more than once (maybe as a fixture for code generation). Ignoring the second definition.', $class->getName())); return; } $this->typeRegistry->registerClass($class); } else { if ($node instanceof \PHPParser_Node_Stmt_Function) { $this->functionParser->setImportedNamespaces($this->importedNamespaces); $function = $this->functionParser->parse($node); if ($this->typeRegistry->hasFunction($functionName = $function->getName(), false)) { $this->analyzer->logger->warning(sprintf('The function "%s" has been defined more than once (probably conditionally). Ignoring the second definition.', $functionName)); return; } $this->typeRegistry->registerFunction($function); } else { if (NodeUtil::isConstantDefinition($node)) { assert($node instanceof \PHPParser_Node_Expr_FuncCall); if (!$node->args[0]->value instanceof \PHPParser_Node_Scalar_String) { return; } $constant = new GlobalConstant($node->args[0]->value->value); $constant->setAstNode($node); $constant->setPhpType($this->typeRegistry->getNativeType('unknown')); if (null !== ($type = $node->args[1]->value->getAttribute('type'))) { $constant->setPhpType($type); } if ($this->typeRegistry->hasConstant($constant->getName(), false)) { $this->analyzer->logger->warning(sprintf('The constant "%s" was defined more than once. Ignoring all but first definition.', $constant->getName())); return; } $this->typeRegistry->registerConstant($constant); } } } }
public function __toString() { $str = 'DefinitionLattice('; $defs = array(); foreach ($this->definitions as $var) { $defs[$var->getName()] = null === $this->definitions[$var] ? null : NodeUtil::getStringRepr($this->definitions[$var]->getNode()); } $str .= json_encode($defs) . ')'; return $str; }
private function handleGoto(\PHPParser_Node_Stmt_Goto $node) { $parent = $node->getAttribute('parent'); while (!NodeUtil::isScopeCreator($parent)) { $newParent = $parent->getAttribute('parent'); if (null === $newParent) { break; } $parent = $newParent; } $nodes = \PHPParser_Finder::create(array($parent))->find('Stmt_Label[name=' . $node->name . ']'); if (!$nodes) { return; } $this->graph->connectIfNotConnected($node, GraphEdge::TYPE_UNCOND, $nodes[0]); }
private function assertNotMatch($code) { $this->computeDefUse($code); $this->assertNotSame($this->def, $this->analysis->getDefiningNode('x', $this->use), 'Found definition was not expected: ' . \Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::getStringRepr($this->def)); }
private function isRewritable(\PHPParser_Node $startNode, AbstractToken $token, array &$imports, AbstractToken &$lastToken = null, &$writeMultiple = false) { $namespaceNode = \Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::findParent($startNode, 'PHPParser_Node_Stmt_Namespace') ?: $this->fixedFile->getAst(); $preserveMultiple = $this->getSetting('preserve_multiple'); $matchedAtLeastOne = false; $writeMultiple = false; while ($token->matches(T_USE)) { $currentImports = array(); $currentLastToken = null; $currentMultiple = false; if (!$this->isSafeToRewrite($token, $currentImports, $currentLastToken, $currentMultiple)) { break; } // If we are supposed to preserve multiples, and we already matched // some single use statements, bail out here. We will come back when // reaching the T_USE of the multiple import statement. if ($currentMultiple && $matchedAtLeastOne && $preserveMultiple) { break; } $this->removeUnused($namespaceNode, $currentImports); $matchedAtLeastOne = true; $imports = array_merge($imports, $currentImports); $lastToken = $currentLastToken; // If we scanned a multiple imports use statement, and we are supposed // to preserve it, bail out here as we have already reached its end. if ($currentMultiple && $preserveMultiple) { $writeMultiple = true; return true; } $token = $lastToken->findNextToken('NO_WHITESPACE')->get(); } return $matchedAtLeastOne; }
private function tryRemoveAssignment(NodeTraversal $t, \PHPParser_Node $n, \PHPParser_Node $exprRoot = null, $inState, $outState) { $parent = $n->getAttribute('parent'); if (NodeUtil::isAssignmentOp($n)) { $lhs = $n->var; $rhs = $n->expr; if ($n instanceof \PHPParser_Node_Expr_AssignList) { $i = 0; foreach ($n->vars as $var) { if (null === $var) { $i += 1; continue; } $newNode = new \PHPParser_Node_Expr_Assign($var, new \PHPParser_Node_Expr_ArrayDimFetch($rhs, new \PHPParser_Node_Scalar_LNumber($i)), $n->getLine()); $newNode->setAttribute('is_list_assign', true); $this->tryRemoveAssignment($t, $newNode, $exprRoot, $inState, $outState); $i += 1; } return; } // Recurse first. Example: dead_x = dead_y = 1; We try to clean up dead_y first. if (null !== $rhs) { $this->tryRemoveAssignment($t, $rhs, $exprRoot, $inState, $outState); $rhs = $lhs->getAttribute('next'); } $scope = $t->getScope(); if (!$lhs instanceof \PHPParser_Node_Expr_Variable || !is_string($lhs->name)) { return; } if (!$scope->isDeclared($lhs->name)) { return; } if (in_array($lhs->name, NodeUtil::$superGlobalNames, true)) { return; } $var = $scope->getVar($lhs->name); $escaped = $this->liveness->getEscapedLocals(); if (isset($escaped[$var])) { return; // Local variable that might be escaped due to closures. } // If we have an identity assignment such as a=a, always remove it // regardless of what the liveness results because it does not // change the result afterward. if (null !== $rhs && $rhs instanceof \PHPParser_Node_Expr_Variable && $var->getName() === $rhs->name && $n instanceof \PHPParser_Node_Expr_Assign) { $this->phpFile->addComment($n->getLine(), Comment::warning('dead_assignment.assignment_to_itself', 'Why assign ``%variable%`` to itself?', array('variable' => '$' . $rhs->name))); return; } // At the moment we miss some dead assignments if a variable is killed, // and defined in the same node of the control flow graph. // Example: $a = 'foo'; return $a = 'bar'; if ($outState->isLive($var)) { return; // Variable is not dead. } // Assignments to references do not need to be used in the // current scope. if ($var->isReference()) { return; } // if ($inState->isLive($var) // /*&& $this->isVariableStillLiveWithinExpression($n, $exprRoot, $var->getName()) */) { // The variable is killed here but it is also live before it. // This is possible if we have say: // if ($X = $a && $a = $C) {..} ; .......; $a = $S; // In this case we are safe to remove "$a = $C" because it is dead. // However if we have: // if ($a = $C && $X = $a) {..} ; .......; $a = $S; // removing "a = C" is NOT correct, although the live set at the node // is exactly the same. // TODO: We need more fine grain CFA or we need to keep track // of GEN sets when we recurse here. Maybe add the comment anyway, and let the user decide? // return; // } if ($n instanceof \PHPParser_Node_Expr_Assign) { if ($n->getAttribute('is_list_assign', false)) { $this->phpFile->addComment($n->getLine(), Comment::warning('dead_assignment.unnecessary_list_assign', 'The assignment to ``$%variable%`` is dead. Consider omitting it like so ``list($first,,$third)``.', array('variable' => $var->getName()))); return; } $this->phpFile->addComment($n->getLine(), Comment::warning('dead_assignment.unnecessary_var_assign', 'The assignment to ``$%variable%`` is dead and can be removed.', array('variable' => $var->getName()))); return; } } }
private function scanVars(Node $node, Node $parent) { switch (true) { case $node instanceof \PHPParser_Node_Stmt_StaticVar: $this->declareVar($node->name, null, true, $node); return; case $node instanceof \PHPParser_Node_Expr_Variable: if (($parent instanceof \PHPParser_Node_Expr_Assign || $parent instanceof \PHPParser_Node_Expr_AssignRef) && is_string($node->name) && $parent->var === $node) { $this->declareVar($node->name, null, true, $node); } // PHP automatically creates a variable (without issuing a warning) if an item is added // to a variable without the variable being declared as array previously. // ``function() { $a[] = 'foo'; }`` is perfectly permissible, and will create ``$a``. if ($parent instanceof \PHPParser_Node_Expr_ArrayDimFetch && $parent->var === $node) { $parentsParent = $parent->getAttribute('parent'); if (($parentsParent instanceof \PHPParser_Node_Expr_Assign || $parentsParent instanceof \PHPParser_Node_Expr_AssignRef) && is_string($node->name) && $parentsParent->var === $parent) { // TODO: We should better track which variables have been initialized // and which have not. This can be done by adding an undefined // type for those that have not. if (!$this->scope->isDeclared($node->name)) { $node->setAttribute('array_initializing_variable', true); } $this->declareVar($node->name, null, true, $node); } } if ($parent instanceof \PHPParser_Node_Arg && $parent->getAttribute('param_expects_ref', false) && is_string($node->name)) { $this->declareVar($node->name, null, true, $node); } return; case $node instanceof \PHPParser_Node_Expr_AssignList: foreach ($node->vars as $var) { if ($var instanceof \PHPParser_Node_Expr_Variable && is_string($var->name)) { $this->declareVar($var->name, null, true, $var); } } $this->scanVars($node->expr, $node); return; case $node instanceof \PHPParser_Node_Stmt_Trait: case $node instanceof \PHPParser_Node_Stmt_Interface: case $node instanceof \PHPParser_Node_Stmt_Class: case NodeUtil::isScopeCreator($node): return; // do not examine their children as they belong to a different scope // do not examine their children as they belong to a different scope case $node instanceof \PHPParser_Node_Stmt_Catch: $this->declareVar($node->var, null, true, $node); $this->scanVars($node->stmts, $node); return; case $node instanceof \PHPParser_Node_Stmt_Foreach: if ($node->keyVar !== null && $node->keyVar instanceof \PHPParser_Node_Expr_Variable && is_string($node->keyVar->name)) { $this->declareVar($node->keyVar->name, null, true, $node->keyVar); } if ($node->valueVar instanceof \PHPParser_Node_Expr_Variable && is_string($node->valueVar->name)) { $this->declareVar($node->valueVar->name, null, true, $node->valueVar); } $this->scanVars($node->stmts, $node); return; default: foreach ($node as $subNode) { if (is_array($subNode)) { foreach ($subNode as $aSubNode) { if (!$aSubNode instanceof \PHPParser_Node) { continue; } $this->scanVars($aSubNode, $node); } } else { if ($subNode instanceof \PHPParser_Node) { $this->scanVars($subNode, $node); } } } } }
/** * Refines an array type with the information about the used key, and assigned element type. * * This also defines the type for the item key if available and non-dynamic. * * @param PhpType $varType * @param PhpType $resultType * @param \PHPParser_Node_Expr_ArrayDimFetch $node * * @return ArrayType */ private function getRefinedArrayType(PhpType $varType, PhpType $resultType, \PHPParser_Node_Expr_ArrayDimFetch $node) { $usedKeyType = $this->getKeyTypeForDimFetch($node); $keyType = $varType->getKeyType(); if ($keyType === $this->typeRegistry->getNativeType('generic_array_key')) { $refinedKeyType = $usedKeyType; } else { $refinedKeyType = $keyType->getLeastSupertype($usedKeyType); } $elementType = $varType->getElementType(); if ($elementType === $this->typeRegistry->getNativeType('generic_array_value')) { $refinedElementType = $resultType; } else { $refinedElementType = $elementType->getLeastSupertype($resultType); } $refinedType = $this->typeRegistry->getArrayType($refinedElementType, $refinedKeyType); // Infer the type of the key if a concrete value is available. NodeUtil::getValue($node->dim)->map(function ($keyName) use(&$refinedType, $resultType) { $refinedType = $refinedType->inferItemType($keyName, $resultType); }); return $refinedType; }
public function visit(NodeTraversal $t, \PHPParser_Node $node, \PHPParser_Node $parent = null) { switch (true) { case $node instanceof \PHPParser_Node_Expr_Variable: if (NodeUtil::isSuperGlobal($node)) { return; } if (is_string($node->name)) { $this->checkNaming($node->getLine(), '$' . $node->name, $node->name, 'local_variable'); } break; case $node instanceof \PHPParser_Node_Stmt_PropertyProperty: $this->checkNaming($node->getLine(), '$' . $node->name, $node->name, 'property_name'); break; case $node instanceof \PHPParser_Node_Stmt_Function: case $node instanceof \PHPParser_Node_Stmt_ClassMethod: $function = $this->typeRegistry->getFunctionByNode($node); // If a class method is constrained by a contract of an interface, or an abstract // class, then there is no point in adding warnings for them. if ($function instanceof ClassMethod && $function->isConstrainedByContract()) { break; } $this->checkNaming($node->getLine(), 'function ' . $node->name . '()', $node->name, 'method_name'); if (null !== $function && null !== ($returnType = $function->getReturnType()) && $returnType->isBooleanType()) { $this->checkNaming($node->getLine(), 'function ' . $node->name . '()', $node->name, 'isser_method_name'); } $this->checkParameters($node->params); break; case $node instanceof \PHPParser_Node_Expr_Closure: $this->checkParameters($node->params); break; case NodeUtil::isMethodContainer($node): $class = $this->typeRegistry->getClassByNode($node)->toMaybeObjectType(); if ($class) { $this->checkNaming($node->getLine(), $class->getShortName(), $class->getShortName(), 'type_name'); if ($class->isInterface()) { $this->checkNaming($node->getLine(), $class->getShortName(), $class->getShortName(), 'interface_name'); } elseif ($class instanceof Clazz) { if ($class->isUtilityClass()) { $this->checkNaming($node->getLine(), $class->getShortName(), $class->getShortName(), 'utility_class_name'); } elseif ($class->isAbstract()) { $this->checkNaming($node->getLine(), $class->getShortName(), $class->getShortName(), 'abstract_class_name'); } // No Else-If here as there might be an abstract exception. // TODO: isSubtype needs to return a TernaryValue so that we // can decide here what to do in the unknown case. // if ($class->isSubTypeOf($this->typeRegistry->getClassOrCreate('Exception'))) { // $this->checkNaming($node->getLine(), $class->getShortName(), $class->getShortName(), 'exception_name'); // } } // The original check only allows final classes to have a private // constructor. We also allow the constructor to be private if it // is declared final, and the class is declared abstract. This is // for example a common practice for utility classes. $method = $class->getMethod('__construct'); if (null !== $method && $method->isPrivate() && !$class->isFinal() && (!$method->isFinal() || !$class->isAbstract())) { if (($method = $class->getMethod('__construct')) && $method->isPrivate() && !$class->isFinal() && (!$method->isFinal() || !$class->isAbstract())) { if ($method->isFinal()) { if ($class->isUtilityClass()) { $this->phpFile->addComment($node->getLine(), Comment::warning('coding_style.non_abstract_util_class', 'Since you have declared the constructor as final, and this seems like a utility class, maybe you should also declare the class as abstract.')); } else { if (null === $method->getAstNode()) { break; } $this->phpFile->addComment($method->getAstNode()->getLine(), Comment::warning('coding_style.non_util_class_with_final_private_constructor', 'Instead of declaring the constructor as final, maybe you should declare the entire class as final.')); } } elseif ($class->isAbstract()) { if ($class->isUtilityClass()) { $this->phpFile->addComment($method->getAstNode()->getLine(), Comment::warning('coding_style.util_class_with_non_final_constructor', 'Since you have declared this class abstract, and the constructor is private, maybe you should also declare the constructor as final.')); } else { $this->phpFile->addComment($method->getAstNode()->getLine(), Comment::warning('coding_style.abstract_non_util_class_with_private_constructor', 'Something seems to be off here. Are you sure you want to declare the constructor as private, and the class as abstract?')); } } else { $this->phpFile->addComment($node->getLine(), Comment::warning('coding_style.non_final_class_with_private_constructor', 'Since you have declared the constructor as private, maybe you should also declare the class as final.')); } } } } break; } }
private function addToUseIfLocal(\PHPParser_Node $varNode, \PHPParser_Node $cfgNode, UseLattice $uses) { if (!NodeUtil::isName($varNode)) { return; } $name = $varNode->name; $var = $this->scope->getVar($name); if (null === $var) { return; } $uses->addUse($var, $cfgNode, $varNode); }
/** * Declares a refined type in {@code scope} for the name represented by * {@code node}. It must be possible to refine the type of the given node in * the given scope, as determined by {@link #getTypeIfRefinable}. */ protected function declareNameInScope(LinkedFlowScope $scope, \PHPParser_Node $node, PhpType $type) { switch (true) { case $node instanceof \PHPParser_Node_Expr_Variable: if (is_string($node->name)) { $scope->inferSlotType($node->name, $type); } break; case $node instanceof \PHPParser_Node_Expr_StaticPropertyFetch: case $node instanceof \PHPParser_Node_Expr_PropertyFetch: if (null !== ($qualifiedName = TypeInference::getQualifiedName($node))) { $origType = $node->getAttribute('type') ?: $this->typeRegistry->getNativeType('unknown'); $scope->inferQualifiedSlot($node, $qualifiedName, $origType, $type); } break; // Something like: if (is_array($functions = getFunctionNames())) { } // Something like: if (is_array($functions = getFunctionNames())) { } case $node instanceof \PHPParser_Node_Expr_Assign: case $node instanceof \PHPParser_Node_Expr_AssignRef: $this->declareNameInScope($scope, $node->var, $type); break; case $node instanceof \PHPParser_Node_Expr_ArrayDimFetch: $dim = \Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::getValue($node->dim); if ($dim->isDefined() && null !== ($slotType = $node->var->getAttribute('type')) && $slotType->isArrayType()) { $newSlotType = $slotType->inferItemType($dim->get(), $type); $this->declareNameInScope($scope, $node->var, $newSlotType); } break; } }
public function getPreciserFunctionReturnTypeKnowingArguments(GlobalFunction $function, array $argValues, array $argTypes) { switch ($function->getName()) { case 'version_compare': switch (count($argTypes)) { case 2: return $this->registry->getNativeType('integer'); case 3: return $this->registry->getNativeType('boolean'); default: return $this->registry->resolveType('integer|boolean'); } case 'unserialize': return $this->registry->getNativeType('unknown_checked'); case 'var_export': if (count($argValues) !== 2) { return null; } if (\Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::isBoolean($argValues[1]) && \Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::getBooleanValue($argValues[1]) === true) { return $this->registry->getNativeType('string'); } return null; case 'min': case 'max': switch (count($argTypes)) { case 0: return null; case 1: if ($argTypes[0]->isArrayType()) { return $argTypes[0]->getElementType(); } return null; default: // TODO: We could make this a bit smarter as some types are always considered // greater/smaller than other types. // See http://de1.php.net/manual/en/language.operators.comparison.php return $this->registry->createUnionType($argTypes); } case 'str_replace': case 'preg_filter': case 'preg_replace': case 'preg_replace_callback': if (isset($argTypes[2])) { if ($argTypes[2]->isUnknownType()) { return $this->registry->getNativeType('unknown'); } if ($argTypes[2]->isArrayType()) { return $this->registry->resolveType('array<string>'); } $nullableScalar = $this->registry->createNullableType($this->registry->getNativeType('scalar')); if ($argTypes[2]->isSubtypeOf($nullableScalar)) { return $this->registry->getNativeType('string'); } } return null; // Use the non-restricted type if we don't know. // Use the non-restricted type if we don't know. default: return null; } }
private function checkBitwiseOperation() { if (!NodeUtil::isEqualityExpression($this->stream->node->left)) { return; } /* * When this method is called, the token stream points to the bitwise operation already. * * 0 === foo() & 5; * ^ * -----------(1) * * (1) The position of the token stream when this method is called. * * We now check that the equality expression on the left side (in the case above, it is an Identical node) ends * with a parenthesis, and the ending parenthesis is started somewhere on the left side side of the equality * operator (===). */ /** @var AbstractToken $closingParenthesis */ $closingParenthesis = $this->stream->token->findPreviousToken('NO_WHITESPACE_OR_COMMENT')->get(); if ($closingParenthesis->matches(')')) { $startToken = $this->stream->node->left->left->getAttribute('start_token'); // The left operator of the equality sign is not assigned an explicit start token, so we do not know where // it started. We can just bail out here, and hope that the closing parenthesis is indeed started on the left // side. This needs to be improved in the SimultaneousTokenAndAstStream over time so that we ideally always // have a start token available. if (null === $startToken) { return; } // The closing parenthesis was started somewhere on the left side, good ! if ($closingParenthesis->isClosing($startToken->findPreviousToken('NO_WHITESPACE_OR_COMMENT')->get())) { return; } } $this->phpFile->addComment($this->stream->node->getLine(), Comment::warning('precedence.possibly_wrong_comparison_of_bitop_result', 'Consider adding parentheses for clarity. Current Interpretation: ``%current_compare%``, Probably Intended Meaning: ``%alternative_compare%``', array('current_compare' => '(' . self::$prettyPrinter->prettyPrintExpr($this->stream->node->left) . ') ' . NodeUtil::getBitOp($this->stream->node) . ' ' . self::$prettyPrinter->prettyPrintExpr($this->stream->node->right), 'alternative_compare' => self::$prettyPrinter->prettyPrintExpr($this->stream->node->left->left) . ' ' . NodeUtil::getEqualOp($this->stream->node->left) . ' (' . self::$prettyPrinter->prettyPrintExpr($this->stream->node->left->right) . ' ' . NodeUtil::getBitOp($this->stream->node) . ' ' . self::$prettyPrinter->prettyPrintExpr($this->stream->node->right) . ')'))); }
private function checkForMissingProperty(\PHPParser_Node_Expr_PropertyFetch $node) { if (!is_string($node->name)) { return; } if (!($objType = $node->var->getAttribute('type'))) { return; } $objType = $objType->restrictByNotNull()->toMaybeObjectType(); if (!$objType) { // TODO: Add support to check on union types. return; } if ($objType->isInterface()) { $this->phpFile->addComment($node->getLine(), Comment::warning('types.property_access_on_interface', 'Accessing "%property_name%" on the interface "%interface_name%" suggest that you code against a concrete implementation. How about adding an ``instanceof`` check?', array('property_name' => $node->name, 'interface_name' => $objType->getName()))); return; } if (!$objType->isNormalized() || $objType->hasProperty($node->name)) { return; } // Ignore all property accesses on ``stdClass`` objects. Currently, we have // no way to describe, or track RECORD types. As such, any messages related to // stdClass are likely wrong, but at least there are too many false-positives // for now. So, we just disable this. if ($objType->isSubtypeOf($this->typeRegistry->getClassOrCreate('stdClass'))) { return; } // Ignore all property reads on ``SimpleXMLElement`` objects. The reasoning // behind this is similar to the exception for ``stdClass`` objects above. // We simply have no reliable way to describe the structure of these objects // for now. So, we disable checks for them to avoid a flood of false-positives. if (!\Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::isAssignmentOp($node->getAttribute('parent')) && $objType->isSubtypeOf($this->typeRegistry->getClassOrCreate('SimpleXMLElement'))) { return; } // Property accesses inside isset() are safe, do not make any noise just yet. if ($node->getAttribute('parent') instanceof \PHPParser_Node_Expr_Isset) { return; } if (null !== ($bestName = $this->getMostSimilarName($node->name, $objType->getPropertyNames()))) { $this->phpFile->addComment($node->getLine(), Comment::error('typos.mispelled_property_name', 'The property "%offending_property_name%" does not exist. Did you mean "%closest_property_name%"?', array('offending_property_name' => $node->name, 'closest_property_name' => $bestName))); } else { // Ignore additional accesses to this property. $objType->addProperty(new Property($node->name)); if (\Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::isAssignmentOp($node->getAttribute('parent'))) { if ($objType->hasMethod('__set')) { $this->phpFile->addComment($node->getLine(), Comment::warning('strict.maybe_undocument_property_write_capability', 'The property ``%property_name%`` does not exist. Since you implemented ``__set``, maybe consider adding a [@property annotation](http://www.phpdoc.org/docs/latest/for-users/tags/property.html).', array('property_name' => $node->name))); return; } } else { if ($objType->hasMethod('__get')) { $this->phpFile->addComment($node->getLine(), Comment::warning('strict.maybe_undocument_property_read_capability', 'The property ``%property_name%`` does not exist. Since you implemented ``__get``, maybe consider adding a [@property annotation](http://www.phpdoc.org/docs/latest/for-users/tags/property.html).', array('property_name' => $node->name))); return; } } $thisType = $this->t->getScope()->getTypeOfThis(); if ($thisType && $thisType->equals($objType)) { $this->phpFile->addComment($node->getLine(), Comment::warning('strict.undeclared_property', 'The property "%property_name%" does not exist. Did you maybe forget to declare it?', array('property_name' => $node->name))); } else { $this->phpFile->addComment($node->getLine(), Comment::error('typos.non_existent_property', 'The property "%property_name%" does not exist.', array('property_name' => $node->name))); } } }
private function assertNotMatch($code) { $this->computeUseDef($code); $analyzedUses = $this->analysis->getUses('x', $this->def); foreach ($this->uses as $use) { if (in_array($use, $analyzedUses, true)) { $this->fail('Did not expect ' . \Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::getStringRepr($use)); } } }
public function checkForeach(\PHPParser_Node_Stmt_Foreach $node) { if ($this->getSetting('foreach.value_as_reference') && $node->byRef && !NodeUtil::canBePassedByRef($node->expr)) { $this->phpFile->addComment($node->expr->getLine(), Comment::error('usage_context.foreach_expr_no_reference', 'The expression ``%expr%`` cannot be used as a reference.', array('expr' => self::$prettyPrinter->prettyPrintExpr($node->expr)))); } $this->checkTraversable($node); }
private function traverseScopeCreator(\PHPParser_Node $node, \PHPParser_Node $parent = null) { assert(NodeUtil::isScopeCreator($node)); $this->curNode = $node; $isScopeActive = $this->getScopeRoot() === $node; if (!$isScopeActive) { $this->pushScopeWithRoot($node); } // args foreach ($node->params as $param) { $this->traverseBranch($param, $node); } // uses from parent scope if ($node instanceof \PHPParser_Node_Expr_Closure) { foreach ($node->uses as $use) { $this->traverseBranch($use, $node); } } // body // For abstract methods the stmts property is null, which we need to check here. if (null !== $node->stmts) { $this->traverseBranch($node->stmts, $node); } if (!$isScopeActive) { $this->popScope(); } }
private function inMethod($phpCode) { // Parse the body of the function. $ast = $this->parser->parse(new \PHPParser_Lexer('<?php class Foo { function foo() {' . $phpCode . '} }')); // Normalize the AST. $traverser = new \PHPParser_NodeTraverser(); $traverser->addVisitor(new \PHPParser_NodeVisitor_NameResolver()); $traverser->addvisitor(new NormalizingNodeVisitor()); $ast = $traverser->traverse($ast); $traverser = new \PHPParser_NodeTraverser(); $traverser->addVisitor(new \PHPParser_NodeVisitor_NodeConnector()); $traverser->traverse($ast); $root = $ast[0]; $node = $root->stmts[0]->stmts; // Create the scope with the assumptions. $scopeCreator = new TypedScopeCreator($this->registry); $assumedScope = $scopeCreator->createScope($node, $scopeCreator->createScope($root, null)); foreach ($this->assumptions as $symbolName => $type) { $var = $assumedScope->getVar($symbolName); if (!$var) { $assumedScope->declareVar($symbolName, $type); } else { $var->setType($type); } } // Create the control graph. $cfa = new ControlFlowAnalysis(); $cfa->process($node); $cfg = $cfa->getGraph(); // Create a simple reverse abstract interpreter. $rai = new SemanticReverseAbstractInterpreter($this->registry); $fi = new ArrayFunctionInterpreter($this->registry); $mi = $this->getMock('Scrutinizer\\PhpAnalyzer\\DataFlow\\TypeInference\\MethodInterpreter\\MethodInterpreterInterface'); $commentParser = new \Scrutinizer\PhpAnalyzer\PhpParser\DocCommentParser($this->registry); // Do the type inference by data-flow analysis. $dfa = new TypeInference($cfg, $rai, $fi, $mi, $commentParser, $assumedScope, $this->registry); $dfa->analyze(); // Get the scope of the implicit return. $this->returnScope = $cfg->getImplicitReturn()->getAttribute(DataFlowAnalysis::ATTR_FLOW_STATE_IN); $this->astGraph = \Scrutinizer\PhpAnalyzer\PhpParser\NodeUtil::dump($node); }