1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

New annotation: @psalm-if-this-is

This commit is contained in:
Olle 2020-07-08 19:51:02 +00:00
parent 290207dd3f
commit 285348efe9
8 changed files with 155 additions and 1 deletions

View File

@ -35,7 +35,7 @@ class DocComment
'mutation-free', 'external-mutation-free', 'immutable', 'readonly',
'allow-private-mutation', 'readonly-allow-private-mutation',
'yield', 'trace', 'import-type', 'flow', 'taint-specialize', 'taint-escape',
'taint-unescape', 'self-out'
'taint-unescape', 'self-out', 'if-this-is'
];
/**

View File

@ -482,6 +482,21 @@ class CommentAnalyzer
}
}
if (isset($parsed_docblock->tags['psalm-if-this-is'])) {
foreach ($parsed_docblock->tags['psalm-if-this-is'] as $offset => $param) {
$line_parts = self::splitDocLine($param);
if (count($line_parts) > 0) {
$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->getLine() + substr_count($comment_text, "\n", 0, $offset),
];
}
}
}
if (isset($parsed_docblock->tags['psalm-flow'])) {
foreach ($parsed_docblock->tags['psalm-flow'] as $param) {
$info->flows[] = trim($param);

View File

@ -20,6 +20,7 @@ use Psalm\Issue\TooManyArguments;
use Psalm\Issue\UndefinedInterfaceMethod;
use Psalm\Issue\UndefinedMagicMethod;
use Psalm\Issue\UndefinedMethod;
use Psalm\Issue\IfThisIsMismatch;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
@ -412,6 +413,18 @@ 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
&& !$storage->if_this_is_type->equals($class_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;

View File

@ -2519,6 +2519,22 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
$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'],
$this->aliases,
$this->function_template_types + $class_template_types,
$this->type_aliases
),
null,
$this->function_template_types + $class_template_types,
$this->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);

View File

@ -46,6 +46,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}>
*/

View File

@ -0,0 +1,8 @@
<?php
namespace Psalm\Issue;
class IfThisIsMismatch extends CodeIssue
{
const ERROR_LEVEL = 4;
const SHORTCODE = 300;
}

View File

@ -79,4 +79,9 @@ class MethodStorage extends FunctionLikeStorage
* @var Type\Union|null
*/
public $self_out_type = null;
/**
* @var Type\Union|null
*/
public $if_this_is_type = null;
}

92
tests/IfThisIsTest.php Normal file
View File

@ -0,0 +1,92 @@
<?php
namespace Psalm\Tests;
class SelfOutTest extends TestCase
{
use Traits\ValidCodeAnalysisTestTrait;
use Traits\InvalidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/
public function providerValidCodeParse()
{
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();
'
]
];
}
/**
* @return array<string, array{0: string, error_message: string}>
*/
public function providerInvalidCodeParse()
{
return [
'blocksWithoutConvert' => [
'<?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->test();
',
'error_message' => 'IfThisIsMismatch'
]
];
}
}