mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Allow assertions on static class properties (#4833)
* Minimal implementation for assertions on static properties * Added inheritance tests * Add support for `ClassName::$var` * Import strpos() to keep phpcs happy * Add support for conditional assertions on static properties
This commit is contained in:
parent
d246932c0c
commit
0b2081b621
@ -1249,9 +1249,16 @@ class AssertionFinder
|
||||
$if_types[$var_id] = [[$assertion->rule[0][0]]];
|
||||
}
|
||||
} elseif (\is_string($assertion->var_id)
|
||||
&& $expr instanceof PhpParser\Node\Expr\MethodCall
|
||||
&& (
|
||||
$expr instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $expr instanceof PhpParser\Node\Expr\StaticCall
|
||||
)
|
||||
) {
|
||||
$if_types[$assertion->var_id] = [[$assertion->rule[0][0]]];
|
||||
$var_id = $assertion->var_id;
|
||||
if (strpos($var_id, 'self::') === 0) {
|
||||
$var_id = $this_class_name . '::' . substr($var_id, 6);
|
||||
}
|
||||
$if_types[$var_id] = [[$assertion->rule[0][0]]];
|
||||
}
|
||||
|
||||
if ($if_types) {
|
||||
@ -1315,9 +1322,16 @@ class AssertionFinder
|
||||
}
|
||||
}
|
||||
} elseif (\is_string($assertion->var_id)
|
||||
&& $expr instanceof PhpParser\Node\Expr\MethodCall
|
||||
&& (
|
||||
$expr instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $expr instanceof PhpParser\Node\Expr\StaticCall
|
||||
)
|
||||
) {
|
||||
$if_types[$assertion->var_id] = [['!' . $assertion->rule[0][0]]];
|
||||
$var_id = $assertion->var_id;
|
||||
if (strpos($var_id, 'self::') === 0) {
|
||||
$var_id = $this_class_name . '::' . substr($var_id, 6);
|
||||
}
|
||||
$if_types[$var_id] = [['!' . $assertion->rule[0][0]]];
|
||||
}
|
||||
|
||||
if ($if_types) {
|
||||
|
@ -670,6 +670,11 @@ class CallAnalyzer
|
||||
$assertion_var_id = $thisName;
|
||||
} elseif (strpos($assertion->var_id, '$this->') === 0 && $thisName !== null) {
|
||||
$assertion_var_id = $thisName . str_replace('$this->', '->', $assertion->var_id);
|
||||
} elseif (strpos($assertion->var_id, 'self::') === 0 && $context->self) {
|
||||
$assertion_var_id = $context->self . str_replace('self::', '::', $assertion->var_id);
|
||||
} elseif (strpos($assertion->var_id, '::$') !== false) {
|
||||
// allow assertions to bring external static props into scope
|
||||
$assertion_var_id = $assertion->var_id;
|
||||
} elseif (isset($context->vars_in_scope[$assertion->var_id])) {
|
||||
$assertion_var_id = $assertion->var_id;
|
||||
}
|
||||
|
@ -388,7 +388,7 @@ class FunctionLikeDocblockParser
|
||||
foreach ($parsed_docblock->tags['psalm-assert'] as $assertion) {
|
||||
$line_parts = CommentAnalyzer::splitDocLine($assertion);
|
||||
|
||||
if (count($line_parts) < 2 || $line_parts[1][0] !== '$') {
|
||||
if (count($line_parts) < 2 || strpos($line_parts[1], '$') === false) {
|
||||
throw new IncorrectDocblockException('Misplaced variable');
|
||||
}
|
||||
|
||||
@ -396,7 +396,7 @@ class FunctionLikeDocblockParser
|
||||
|
||||
$info->assertions[] = [
|
||||
'type' => $line_parts[0],
|
||||
'param_name' => substr($line_parts[1], 1),
|
||||
'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -405,13 +405,13 @@ class FunctionLikeDocblockParser
|
||||
foreach ($parsed_docblock->tags['psalm-assert-if-true'] as $assertion) {
|
||||
$line_parts = CommentAnalyzer::splitDocLine($assertion);
|
||||
|
||||
if (count($line_parts) < 2 || $line_parts[1][0] !== '$') {
|
||||
if (count($line_parts) < 2 || strpos($line_parts[1], '$') === false) {
|
||||
throw new IncorrectDocblockException('Misplaced variable');
|
||||
}
|
||||
|
||||
$info->if_true_assertions[] = [
|
||||
'type' => $line_parts[0],
|
||||
'param_name' => substr($line_parts[1], 1),
|
||||
'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -420,13 +420,13 @@ class FunctionLikeDocblockParser
|
||||
foreach ($parsed_docblock->tags['psalm-assert-if-false'] as $assertion) {
|
||||
$line_parts = CommentAnalyzer::splitDocLine($assertion);
|
||||
|
||||
if (count($line_parts) < 2 || $line_parts[1][0] !== '$') {
|
||||
if (count($line_parts) < 2 || strpos($line_parts[1], '$') === false) {
|
||||
throw new IncorrectDocblockException('Misplaced variable');
|
||||
}
|
||||
|
||||
$info->if_false_assertions[] = [
|
||||
'type' => $line_parts[0],
|
||||
'param_name' => substr($line_parts[1], 1),
|
||||
'param_name' => $line_parts[1][0] === '$' ? substr($line_parts[1], 1) : $line_parts[1],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\PhpVisitor\Reflector;
|
||||
|
||||
use function array_filter;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function end;
|
||||
use function explode;
|
||||
use PhpParser;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use Psalm\Aliases;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\CodeLocation;
|
||||
@ -26,11 +20,19 @@ use Psalm\Storage\FunctionLikeParameter;
|
||||
use Psalm\Storage\FunctionLikeStorage;
|
||||
use Psalm\Storage\MethodStorage;
|
||||
use Psalm\Type;
|
||||
|
||||
use function array_filter;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function preg_split;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function trim;
|
||||
use function preg_split;
|
||||
use function strlen;
|
||||
|
||||
class FunctionLikeDocblockScanner
|
||||
{
|
||||
@ -1104,7 +1106,7 @@ class FunctionLikeDocblockScanner
|
||||
}
|
||||
|
||||
$storage->assertions[] = new \Psalm\Storage\Assertion(
|
||||
'$' . $assertion['param_name'],
|
||||
(strpos($assertion['param_name'], '$') === false ? '$' : '') . $assertion['param_name'],
|
||||
[$assertion_type_parts]
|
||||
);
|
||||
}
|
||||
@ -1143,7 +1145,7 @@ class FunctionLikeDocblockScanner
|
||||
}
|
||||
|
||||
$storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
|
||||
'$' . $assertion['param_name'],
|
||||
(strpos($assertion['param_name'], '$') === false ? '$' : '') . $assertion['param_name'],
|
||||
[$assertion_type_parts]
|
||||
);
|
||||
}
|
||||
@ -1182,7 +1184,7 @@ class FunctionLikeDocblockScanner
|
||||
}
|
||||
|
||||
$storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
|
||||
'$' . $assertion['param_name'],
|
||||
(strpos($assertion['param_name'], '$') === false ? '$' : '') . $assertion['param_name'],
|
||||
[$assertion_type_parts]
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
@ -1289,6 +1290,130 @@ class AssertAnnotationTest extends TestCase
|
||||
[],
|
||||
'8.0'
|
||||
],
|
||||
'assertStaticSelf' => [
|
||||
'<?php
|
||||
final class C {
|
||||
/** @var null|int */
|
||||
private static $q = null;
|
||||
|
||||
/** @psalm-assert int self::$q */
|
||||
private static function prefillQ(): void {
|
||||
self::$q = 123;
|
||||
}
|
||||
|
||||
public static function getQ(): int {
|
||||
self::prefillQ();
|
||||
return self::$q;
|
||||
}
|
||||
}
|
||||
?>'
|
||||
],
|
||||
'assertIfTrueStaticSelf' => [
|
||||
'<?php
|
||||
final class C {
|
||||
/** @var null|int */
|
||||
private static $q = null;
|
||||
|
||||
/** @psalm-assert-if-true int self::$q */
|
||||
private static function prefillQ(): bool {
|
||||
if (rand(0,1)) {
|
||||
self::$q = 123;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getQ(): int {
|
||||
if (self::prefillQ()) {
|
||||
return self::$q;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
?>'
|
||||
],
|
||||
'assertIfFalseStaticSelf' => [
|
||||
'<?php
|
||||
final class C {
|
||||
/** @var null|int */
|
||||
private static $q = null;
|
||||
|
||||
/** @psalm-assert-if-false int self::$q */
|
||||
private static function prefillQ(): bool {
|
||||
if (rand(0,1)) {
|
||||
self::$q = 123;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getQ(): int {
|
||||
if (self::prefillQ()) {
|
||||
return -1;
|
||||
}
|
||||
return self::$q;
|
||||
}
|
||||
}
|
||||
?>'
|
||||
],
|
||||
'assertStaticByInheritedMethod' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var null|int */
|
||||
protected static $q = null;
|
||||
|
||||
/** @psalm-assert int self::$q */
|
||||
protected static function prefillQ(): void {
|
||||
self::$q = 123;
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
public static function getQ(): int {
|
||||
self::prefillQ();
|
||||
return self::$q;
|
||||
}
|
||||
}
|
||||
?>'
|
||||
],
|
||||
'assertInheritedStatic' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var null|int */
|
||||
protected static $q = null;
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
/** @psalm-assert int self::$q */
|
||||
protected static function prefillQ(): void {
|
||||
self::$q = 123;
|
||||
}
|
||||
public static function getQ(): int {
|
||||
self::prefillQ();
|
||||
return self::$q;
|
||||
}
|
||||
}
|
||||
?>'
|
||||
],
|
||||
'assertStaticOnUnrelatedClass' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var null|int */
|
||||
public static $q = null;
|
||||
}
|
||||
|
||||
class B {
|
||||
/** @psalm-assert int A::$q */
|
||||
private static function prefillQ(): void {
|
||||
A::$q = 123;
|
||||
}
|
||||
public static function getQ(): int {
|
||||
self::prefillQ();
|
||||
return A::$q;
|
||||
}
|
||||
}
|
||||
?>'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -1480,7 +1605,8 @@ class AssertAnnotationTest extends TestCase
|
||||
|
||||
if ($bar) {}
|
||||
}',
|
||||
'error_message' => 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:19:29',
|
||||
'error_message' => 'RedundantConditionGivenDocblockType - src'
|
||||
. DIRECTORY_SEPARATOR . 'somefile.php:19:29',
|
||||
],
|
||||
'assertOneOfStrings' => [
|
||||
'<?php
|
||||
|
Loading…
x
Reference in New Issue
Block a user