mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
parent
2c36a10ac8
commit
30cbcb6c36
@ -102,5 +102,6 @@
|
||||
<TypeDoesNotContainNull errorLevel="info" />
|
||||
<MissingDocblockType errorLevel="info" />
|
||||
<ImplementedReturnTypeMismatch errorLevel="info" />
|
||||
<ImplementedParamTypeMismatch errorLevel="info" />
|
||||
</issueHandlers>
|
||||
</psalm>
|
||||
|
@ -102,6 +102,7 @@
|
||||
<TypeDoesNotContainNull errorLevel="info" />
|
||||
<MissingDocblockType errorLevel="info" />
|
||||
<ImplementedReturnTypeMismatch errorLevel="info" />
|
||||
<ImplementedParamTypeMismatch errorLevel="info" />
|
||||
|
||||
<!-- level 6 issues - really bad things -->
|
||||
|
||||
|
@ -102,6 +102,7 @@
|
||||
<TypeDoesNotContainNull errorLevel="info" />
|
||||
<MissingDocblockType errorLevel="info" />
|
||||
<ImplementedReturnTypeMismatch errorLevel="info" />
|
||||
<ImplementedParamTypeMismatch errorLevel="info" />
|
||||
|
||||
<!-- level 6 issues - really bad things -->
|
||||
|
||||
|
@ -102,6 +102,7 @@
|
||||
<TypeDoesNotContainNull errorLevel="info" />
|
||||
<MissingDocblockType errorLevel="info" />
|
||||
<ImplementedReturnTypeMismatch errorLevel="info" />
|
||||
<ImplementedParamTypeMismatch errorLevel="info" />
|
||||
|
||||
<!-- level 6 issues - really bad things -->
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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.
|
||||
|
@ -1248,6 +1248,10 @@ class Config
|
||||
return 'MethodSignatureMismatch';
|
||||
}
|
||||
|
||||
if ($issue_type === 'ImplementedParamTypeMismatch') {
|
||||
return 'MoreSpecificImplementedParamType';
|
||||
}
|
||||
|
||||
if ($issue_type === 'MixedArgumentTypeCoercion'
|
||||
|| $issue_type === 'MixedPropertyTypeCoercion'
|
||||
|| $issue_type === 'MixedReturnTypeCoercion'
|
||||
|
@ -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
|
||||
|
@ -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'],
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
6
src/Psalm/Issue/ImplementedParamTypeMismatch.php
Normal file
6
src/Psalm/Issue/ImplementedParamTypeMismatch.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class ImplementedParamTypeMismatch extends CodeIssue
|
||||
{
|
||||
}
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
2
tests/fixtures/symlinktest/ignored/b
vendored
2
tests/fixtures/symlinktest/ignored/b
vendored
@ -1 +1 @@
|
||||
/Users/matthewbrown/Desktop/vimeo/git/psalm/tests/fixtures/symlinktest/a
|
||||
/Users/brownma/Desktop/git/psalm/tests/fixtures/symlinktest/a
|
Loading…
x
Reference in New Issue
Block a user