/** * @return null * Analyze the node associated with this object * in the given context */ public function analyze(Context $context, CodeBase $code_base) : Context { // Don't do anything if we care about being // fast if (Config::get()->quick_mode) { return $context; } if (!$this->hasNode()) { return $context; } // Closures depend on the context surrounding them such // as for getting `use(...)` variables. Since we don't // have them, we can't re-analyze them until we change // that. // // TODO: Store the parent context on Analyzable objects if ($this->getNode()->kind === \ast\AST_CLOSURE) { return $context; } // Don't go deeper than one level in if ($this->recursion_depth++ > 2) { return $context; } // Analyze the node in a cloned context so that we // don't overwrite anything return (new BlockAnalysisVisitor($code_base, clone $context))($this->getNode()); }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeParentConstructorCalled(CodeBase $code_base, Clazz $clazz) { // Only look at classes configured to require a call // to its parent constructor if (!in_array($clazz->getName(), Config::get()->parent_constructor_required)) { return; } // Don't worry about internal classes if ($clazz->isInternal()) { return; } // Don't worry if there's no parent class if (!$clazz->hasParentClassFQSEN()) { return; } if (!$code_base->hasClassWithFQSEN($clazz->getParentClassFQSEN())) { // This is an error, but its caught elsewhere. We'll // just roll through looking for other errors return; } $parent_clazz = $code_base->getClassByFQSEN($clazz->getParentClassFQSEN()); if (!$parent_clazz->isAbstract() && !$clazz->getIsParentConstructorCalled()) { Issue::emit(Issue::TypeParentConstructorCalled, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $clazz->getFQSEN(), (string) $parent_clazz->getFQSEN()); } }
public function testClassContext() { $code = "<?php\n class C {\n private function f() {\n return 42;\n }\n }"; $stmt_list_node = \ast\parse_code($code, Config::get()->ast_version); $class_node = $stmt_list_node->children[0]; $context = new Context(); $context = (new ParseVisitor($this->code_base, $context))($class_node); $stmt_list_node = $class_node->children['stmts']; $method_node = $stmt_list_node->children[0]; $context = (new ParseVisitor($this->code_base, $context))($method_node); }
/** * @return string * The path of the file relative to the project * root directory */ public function getProjectRelativePath() : string { $cwd_relative_path = $this->file; // Get a path relative to the project root $path = str_replace(Config::get()->getProjectRootDirectory(), '', realpath($cwd_relative_path) ?: $cwd_relative_path); // Strip any beginning directory separators if (0 === ($pos = strpos($path, DIRECTORY_SEPARATOR))) { $path = substr($path, $pos + 1); } return $path; }
/** * Take a look at all globally accessible elements and see if * we can find any dead code that is never referenced * * @return void */ public static function analyzeReferenceCounts(CodeBase $code_base) { // Check to see if dead code detection is enabled. Keep // in mind that the results here are just a guess and // we can't tell with certainty that anything is // definitely unreferenced. if (!Config::get()->dead_code_detection) { return; } // Get the count of all known elements $total_count = count($code_base->getMethodMap(), COUNT_RECURSIVE) + count($code_base->getPropertyMap(), COUNT_RECURSIVE) + count($code_base->getConstantMap(), COUNT_RECURSIVE) + count($code_base->getClassMap(), COUNT_RECURSIVE); $i = 0; $analyze_list = function ($list) use($code_base, &$i, $total_count) { foreach ($list as $name => $element) { CLI::progress('dead code', ++$i / $total_count); self::analyzeElementReferenceCounts($code_base, $element); } }; $analyze_map = function ($map) use($code_base, &$i, $total_count) { foreach ($map as $fqsen_string => $list) { foreach ($list as $name => $element) { CLI::progress('dead code', ++$i / $total_count); // Don't worry about internal elements if ($element->getContext()->isInternal()) { continue; } $element_fqsen = $element->getFQSEN(); if ($element_fqsen instanceof FullyQualifiedClassElement) { $class_fqsen = $element->getDefiningClassFQSEN(); // Don't analyze elements defined in a parent // class if ((string) $class_fqsen !== $fqsen_string) { continue; } $defining_class = $element->getDefiningClass($code_base); // Don't analyze elements on interfaces or on // abstract classes, as they're uncallable. if ($defining_class->isInterface() || $defining_class->isAbstract() || $defining_class->isTrait()) { continue; } // Ignore magic methods if ($element instanceof Method && $element->getIsMagic()) { continue; } } self::analyzeElementReferenceCounts($code_base, $element); } } }; $analyze_map($code_base->getMethodMap()); $analyze_map($code_base->getPropertyMap()); $analyze_map($code_base->getConstantMap()); $analyze_list($code_base->getClassMap()); }
/** * Scan a list of files, applying the given closure to every * AST node * * @param string[] $file_list * A list of files to scan * * @param \Closure $visit_node * A closure that is to be applied to every AST node * * @return void */ public static function scanFileList(array $file_list, \Closure $visit_node) { foreach ($file_list as $file_path) { // Convert the file to an Abstract Syntax Tree // before passing it on to the recursive version // of this method $node = \ast\parse_file($file_path, Config::get()->ast_version); // Skip empty files if (!$node) { continue; } self::scanNodeInFile($node, $file_path, $visit_node); } }
/** * @return null * Analyze the node associated with this object * in the given context */ public function analyze(Context $context, CodeBase $code_base) : Context { // Don't do anything if we care about being // fast if (Config::get()->quick_mode) { return $context; } if (!$this->hasNode()) { return $context; } // Don't go deeper than one level in if ($this->recursion_depth++ > 2) { return $context; } // Analyze the node in a cloned context so that we // don't overwrite anything $context = (new Phan())->analyzeNodeInContext($this->getNode(), clone $context, $code_base); return $context; }
/** * Take a look at all globally accessible elements and see if * we can find any dead code that is never referenced * * @return void */ public static function analyzeReferenceCounts(CodeBase $code_base) { // Check to see if dead code detection is enabled. Keep // in mind that the results here are just a guess and // we can't tell with certainty that anything is // definitely unreferenced. if (!Config::get()->dead_code_detection) { return; } // Get the count of all known elements $total_count = $code_base->totalElementCount(); $i = 0; // Functions self::analyzeElementListReferenceCounts($code_base, $code_base->getFunctionMap(), Issue::UnreferencedMethod, $total_count, $i); // Constants self::analyzeElementListReferenceCounts($code_base, $code_base->getGlobalConstantMap(), Issue::UnreferencedConstant, $total_count, $i); // Classes self::analyzeElementListReferenceCounts($code_base, $code_base->getClassMap(), Issue::UnreferencedClass, $total_count, $i); // Class Maps foreach ($code_base->getClassMapMap() as $class_map) { self::analyzeClassMapReferenceCounts($code_base, $class_map, $total_count, $i); } }
public static function err(int $etype, string $msg, string $file = '', int $lineno = 0) { $log = self::getInstance(); if ($etype == self::EFATAL) { self::display(); // Something went wrong - abort if ($file) { throw new \Exception("{$file}:{$lineno} {$msg}"); } else { throw new \Exception($msg); } } // If configured to do so, prepend the message // with a trace ID which indicates where the issue // came from allowing us to group on unique classes // of issues if (Config::get()->emit_trace_id) { $msg = self::traceId(debug_backtrace()[1]) . ' ' . $msg; } if ($etype & $log->output_mask) { $ukey = md5($file . $lineno . $etype . $msg); $log->msgs[$ukey] = ['file' => $file, 'lineno' => $lineno, 'etype' => $etype, 'msg' => $msg]; } }
private function addMethodWithScopeAndName(FunctionInterface $method, string $scope, string $name) { $this->method_map[$scope][$name] = $method; // If we're doing dead code detection, map the name // directly to the method so we can quickly look up // all methods with that name to add a possible // reference if (Config::get()->dead_code_detection) { $this->method_name_map[strtolower($name)][] = $method; } // Associate the element with the file it was found in $this->getFileByPath($method->getFileRef()->getFile())->addMethodFQSEN($method->getFQSEN()); }
/** * Get a Context after parsing the given * bit of code. */ private function contextForCode(string $code_stub) : Context { return Analysis::parseNodeInContext($this->code_base, new Context(), \ast\parse_code('<?php ' . $code_stub, Config::get()->ast_version)); }
/** * Visit a node with kind `\ast\AST_RETURN` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitReturn(Node $node) : Context { if (Config::get()->backward_compatibility_checks) { (new ContextNode($this->code_base, $this->context, $node))->analyzeBackwardCompatibility(); } // Make sure we're actually returning from a method. if (!$this->context->isInFunctionLikeScope()) { return $this->context; } // Get the method/function/closure we're in $method = $this->context->getFunctionLikeInScope($this->code_base); assert(!empty($method), "We're supposed to be in either method or closure scope."); // Mark the method as returning something $method->setHasReturn(($node->children['expr'] ?? null) !== null); return $this->context; }
/** * @return string * The relative path appended to the project root directory. * * @suppress PhanUnreferencedMethod */ public static function projectPath(string $relative_path) { return implode(DIRECTORY_SEPARATOR, [Config::get()->getProjectRootDirectory(), $relative_path]); }
/** * Make sure signatures line up between methods and the * methods they override * * @see https://en.wikipedia.org/wiki/Liskov_substitution_principle */ private static function analyzeOverrideSignature(CodeBase $code_base, Method $method) { if (!Config::get()->analyze_signature_compatibility) { return; } // Hydrate the class this method is coming from in // order to understand if its an override or not $class = $method->getClass($code_base); $class->hydrate($code_base); // Check to see if the method is an override // $method->analyzeOverride($code_base); // Make sure we're actually overriding something if (!$method->getIsOverride()) { return; } // Dont' worry about signatures lining up on // constructors. We just want to make sure that // calling a method on a subclass won't cause // a runtime error. We usually know what we're // constructing at instantiation time, so there // is less of a risk. if ($method->getName() == '__construct') { return; } // Get the method that is being overridden $o_method = $method->getOverriddenMethod($code_base); // Get the class that the overridden method lives on $o_class = $o_method->getClass($code_base); // PHP doesn't complain about signature mismatches // with traits, so neither shall we if ($o_class->isTrait()) { return; } // Get the parameters for that method $o_parameter_list = $o_method->getParameterList(); // If we have a parent type defined, map the method's // return type and parameter types through it $type_option = $class->getParentTypeOption(); // Map overridden method parameter types through any // template type parameters we may have if ($type_option->isDefined()) { $o_parameter_list = array_map(function (Parameter $parameter) use($type_option, $code_base) : Parameter { if (!$parameter->getUnionType()->hasTemplateType()) { return $parameter; } $mapped_parameter = clone $parameter; $mapped_parameter->setUnionType($mapped_parameter->getUnionType()->withTemplateParameterTypeMap($type_option->get()->getTemplateParameterTypeMap($code_base))); return $mapped_parameter; }, $o_parameter_list); } // Map overridden method return type through any template // type parameters we may have $o_return_union_type = $o_method->getUnionType(); if ($type_option->isDefined() && $o_return_union_type->hasTemplateType()) { $o_return_union_type = $o_return_union_type->withTemplateParameterTypeMap($type_option->get()->getTemplateParameterTypeMap($code_base)); } // Determine if the signatures match up $signatures_match = true; // Make sure the count of parameters matches if ($method->getNumberOfRequiredParameters() > $o_method->getNumberOfRequiredParameters()) { $signatures_match = false; } else { if ($method->getNumberOfParameters() < $o_method->getNumberOfParameters()) { $signatures_match = false; // If parameter counts match, check their types } else { foreach ($method->getParameterList() as $i => $parameter) { if (!isset($o_parameter_list[$i])) { continue; } $o_parameter = $o_parameter_list[$i]; // Changing pass by reference is not ok // @see https://3v4l.org/Utuo8 if ($parameter->isPassByReference() != $o_parameter->isPassByReference()) { $signatures_match = false; break; } // A stricter type on an overriding method is cool if ($o_parameter->getUnionType()->isEmpty() || $o_parameter->getUnionType()->isType(MixedType::instance())) { continue; } // Its not OK to have a more relaxed type on an // overriding method // // https://3v4l.org/XTm3P if ($parameter->getUnionType()->isEmpty()) { $signatures_match = false; break; } // If we have types, make sure they line up // // TODO: should we be expanding the types on $o_parameter // via ->asExpandedTypes($code_base)? // // @see https://3v4l.org/ke3kp if (!$o_parameter->getUnionType()->canCastToUnionType($parameter->getUnionType())) { $signatures_match = false; break; } } } } // Return types should be mappable if (!$o_return_union_type->isEmpty()) { if (!$method->getUnionType()->asExpandedTypes($code_base)->canCastToUnionType($o_return_union_type)) { $signatures_match = false; } } // Static or non-static should match if ($method->isStatic() != $o_method->isStatic()) { if ($o_method->isStatic()) { Issue::maybeEmit($code_base, $method->getContext(), Issue::AccessStaticToNonStatic, $method->getFileRef()->getLineNumberStart(), $o_method->getFQSEN()); } else { Issue::maybeEmit($code_base, $method->getContext(), Issue::AccessNonStaticToStatic, $method->getFileRef()->getLineNumberStart(), $o_method->getFQSEN()); } } if ($o_method->returnsRef() && !$method->returnsRef()) { $signatures_match = false; } if (!$signatures_match) { if ($o_method->isInternal()) { Issue::maybeEmit($code_base, $method->getContext(), Issue::ParamSignatureMismatchInternal, $method->getFileRef()->getLineNumberStart(), $method, $o_method); } else { Issue::maybeEmit($code_base, $method->getContext(), Issue::ParamSignatureMismatch, $method->getFileRef()->getLineNumberStart(), $method, $o_method, $o_method->getFileRef()->getFile(), $o_method->getFileRef()->getLineNumberStart()); } } // Access must be compatible if ($o_method->isProtected() && $method->isPrivate() || $o_method->isPublic() && !$method->isPublic()) { if ($o_method->isInternal()) { Issue::maybeEmit($code_base, $method->getContext(), Issue::AccessSignatureMismatchInternal, $method->getFileRef()->getLineNumberStart(), $method, $o_method); } else { Issue::maybeEmit($code_base, $method->getContext(), Issue::AccessSignatureMismatch, $method->getFileRef()->getLineNumberStart(), $method, $o_method, $o_method->getFileRef()->getFile(), $o_method->getFileRef()->getLineNumberStart()); } } }
/** * Once we know what the universe looks like we * can scan for more complicated issues. * * @param CodeBase $code_base * The global code base holding all state * * @param string $file_path * A list of files to scan * * @return Context */ public static function analyzeFile(CodeBase $code_base, string $file_path) : Context { // Convert the file to an Abstract Syntax Tree // before passing it on to the recursive version // of this method $node = \ast\parse_file($file_path, Config::get()->ast_version); // Set the file on the context $context = (new Context())->withFile($file_path); // Ensure we have some content if (empty($node)) { Issue::emit(Issue::EmptyFile, $file_path, 0, $file_path); return $context; } // Start recursively analyzing the tree return self::analyzeNodeInContext($code_base, $context, $node); }
/** * @return bool * True if this Type can be cast to the given Type * cleanly */ public function canCastToType(Type $type) : bool { if ($this === $type) { return true; } $s = strtolower((string) $this); $d = strtolower((string) $type); if ($s[0] == '\\') { $s = substr($s, 1); } if ($d[0] == '\\') { $d = substr($d, 1); } if ($s === $d) { return true; } if (Config::get()->scalar_implicit_cast) { if ($type->isScalar() && $this->isScalar()) { return true; } } if ($s === 'int' && $d === 'float') { return true; // int->float is ok } if (($s === 'array' || $s === 'string' || strpos($s, '[]') !== false || $s === 'closure') && $d === 'callable') { return true; } if ($s === 'object' && !$type->isScalar() && $d !== 'array') { return true; } if ($d === 'object' && !$this->isScalar() && $s !== 'array') { return true; } if (strpos($s, '[]') !== false && ($d == 'array' || $d == '\\ArrayAccess')) { return true; } if (strpos($d, '[]') !== false && $s === 'array') { return true; } if ($s === 'callable' && $d === 'closure') { return true; } if (($pos = strrpos($d, '\\')) !== false) { if ('\\' !== $this->getNamespace()) { if (trim($this->getNamespace() . '\\' . $s, '\\') == $d) { return true; } } else { if (substr($d, $pos + 1) === $s) { return true; // Lazy hack, but... } } } if (($pos = strrpos($s, '\\')) !== false) { if ('\\' !== $type->getNamespace()) { if (trim($type->getNamespace() . '\\' . $d, '\\') == $s) { return true; } } else { if (substr($s, $pos + 1) === $d) { return true; // Lazy hack, but... } } } return false; }
/** * Emit all collected issues * * @return void */ private static function display() { $collector = self::$issueCollector; if (Config::get()->progress_bar) { fwrite(STDERR, "\n"); } $printer = self::$printer; foreach ($collector->getCollectedIssues() as $issue) { $printer->print($issue); } if ($printer instanceof BufferedPrinterInterface) { $printer->flush(); } }
declare (strict_types=1); // Phan does a ton of GC and this offers a major speed // improvment if your system can handle it (which it // should be able to) gc_disable(); // Check the environment to make sure Phan can run successfully require_once __DIR__ . '/requirements.php'; // Build a code base based on PHP internally defined // functions, methods and classes before loading our // own $code_base = (require_once __DIR__ . '/codebase.php'); require_once __DIR__ . '/Phan/Bootstrap.php'; use Phan\CLI; use Phan\CodeBase; use Phan\Config; use Phan\Phan; // Create our CLI interface and load arguments $cli = new CLI(); $file_list = $cli->getFileList(); // If requested, expand the file list to a set of // all files that should be re-analyzed if (Config::get()->expand_file_list) { assert((bool) Config::get()->stored_state_file_path, 'Requesting an expanded dependency list can only ' . ' be done if a state-file is defined'); // Analyze the file list provided via the CLI $file_list = Phan::expandedFileList($code_base, $file_list); } // Analyze the file list provided via the CLI $is_issue_found = Phan::analyzeFileList($code_base, $file_list); // Provide an exit status code based on if // issues were found exit($is_issue_found ? EXIT_ISSUES_FOUND : EXIT_SUCCESS);
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitProp(Node $node) : Context { $property_name = $node->children['prop']; // Things like $foo->$bar if (!is_string($property_name)) { return $this->context; } assert(is_string($property_name), "Property must be string"); try { $class_list = (new ContextNode($this->code_base, $this->context, $node->children['expr']))->getClassList(); } catch (CodeBaseException $exception) { // This really shouldn't happen since the code // parsed cleanly. This should fatal. // throw $exception; return $this->context; } catch (\Exception $exception) { // If we can't figure out what kind of a class // this is, don't worry about it return $this->context; } foreach ($class_list as $clazz) { // Check to see if this class has the property or // a setter if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { if (!$clazz->hasMethodWithName($this->code_base, '__set')) { continue; } } try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (IssueException $exception) { Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); return $this->context; } if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) { $this->emitIssue(Issue::TypeMismatchProperty, $node->lineno ?? 0, (string) $this->right_type, "{$clazz->getFQSEN()}::{$property->getName()}", (string) $property->getUnionType()); return $this->context; } else { // If we're assigning to an array element then we don't // know what the constitutation of the parameter is // outside of the scope of this assignment, so we add to // its union type rather than replace it. if ($this->is_dim_assignment) { $property->getUnionType()->addUnionType($this->right_type); } } // After having checked it, add this type to it $property->getUnionType()->addUnionType($this->right_type); return $this->context; } $std_class_fqsen = FullyQualifiedClassName::getStdClassFQSEN(); if (Config::get()->allow_missing_properties || !empty($class_list) && $class_list[0]->getFQSEN() == $std_class_fqsen) { try { // Create the property $property = (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name); $property->getUnionType()->addUnionType($this->right_type); } catch (\Exception $exception) { // swallow it } } elseif (!empty($class_list)) { $this->emitIssue(Issue::UndeclaredProperty, $node->lineno ?? 0, "{$class_list[0]->getFQSEN()}->{$property_name}"); } else { // If we hit this part, we couldn't figure out // the class, so we ignore the issue } return $this->context; }
/** * @return Comment * A comment built by parsing the given doc block * string. */ public static function fromStringInContext(string $comment, Context $context) : Comment { if (!Config::get()->read_type_annotations) { return new Comment(false, [], [], [], new None(), new UnionType(), []); } $is_deprecated = false; $variable_list = []; $parameter_list = []; $template_type_list = []; $inherited_type = new None(); $return_union_type = new UnionType(); $suppress_issue_list = []; $lines = explode("\n", $comment); foreach ($lines as $line) { if (strpos($line, '@param') !== false) { $parameter_list[] = self::parameterFromCommentLine($context, $line); } elseif (stripos($line, '@var') !== false) { $variable_list[] = self::parameterFromCommentLine($context, $line); } elseif (stripos($line, '@template') !== false) { // Make sure support for generic types is enabled if (Config::get()->generic_types_enabled) { if ($template_type = self::templateTypeFromCommentLine($context, $line)) { $template_type_list[] = $template_type; } } } elseif (stripos($line, '@inherits') !== false) { // Make sure support for generic types is enabled if (Config::get()->generic_types_enabled) { $inherited_type = self::inheritsFromCommentLine($context, $line); } } elseif (stripos($line, '@return') !== false) { $return_union_type = self::returnTypeFromCommentLine($context, $line); } elseif (stripos($line, '@suppress') !== false) { $suppress_issue_list[] = self::suppressIssueFromCommentLine($line); } if (($pos = stripos($line, '@deprecated')) !== false) { if (preg_match('/@deprecated\\b/', $line, $match)) { $is_deprecated = true; } } } return new Comment($is_deprecated, $variable_list, $parameter_list, $template_type_list, $inherited_type, $return_union_type, $suppress_issue_list); }
/** * @return string * A string representation of the union type begotten from * the first statement in the statement list in the given * code. */ private function typeStringFromCode(string $code) : string { return UnionType::fromNode($this->context, $this->code_base, \ast\parse_code($code, Config::get()->ast_version)->children[0])->asExpandedTypes($this->code_base)->__toString(); }
/** * Once we know what the universe looks like we * can scan for more complicated issues. * * @param CodeBase $code_base * The global code base holding all state * * @param string $file_path * A list of files to scan * * @return Context */ public static function analyzeFile(CodeBase $code_base, string $file_path) : Context { // Set the file on the context $context = (new Context())->withFile($file_path); // Convert the file to an Abstract Syntax Tree // before passing it on to the recursive version // of this method try { $node = \ast\parse_file(Config::projectPath($file_path), Config::get()->ast_version); } catch (\ParseError $parse_error) { Issue::maybeEmit($code_base, $context, Issue::SyntaxError, $parse_error->getLine(), $parse_error->getMessage()); return $context; } // Ensure we have some content if (empty($node)) { Issue::maybeEmit($code_base, $context, Issue::EmptyFile, 0, $file_path); return $context; } return (new BlockAnalysisVisitor($code_base, $context))($node); }
/** * Perform some backwards compatibility checks on a node * * @return void */ public function analyzeBackwardCompatibility() { if (!Config::get()->backward_compatibility_checks) { return; } if (empty($this->node->children['expr'])) { return; } if ($this->node->kind === \ast\AST_STATIC_CALL || $this->node->kind === \ast\AST_METHOD_CALL) { return; } $llnode = $this->node; if ($this->node->kind !== \ast\AST_DIM) { if (!$this->node->children['expr'] instanceof Node) { return; } if ($this->node->children['expr']->kind !== \ast\AST_DIM) { (new ContextNode($this->code_base, $this->context, $this->node->children['expr']))->analyzeBackwardCompatibility(); return; } $temp = $this->node->children['expr']->children['expr']; $llnode = $this->node->children['expr']; $lnode = $temp; } else { $temp = $this->node->children['expr']; $lnode = $temp; } if (!($temp->kind == \ast\AST_PROP || $temp->kind == \ast\AST_STATIC_PROP)) { return; } while ($temp instanceof Node && ($temp->kind == \ast\AST_PROP || $temp->kind == \ast\AST_STATIC_PROP)) { $llnode = $lnode; $lnode = $temp; // Lets just hope the 0th is the expression // we want $temp = array_values($temp->children)[0]; } if (!$temp instanceof Node) { return; } // Foo::$bar['baz'](); is a problem // Foo::$bar['baz'] is not if ($lnode->kind === \ast\AST_STATIC_PROP && $this->node->kind !== \ast\AST_CALL) { return; } // $this->$bar['baz']; is a problem // $this->bar['baz'] is not if ($lnode->kind === \ast\AST_PROP && !$lnode->children['prop'] instanceof Node && !$llnode->children['prop'] instanceof Node) { return; } if (($lnode->children['prop'] instanceof Node && $lnode->children['prop']->kind == \ast\AST_VAR || !empty($lnode->children['class']) && $lnode->children['class'] instanceof Node && ($lnode->children['class']->kind == \ast\AST_VAR || $lnode->children['class']->kind == \ast\AST_NAME) || !empty($lnode->children['expr']) && $lnode->children['expr'] instanceof Node && ($lnode->children['expr']->kind == \ast\AST_VAR || $lnode->children['expr']->kind == \ast\AST_NAME)) && ($temp->kind == \ast\AST_VAR || $temp->kind == \ast\AST_NAME)) { $ftemp = new \SplFileObject($this->context->getFile()); $ftemp->seek($this->node->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '}[') === false || strpos($line, ']}') === false || strpos($line, '>{') === false) { Issue::maybeEmit($this->code_base, $this->context, Issue::CompatiblePHP7, $this->node->lineno ?? 0); } } }
/** * Analyze the parameters and arguments for a call * to the given method or function * * @param CodeBase $code_base * @param Method $method * @param Node $node * * @return null */ private function analyzeCallToMethod(CodeBase $code_base, Method $method, Node $node) { if (Database::isEnabled()) { // Store the call to the method so we can track // dependencies later (new CalledBy((string) $method->getFQSEN(), $this->context))->write(Database::get()); } // Create variables for any pass-by-reference // parameters $argument_list = $node->children['args']; foreach ($argument_list->children as $i => $argument) { $parameter = $method->getParameterList()[$i] ?? null; if (!$parameter) { continue; } // If pass-by-reference, make sure the variable exists // or create it if it doesn't. if ($parameter->isPassByReference()) { if ($argument->kind == \ast\AST_VAR) { // We don't do anything with it; just create it // if it doesn't exist $variable = AST::getOrCreateVariableFromNodeInContext($argument, $this->context, $this->code_base); } else { if ($argument->kind == \ast\AST_STATIC_PROP || $argument->kind == \ast\AST_PROP) { $property_name = $argument->children['prop']; if (is_string($property_name)) { // We don't do anything with it; just create it // if it doesn't exist try { $property = AST::getOrCreatePropertyFromNodeInContext($argument->children['prop'], $argument, $this->context, $this->code_base); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, $exception->getMessage(), $this->context->getFile(), $node->lineno); } catch (NodeException $exception) { // If we can't figure out what kind of a call // this is, don't worry about it } } else { // This is stuff like `Class->$foo`. I'm ignoring // it. } } } } } // Confirm the argument types are clean ArgumentType::analyze($method, $node, $this->context, $this->code_base); // Take another pass over pass-by-reference parameters // and assign types to passed in variables foreach ($argument_list->children as $i => $argument) { $parameter = $method->getParameterList()[$i] ?? null; if (!$parameter) { continue; } // If the parameter is pass-by-reference and we're // passing a variable in, see if we should pass // the parameter and variable types to eachother $variable = null; if ($parameter->isPassByReference()) { if ($argument->kind == \ast\AST_VAR) { $variable = AST::getOrCreateVariableFromNodeInContext($argument, $this->context, $this->code_base); } else { if ($argument->kind == \ast\AST_STATIC_PROP || $argument->kind == \ast\AST_PROP) { $property_name = $argument->children['prop']; if (is_string($property_name)) { // We don't do anything with it; just create it // if it doesn't exist try { $variable = AST::getOrCreatePropertyFromNodeInContext($argument->children['prop'], $argument, $this->context, $this->code_base); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, $exception->getMessage(), $this->context->getFile(), $node->lineno); } catch (NodeException $exception) { // If we can't figure out what kind of a call // this is, don't worry about it } } else { // This is stuff like `Class->$foo`. I'm ignoring // it. } } } if ($variable) { $variable->getUnionType()->addUnionType($parameter->getUnionType()); } } } // If we're in quick mode, don't retest methods based on // parameter types passed in if (Config::get()->quick_mode) { return; } // We're going to hunt to see if any of the arguments // have a mismatch with the parameters. If so, we'll // re-check the method to see how the parameters impact // its return type $has_argument_parameter_mismatch = false; // Now that we've made sure the arguments are sufficient // for definitions on the method, we iterate over the // arguments again and add their types to the parameter // types so we can test the method again $argument_list = $node->children['args']; // We create a copy of the parameter list so we can switch // back to it after $original_parameter_list = $method->getParameterList(); foreach ($argument_list->children as $i => $argument) { $parameter = $method->getParameterList()[$i] ?? null; if (!$parameter) { continue; } // If the parameter has no type, pass the // argument's type to it if ($parameter->getUnionType()->isEmpty()) { $has_argument_parameter_mismatch = true; $argument_type = UnionType::fromNode($this->context, $this->code_base, $argument); // If this isn't an internal function or method // and it has no type, add the argument's type // to it so we can compare it to subsequent // calls if (!$parameter->getContext()->isInternal()) { // Clone the parameter in the original // parameter list so we can reset it // later $original_parameter_list[$i] = clone $parameter; // Then set the new type on that parameter based // on the argument's type. We'll use this to // retest the method with the passed in types $parameter->getUnionType()->addUnionType($argument_type); } } } // Now that we know something about the parameters used // to call the method, we can reanalyze the method with // the types of the parameter, making sure we don't get // into an infinite loop of checking calls to the current // method in scope if ($has_argument_parameter_mismatch && !$method->getContext()->isInternal() && (!$this->context->isMethodScope() || $method->getFQSEN() !== $this->context->getMethodFQSEN())) { $method->analyze($method->getContext(), $code_base); } // Reset to the original parameter list after having // tested the parameters with the types passed in $method->setParameterList($original_parameter_list); }
/** * Visit a node with kind `\ast\AST_RETURN` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitReturn(Node $node) : Context { if (Config::get()->backward_compatibility_checks) { (new ContextNode($this->code_base, $this->context, $node))->analyzeBackwardCompatibility(); } // Make sure we're actually returning from a method. if (!$this->context->isMethodScope() && !$this->context->isClosureScope()) { return $this->context; } // Get the method/function/closure we're in $method = null; if ($this->context->isClosureScope()) { $method = $this->context->getClosureInScope($this->code_base); } elseif ($this->context->isMethodScope()) { $method = $this->context->getMethodInScope($this->code_base); } assert(!empty($method), "We're supposed to be in either method or closure scope."); // Mark the method as returning something $method->setHasReturn(true); return $this->context; }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitProp(Node $node) : Context { $property_name = $node->children['prop']; // Things like $foo->$bar if (!is_string($property_name)) { return $this->context; } assert(is_string($property_name), "Property must be string in context {$this->context}"); try { $clazz = (new ContextNode($this->code_base, $this->context, $node))->getClass(); } catch (CodeBaseException $exception) { Log::err(Log::EFATAL, $exception->getMessage(), $this->context->getFile(), $node->lineno); } catch (\Exception $exception) { // If we can't figure out what kind of a class // this is, don't worry about it return $this->context; } if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { // Check to see if the class has a __set method if (!$clazz->hasMethodWithName($this->code_base, '__set')) { if (Config::get()->allow_missing_properties) { try { // Create the property (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name); } catch (\Exception $exception) { // swallow it } } else { Log::err(Log::EAVAIL, "Missing property with name '{$property_name}'", $this->context->getFile(), $node->lineno); } } return $this->context; } try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (AccessException $exception) { Log::err(Log::EACCESS, $exception->getMessage(), $this->context->getFile(), $node->lineno); return $this->context; } if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) { Log::err(Log::ETYPE, "assigning {$this->right_type} to property but {$clazz->getFQSEN()}::{$property->getName()} is {$property->getUnionType()}", $this->context->getFile(), $node->lineno); return $this->context; } // After having checked it, add this type to it $property->getUnionType()->addUnionType($this->right_type); return $this->context; }
/** * Look for a .phan/config file up to a few directories * up the hierarchy and apply anything in there to * the configuration. */ private function maybeReadConfigFile() { // If the file doesn't exist here, try a directory up $config_file_name = implode(DIRECTORY_SEPARATOR, [Config::get()->getProjectRootDirectory(), '.phan', 'config.php']); // Totally cool if the file isn't there if (!file_exists($config_file_name)) { return; } // Read the configuration file $config = (require $config_file_name); // Write each value to the config foreach ($config as $key => $value) { Config::get()->__set($key, $value); } }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitProp(Node $node) : Context { $property_name = $node->children['prop']; // Things like $foo->$bar if (!is_string($property_name)) { return $this->context; } assert(is_string($property_name), "Property must be string in context {$this->context}"); try { $class_list = (new ContextNode($this->code_base, $this->context, $node->children['expr']))->getClassList(); } catch (CodeBaseException $exception) { // This really shouldn't happen since the code // parsed cleanly. This should fatal. // throw $exception; return $this->context; } catch (\Exception $exception) { // If we can't figure out what kind of a class // this is, don't worry about it return $this->context; } foreach ($class_list as $clazz) { // Check to see if this class has the property or // a setter if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) { if (!$clazz->hasMethodWithName($this->code_base, '__set')) { continue; } } try { $property = $clazz->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (IssueException $exception) { Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance()); return $this->context; } if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) { $this->emitIssue(Issue::TypeMismatchProperty, $node->lineno ?? 0, (string) $this->right_type, "{$clazz->getFQSEN()}::{$property->getName()}", (string) $property->getUnionType()); return $this->context; } // After having checked it, add this type to it $property->getUnionType()->addUnionType($this->right_type); return $this->context; } if (Config::get()->allow_missing_properties) { try { // Create the property (new ContextNode($this->code_base, $this->context, $node))->getOrCreateProperty($property_name); } catch (\Exception $exception) { // swallow it } } elseif (!empty($class_list)) { $this->emitIssue(Issue::UndeclaredProperty, $node->lineno ?? 0, $property_name); } else { // If we hit this part, we couldn't figure out // the class, so we ignore the issue } return $this->context; }
/** * @return bool * True if the database is enabled */ public static function isEnabled() : bool { return (bool) Config::get()->stored_state_file_path; }
/** * @param UnionType $target * A type to check to see if this can cast to it * * @return bool * True if this type is allowed to cast to the given type * i.e. int->float is allowed while float->int is not. * * @see \Phan\Deprecated\Pass2::type_check * Formerly 'function type_check' */ public function canCastToUnionType(UnionType $target) : bool { // Fast-track most common cases first // If either type is unknown, we can't call it // a success if ($this->isEmpty() || $target->isEmpty()) { return true; } // T === T if ($this->isEqualTo($target)) { return true; } if (Config::get()->null_casts_as_any_type) { // null <-> null if ($this->isType(NullType::instance()) || $target->isType(NullType::instance())) { return true; } } // mixed <-> mixed if ($target->hasType(MixedType::instance()) || $this->hasType(MixedType::instance())) { return true; } // int -> float if ($this->isType(IntType::instance()) && $target->isType(FloatType::instance())) { return true; } // Check conversion on the cross product of all // type combinations and see if any can cast to // any. foreach ($this->getTypeList() as $source_type) { if (empty($source_type)) { continue; } foreach ($target->getTypeList() as $target_type) { if (empty($target_type)) { continue; } if ($source_type->canCastToType($target_type)) { return true; } } } // Only if no source types can be cast to any target // types do we say that we cannot perform the cast return false; }