/** * 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()); } } }
/** * @return array * Get a map from column name to row values for * this instance */ public function toRow() : array { return ['scope_name' => $this->primaryKeyValue(), 'fqsen' => (string) $this->method->getFQSEN(), 'name' => (string) $this->method->getName(), 'type' => (string) $this->method->getUnionType(), 'flags' => $this->method->getFlags(), 'context' => base64_encode(serialize($this->method->getContext())), 'is_deprecated' => $this->method->isDeprecated(), 'number_of_required_parameters' => $this->method->getNumberOfRequiredParameters(), 'number_of_optional_parameters' => $this->method->getNumberOfOptionalParameters(), 'is_dynamic' => $this->method->isDynamic()]; }
/** * 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; } }
/** * 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; } }