See ./phan -h for command line usage, or take a look at \Phan\CLI.php for more details on CLI usage.
Beispiel #1
0
 /**
  * @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());
     }
 }
Beispiel #3
0
 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);
 }
Beispiel #4
0
 /**
  * @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;
 }
Beispiel #5
0
 /**
  * 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());
 }
Beispiel #6
0
 /**
  * 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);
     }
 }
Beispiel #7
0
 /**
  * @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;
 }
Beispiel #8
0
 /**
  * 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);
     }
 }
Beispiel #9
0
 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];
     }
 }
Beispiel #10
0
 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());
 }
Beispiel #11
0
 /**
  * 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));
 }
Beispiel #12
0
 /**
  * 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;
 }
Beispiel #13
0
 /**
  * @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]);
 }
Beispiel #14
0
 /**
  * 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());
         }
     }
 }
Beispiel #15
0
 /**
  * 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);
 }
Beispiel #16
0
 /**
  * @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;
 }
Beispiel #17
0
 /**
  * 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();
     }
 }
Beispiel #18
0
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);
Beispiel #19
0
 /**
  * @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;
 }
Beispiel #20
0
 /**
  * @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);
 }
Beispiel #21
0
 /**
  * @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();
 }
Beispiel #22
0
 /**
  * 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);
 }
Beispiel #23
0
 /**
  * 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);
         }
     }
 }
Beispiel #24
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);
 }
Beispiel #25
0
 /**
  * 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;
 }
Beispiel #26
0
 /**
  * @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;
 }
Beispiel #27
0
 /**
  * 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);
     }
 }
Beispiel #28
0
 /**
  * @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;
 }
Beispiel #29
0
 /**
  * @return bool
  * True if the database is enabled
  */
 public static function isEnabled() : bool
 {
     return (bool) Config::get()->stored_state_file_path;
 }
Beispiel #30
0
 /**
  * @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;
 }