Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4364,7 +4364,7 @@ private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflec
public function processVirtualAssign(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback): ExpressionResult
{
$assignHandler = $this->container->getByType(AssignHandler::class);
$virtualAssignNodeCallback = new VirtualAssignNodeCallback($nodeCallback);
$virtualAssignNodeCallback = VirtualAssignNodeCallback::create($nodeCallback);
$target = $assignHandler->prepareTarget(
$this,
$scope,
Expand Down
23 changes: 22 additions & 1 deletion src/Analyser/VirtualAssignNodeCallback.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,31 @@ final class VirtualAssignNodeCallback implements ShallowNodeCallback
/**
* @param callable(Node $node, Scope $scope): void $originalNodeCallback
*/
public function __construct(private mixed $originalNodeCallback)
private function __construct(private mixed $originalNodeCallback)
{
}

/**
* Rebuilds the chain instead of wrapping it so that GatheringNodeCallback
* layers stay on the outside. Hiding a gatherer behind this filter would let
* FiberNodeScopeResolver defer it into a fiber, and a parked fiber can run
* the gatherer long after the caller already read its result.
*
* @param callable(Node $node, Scope $scope): void $nodeCallback
* @return callable(Node $node, Scope $scope): void
*/
public static function create(callable $nodeCallback): callable
{
if ($nodeCallback instanceof GatheringNodeCallback) {
return new GatheringNodeCallback(
self::create($nodeCallback->getGatherer()),
self::create($nodeCallback->getInner()),
);
}

return new self($nodeCallback);
}

public function __invoke(Node $node, Scope $scope): void
{
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
Expand Down
35 changes: 35 additions & 0 deletions tests/PHPStan/Rules/DeadCode/NoopRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

namespace PHPStan\Rules\DeadCode;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Node\Printer\Printer;
use PHPStan\Node\PropertyAssignNode;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\VerbosityLevel;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
Expand All @@ -19,6 +24,30 @@ protected function getRule(): Rule
return new NoopRule(new ExprPrinter(new Printer()));
}

protected function getCollectors(): array
{
// Asks for the type of a not-yet-walked expression, the same way
// TypesAssignedToPropertiesRule does, so FiberNodeScopeResolver parks
// the node callback on PropertyAssignNode. The hasAssign gatherer
// feeding NoopExpressionNode must not be delayed by that
// (https://github.com/phpstan/phpstan/issues/15038).
return [
new /** @implements Collector<PropertyAssignNode, string> */ class implements Collector {

public function getNodeType(): string
{
return PropertyAssignNode::class;
}

public function processNode(Node $node, Scope $scope): string
{
return $scope->getType($node->getAssignedExpr())->describe(VerbosityLevel::typeOnly());
}

},
];
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/noop.php'], [
Expand Down Expand Up @@ -173,6 +202,12 @@ public function testBug13698(): void
]);
}

#[RequiresPhp('>= 8.4.0')]
public function testBug15038(): void
{
$this->analyse([__DIR__ . '/data/bug-15038.php'], []);
}

#[RequiresPhp('>= 8.5.0')]
public function testPipeOperator(): void
{
Expand Down
84 changes: 84 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-15038.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php // lint >= 8.4

namespace Bug15038;

enum Counter {
case A;
case B;
}

class Statistics {
public private(set) int $counterA = 0;
public private(set) int $counterB = 0;

public function inc(Counter $counter): void {
match ($counter) {
Counter::A => $this->counterA++,
Counter::B => $this->counterB++,
};
}
}

$s = new Statistics();
$s->inc(Counter::A);
$s->inc(Counter::A);
$s->inc(Counter::A);

var_dump($s->counterA);

class MoreVirtualAssigns
{

public int $a = 0;

public int $b = 0;

/** @var array{a: int, b: int} */
public array $arr = ['a' => 0, 'b' => 0];

public function postDec(Counter $counter): void
{
match ($counter) {
Counter::A => $this->a--,
Counter::B => $this->b--,
};
}

public function preIncDec(Counter $counter): void
{
match ($counter) {
Counter::A => ++$this->a,
Counter::B => --$this->b,
};
}

public function offset(Counter $counter): void
{
match ($counter) {
Counter::A => $this->arr['a']++,
Counter::B => $this->arr['b']--,
};
}

public function nestedMatch(Counter $counter): void
{
match ($counter) {
Counter::A => match (true) {
default => $this->a++,
},
Counter::B => $this->b++,
};
}

public function booleanOperators(bool $cond): void
{
$cond && ($this->a++ > 0);
$cond || ($this->b-- > 0);
}

public function arrayLiteral(): void
{
[$this->a++];
}

}
Loading