1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Add ImplementedParamTypeMismatch issue

Fixes #1633
This commit is contained in:
Brown 2019-05-14 15:44:46 -04:00
parent 2c36a10ac8
commit 30cbcb6c36
14 changed files with 243 additions and 87 deletions

View File

@ -102,5 +102,6 @@
<TypeDoesNotContainNull errorLevel="info" />
<MissingDocblockType errorLevel="info" />
<ImplementedReturnTypeMismatch errorLevel="info" />
<ImplementedParamTypeMismatch errorLevel="info" />
</issueHandlers>
</psalm>

View File

@ -102,6 +102,7 @@
<TypeDoesNotContainNull errorLevel="info" />
<MissingDocblockType errorLevel="info" />
<ImplementedReturnTypeMismatch errorLevel="info" />
<ImplementedParamTypeMismatch errorLevel="info" />
<!-- level 6 issues - really bad things -->

View File

@ -102,6 +102,7 @@
<TypeDoesNotContainNull errorLevel="info" />
<MissingDocblockType errorLevel="info" />
<ImplementedReturnTypeMismatch errorLevel="info" />
<ImplementedParamTypeMismatch errorLevel="info" />
<!-- level 6 issues - really bad things -->

View File

@ -102,6 +102,7 @@
<TypeDoesNotContainNull errorLevel="info" />
<MissingDocblockType errorLevel="info" />
<ImplementedReturnTypeMismatch errorLevel="info" />
<ImplementedParamTypeMismatch errorLevel="info" />
<!-- level 6 issues - really bad things -->

View File

@ -163,6 +163,7 @@
<xs:element name="ForbiddenCode" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ForbiddenEcho" type="IssueHandlerType" minOccurs="0" />
<xs:element name="NoValue" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImplementedParamTypeMismatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImplementedReturnTypeMismatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImplicitToStringCast" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InaccessibleClassConstant" type="IssueHandlerType" minOccurs="0" />

View File

@ -271,6 +271,22 @@ Emitted when Psalm encounters an echo statement and the `forbidEcho` flag in you
echo("bah");
```
### ImplementedParamTypeMismatch
Emitted when a class that inherits another, or implements an interface, has docblock param type that's entirely different to the parent. Subclasses of the parent return type are permitted, in docblocks.
```php
class D {
/** @param string $a */
public function foo($a): void {}
}
class E extends D {
/** @param int $a */
public function foo($a): void {}
}
```
### ImplementedReturnTypeMismatch
Emitted when a class that inherits another, or implements an interface, has docblock return type that's entirely different to the parent. Subclasses of the parent return type are permitted, in docblocks.

View File

@ -1248,6 +1248,10 @@ class Config
return 'MethodSignatureMismatch';
}
if ($issue_type === 'ImplementedParamTypeMismatch') {
return 'MoreSpecificImplementedParamType';
}
if ($issue_type === 'MixedArgumentTypeCoercion'
|| $issue_type === 'MixedPropertyTypeCoercion'
|| $issue_type === 'MixedReturnTypeCoercion'

View File

@ -7,6 +7,7 @@ use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Issue\DeprecatedMethod;
use Psalm\Issue\ImplementedParamTypeMismatch;
use Psalm\Issue\ImplementedReturnTypeMismatch;
use Psalm\Issue\InaccessibleMethod;
use Psalm\Issue\InternalMethod;
@ -733,6 +734,57 @@ class MethodAnalyzer extends FunctionLikeAnalyzer
$implementer_param = $implementer_method_storage->params[$i];
if ($prevent_method_signature_mismatch
&& !$guide_classlike_storage->user_defined
&& $guide_param->type
) {
$implementer_param_type = $implementer_method_storage->params[$i]->signature_type;
$guide_param_signature_type = $guide_param->type;
$or_null_guide_param_signature_type = $guide_param->signature_type
? clone $guide_param->signature_type
: null;
if ($or_null_guide_param_signature_type) {
$or_null_guide_param_signature_type->addType(new Type\Atomic\TNull);
}
if ($cased_guide_method_id === 'Serializable::unserialize') {
$guide_param_signature_type = null;
$or_null_guide_param_signature_type = null;
}
if (!$guide_param->type->hasMixed()
&& !$guide_param->type->from_docblock
&& ($implementer_param_type || $guide_param_signature_type)
) {
if ($implementer_param_type
&& (!$guide_param_signature_type
|| strtolower($implementer_param_type->getId())
!== strtolower($guide_param_signature_type->getId()))
&& (!$or_null_guide_param_signature_type
|| strtolower($implementer_param_type->getId())
!== strtolower($or_null_guide_param_signature_type->getId()))
) {
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong type \'' .
$implementer_param_type . '\', expecting \'' .
$guide_param_signature_type . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
?: $code_location
)
)) {
return false;
}
return null;
}
}
}
if ($prevent_method_signature_mismatch
&& $guide_classlike_storage->user_defined
&& $implementer_param->signature_type
@ -790,8 +842,7 @@ class MethodAnalyzer extends FunctionLikeAnalyzer
}
}
if ($guide_classlike_storage->user_defined
&& $implementer_param->type
if ($implementer_param->type
&& $guide_param->type
&& $implementer_param->type->getId() !== $guide_param->type->getId()
) {
@ -832,21 +883,44 @@ class MethodAnalyzer extends FunctionLikeAnalyzer
$codebase,
$guide_method_storage_param_type,
$implementer_method_storage_param_type,
false,
false
!$guide_classlike_storage->user_defined,
!$guide_classlike_storage->user_defined,
$has_scalar_match,
$type_coerced,
$type_coerced_from_mixed
)) {
if (IssueBuffer::accepts(
new MoreSpecificImplementedParamType(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong type \'' .
$implementer_method_storage_param_type->getId() . '\', expecting \'' .
$guide_method_storage_param_type->getId() . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
?: $code_location
),
$suppressed_issues
)) {
return false;
// is the declared return type more specific than the inferred one?
if ($type_coerced) {
if ($guide_classlike_storage->user_defined) {
if (IssueBuffer::accepts(
new MoreSpecificImplementedParamType(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id
. ' has the more specific type \'' .
$implementer_method_storage_param_type->getId() . '\', expecting \'' .
$guide_method_storage_param_type->getId() . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
?: $code_location
),
$suppressed_issues
)) {
return false;
}
}
} else {
if (IssueBuffer::accepts(
new ImplementedParamTypeMismatch(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong type \'' .
$implementer_method_storage_param_type->getId() . '\', expecting \'' .
$guide_method_storage_param_type->getId() . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
?: $code_location
),
$suppressed_issues
)) {
return false;
}
}
}
}
@ -866,49 +940,6 @@ class MethodAnalyzer extends FunctionLikeAnalyzer
return null;
}
$implemeneter_param_type = $implementer_method_storage->params[$i]->type;
$or_null_guide_type = $guide_param->signature_type
? clone $guide_param->signature_type
: null;
if ($or_null_guide_type) {
$or_null_guide_type->addType(new Type\Atomic\TNull);
}
if (!$guide_classlike_storage->user_defined
&& $guide_param->type
&& !$guide_param->type->hasMixed()
&& !$guide_param->type->from_docblock
&& ($cased_guide_method_id !== 'SoapClient::__soapCall' || $implemeneter_param_type)
&& (
!$implemeneter_param_type
|| (
strtolower($implemeneter_param_type->getId()) !== strtolower($guide_param->type->getId())
&& (
!$or_null_guide_type
|| strtolower($implemeneter_param_type->getId())
!== strtolower($or_null_guide_type->getId())
)
)
)
) {
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong type \'' .
$implemeneter_param_type . '\', expecting \'' .
$guide_param->type . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
?: $code_location
)
)) {
return false;
}
return null;
}
}
if ($guide_classlike_storage->user_defined

View File

@ -11908,7 +11908,7 @@ return [
'SoapClient::__setCookie' => ['', 'name'=>'string', 'value='=>'string'],
'SoapClient::__setLocation' => ['string', 'new_location='=>'string'],
'SoapClient::__setSoapHeaders' => ['bool', 'soapheaders='=>''],
'SoapClient::__soapCall' => ['', 'function_name'=>'string', 'arguments'=>'array', 'options='=>'array', 'input_headers='=>'', '&w_output_headers='=>'array'],
'SoapClient::__soapCall' => ['', 'function_name'=>'string', 'arguments'=>'array', 'options='=>'array', 'input_headers='=>'SoapHeader|array', '&w_output_headers='=>'array'],
'SoapClient::SoapClient' => ['object', 'wsdl'=>'mixed', 'options='=>'array'],
'SoapFault::__clone' => ['void'],
'SoapFault::__construct' => ['void', 'faultcode'=>'string', 'faultstring'=>'string', 'faultactor='=>'string', 'detail='=>'string', 'faultname='=>'string', 'headerfault='=>'string'],

View File

@ -306,7 +306,7 @@ class Reflection
$is_optional = (bool)$param->isOptional();
return new FunctionLikeParameter(
$parameter = new FunctionLikeParameter(
$param_name,
(bool)$param->isPassedByReference(),
$param_type,
@ -316,6 +316,10 @@ class Reflection
$param_type->isNullable(),
$param->isVariadic()
);
$parameter->signature_type = Type::getMixed();
return $parameter;
}
/**

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class ImplementedParamTypeMismatch extends CodeIssue
{
}

View File

@ -522,7 +522,7 @@ class MagicMethodAnnotationTest extends TestCase
/** @method D foo(int $s) */
class B extends A {}',
'error_message' => 'MoreSpecificImplementedParamType - src/somefile.php:11:21',
'error_message' => 'ImplementedParamTypeMismatch - src/somefile.php:11:21',
],
];
}

View File

@ -11,7 +11,7 @@ class MethodSignatureTest extends TestCase
/**
* @return void
*/
public function testExtendDocblockParamType()
public function testExtendSoapClientWithDocblockTypes()
{
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
@ -28,7 +28,7 @@ class MethodSignatureTest extends TestCase
* @param string $function_name
* @param array<mixed> $arguments
* @param array<mixed> $options default null
* @param array<mixed> $input_headers default null
* @param array|SoapHeader $input_headers default null
* @param array<mixed> $output_headers default null
* @return mixed
*/
@ -41,21 +41,26 @@ class MethodSignatureTest extends TestCase
) {
return $_GET["foo"];
}
}
}'
);
class B extends SoapClient
{
public function __soapCall(
$function_name,
$arguments,
$options = [],
$input_headers = [],
&$output_headers = []
) {
return $_GET["foo"];
}
}
$this->analyzeFile('somefile.php', new Context());
}
/**
* @return void
*/
public function testExtendSoapClientWithNoDocblockTypes()
{
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
return;
}
$this->addFile(
'somefile.php',
'<?php
class C extends SoapClient
{
public function __soapCall(
@ -73,6 +78,79 @@ class MethodSignatureTest extends TestCase
$this->analyzeFile('somefile.php', new Context());
}
/**
* @return void
*/
public function testExtendSoapClientWithParamType()
{
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
return;
}
$this->addFile(
'somefile.php',
'<?php
class C extends SoapClient
{
public function __soapCall(
string $function_name,
$arguments,
$options = [],
$input_headers = [],
&$output_headers = []
) {
return $_GET["foo"];
}
}'
);
$this->analyzeFile('somefile.php', new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage ImplementedParamTypeMismatch
*
* @return void
*/
public function testExtendDocblockParamTypeWithWrongDocblockParam()
{
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
return;
}
$this->addFile(
'somefile.php',
'<?php
class A extends SoapClient
{
/**
* @param string $function_name
* @param string $arguments
* @param array<mixed> $options default null
* @param array<mixed> $input_headers default null
* @param array<mixed> $output_headers default null
* @return mixed
*/
public function __soapCall(
$function_name,
$arguments,
$options = [],
$input_headers = [],
&$output_headers = []
) {
}
}'
);
$this->analyzeFile('somefile.php', new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage MethodSignatureMismatch
@ -92,14 +170,6 @@ class MethodSignatureTest extends TestCase
'<?php
class A extends SoapClient
{
/**
* @param string $function_name
* @param string $arguments
* @param array<mixed> $options default null
* @param array<mixed> $input_headers default null
* @param array<mixed> $output_headers default null
* @return mixed
*/
public function __soapCall(
$function_name,
string $arguments,
@ -276,7 +346,10 @@ class MethodSignatureTest extends TestCase
/** @var int */
private $id = 1;
public function unserialize(string $serialized) : void
/**
* @param string $serialized
*/
public function unserialize($serialized) : void
{
[
$this->id,
@ -870,6 +943,23 @@ class MethodSignatureTest extends TestCase
'error_message' => 'MethodSignatureMismatch',
['MoreSpecificImplementedParamType']
],
'preventImplementingSerializableWithType' => [
'<?php
class Foo implements \Serializable {
public function unserialize(string $serialized) {}
public function serialize() {}
}',
'error_message' => 'MethodSignatureMismatch',
],
'preventImplementingSerializableWithWrongDocblockType' => [
'<?php
class Foo implements \Serializable {
/** @param int $serialized */
public function unserialize($serialized) {}
public function serialize() {}
}',
'error_message' => 'ImplementedParamTypeMismatch',
],
];
}
}

View File

@ -1 +1 @@
/Users/matthewbrown/Desktop/vimeo/git/psalm/tests/fixtures/symlinktest/a
/Users/brownma/Desktop/git/psalm/tests/fixtures/symlinktest/a