mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
commit
8b07e69645
@ -237,6 +237,7 @@
|
||||
<xs:element name="FalseOperand" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ForbiddenCode" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ForbiddenEcho" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="IfThisIsMismatch" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ImplementationRequirementViolation" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ImplementedParamTypeMismatch" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ImplementedReturnTypeMismatch" type="IssueHandlerType" minOccurs="0" />
|
||||
|
@ -1,9 +1,10 @@
|
||||
# Adding assertions
|
||||
|
||||
Psalm has three docblock annotations that allow you to specify that a function verifies facts about variables and properties:
|
||||
Psalm has four docblock annotations that allow you to specify that a function verifies facts about variables and properties:
|
||||
|
||||
- `@psalm-assert` (used when throwing an exception)
|
||||
- `@psalm-assert-if-true`/`@psalm-assert-if-false` (used when returning a `bool`)
|
||||
- `@psalm-if-this-is` (used when calling a method)
|
||||
|
||||
A list of acceptable assertions [can be found here](assertion_syntax.md).
|
||||
|
||||
@ -152,3 +153,39 @@ if( $result->hasException() ) {
|
||||
|
||||
Please note that the example above only works if you enable [method call memoization](https://psalm.dev/docs/running_psalm/configuration/#memoizemethodcallresults)
|
||||
in the config file or annotate the class as [immutable](https://psalm.dev/docs/annotating_code/supported_annotations/#psalm-immutable).
|
||||
|
||||
|
||||
You can also make sure, when calling a method, that its object has some specific template arguments:
|
||||
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class a {
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $data;
|
||||
/**
|
||||
* @param T $data
|
||||
*/
|
||||
public function __construct($data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
/**
|
||||
* @psalm-if-this-is a<int>
|
||||
*/
|
||||
public function test(): void {
|
||||
}
|
||||
}
|
||||
|
||||
$i = new a(123);
|
||||
$i->test();
|
||||
|
||||
$i = new a("test");
|
||||
// IfThisIsMismatch - Class is not a<int> as required by psalm-if-this-is
|
||||
$i->test();
|
||||
```
|
||||
|
@ -132,7 +132,7 @@ function addString(?string $s) {
|
||||
|
||||
`@psalm-suppress all` can be used to suppress all issues instead of listing them individually.
|
||||
|
||||
### `@psalm-assert`, `@psalm-assert-if-true` and `@psalm-assert-if-false`
|
||||
### `@psalm-assert`, `@psalm-assert-if-true`, `@psalm-assert-if-false` and `@psalm-if-this-is`
|
||||
|
||||
See [Adding assertions](adding_assertions.md).
|
||||
|
||||
|
@ -29,6 +29,7 @@
|
||||
- [FalseOperand](issues/FalseOperand.md)
|
||||
- [ForbiddenCode](issues/ForbiddenCode.md)
|
||||
- [ForbiddenEcho](issues/ForbiddenEcho.md)
|
||||
- [IfThisIsMismatch](issues/IfThisIsMismatch.md)
|
||||
- [ImplementationRequirementViolation](issues/ImplementationRequirementViolation.md)
|
||||
- [ImplementedParamTypeMismatch](issues/ImplementedParamTypeMismatch.md)
|
||||
- [ImplementedReturnTypeMismatch](issues/ImplementedReturnTypeMismatch.md)
|
||||
|
35
docs/running_psalm/issues/IfThisIsMismatch.md
Normal file
35
docs/running_psalm/issues/IfThisIsMismatch.md
Normal file
@ -0,0 +1,35 @@
|
||||
# IfThisIsMismatch
|
||||
|
||||
Emitted when the type in `@psalm-if-this-is` annotation cannot be contained by the object's actual type.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class a {
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $data;
|
||||
/**
|
||||
* @param T $data
|
||||
*/
|
||||
public function __construct($data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
/**
|
||||
* @psalm-if-this-is a<int>
|
||||
*/
|
||||
public function test(): void {
|
||||
}
|
||||
}
|
||||
|
||||
$i = new a(123);
|
||||
$i->test();
|
||||
|
||||
$i = new a("test");
|
||||
// IfThisIsMismatch - Class is not a<int> as required by psalm-if-this-is
|
||||
$i->test();
|
||||
```
|
@ -39,7 +39,7 @@ class DocComment
|
||||
'yield', 'trace', 'import-type', 'flow', 'taint-specialize', 'taint-escape',
|
||||
'taint-unescape', 'self-out', 'consistent-constructor', 'stub-override',
|
||||
'require-extends', 'require-implements', 'param-out', 'ignore-var',
|
||||
'consistent-templates',
|
||||
'consistent-templates', 'if-this-is',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -8,6 +8,8 @@ use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Issue\IfThisIsMismatch;
|
||||
use Psalm\Issue\InvalidMethodCall;
|
||||
use Psalm\Issue\InvalidScope;
|
||||
use Psalm\Issue\NullReference;
|
||||
@ -434,6 +436,24 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
||||
// TODO: When should a method have a storage?
|
||||
if ($codebase->methods->hasStorage($method_id)) {
|
||||
$storage = $codebase->methods->getStorage($method_id);
|
||||
if ($storage->if_this_is_type
|
||||
&& !UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$class_type,
|
||||
$storage->if_this_is_type
|
||||
)
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new IfThisIsMismatch(
|
||||
'Class is not ' . (string) $storage->if_this_is_type
|
||||
. ' as required by psalm-if-this-is',
|
||||
new CodeLocation($source, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// keep going
|
||||
}
|
||||
}
|
||||
if ($storage->self_out_type) {
|
||||
$self_out_type = $storage->self_out_type;
|
||||
$context->vars_in_scope[$lhs_var_id] = $self_out_type;
|
||||
|
@ -182,6 +182,24 @@ class FunctionLikeDocblockParser
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['psalm-if-this-is'])) {
|
||||
foreach ($parsed_docblock->tags['psalm-if-this-is'] as $offset => $param) {
|
||||
$line_parts = CommentAnalyzer::splitDocLine($param);
|
||||
|
||||
$line_parts[0] = str_replace("\n", '', preg_replace('@^[ \t]*\*@m', '', $line_parts[0]));
|
||||
|
||||
$info->if_this_is = [
|
||||
'type' => str_replace("\n", '', $line_parts[0]),
|
||||
'line_number' => $comment->getStartLine() + substr_count(
|
||||
$comment->getText(),
|
||||
"\n",
|
||||
0,
|
||||
$offset - $comment->getStartFilePos()
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['psalm-taint-sink'])) {
|
||||
foreach ($parsed_docblock->tags['psalm-taint-sink'] as $param) {
|
||||
$param_parts = preg_split('/\s+/', trim($param));
|
||||
|
@ -283,6 +283,22 @@ class FunctionLikeDocblockScanner
|
||||
$storage->self_out_type = $out_type;
|
||||
}
|
||||
|
||||
if ($docblock_info->if_this_is
|
||||
&& $storage instanceof MethodStorage) {
|
||||
$out_type = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$docblock_info->if_this_is['type'],
|
||||
$aliases,
|
||||
$function_template_types + $class_template_types,
|
||||
$type_aliases
|
||||
),
|
||||
null,
|
||||
$function_template_types + $class_template_types,
|
||||
$type_aliases
|
||||
);
|
||||
$storage->if_this_is_type = $out_type;
|
||||
}
|
||||
|
||||
foreach ($docblock_info->taint_sink_params as $taint_sink_param) {
|
||||
$param_name = substr($taint_sink_param['name'], 1);
|
||||
|
||||
|
@ -56,6 +56,11 @@ class FunctionDocblockComment
|
||||
*/
|
||||
public $self_out;
|
||||
|
||||
/**
|
||||
* @var array{type:string, line_number: int}|null
|
||||
*/
|
||||
public $if_this_is;
|
||||
|
||||
/**
|
||||
* @var array<int, array{name:string, type:string, line_number: int}>
|
||||
*/
|
||||
|
8
src/Psalm/Issue/IfThisIsMismatch.php
Normal file
8
src/Psalm/Issue/IfThisIsMismatch.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class IfThisIsMismatch extends CodeIssue
|
||||
{
|
||||
const ERROR_LEVEL = 4;
|
||||
const SHORTCODE = 300;
|
||||
}
|
@ -90,6 +90,10 @@ class MethodStorage extends FunctionLikeStorage
|
||||
*/
|
||||
public $self_out_type;
|
||||
|
||||
/**
|
||||
* @var Type\Union|null
|
||||
*/
|
||||
public $if_this_is_type = null;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
|
200
tests/IfThisIsTest.php
Normal file
200
tests/IfThisIsTest.php
Normal file
@ -0,0 +1,200 @@
|
||||
<?php
|
||||
namespace Psalm\Tests;
|
||||
|
||||
class IfThisIsTest extends TestCase
|
||||
{
|
||||
use Traits\ValidCodeAnalysisTestTrait;
|
||||
use Traits\InvalidCodeAnalysisTestTrait;
|
||||
|
||||
/**
|
||||
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
||||
*/
|
||||
public function providerValidCodeParse(): iterable
|
||||
{
|
||||
return [
|
||||
'worksAfterConvert' => [
|
||||
'<?php
|
||||
interface I {
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function test();
|
||||
}
|
||||
|
||||
class F implements I
|
||||
{
|
||||
/**
|
||||
* @psalm-self-out I
|
||||
* @return void
|
||||
*/
|
||||
public function convert() {}
|
||||
|
||||
/**
|
||||
* @psalm-if-this-is I
|
||||
* @return void
|
||||
*/
|
||||
public function test() {}
|
||||
}
|
||||
|
||||
$f = new F();
|
||||
$f->convert();
|
||||
$f->test();
|
||||
'
|
||||
],
|
||||
'withTemplate' => [
|
||||
'<?php
|
||||
class Frozen {}
|
||||
class Unfrozen {}
|
||||
|
||||
/**
|
||||
* @template T of Frozen|Unfrozen
|
||||
*/
|
||||
class Foo
|
||||
{
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $state;
|
||||
|
||||
/**
|
||||
* @param T $state
|
||||
*/
|
||||
public function __construct($state)
|
||||
{
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $val
|
||||
* @psalm-if-this-is Foo<Unfrozen>
|
||||
* @return void
|
||||
*/
|
||||
public function set($name, $val)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Foo<Frozen>
|
||||
*/
|
||||
public function freeze()
|
||||
{
|
||||
/** @var Foo<Frozen> */
|
||||
$f = clone $this;
|
||||
return $f;
|
||||
}
|
||||
}
|
||||
|
||||
$f = new Foo(new Unfrozen());
|
||||
$f->set("asd", 10);
|
||||
'
|
||||
],
|
||||
'subclass' => [
|
||||
'<?php
|
||||
class G
|
||||
{
|
||||
/**
|
||||
* @psalm-if-this-is G
|
||||
* @return void
|
||||
*/
|
||||
public function test() {}
|
||||
}
|
||||
|
||||
class F extends G
|
||||
{
|
||||
}
|
||||
|
||||
$f = new F();
|
||||
$f->test();
|
||||
'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: string, error_message: string}>
|
||||
*/
|
||||
public function providerInvalidCodeParse(): iterable
|
||||
{
|
||||
return [
|
||||
'failsWithWrongTemplate1' => [
|
||||
'<?php
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class a {
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $data;
|
||||
/**
|
||||
* @param T $data
|
||||
*/
|
||||
public function __construct($data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
/**
|
||||
* @psalm-if-this-is a<int>
|
||||
*/
|
||||
public function test(): void {
|
||||
}
|
||||
}
|
||||
|
||||
$i = new a("test");
|
||||
$i->test();
|
||||
',
|
||||
'error_message' => 'IfThisIsMismatch'
|
||||
],
|
||||
'failsWithWrongTemplate2' => [
|
||||
'<?php
|
||||
class Frozen {}
|
||||
class Unfrozen {}
|
||||
|
||||
/**
|
||||
* @template T of Frozen|Unfrozen
|
||||
*/
|
||||
class Foo
|
||||
{
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $state;
|
||||
|
||||
/**
|
||||
* @param T $state
|
||||
*/
|
||||
public function __construct($state)
|
||||
{
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $val
|
||||
* @psalm-if-this-is Foo<Unfrozen>
|
||||
* @return void
|
||||
*/
|
||||
public function set($name, $val) {}
|
||||
|
||||
/**
|
||||
* @return Foo<Frozen>
|
||||
*/
|
||||
public function freeze()
|
||||
{
|
||||
/** @var Foo<Frozen> */
|
||||
$f = clone $this;
|
||||
return $f;
|
||||
}
|
||||
}
|
||||
|
||||
$f = new Foo(new Unfrozen());
|
||||
$f->set("asd", 10);
|
||||
$g = $f->freeze();
|
||||
$g->set("asd", 20); // Fails
|
||||
',
|
||||
'error_message' => 'IfThisIsMismatch'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user