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:
parent
290207dd3f
commit
285348efe9
@ -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'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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}>
|
||||
*/
|
||||
|
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;
|
||||
}
|
@ -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
92
tests/IfThisIsTest.php
Normal 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'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user