1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +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:
Bruce Weirdan 2020-12-21 19:05:44 +02:00 committed by Daniil Gentili
parent d246932c0c
commit 0b2081b621
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
5 changed files with 170 additions and 23 deletions

View File

@ -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) {

View File

@ -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;
}

View File

@ -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],
];
}
}

View File

@ -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]
);
}

View File

@ -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