Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion src/Analyser/StmtHandler/ExpressionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ public function processStmt(
);
$scope = $scope->applySpecifiedTypes($specifiedTypes);

if ($specifiedTypes->isEquality()) {
if (
$specifiedTypes->isEquality()
&& $stmt->expr instanceof Expr\CallLike
&& !$this->typeSpecifier->callOperandsContainNonPureCall($stmt->expr, $scope)
) {
// Statement counterpart of the equality handling in filterByTruthyValue():
// store the call's true result so a duplicate void assertion statement is
// reported as always-true. We assign directly because void calls have no
Expand Down
16 changes: 16 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,22 @@ private function findNonPureCall(Node $node, Scope $scope, bool &$containsCall):
}
}

return $this->findNonPureCallInSubNodes($node, $scope, $containsCall);
}

/**
* Whether the arguments of the call, or what it is called on, contain a call
* that isn't known to be pure. The purity of the call itself is not considered.
*/
public function callOperandsContainNonPureCall(Expr\CallLike $call, Scope $scope): bool
{
$containsCall = false;

return $this->findNonPureCallInSubNodes($call, $scope, $containsCall);
}

private function findNonPureCallInSubNodes(Node $node, Scope $scope, bool &$containsCall): bool
{
foreach ($node->getSubNodeNames() as $subNodeName) {
$subNode = $node->$subNodeName;
if ($subNode instanceof Node) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,17 @@ public function testBug14705Php8(): void
]);
}

public function testBug15328(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-15328.php'], [
[
'Call to function assertSame() with int and int will always evaluate to true.',
83,
],
]);
}

public function testBug2755(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,17 @@ public function testBug14705(): void
]);
}

public function testBug15328(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-15328.php'], [
[
'Call to method Bug15328\Assert::assertSameMethod() with int and int will always evaluate to true.',
111,
],
]);
}

public function testInTrait(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,17 @@ public function testBug13566(): void
$this->analyse([__DIR__ . '/data/bug-13566.php'], []);
}

public function testBug15328(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-15328.php'], [
[
'Call to static method Bug15328\Assert::assertSameStatic() with int and int will always evaluate to true.',
97,
],
]);
}

public function testInTrait(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
112 changes: 112 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-15328.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php declare(strict_types = 1);

namespace Bug15328;

/**
* @template ExpectedType
*
* @param ExpectedType $expected
* @param mixed $actual
*
* @phpstan-assert =ExpectedType $actual
*/
function assertSame($expected, $actual): void
{
if ($expected !== $actual) {
throw new \Exception;
}
}

final class Counter
{
private int $count = 0;

/** @phpstan-impure */
public function next(): int
{
return ++$this->count;
}
}

final class Assert
{

/**
* @template ExpectedType
*
* @param ExpectedType $expected
* @param mixed $actual
*
* @phpstan-assert =ExpectedType $actual
*/
public static function assertSameStatic($expected, $actual): void
{
}

/**
* @template ExpectedType
*
* @param ExpectedType $expected
* @param mixed $actual
*
* @phpstan-assert =ExpectedType $actual
*/
public function assertSameMethod($expected, $actual): void
{
}

}

function impureMethod(int $key): void
{
$counter = new Counter;

assertSame($key, $counter->next());
assertSame($key, $counter->next());
}

function impureFunction(int $key): void
{
assertSame($key, random_int(1, 10));
assertSame($key, random_int(1, 10));
}

function impureExpected(int $key): void
{
assertSame(random_int(1, 10), $key);
assertSame(random_int(1, 10), $key);
}

function pure(int $key, int $value): void
{
assertSame($key, $value);
assertSame($key, $value);
}

function staticMethod(int $key, Counter $counter): void
{
Assert::assertSameStatic($key, $counter->next());
Assert::assertSameStatic($key, $counter->next());
Assert::assertSameStatic($key, random_int(1, 10));
Assert::assertSameStatic($key, random_int(1, 10));
}

function staticMethodPure(int $key, int $value): void
{
Assert::assertSameStatic($key, $value);
Assert::assertSameStatic($key, $value);
}

function method(int $key, Counter $counter, Assert $assert): void
{
$assert->assertSameMethod($key, $counter->next());
$assert->assertSameMethod($key, $counter->next());
$assert->assertSameMethod($key, random_int(1, 10));
$assert->assertSameMethod($key, random_int(1, 10));
}

function methodPure(int $key, int $value, Assert $assert): void
{
$assert->assertSameMethod($key, $value);
$assert->assertSameMethod($key, $value);
}
Loading