public static emit ( string $type, string $file, integer $line, variadic |
||
$type | string | The type of the issue |
$file | string | The name of the file where the issue was found |
$line | integer | The line number (start) where the issue was found |
$template_parameters | variadic |
Any template parameters required for the issue message |
return | void |
/** * 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()); } }
/** * 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()) { Issue::emit(Issue::RedefineClassInternal, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $clazz, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $original_class); // Otherwise, print the coordinates of the original // definition } else { Issue::emit(Issue::RedefineClass, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $clazz, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $original_class, $original_class->getContext()->getFile(), $original_class->getContext()->getLineNumberStart()); } return; }
/** * @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 $issue_type) : bool { if (!$code_base->hasClassWithFQSEN($fqsen)) { Issue::emit($issue_type, $clazz->getContext()->getFile(), $clazz->getContext()->getLineNumberStart(), (string) $fqsen); return false; } return true; }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeDuplicateFunction(CodeBase $code_base, FunctionInterface $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 ($original_method->isInternal()) { Issue::emit(Issue::RedefineFunctionInternal, $method->getFileRef()->getFile(), $method->getFileRef()->getLineNumberStart(), $method_name, $method->getFileRef()->getFile(), $method->getFileRef()->getLineNumberStart()); } else { Issue::emit(Issue::RedefineFunction, $method->getFileRef()->getFile(), $method->getFileRef()->getLineNumberStart(), $method_name, $method->getFileRef()->getFile(), $method->getFileRef()->getLineNumberStart(), $original_method->getFileRef()->getFile(), $original_method->getFileRef()->getLineNumberStart()); } }
/** * 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->getTypeSet() 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)) { Issue::emit(Issue::UndeclaredTypeProperty, $property->getContext()->getFile(), $property->getContext()->getLineNumberStart(), (string) $type_fqsen); } } } }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeParameterTypes(CodeBase $code_base, FunctionInterface $method) { // Look at each method parameter foreach ($method->getParameterList() as $parameter) { $union_type = $parameter->getUnionType(); // Look at each type in the parameter's Union Type foreach ($union_type->getTypeSet() 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)) { Issue::emit(Issue::UndeclaredTypeParameter, $method->getFileRef()->getFile(), $method->getFileRef()->getLineNumberStart(), (string) $type_fqsen); } } } }
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()) { Issue::emit(Issue::ContextNotObject, $this->context->getFile(), $node->lineno ?? 0, $class_name); return new UnionType(); } // Reference to a parent class if ($class_name === 'parent') { $class = $this->context->getClassInScope($this->code_base); if (!$class->hasParentClassFQSEN()) { Issue::emit(Issue::ParentlessClass, $this->context->getFile(), $node->lineno ?? 0, (string) $class->getFQSEN()); return new UnionType(); } return Type::fromFullyQualifiedString((string) $class->getParentClassFQSEN())->asUnionType(); } return Type::fromFullyQualifiedString((string) $this->context->getClassFQSEN())->asUnionType(); }
/** * Once we know what the universe looks like we * can scan for more complicated issues. * * @param CodeBase $code_base * The global code base holding all state * * @param string $file_path * A list of files to scan * * @return Context */ public static function analyzeFile(CodeBase $code_base, string $file_path) : Context { // Convert the file to an Abstract Syntax Tree // before passing it on to the recursive version // of this method $node = \ast\parse_file($file_path, Config::get()->ast_version); // Set the file on the context $context = (new Context())->withFile($file_path); // Ensure we have some content if (empty($node)) { Issue::emit(Issue::EmptyFile, $file_path, 0, $file_path); return $context; } // Start recursively analyzing the tree return self::analyzeNodeInContext($code_base, $context, $node); }
/** * @return 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) { Issue::emit(Issue::ParamReqAfterOpt, $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; }
/** * 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(), function (UnionType $node_type) use($context, $method) { // "arg#1(pieces) is %s but {$method->getFQSEN()}() takes array when passed only 1 arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'pieces', (string) $method->getFQSEN(), 'string', 'array']); }); 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())) { Issue::emit(Issue::ParamSpecial1, $context->getFile(), $context->getLineNumberStart(), 2, 'glue', (string) $arg2_type, (string) $method->getFQSEN(), 'string', 1, 'array'); } } else { if ((string) $arg1_type == 'string') { if (!$arg2_type->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::emit(Issue::ParamSpecial1, $context->getFile(), $context->getLineNumberStart(), 2, 'pieces', (string) $arg2_type, (string) $method->getFQSEN(), 'array', 1, 'string'); } } } 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) { Issue::emit(Issue::ParamTooFewInternal, $context->getFile(), $context->getLineNumberStart(), $argcount, (string) $method->getFQSEN(), $method->getNumberOfRequiredParameters()); return; } self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 1], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "The last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial3)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); for ($i = 0; $i < $argcount - 1; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method, $i) { // "arg#".($i+1)." is %s but {$method->getFQSEN()}() takes array" return Issue::fromType(Issue::ParamTypeMismatch)($context->getFile(), $context->getLineNumberStart(), [$i + 1, (string) $node_type, (string) $method->getFQSEN(), 'array']); }); } return; case 'array_diff_uassoc': case 'array_uintersect_uassoc': if ($argcount < 4) { Issue::emit(Issue::ParamTooFewInternal, $context->getFile(), $context->getLineNumberStart(), $argcount, (string) $method->getFQSEN(), $method->getNumberOfRequiredParameters()); 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(), function (UnionType $node_type) use($context, $method) { // "The last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial3)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); self::analyzeNodeUnionTypeCast($arglist->children[$argcount - 2], $context, $code_base, CallableType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { // "The second last argument to {$method->getFQSEN()} must be a callable" return Issue::fromType(Issue::ParamSpecial4)($context->getFile(), $context->getLineNumberStart(), [(string) $method->getFQSEN(), 'callable']); }); for ($i = 0; $i < $argcount - 2; $i++) { self::analyzeNodeUnionTypeCast($arglist->children[$i], $context, $code_base, ArrayType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method, $i) { // "arg#".($i+1)." is %s but {$method->getFQSEN()}() takes array" return Issue::fromType(Issue::ParamTypeMismatch)($context->getFile(), $context->getLineNumberStart(), [$i + 1, (string) $node_type, (string) $method->getFQSEN(), '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(), function (UnionType $node_type) use($context, $method) { // "arg#1(token) is %s but {$method->getFQSEN()}() takes string when passed only one arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'token', (string) $node_type, (string) $method->getFQSEN(), 'string']); }); } // 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(), function (UnionType $node_type) use($context, $method) { // "arg#1(values) is %s but {$method->getFQSEN()}() takes array when passed only one arg" return Issue::fromType(Issue::ParamSpecial2)($context->getFile(), $context->getLineNumberStart(), [1, 'values', (string) $node_type, (string) $method->getFQSEN(), 'array']); })) { return; } } // The arginfo check will handle the other case break; default: break; } }
/** * @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 = (new ContextNode($this->code_base, $this->context, $node->children['class']))->getClassList(); } catch (CodeBaseException $exception) { Issue::emit(Issue::UndeclaredClassCatch, $this->context->getFile(), $node->lineno ?? 0, (string) $exception->getFQSEN()); } $variable_name = (new ContextNode($this->code_base, $this->context, $node->children['var']))->getVariableName(); 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; }
/** * 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; } 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']; $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)) { $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; } 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::emit(Issue::CompatiblePHP7, $this->context->getFile(), $this->node->lineno ?? 0); } } }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeElementReferenceCounts(CodeBase $code_base, TypedStructuralElement $element, string $issue_type) { // Don't worry about internal elements if ($element->getContext()->isInternal()) { return; } if ($element->getReferenceCount($code_base) < 1) { if ($element->hasSuppressIssue($issue_type)) { return; } if ($element instanceof Addressable) { Issue::emit($issue_type, $element->getContext()->getFile(), $element->getContext()->getLineNumberStart(), (string) $element->getFQSEN()); } else { Issue::emit($issue_type, $element->getContext()->getFile(), $element->getContext()->getLineNumberStart(), (string) $element); } } }
/** * @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 both left and right are arrays, then this is array // concatenation. if ($left->isGenericArray() && $right->isGenericArray()) { if ($left->isEqualTo($right)) { return $left; } return ArrayType::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())) { Issue::emit(Issue::TypeInvalidRightOperand, $this->context->getFile(), $node->lineno ?? 0); return new UnionType(); } elseif ($right_is_array && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::emit(Issue::TypeInvalidLeftOperand, $this->context->getFile(), $node->lineno ?? 0); return new UnionType(); } elseif ($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()]); }
/** * 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; } // If something goes wrong will getting the type of // a property, we'll store it as a future union // type and try to figure it out later $future_union_type = null; try { // Get the type of the default $union_type = UnionType::fromNode($this->context, $this->code_base, $child_node->children['default'], false); } catch (IssueException $exception) { $future_union_type = new FutureUnionType($this->code_base, $this->context, $child_node->children['default']); $union_type = new UnionType(); } // Don't set 'null' as the type if thats the default // given that its the default default. if ($union_type->isType(NullType::instance())) { $union_type = new UnionType(); } $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(clone $this->context->withLineNumberStart($child_node->lineno ?? 0), is_string($child_node->children['name']) ? $child_node->children['name'] : '_error_', $union_type, $node->flags ?? 0); // Add the property to the class $clazz->addProperty($this->code_base, $property); $property->setSuppressIssueList($comment->getSuppressIssueList()); // Look for any @var declarations if ($variable = $comment->getVariableList()[$i] ?? null) { if ((string) $union_type != 'null' && !$union_type->canCastToUnionType($variable->getUnionType())) { Issue::emit(Issue::TypeMismatchProperty, $this->context->getFile(), $child_node->lineno ?? 0, (string) $union_type, (string) $property->getFQSEN(), (string) $variable->getUnionType()); } // Set the declared type to the doc-comment type and add // |null if the default value is null $property->getUnionType()->addUnionType($variable->getUnionType()); } // Wait until after we've added the (at)var type // before setting the future so that calling // $property->getUnionType() doesn't force the // future to be reified. if (!empty($future_union_type)) { $property->setFutureUnionType($future_union_type); } } return $this->context; }
/** * Check to see if the given Clazz is a duplicate * * @return null */ public static function analyzeElementReferenceCounts(CodeBase $code_base, AddressableElement $element, string $issue_type) { // Don't worry about internal elements if ($element->isInternal()) { return; } /* print "digraph G {\n"; foreach ($element->getReferenceList() as $file_ref) { print "\t\"{$file_ref->getFile()}\" -> \"{$element->getFileRef()->getFile()}\";\n"; } print "}\n"; */ if ($element->getReferenceCount($code_base) < 1) { if ($element->hasSuppressIssue($issue_type)) { return; } if ($element instanceof AddressableElement) { Issue::emit($issue_type, $element->getFileRef()->getFile(), $element->getFileRef()->getLineNumberStart(), (string) $element->getFQSEN()); } else { Issue::emit($issue_type, $element->getFileRef()->getFile(), $element->getFileRef()->getLineNumberStart(), (string) $element); } } }
/** * @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 $issue_type) { if ($this->parent_node instanceof Node && $this->parent_node->kind == \ast\AST_STMT_LIST) { Issue::emit($issue_type, $this->context->getFile(), $node->lineno ?? 0); } }
/** * @param Context $context * The context in which the node appears * * @param CodeBase $code_base * * @param Node $node * An AST node representing a function * * @return Func * A Func representing the AST node in the * given context */ public static function fromNode(Context $context, CodeBase $code_base, Decl $node) : Func { // Parse the comment above the function to get // extra meta information about the function. $comment = Comment::fromStringInContext($node->docComment ?? '', $context); // @var Parameter[] // The list of parameters specified on the // function $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 function object from what // we know so far $func = new Func($context, (string) $node->name, new UnionType(), $node->flags ?? 0); // If the function is Analyzable, set the node so that // we can come back to it whenever we like and // rescan it $func->setNode($node); // Set the parameter list on the function $func->setParameterList($parameter_list); $func->setNumberOfRequiredParameters(array_reduce($parameter_list, function (int $carry, Parameter $parameter) : int { return $carry + ($parameter->isRequired() ? 1 : 0); }, 0)); $func->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 // function is deprecated $func->setIsDeprecated($comment->isDeprecated()); $func->setSuppressIssueList($comment->getSuppressIssueList()); // Take a look at function return types if ($node->children['returnType'] !== null) { // Get the type of the parameter $union_type = UnionType::fromNode($context, $code_base, $node->children['returnType']); $func->getUnionType()->addUnionType($union_type); } if ($comment->hasReturnUnionType()) { // See if we have a return type specified in the comment $union_type = $comment->getReturnType(); assert(!$union_type->hasSelfType(), "Function referencing self in {$context}"); $func->getUnionType()->addUnionType($union_type); } // Add params to local scope for user functions if (!$func->isInternal()) { $parameter_offset = 0; foreach ($func->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->isEqualTo(NullType::instance()->asUnionType())) { if (!$default_type->isEqualTo(NullType::instance()->asUnionType()) && !$default_type->canCastToUnionType($parameter->getUnionType())) { Issue::emit(Issue::TypeMismatchDefault, $context->getFile(), $node->lineno ?? 0, (string) $parameter->getUnionType(), $parameter->getName(), (string) $default_type); } $parameter->getUnionType()->addUnionType($default_type); } // 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 $func; }
/** * @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, Decl $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, (string) $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()); $method->setSuppressIssueList($comment->getSuppressIssueList()); // Take a look at method return types if ($node->children['returnType'] !== null) { // Get the type of the parameter $union_type = UnionType::fromNode($context, $code_base, $node->children['returnType']); $method->getUnionType()->addUnionType($union_type); } 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->isEqualTo(NullType::instance()->asUnionType())) { if (!$default_type->isEqualTo(NullType::instance()->asUnionType()) && !$default_type->canCastToUnionType($parameter->getUnionType())) { Issue::emit(Issue::TypeMismatchDefault, $context->getFile(), $node->lineno ?? 0, (string) $parameter->getUnionType(), $parameter->getName(), (string) $default_type); } $parameter->getUnionType()->addUnionType($default_type); } // 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; }
/** * @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) { Phan::getIssueCollector()->collectIssue($exception->getIssueInstance()); return $this->context; } if (!$this->right_type->canCastToExpandedUnionType($property->getUnionType(), $this->code_base)) { Issue::emit(Issue::TypeMismatchProperty, $this->context->getFile(), $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)) { Issue::emit(Issue::UndeclaredProperty, $this->context->getFile(), $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; }