Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,9 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
));
$specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
if ($specifiedTypes !== null) {
return $specifiedTypes;
return $specifiedTypes
->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope))
->setRootExpr($specifiedTypes->getRootExpr());
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/Analyser/ExprHandler/StaticCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,9 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
));
$specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
if ($specifiedTypes !== null) {
return $specifiedTypes;
return $specifiedTypes
->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope))
->setRootExpr($specifiedTypes->getRootExpr());
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Type/StaticType.php
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop
}

if (!$isFinal || $type instanceof ThisType) {
return $traverse($type);
return RecursionGuard::run($type, static fn () => $traverse($type));
}

return $traverse($type->getStaticObjectType());
Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1593,6 +1593,15 @@ public function testBug9172(): void
$this->assertNotEmpty($errors);
}

#[RequiresPhp('>= 8.1.0')]
public function testPr5880(): void
{
// endless loop
$errors = $this->runAnalyse(__DIR__ . '/data/pr-5880.php');
$this->assertCount(1, $errors);
$this->assertSame('Call to an undefined method (T of PR5880EndlessRecursion\A = PR5880EndlessRecursion\A|PR5880EndlessRecursion\B)|T of PR5880EndlessRecursion\B = PR5880EndlessRecursion\A|PR5880EndlessRecursion\B::a().', $errors[0]->getMessage());
}

public function testBug14707(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-14707.php');
Expand Down
37 changes: 37 additions & 0 deletions tests/PHPStan/Analyser/data/pr-5880.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace PR5880EndlessRecursion;

class A {
public function a(): void {}
}
class B {}

/** @template-covariant T of A|B = A|B */
interface FooInterface
{
/**
* @phpstan-assert-if-true static<A> $this
*/
public function isA(): bool;

/** @return T */
public function get(): A|B;
}

/**
* @template-covariant T of A|B = A|B
* @implements FooInterface<T>
*/
abstract class Foo implements FooInterface
{
public function other(): void
{
if ($this->isA()) {
$this->get()->a();
}
}

}
33 changes: 33 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14829.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@

use function PHPStan\Testing\assertType;

class Checker
{

/** @phpstan-assert-if-true =non-empty-string $path */
public function isReadable(string $path): bool
{
return $path !== '';
}

/** @phpstan-assert-if-true =non-empty-string $path */
public static function staticIsReadable(string $path): bool
{
return $path !== '';
}

}

function testFunction(string $path): void
{
// is_readable() has @phpstan-assert-if-true in stubs/file.stub
Expand All @@ -17,3 +34,19 @@ function testFunction(string $path): void
}
assertType('true', is_readable($path));
}

function testMethod(Checker $c, string $path): void
{
if ($c->isReadable($path)) {
assertType('true', $c->isReadable($path));
assertType('non-empty-string', $path);
}
}

function testStaticMethod(string $path): void
{
if (Checker::staticIsReadable($path)) {
assertType('true', Checker::staticIsReadable($path));
assertType('non-empty-string', $path);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,31 @@ public function testBug8555(): void
$this->analyse([__DIR__ . '/data/bug-8555.php'], []);
}

public function testSelfContradiction(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/self-contradiction.php'], [
[
'Result of && is always false.',
25,
],
[
'Result of && is always false.',
51,
],
[
'Result of && is always false.',
77,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Result of && is always false.',
103,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
]);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug14807(): void
{
Expand Down
113 changes: 113 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/self-contradiction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

namespace SelfContradiction;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Scalar;

class AssertStaticCall {
/**
* @phpstan-assert-if-true Scalar|ClassConstFetch|ConstFetch $node
*/
private static function isSubjectNode(Expr $node): bool
{
return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch;
}

/**
* @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null
*/
private function getSubjectAndValue(Identical $comparison): ?array
{
if (self::isSubjectNode($comparison->left) && !self::isSubjectNode($comparison->left)) {
return ['subject' => $comparison->right, 'value' => $comparison->left];
}

if (!self::isSubjectNode($comparison->left) && self::isSubjectNode($comparison->right)) {
return ['subject' => $comparison->left, 'value' => $comparison->right];
}

return null;
}
}

class AssertMethodCall {
/**
* @phpstan-assert-if-true Scalar|ClassConstFetch|ConstFetch $node
*/
private function isSubjectNode(Expr $node): bool
{
return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch;
}

/**
* @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null
*/
private function getSubjectAndValue(Identical $comparison): ?array
{
if ($this->isSubjectNode($comparison->left) && !$this->isSubjectNode($comparison->left)) {
return ['subject' => $comparison->right, 'value' => $comparison->left];
}

if (!$this->isSubjectNode($comparison->left) && $this->isSubjectNode($comparison->right)) {
return ['subject' => $comparison->left, 'value' => $comparison->right];
}

return null;
}
}

class AssertStaticConditionalReturn {
/**
* @return ($node is Scalar|ClassConstFetch|ConstFetch ? true : false)
*/
private static function isSubjectNode(Expr $node): bool
{
return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch;
}

/**
* @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null
*/
private function getSubjectAndValue(Identical $comparison): ?array
{
if (self::isSubjectNode($comparison->left) && !self::isSubjectNode($comparison->left)) {
return ['subject' => $comparison->right, 'value' => $comparison->left];
}

if (!self::isSubjectNode($comparison->left) && self::isSubjectNode($comparison->right)) {
return ['subject' => $comparison->left, 'value' => $comparison->right];
}

return null;
}
}

class AssertInstanceConditionalReturn {
/**
* @return ($node is Scalar|ClassConstFetch|ConstFetch ? true : false)
*/
private function isSubjectNode(Expr $node): bool
{
return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch;
}

/**
* @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null
*/
private function getSubjectAndValue(Identical $comparison): ?array
{
if ($this->isSubjectNode($comparison->left) && !$this->isSubjectNode($comparison->left)) {
return ['subject' => $comparison->right, 'value' => $comparison->left];
}

if (!$this->isSubjectNode($comparison->left) && $this->isSubjectNode($comparison->right)) {
return ['subject' => $comparison->left, 'value' => $comparison->right];
}

return null;
}
}
Loading