/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeDuplicateClass(CodeBase $code_base, Clazz $clazz) { // Determine if its a duplicate by looking to see if // the FQSEN is suffixed with an alternate ID. if (!$clazz->getFQSEN()->isAlternate()) { return; } $original_fqsen = $clazz->getFQSEN()->getCanonicalFQSEN(); if (!$code_base->hasClassWithFQSEN($original_fqsen)) { // If there's a missing class we'll catch that // elsewhere return; } // Get the original class $original_class = $code_base->getClassByFQSEN($original_fqsen); // Check to see if the original definition was from // an internal class if ($original_class->isInternal()) { Log::err(Log::EREDEF, "{$clazz} defined at " . "{$clazz->getContext()->getFile()}:{$clazz->getContext()->getLineNumberStart()} " . "was previously defined as {$original_class} internally", $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); // Otherwise, print the coordinates of the original // definition } else { Log::err(Log::EREDEF, "{$clazz} defined at " . "{$clazz->getContext()->getFile()}:{$clazz->getContext()->getLineNumberStart()} " . "was previously defined as {$original_class} at " . "{$original_class->getContext()->getFile()}:{$original_class->getContext()->getLineNumberStart()}", $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); } return; }
/** * 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()) { Log::err(Log::ETYPE, "{$clazz->getFQSEN()} extends {$parent_clazz->getFQSEN()} but doesn't call parent::__construct()", $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); } }
/** * @return bool * True if the FQSEN exists. If not, a log line is emitted */ private static function fqsenExistsForClass(FQSEN $fqsen, CodeBase $code_base, Clazz $clazz, string $message_template) : bool { if (!$code_base->hasClassWithFQSEN($fqsen)) { Log::err(Log::EUNDEF, sprintf($message_template, $fqsen), $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); return false; } return true; }
/** * @return bool * True if the FQSEN exists. If not, a log line is emitted */ private static function fqsenExistsForClass(FQSEN $fqsen, CodeBase $code_base, Clazz $clazz) : bool { if (!$code_base->hasClassWithFQSEN($fqsen)) { Log::err(Log::EUNDEF, "Trying to inherit from unknown class {$fqsen}", $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart()); return false; } return true; }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzePropertyTypes(CodeBase $code_base, Clazz $clazz) { foreach ($clazz->getPropertyList($code_base) as $property) { $union_type = $property->getUnionType(); // Look at each type in the parameter's Union Type foreach ($union_type->getTypeList() as $type) { // If its a native type or a reference to // self, its OK if ($type->isNativeType() || $type->isSelfType()) { continue; } // Otherwise, make sure the class exists $type_fqsen = $type->asFQSEN(); if (!$code_base->hasClassWithFQSEN($type_fqsen)) { Log::err(Log::EUNDEF, "property of undeclared type {$type_fqsen}", $property->getContext()->getFile(), $property->getContext()->getLineNumberStart()); } } } }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeDuplicateFunction(CodeBase $code_base, Method $method) { $fqsen = $method->getFQSEN(); if (!$fqsen->isAlternate()) { return; } $original_fqsen = $fqsen->getCanonicalFQSEN(); if (!$code_base->hasMethod($original_fqsen)) { return; } $original_method = $code_base->getMethod($original_fqsen); $method_name = $method->getName(); if ('internal' === $original_method->getContext()->getFile()) { // If its in an conditional and the original is an // internal method, presume its all OK. if ($method->getContext()->getIsConditional()) { return; } Log::err(Log::EREDEF, "Function {$method_name} defined at {$method->getContext()->getFile()}:{$method->getContext()->getLineNumberStart()} was previously defined internally", $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()); } else { Log::err(Log::EREDEF, "Function {$method_name} defined at {$method->getContext()->getFile()}:{$method->getContext()->getLineNumberStart()} was previously defined at {$original_method->getContext()->getFile()}:{$original_method->getContext()->getLineNumberStart()}", $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()); } }
/** * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return string * The class name represented by the given call */ public function visitNew(Node $node) : string { // Things of the form `new $class_name();` if ($node->children['class']->kind == \ast\AST_VAR) { return ''; } // Things of the form `new $method->name()` if ($node->children['class']->kind !== \ast\AST_NAME) { return ''; } $class_name = $node->children['class']->children['name']; if (!in_array($class_name, ['self', 'static', 'parent'])) { return AST::qualifiedName($this->context, $node->children['class']); } if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, "Cannot access {$class_name}:: when no class scope is active", $this->context->getFile(), $node->lineno); return ''; } if ($class_name == 'static') { return (string) $this->context->getClassFQSEN(); } if ($class_name == 'self') { if ($this->context->isGlobalScope()) { assert(false, "Unimplemented branch is required for {$this->context}"); } else { return (string) $this->context->getClassFQSEN(); } } if ($class_name == 'parent') { $clazz = $this->context->getClassInScope($this->code_base); if (!$clazz->hasParentClassFQSEN()) { return ''; } return (string) $clazz->getParentClassFQSEN(); } return ''; }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeElementReferenceCounts(CodeBase $code_base, TypedStructuralElement $element) { // Don't worry about internal elements if ($element->getContext()->isInternal()) { return; } if ($element->getReferenceCount($code_base) < 1) { if ($element instanceof Addressable) { Log::err(Log::ENOOP, "{$element->getFQSEN()} may have zero references", $element->getContext()->getFile(), $element->getContext()->getLineNumberStart()); } else { Log::err(Log::ENOOP, "{$element} may have zero references", $element->getContext()->getFile(), $element->getContext()->getLineNumberStart()); } } }
/** * @return bool * False if the class name doesn't point to a known class */ private function classExistsOrIsNative(Node $node) : bool { if ($this->classExists()) { return true; } $type = UnionType::fromStringInContext($this->class_name, $this->context); if ($type->isNativeType()) { return true; } Log::err(Log::EUNDEF, "reference to undeclared class {$this->class_fqsen}", $this->context->getFile(), $node->lineno); return false; }
/** * Create and read command line arguments, configuring * \Phan\Config as a side effect. */ public function __construct() { global $argv; // file_put_contents('/tmp/file', implode("\n", $argv)); // Parse command line args // still available: g,j,k,n,t,u,v,w,z $opts = getopt("f:m:o:c:aeqbrpid:s:3:y:l:xh::", ['fileset:', 'output-mode:', 'output:', 'parent-constructor-required:', 'expanded-dependency-list', 'dump-ast', 'quick', 'backward-compatibility-checks', 'reanalyze-file-list', 'progress-bar', 'ignore-undeclared', 'project-root-directory:', 'state-file:', 'exclude-directory-list:', 'minimum-severity:', 'directory:', 'dead-code-detection', 'help']); // Determine the root directory of the project from which // we root all relative paths passed in as args Config::get()->setProjectRootDirectory($opts['d'] ?? getcwd()); // Now that we have a root directory, attempt to read a // configuration file `.phan/config.php` if it exists $this->maybeReadConfigFile(); foreach ($opts ?? [] as $key => $value) { switch ($key) { case 'h': case 'help': $this->usage(); break; case 'f': case 'fileset': $file_list = is_array($value) ? $value : [$value]; foreach ($file_list as $file_name) { if (is_file($file_name) && is_readable($file_name)) { $this->file_list = array_merge($this->file_list, file($file_name, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); } else { error_log("Unable to read file {$file_name}"); } } break; case 'l': case 'directory': $directory_list = is_array($value) ? $value : [$value]; foreach ($directory_list as $directory_name) { $this->file_list = array_merge($this->file_list, $this->directoryNameToFileList($directory_name)); } break; case 'm': case 'output-mode': if (!in_array($value, ['text', 'codeclimate'])) { $this->usage("Unknown output mode: {$value}"); } Log::setOutputMode($value); break; case 'c': case 'parent-constructor-required': Config::get()->parent_constructor_required = explode(',', $value); break; case 'q': case 'quick': Config::get()->quick_mode = true; break; case 'b': case 'backward-compatibility-checks': Config::get()->backward_compatibility_checks = true; break; case 'p': case 'progress-bar': Config::get()->progress_bar = true; break; case 'a': case 'dump-ast': Config::get()->dump_ast = true; break; case 'e': case 'expanded-dependency-list': Config::get()->expanded_dependency_list = true; break; case 'o': case 'output': Log::setFilename($value); break; case 'i': case 'ignore-undeclared': Log::setOutputMask(Log::getOutputMask() ^ Issue::CATEGORY_UNDEFINED); break; case '3': case 'exclude-directory-list': Config::get()->exclude_analysis_directory_list = explode(',', $value); break; case 's': case 'state-file': Config::get()->stored_state_file_path = $value; break; case 'r': case 'reanalyze-file-list': Config::get()->reanalyze_file_list = true; break; case 'y': case 'minimum-severity': Config::get()->minimum_severity = $value; break; case 'd': case 'project-root-directory': // We handle this flag before parsing options so // that we can get the project root directory to // base other config flags values on break; case 'x': case 'dead-code-detection': Config::get()->dead_code_detection = true; break; default: $this->usage("Unknown option '-{$key}'"); break; } } $pruneargv = array(); foreach ($opts ?? [] as $opt => $value) { foreach ($argv as $key => $chunk) { $regex = '/^' . (isset($opt[1]) ? '--' : '-') . $opt . '/'; if ($chunk == $value && $argv[$key - 1][0] == '-' || preg_match($regex, $chunk)) { array_push($pruneargv, $key); } } } while ($key = array_pop($pruneargv)) { unset($argv[$key]); } foreach ($argv as $arg) { if ($arg[0] == '-') { $this->usage("Unknown option '{$arg}'"); } } // Merge in any remaining args on the CLI $this->file_list = array_merge($this->file_list, array_slice($argv, 1)); // Merge in any files given in the config $this->file_list = array_merge($this->file_list, Config::get()->file_list); // Merge in any directories given in the config foreach (Config::get()->directory_list as $directory_name) { $this->file_list = array_merge($this->file_list, $this->directoryNameToFileList($directory_name)); } }
/** * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return string * The class name represented by the given call */ public function visitNew(Node $node) : string { // Things of the form `new $class_name();` if ($node->children['class']->kind == \ast\AST_VAR) { return ''; } // Anonymous class // $v = new class { ... } if ($node->children['class']->kind == \ast\AST_CLASS && $node->children['class']->flags & \ast\flags\CLASS_ANONYMOUS) { return (new ContextNode($this->code_base, $this->context, $node->children['class']))->getUnqualifiedNameForAnonymousClass(); } // Things of the form `new $method->name()` if ($node->children['class']->kind !== \ast\AST_NAME) { return ''; } $class_name = $node->children['class']->children['name']; if (!in_array($class_name, ['self', 'static', 'parent'])) { return (string) UnionTypeVisitor::unionTypeFromClassNode($this->code_base, $this->context, $node->children['class']); } if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, "Cannot access {$class_name}:: when no class scope is active", $this->context->getFile(), $node->lineno); return ''; } if ($class_name == 'static') { return (string) $this->context->getClassFQSEN(); } if ($class_name == 'self') { if ($this->context->isGlobalScope()) { assert(false, "Unimplemented branch is required for {$this->context}"); } else { return (string) $this->context->getClassFQSEN(); } } if ($class_name == 'parent') { $clazz = $this->context->getClassInScope($this->code_base); if (!$clazz->hasParentClassFQSEN()) { return ''; } return (string) $clazz->getParentClassFQSEN(); } return ''; }
private function visitClassNode(Node $node) : UnionType { // Things of the form `new $class_name();` if ($node->kind == \ast\AST_VAR) { return new UnionType(); } // Anonymous class of form `new class { ... }` if ($node->kind == \ast\AST_CLASS && $node->flags & \ast\flags\CLASS_ANONYMOUS) { // Generate a stable name for the anonymous class $anonymous_class_name = (new ContextNode($this->code_base, $this->context, $node))->getUnqualifiedNameForAnonymousClass(); // Turn that into a fully qualified name $fqsen = FullyQualifiedClassName::fromStringInContext($anonymous_class_name, $this->context); // Turn that into a union type return Type::fromFullyQualifiedString((string) $fqsen)->asUnionType(); } // Things of the form `new $method->name()` if ($node->kind !== \ast\AST_NAME) { return new UnionType(); } // Get the name of the class $class_name = $node->children['name']; // If this is a straight-forward class name, recurse into the // class node and get its type if (!Type::isSelfTypeString($class_name)) { // TODO: does anyone else call this method? return self::unionTypeFromClassNode($this->code_base, $this->context, $node); } // This is a self-referential node if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, "Cannot access {$class_name} when not in a class scope", $this->context->getFile(), $node->lineno); return new UnionType(); } // Reference to a parent class if ($class_name === 'parent') { $class = $this->context->getClassInScope($this->code_base); if (!$class->hasParentClassFQSEN()) { Log::err(Log::ESTATIC, "Reference to parent of parentless class {$class->getFQSEN()}", $this->context->getFile(), $node->lineno); return new UnionType(); } return Type::fromFullyQualifiedString((string) $class->getParentClassFQSEN())->asUnionType(); } return Type::fromFullyQualifiedString((string) $this->context->getClassFQSEN())->asUnionType(); }
/** * @param Node $node * A node to check types on * * @return UnionType * The resulting type(s) of the binary operation */ public function visitBinaryAdd(Node $node) : UnionType { $left = UnionType::fromNode($this->context, $this->code_base, $node->children['left']); $right = UnionType::fromNode($this->context, $this->code_base, $node->children['right']); // fast-track common cases if ($left->isType(IntType::instance()) && $right->isType(IntType::instance())) { return IntType::instance()->asUnionType(); } if (($left->isType(IntType::instance()) || $left->isType(FloatType::instance())) && ($right->isType(IntType::instance()) || $right->isType(FloatType::instance()))) { return FloatType::instance()->asUnionType(); } $left_is_array = !empty($left->genericArrayElementTypes()) && empty($left->nonGenericArrayTypes()); $right_is_array = !empty($right->genericArrayElementTypes()) && empty($right->nonGenericArrayTypes()); if ($left_is_array && !$right->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::ETYPE, "invalid operator: left operand is array and right is not", $this->context->getFile(), $node->lineno); return new UnionType(); } else { if ($right_is_array && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::ETYPE, "invalid operator: right operand is array and left is not", $this->context->getFile(), $node->lineno); return new UnionType(); } else { if ($left_is_array || $right_is_array) { // If it is a '+' and we know one side is an array // and the other is unknown, assume array return ArrayType::instance()->asUnionType(); } } } return new UnionType([IntType::instance(), FloatType::instance()]); }
/** * Check to see if the given Clazz is a duplicate * * @param Method $method * The method we're analyzing arguments for * * @param Node $node * The node holding the method call we're looking at * * @param Context $context * The context in which we see the call * * @param CodeBase $code_base * * @return null * * @see \Phan\Deprecated\Pass2::arg_check * Formerly `function arg_check` */ private static function analyzeInternalArgumentType(Method $method, Node $node, Context $context, CodeBase $code_base) { $arglist = $node->children['args']; $argcount = count($arglist->children); switch ($method->getName()) { case 'join': case 'implode': // (string glue, array pieces), // (array pieces, string glue) or // (array pieces) if ($argcount == 1) { self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(pieces) is %s but {$method->getFQSEN()}() takes array when passed only 1 arg"); return; } else { if ($argcount == 2) { $arg1_type = UnionType::fromNode($context, $code_base, $arglist->children[0]); $arg2_type = UnionType::fromNode($context, $code_base, $arglist->children[1]); if ((string) $arg1_type == 'array') { if (!$arg1_type->canCastToUnionType(StringType::instance()->asUnionType())) { Log::err(Log::EPARAM, "arg#2(glue) is {$arg2_type} but {$method->getFQSEN()}() takes string when arg#1 is array", $context->getFile(), $context->getLineNumberStart()); } } else { if ((string) $arg1_type == 'string') { if (!$arg2_type->canCastToUnionType(ArrayType::instance()->asUnionType())) { Log::err(Log::EPARAM, "arg#2(pieces) is {$arg2_type} but {$method->getFQSEN()}() takes array when arg#1 is string", $context->getFile(), $context->getLineNumberStart()); } } } return; } } // Any other arg counts we will let the regular // checks handle break; case 'array_udiff': case 'array_diff_uassoc': case 'array_uintersect_assoc': case 'array_intersect_ukey': if ($argcount < 3) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$method->getFQSEN()}() which requires {$method->getNumberOfRequiredParameters()} arg(s)", $context->getFile(), $context->getLineNumberStart()); return; } self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), "The last argument to {$method->getFQSEN()} must be a callable"); for ($i = 0; $i < $argcount - 1; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, CallableType::instance()->asUnionType(), "arg#" . ($i + 1) . " is %s but {$method->getFQSEN()}() takes array"); } return; case 'array_diff_uassoc': case 'array_uintersect_uassoc': if ($argcount < 4) { Log::err(Log::EPARAM, "call with {$argcount} arg(s) to {$method->getFQSEN()}() which requires {$method->getNumberOfRequiredParameters()} arg(s)", $context->getFile(), $context->getLineNumberStart()); return; } // The last 2 arguments must be a callable and there // can be a variable number of arrays before it self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), "The last argument to {$method->getFQSEN()} must be a callable"); self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 2], $context, $code_base, CallableType::instance()->asUnionType(), "The second last argument to {$method->getFQSEN()} must be a callable"); for ($i = 0; $i < $argcount - 2; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#" . ($i + 1) . " is %s but {$method->getFQSEN()}() takes array"); } return; case 'strtok': // (string str, string token) or (string token) if ($argcount == 1) { // If we have just one arg it must be a string token self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(token) is %s but {$method->getFQSEN()}() takes string when passed only one arg"); } // The arginfo check will handle the other case break; case 'min': case 'max': if ($argcount == 1) { // If we have just one arg it must be an array if (!self::analyzeNodeUnionTypeCast($arglist->children[0], $context, $code_base, ArrayType::instance()->asUnionType(), "arg#1(values) is %s but {$method->getFQSEN()}() takes array when passed only one arg")) { return; } } // The arginfo check will handle the other case break; default: break; } }
/** * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return string * The class name represented by the given call */ public function visitProp(Node $node) : string { if (!($node->children['expr']->kind == \ast\AST_VAR && !$node->children['expr']->children['name'] instanceof Node)) { return ''; } // $var->prop->method() $var = $node->children['expr']; $class = null; if ($var->children['name'] == 'this') { // If we're not in a class scope, 'this' won't work if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } // $this->$node->method() if ($node->children['prop'] instanceof Node) { // Too hard. Giving up. return ''; } $class = $this->context->getClassInScope($this->code_base); } else { // Get the list of viable class types for the // variable $union_type = AST::varUnionType($this->context, $var)->nonNativeTypes()->nonGenericArrayTypes(); if ($union_type->isEmpty()) { return ''; } $class_fqsen = $union_type->head()->asFQSEN(); if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) { return ''; } $class = $this->code_base->getClassByFQSEN($class_fqsen); } $property_name = $node->children['prop']; if (!$class->hasPropertyWithName($this->code_base, $property_name)) { // If we can't find the property, there's // no type. Thie issue should be caught // elsewhere. return ''; } try { $property = $class->getPropertyByNameInContext($this->code_base, $property_name, $this->context); } catch (AccessException $exception) { Log::err(Log::EACCESS, $exception->getMessage(), $this->context->getFile(), $node->lineno); return ''; } $union_type = $property->getUnionType()->nonNativeTypes(); if ($union_type->isEmpty()) { // If we don't have a type on the property we // can't figure out the class type. return ''; } else { // Return the first type on the property // that could be a reference to a class return (string) $union_type->head()->asFQSEN(); } // No such property was found, or none were classes // that could be found return ''; }
/** * Create and read command line arguments, configuring * \Phan\Config as a side effect. */ public function __construct() { global $argv; // file_put_contents('/tmp/file', implode("\n", $argv)); // Parse command line args $opts = getopt("f:m:o:c:haqbrpid:s:3:t::"); // Determine the root directory of the project from which // we root all relative paths passed in as args Config::get()->setProjectRootDirectory($opts['d'] ?? getcwd()); // Now that we have a root directory, attempt to read a // configuration file `.phan/config.php` if it exists $this->maybeReadConfigFile(); foreach ($opts ?? [] as $key => $value) { switch ($key) { case 'h': $this->usage(); break; case 'f': if (is_file($value) && is_readable($value)) { $this->file_list = file($value, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); } else { Log::err(Log::EFATAL, "Unable to open {$value}"); } break; case 'm': if (!in_array($value, ['text', 'codeclimate'])) { $this->usage("Unknown output mode: {$value}"); } Log::setOutputMode($value); break; case 'c': Config::get()->parent_constructor_required = explode(',', $value); break; case 'q': Config::get()->quick_mode = true; break; case 'b': Config::get()->backward_compatibility_checks = true; break; case 'p': Config::get()->progress_bar = true; break; case 'a': Config::get()->dump_ast = true; break; case 'o': Log::setFilename($value); break; case 'i': Log::setOutputMask(Log::getOutputMask() ^ Log::EUNDEF); break; case 't': Config::get()->emit_trace_id = true; break; case '3': Config::get()->exclude_analysis_directory_list = explode(',', $value); break; case 's': Config::get()->stored_state_file_path = $value; break; case 'r': Config::get()->reanalyze_file_list = true; break; case 'd': // We handle this flag before parsing options so // that we can get the project root directory to // base other config flags values on break; default: $this->usage("Unknown option '-{$key}'"); break; } } $pruneargv = array(); foreach ($opts ?? [] as $opt => $value) { foreach ($argv as $key => $chunk) { $regex = '/^' . (isset($opt[1]) ? '--' : '-') . $opt . '/'; if ($chunk == $value && $argv[$key - 1][0] == '-' || preg_match($regex, $chunk)) { array_push($pruneargv, $key); } } } while ($key = array_pop($pruneargv)) { unset($argv[$key]); } if (empty($this->file_list) && count($argv) < 2) { // Log::err(Log::EFATAL, "No files to analyze"); } foreach ($argv as $arg) { if ($arg[0] == '-') { $this->usage("Unknown option '{$arg}'"); } } $this->file_list = array_merge($this->file_list, array_slice($argv, 1)); }
/** * Perform some backwards compatibility checks on a node * * @param Context $context * The context in which the node appears * * @param Node $node * The node we'd like to check * * @return null * * @see \Phan\Deprecated::bc_check * Formerly `function bc_check` */ public static function backwardCompatibilityCheck(Context $context, Node $node) { if (!$node->children['expr'] instanceof \node\Node) { return; } if ($node->children['expr']->kind !== \node\node_DIM) { return; } $temp = $node->children['expr']->children['expr']; $lnode = $temp; if (!($temp->kind == \node\node_PROP || $temp->kind == \node\node_STATIC_PROP)) { return; } while ($temp instanceof \node\Node && ($temp->kind == \node\node_PROP || $temp->kind == \node\node_STATIC_PROP)) { $lnode = $temp; // Lets just hope the 0th is the expression // we want $temp = array_values($temp->children)[0]; } if (!$temp instanceof \node\Node) { return; } if ($lnode->children['prop'] instanceof \node\Node && $lnode->children['prop']->kind == \node\node_VAR && ($temp->kind == \node\node_VAR || $temp->kind == \node\node_NAME)) { $ftemp = new \SplFileObject($context->getFile()); $ftemp->seek($node->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '}[') === false || strpos($line, ']}') === false || strpos($line, '>{') === false) { Log::err(Log::ECOMPAT, "expression may not be PHP 7 compatible", $context->getFile(), $node->lineno); } } }
/** * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitCatch(Node $node) : Context { try { $union_type = UnionTypeVisitor::unionTypeFromClassNode($this->code_base, $this->context, $node->children['class']); $class_list = AST::classListFromNodeInContext($this->code_base, $this->context, $node->children['class']); } catch (CodeBaseException $exception) { Log::err(Log::EUNDEF, "catching undeclared class {$exception->getFQSEN()}", $this->context->getFile(), $node->lineno); } $variable_name = AST::variableName($node->children['var']); if (!empty($variable_name)) { $variable = Variable::fromNodeInContext($node->children['var'], $this->context, $this->code_base, false); if (!$union_type->isEmpty()) { $variable->setUnionType($union_type); } $this->context->addScopeVariable($variable); } return $this->context; }
/** * @return Parameter[] * A list of parameters from an AST node. * * @see \Phan\Deprecated\Pass1::node_paramlist * Formerly `function node_paramlist` */ public static function listFromNode(Context $context, CodeBase $code_base, Node $node) : array { assert($node instanceof Node, "node was not an \\ast\\Node"); $parameter_list = []; $is_optional_seen = false; foreach ($node->children ?? [] as $i => $child_node) { $parameter = Parameter::fromNode($context, $code_base, $child_node); if (!$parameter->isOptional() && $is_optional_seen) { Log::err(Log::EPARAM, "required arg follows optional", $context->getFile(), $node->lineno ?? 0); } else { if ($parameter->isOptional() && !$is_optional_seen && $parameter->getUnionType()->isEmpty()) { $is_optional_seen = true; } } $parameter_list[] = $parameter; } return $parameter_list; }
/** * 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_list * A list of files to scan * * @return Context */ public 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)) { Log::err(Log::EUNDEF, "Empty or missing file {$file_path}", $file_path, 0); return $context; } // Start recursively analyzing the tree return $this->analyzeNodeInContext($code_base, $context, $node); }
/** * @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; }
/** * Visit a node with kind `\ast\AST_PROP_DECL` * * @param Node $node * A node to parse * * @return Context * A new or an unchanged context resulting from * parsing the node */ public function visitPropDecl(Node $node) : Context { // Bomb out if we're not in a class context $clazz = $this->getContextClass(); // Get a comment on the property declaration $comment = Comment::fromStringInContext($node->children[0]->docComment ?? '', $this->context); foreach ($node->children ?? [] as $i => $child_node) { // Ignore children which are not property elements if (!$child_node || $child_node->kind != \ast\AST_PROP_ELEM) { continue; } // @var UnionType $type = UnionType::fromNode($this->context, $this->code_base, $child_node->children['default']); $property_name = $child_node->children['name']; assert(is_string($property_name), 'Property name must be a string. ' . 'Got ' . print_r($property_name, true) . ' at ' . $this->context); $property = new Property($this->context->withLineNumberStart($child_node->lineno ?? 0)->withLineNumberEnd($child_node->endLineno ?? -1), is_string($child_node->children['name']) ? $child_node->children['name'] : '_error_', $type, $node->flags ?? 0); // Add the property to the class $clazz->addProperty($this->code_base, $property); // Look for any @var declarations if ($variable = $comment->getVariableList()[$i] ?? null) { if ((string) $type != 'null' && !$type->canCastToUnionType($variable->getUnionType())) { Log::err(Log::ETYPE, "assigning {$type} to property but {$property->getFQSEN()} is {$variable->getUnionType()}", $this->context->getFile(), $child_node->lineno); } // Set the declared type to the doc-comment type and add // |null if the default value is null $property->getUnionType()->addUnionType($variable->getUnionType()); } } 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 visitCatch(Node $node) : Context { // Get the name of the class $class_name = $node->children['class']->children['name']; $clazz = null; // If we can't figure out the class name (which happens // from time to time), then give up if (!empty($class_name)) { $class_fqsen = FullyQualifiedClassName::fromStringInContext($class_name, $this->context); // Check to see if the class actually exists if ($this->code_base->hasClassWithFQSEN($class_fqsen)) { $clazz = $this->code_base->getClassByFQSEN($class_fqsen); } else { Log::err(Log::EUNDEF, "call to method on undeclared class {$class_name}", $this->context->getFile(), $node->lineno); } } $variable_name = AST::variableName($node->children['var']); if (!empty($variable_name)) { $variable = Variable::fromNodeInContext($node->children['var'], $this->context, $this->code_base, false); if ($clazz) { $variable->setUnionType($clazz->getUnionType()); } $this->context->addScopeVariable($variable); } return $this->context; }
/** * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return string * The class name represented by the given call */ public function visitMethodCall(Node $node) : string { if ($node->children['expr']->kind == \ast\AST_VAR) { if ($node->children['expr']->children['name'] instanceof Node) { return ''; } // $var->method() if ($node->children['expr']->children['name'] == 'this') { if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } return (string) $this->context->getClassFQSEN(); } $variable_name = $node->children['expr']->children['name']; if (!$this->context->getScope()->hasVariableWithName($variable_name)) { // Got lost, couldn't find the variable in the current scope // If it really isn't defined, it will be caught by the // undefined var error return ''; } $variable = $this->context->getScope()->getVariableWithName($variable_name); // Hack - loop through the possible types of the var and assume // first found class is correct foreach ($variable->getUnionType()->nonGenericArrayTypes()->getTypeList() as $type) { $child_class_fqsen = FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); if ($this->code_base->hasClassWithFQSEN($child_class_fqsen)) { return (string) FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); } } // Could not find name return ''; } if ($node->children['expr']->kind == \ast\AST_PROP) { $prop = $node->children['expr']; if (!($prop->children['expr']->kind == \ast\AST_VAR && !$prop->children['expr']->children['name'] instanceof Node)) { return ''; } // $var->prop->method() $var = $prop->children['expr']; if ($var->children['name'] == 'this') { // If we're not in a class scope, 'this' won't work if (!$this->context->isInClassScope()) { Log::err(Log::ESTATIC, 'Using $this when not in object context', $this->context->getFile(), $node->lineno); return ''; } // Get the class in scope $clazz = $this->code_base->getClassByFQSEN($this->context->getClassFQSEN()); if ($prop->children['prop'] instanceof Node) { // $this->$prop->method() - too dynamic, give up return ''; } $property_name = $prop->children['prop']; if ($clazz->hasPropertyWithName($this->code_base, $property_name)) { 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 ''; } // Find the first viable property type foreach ($property->getUnionType()->nonGenericArrayTypes()->getTypeList() as $type) { $class_fqsen = FullyQualifiedClassName::fromStringInContext((string) $type, $this->context); if ($this->code_base->hasClassWithFQSEN($class_fqsen)) { return (string) $class_fqsen; } } } // No such property was found, or none were classes // that could be found return ''; } return ''; } if ($node->children['expr']->kind == \ast\AST_METHOD_CALL) { // Get the type returned by the first method // call. $union_type = UnionType::fromNode($this->context, $this->code_base, $node->children['expr']); // Find the subset of types that are viable // classes $viable_class_types = $union_type->nonNativeTypes()->nonGenericArrayTypes(); // If there are no non-native types, give up if ($viable_class_types->isEmpty()) { return ''; } // Return the first non-native type in the // list and hope its a class return (string) $viable_class_types->head(); } return ''; }
/** * @param Context $context * The context in which the node appears * * @param CodeBase $code_base * * @param Node $node * An AST node representing a method * * @return Method * A Method representing the AST node in the * given context * * * @see \Phan\Deprecated\Pass1::node_func * Formerly 'function node_func' */ public static function fromNode(Context $context, CodeBase $code_base, Node $node) : Method { // Parse the comment above the method to get // extra meta information about the method. $comment = Comment::fromStringInContext($node->docComment ?? '', $context); // @var Parameter[] // The list of parameters specified on the // method $parameter_list = Parameter::listFromNode($context, $code_base, $node->children['params']); // Add each parameter to the scope of the function foreach ($parameter_list as $parameter) { $context = $context->withScopeVariable($parameter); } // Create the skeleton method object from what // we know so far $method = new Method($context, $node->name, new UnionType(), $node->flags ?? 0); // If the method is Analyzable, set the node so that // we can come back to it whenever we like and // rescan it $method->setNode($node); // Set the parameter list on the method $method->setParameterList($parameter_list); $method->setNumberOfRequiredParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isRequired() ? 1 : 0); }, 0)); $method->setNumberOfOptionalParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isOptional() ? 1 : 0); }, 0)); // Check to see if the comment specifies that the // method is deprecated $method->setIsDeprecated($comment->isDeprecated()); // Take a look at method return types if ($node->children['returnType'] !== null) { $union_type = UnionType::fromSimpleNode($context, $node->children['returnType']); $method->getUnionType()->addUnionType($union_type); } else { if ($comment->hasReturnUnionType()) { // See if we have a return type specified in the comment $union_type = $comment->getReturnType(); if ($union_type->hasSelfType()) { // We can't actually figure out 'static' at this // point, but fill it in regardless. It will be partially // correct if ($context->hasClassFQSEN()) { // n.b.: We're leaving the reference to self, static // or $this in the type because I'm guessing // it doesn't really matter. Apologies if it // ends up being an issue. $union_type->addUnionType($context->getClassFQSEN()->asUnionType()); } } $method->getUnionType()->addUnionType($union_type); } } // Add params to local scope for user functions if ($context->getFile() != 'internal') { $parameter_offset = 0; foreach ($method->getParameterList() as $i => $parameter) { if ($parameter->getUnionType()->isEmpty()) { // If there is no type specified in PHP, check // for a docComment with @param declarations. We // assume order in the docComment matches the // parameter order in the code if ($comment->hasParameterWithNameOrOffset($parameter->getName(), $parameter_offset)) { $comment_type = $comment->getParameterWithNameOrOffset($parameter->getName(), $parameter_offset)->getUnionType(); $parameter->getUnionType()->addUnionType($comment_type); } } // If there's a default value on the parameter, check to // see if the type of the default is cool with the // specified type. if ($parameter->hasDefaultValue()) { $default_type = $parameter->getDefaultValueType(); if (!$default_type->canCastToUnionType($parameter->getUnionType())) { Log::err(Log::ETYPE, "Default value for {$parameter->getUnionType()} \${$parameter->getName()} can't be {$default_type}", $context->getFile(), $node->lineno); } // If we have no other type info about a parameter, // just because it has a default value of null // doesn't mean that is its type. Any type can default // to null if ((string) $default_type === 'null' && !$parameter->getUnionType()->isEmpty()) { $parameter->getUnionType()->addType(NullType::instance()); } } ++$parameter_offset; } } return $method; }
/** * Visit a node with kind `\ast\AST_METHOD_CALL` * * @param Node $node * A node of the type indicated by the method name that we'd * like to figure out the type that it produces. * * @return UnionType * The set of types that are possibly produced by the * given node */ public function visitMethodCall(Node $node) : UnionType { $class_name = AST::classNameFromNode($this->context, $this->code_base, $node); if (empty($class_name)) { return new UnionType(); } $class_fqsen = FullyQualifiedClassName::fromstringInContext($class_name, $this->context); assert($this->code_base->hasClassWithFQSEN($class_fqsen), "Class {$class_fqsen} must exist"); $clazz = $this->code_base->getClassByFQSEN($class_fqsen); $method_name = $node->children['method']; // Give up on any complicated nonsense where the // method name is a variable such as in // `$variable->$function_name()`. if ($method_name instanceof Node) { return new UnionType(); } // Method names can some times turn up being // other method calls. assert(is_string($method_name), "Method name must be a string. Something else given."); if (!$clazz->hasMethodWithName($this->code_base, $method_name)) { Log::err(Log::EUNDEF, "call to undeclared method {$class_fqsen}->{$method_name}()", $this->context->getFile(), $node->lineno); return new UnionType(); } $method = $clazz->getMethodByNameInContext($this->code_base, $method_name, $this->context); return $method->getUnionType(); }
/** * @param CodeBase $code_base * The global code base holding all state * * @return Clazz * Get the class in this scope, or fail real hard */ public function getClassInScope(CodeBase $code_base) : Clazz { assert($this->isInClassScope(), "Must be in class scope to get class"); if (!$code_base->hasClassWithFQSEN($this->getClassFQSEN())) { Log::err(Log::EFATAL, "Cannot find class with FQSEN {$this->getClassFQSEN()} in context {$this}", $this->getFile(), 0); } return $code_base->getClassByFQSEN($this->getClassFQSEN()); }
/** * Perform some backwards compatibility checks on a node * * @param Context $context * The context in which the node appears * * @param Node $node * The node we'd like to check * * @return null * * @see \Phan\Deprecated::bc_check * Formerly `function bc_check` */ public static function backwardCompatibilityCheck(Context $context, Node $node) { if (empty($node->children['expr'])) { return; } if ($node->kind !== \ast\AST_DIM) { if (!$node->children['expr'] instanceof Node) { return; } if ($node->children['expr']->kind !== \ast\AST_DIM) { AST::backwardCompatibilityCheck($context, $node->children['expr']); return; } $temp = $node->children['expr']->children['expr']; $lnode = $temp; } else { $temp = $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)) { $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 && $node->kind !== \ast\AST_CALL) { 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)) && ($temp->kind == \ast\AST_VAR || $temp->kind == \ast\AST_NAME)) { $ftemp = new \SplFileObject($context->getFile()); $ftemp->seek($node->lineno - 1); $line = $ftemp->current(); unset($ftemp); if (strpos($line, '}[') === false || strpos($line, ']}') === false || strpos($line, '>{') === false) { Log::err(Log::ECOMPAT, "expression may not be PHP 7 compatible", $context->getFile(), $node->lineno ?? 0); } } }
/** * @param Node $node * A node to check to see if its a no-op * * @param string $message * A message to emit if its a no-op * * @return null */ private function analyzeNoOp(Node $node, string $message) { if ($this->parent_node instanceof Node && $this->parent_node->kind == \ast\AST_STMT_LIST) { Log::err(Log::ENOOP, $message, $this->context->getFile(), $node->lineno); } }
/** * Create and read command line arguments, configuring * \Phan\Config as a side effect. */ public function __construct() { global $argv; // Parse command line args $opts = getopt("f:m:o:c:haqbrpis:3:t::"); foreach ($opts ?? [] as $key => $value) { switch ($key) { case 'h': $this->usage(); break; case 'f': if (is_file($value) && is_readable($value)) { $this->file_list = file($value, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); } else { Log::err(Log::EFATAL, "Unable to open {$value}"); } break; case 'm': if (!in_array($value, ['verbose', 'short', 'json', 'csv'])) { $this->usage("Unknown output mode: {$value}"); } Log::setOutputMode($value); break; case 'c': Config::get()->parent_constructor_required = explode(',', $value); break; case 'q': Config::get()->quick_mode = true; break; case 'b': Config::get()->backward_compatibility_checks = true; break; case 'p': Config::get()->progress_bar = true; break; case 'a': Config::get()->dump_ast = true; break; case 'o': Log::setFilename($value); break; case 'i': Log::setOutputMask(Log::getOutputMask() ^ Log::EUNDEF); break; case 't': Config::get()->emit_trace_id = true; break; case '3': Config::get()->third_party_directory_list = explode(',', $value); break; case 's': Config::get()->serialized_code_base_file = $value; break; case 'r': Config::get()->reanalyze_file_list = true; break; default: $this->usage("Unknown option '-{$key}'"); break; } } $pruneargv = array(); foreach ($opts ?? [] as $opt => $value) { foreach ($argv as $key => $chunk) { $regex = '/^' . (isset($opt[1]) ? '--' : '-') . $opt . '/'; if ($chunk == $value && $argv[$key - 1][0] == '-' || preg_match($regex, $chunk)) { array_push($pruneargv, $key); } } } while ($key = array_pop($pruneargv)) { unset($argv[$key]); } if (empty($this->file_list) && count($argv) < 2) { Log::err(Log::EFATAL, "No files to analyze"); } foreach ($argv as $arg) { if ($arg[0] == '-') { $this->usage("Unknown option '{$arg}'"); } } $this->file_list = array_merge($this->file_list, array_slice($argv, 1)); }