public static maybeEmit ( CodeBase $code_base, |
||
$code_base | CodeBase | The code base within which we're operating |
$context | The context in which the node we're going to be looking at exits. | |
$issue_type | string | The type of issue to emit such as Issue::ParentlessClass |
$lineno | integer | The line number where the issue was found |
리턴 | void |
/** * 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 ($original_fqsen instanceof FullyQualifiedFunctionName) { if (!$code_base->hasFunctionWithFQSEN($original_fqsen)) { return; } $original_method = $code_base->getFunctionByFQSEN($original_fqsen); } else { if (!$code_base->hasMethodWithFQSEN($original_fqsen)) { return; } $original_method = $code_base->getMethodByFQSEN($original_fqsen); } $method_name = $method->getName(); if (!$method->hasSuppressIssue(Issue::RedefineFunction)) { if ($original_method->isInternal()) { Issue::maybeEmit($code_base, $method->getContext(), Issue::RedefineFunctionInternal, $method->getFileRef()->getLineNumberStart(), $method_name, $method->getFileRef()->getFile(), $method->getFileRef()->getLineNumberStart()); } else { Issue::maybeEmit($code_base, $method->getContext(), Issue::RedefineFunction, $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 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::maybeEmit($code_base, $clazz->getContext(), Issue::TypeParentConstructorCalled, $clazz->getFileRef()->getLineNumberStart(), (string) $clazz->getFQSEN(), (string) $parent_clazz->getFQSEN()); } }
/** * 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) { try { $union_type = $property->getUnionType(); } catch (IssueException $exception) { Issue::maybeEmitInstance($code_base, $property->getContext(), $exception->getIssueInstance()); continue; } // 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; } if ($type instanceof TemplateType) { if ($property->isStatic()) { Issue::maybeEmit($code_base, $property->getContext(), Issue::TemplateTypeStaticProperty, $property->getFileRef()->getLineNumberStart(), (string) $property->getFQSEN()); } } else { // Make sure the class exists $type_fqsen = $type->asFQSEN(); if (!$code_base->hasClassWithFQSEN($type_fqsen) && !$type instanceof TemplateType && (!$property->hasDefiningFQSEN() || $property->getDefiningFQSEN() == $property->getFQSEN())) { Issue::maybeEmit($code_base, $property->getContext(), Issue::UndeclaredTypeProperty, $property->getFileRef()->getLineNumberStart(), (string) $property->getFQSEN(), (string) $type_fqsen); } } } } }
/** * @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::maybeEmit($code_base, $clazz->getContext(), $issue_type, $clazz->getFileRef()->getLineNumberStart(), (string) $fqsen); return false; } return true; }
/** * @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()) || $left->isType(ArrayType::instance()); $right_is_array = !empty($right->genericArrayElementTypes()) && empty($right->nonGenericArrayTypes()) || $right->isType(ArrayType::instance()); if ($left_is_array && !$right->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::maybeEmit($this->code_base, $this->context, Issue::TypeInvalidRightOperand, $node->lineno ?? 0); return new UnionType(); } elseif ($right_is_array && !$left->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::maybeEmit($this->code_base, $this->context, Issue::TypeInvalidLeftOperand, $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()]); }
/** * 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); }
/** * @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 */ public static function fromNode(Context $context, CodeBase $code_base, Decl $node, FullyQualifiedMethodName $fqsen) : Method { // Create the skeleton method object from what // we know so far $method = new Method($context, (string) $node->name, new UnionType(), $node->flags ?? 0, $fqsen); // 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) { $method->getInternalScope()->addVariable($parameter); } // 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()); if ($method->getIsMagicCall() || $method->getIsMagicCallStatic()) { $method->setNumberOfOptionalParameters(999); $method->setNumberOfRequiredParameters(0); } // 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->isInClassScope()) { // 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 (!$method->isInternal()) { $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::maybeEmit($code_base, $context, Issue::TypeMismatchDefault, $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; }
/** * 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()); } } }
/** * 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); } } }
/** * Check to see if signatures match * * @return void */ public static function analyzeComposition(CodeBase $code_base, Clazz $class) { // Get the Class's FQSEN $fqsen = $class->getFQSEN(); // Get the list of all inherited classes. $inherited_class_list = $class->getInheritedClassList($code_base); // No chance of failed composition if we don't inherit from // lots of stuff. if (count($inherited_class_list) < 2) { return; } // For each property, find out every inherited class that defines it // and check to see if the types line up. foreach ($class->getPropertyList($code_base) as $property) { try { $property_union_type = $property->getUnionType(); } catch (IssueException $exception) { $property_union_type = new UnionType(); } // Check for that property on each inherited // class/trait/interface foreach ($inherited_class_list as $inherited_class) { // Skip any classes/traits/interfaces not defining that // property if (!$inherited_class->hasPropertyWithName($code_base, $property->getName())) { continue; } // We don't call `getProperty` because that will create // them in some circumstances. $inherited_property_map = $inherited_class->getPropertyMap($code_base); if (!isset($inherited_property_map[$property->getName()])) { continue; } // Get the inherited property $inherited_property = $inherited_property_map[$property->getName()]; // Figure out if this property type can cast to the // inherited definition's type. $can_cast = $property_union_type->canCastToExpandedUnionType($inherited_property->getUnionType(), $code_base); if ($can_cast) { continue; } // Don't emit an issue if the property suppresses the issue if ($property->hasSuppressIssue(Issue::IncompatibleCompositionProp)) { continue; } Issue::maybeEmit($code_base, $property->getContext(), Issue::IncompatibleCompositionProp, $property->getFileRef()->getLineNumberStart(), (string) $class->getFQSEN(), (string) $inherited_class->getFQSEN(), $property->getName(), (string) $class->getFQSEN(), $class->getFileRef()->getFile(), $class->getFileRef()->getLineNumberStart()); } } // TODO: This has too much overlap with PhanParamSignatureMismatch // and we should figure out how to merge it. /* $method_map = $code_base->getMethodMapByFullyQualifiedClassName($fqsen); // For each method, find out every inherited class that defines it // and check to see if the types line up. foreach ($method_map as $i => $method) { $method_union_type = $method->getUnionType(); // We don't need to analyze constructors for signature // compatibility if ($method->getName() == '__construct') { continue; } // Get the method parameter list // Check for that method on each inherited // class/trait/interface foreach ($inherited_class_list as $inherited_class) { // Skip anything that doesn't define this method if (!$inherited_class->hasMethodWithName($code_base, $method->getName())) { continue; } $inherited_method = $inherited_class->getMethodByName($code_base, $method->getName()); if ($method == $inherited_method) { continue; } // Figure out if this method return type can cast to the // inherited definition's return type. $is_compatible = $method_union_type->canCastToExpandedUnionType( $inherited_method->getUnionType(), $code_base ); $inherited_method_parameter_map = $inherited_method->getParameterList(); // Figure out if all of the parameter types line up foreach ($method->getParameterList() as $i => $parameter) { $is_compatible = ( $is_compatible && isset($inherited_method_parameter_map[$i]) && $parameter->getUnionType()->canCastToExpandedUnionType( ($inherited_method_parameter_map[$i])->getUnionType(), $code_base ) ); } if ($is_compatible) { continue; } // Don't emit an issue if the method suppresses the issue if ($method->hasSuppressIssue(Issue::IncompatibleCompositionMethod)) { continue; } Issue::maybeEmit( $code_base, $method->getContext(), Issue::IncompatibleCompositionMethod, $method->getFileRef()->getLineNumberStart(), (string)$method, (string)$inherited_method, $inherited_method->getFileRef()->getFile(), $inherited_method->getFileRef()->getLineNumberStart() ); } } */ }
/** * @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::maybeEmit($code_base, $context, Issue::TypeMismatchDefault, $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; }
/** * @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::maybeEmit($code_base, $context, Issue::ParamReqAfterOpt, $node->lineno ?? 0); } elseif ($parameter->isOptional() && !$is_optional_seen && $parameter->getVariadicElementUnionType()->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 * 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($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; } // Whenever we enter a file, we copy all global scope // variables to the local scope $context->getScope()->copyGlobalToLocal(); // Start recursively analyzing the tree return self::analyzeNodeInContext($code_base, $context, $node); }
/** * Check to see if the given Clazz is a duplicate * * @param FunctionInterface $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(FunctionInterface $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; } elseif ($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::maybeEmit($code_base, $context, Issue::ParamSpecial1, $context->getLineNumberStart(), 2, 'glue', (string) $arg2_type, (string) $method->getFQSEN(), 'string', 1, 'array'); } } elseif ((string) $arg1_type == 'string') { if (!$arg2_type->canCastToUnionType(ArrayType::instance()->asUnionType())) { Issue::maybeEmit($code_base, $context, Issue::ParamSpecial1, $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::maybeEmit($code_base, $context, Issue::ParamTooFewInternal, $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::maybeEmit($code_base, $context, Issue::ParamTooFewInternal, $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, StringType::instance()->asUnionType(), function (UnionType $node_type) use($context, $method) { 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; } }
/** * 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; } // Skip methods that are overrides of other methods if ($element instanceof ClassElement) { if ($element->getIsOverride()) { return; } $class_fqsen = $element->getClassFQSEN(); // Don't analyze elements defined in a parent // class if ((string) $class_fqsen !== $element->getFQSEN()) { return; } $defining_class = $element->getClass($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()) { return; } // Ignore magic methods if ($element instanceof Method) { // Doubly nested so that `$element` shows // up as Method in Phan. if ($element->getIsMagic()) { return; } } } // Skip properties on classes that have a magic // __get or __set method given that we can't track // their access if ($element instanceof Property) { $defining_class = $element->getClass($code_base); if ($defining_class->hasGetOrSetMethod($code_base)) { 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::maybeEmit($code_base, $element->getContext(), $issue_type, $element->getFileRef()->getLineNumberStart(), (string) $element->getFQSEN()); } else { Issue::maybeEmit($code_base, $element->getContext(), $issue_type, $element->getFileRef()->getLineNumberStart(), (string) $element); } } }