/**
* 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());
}
}
}