Skip to content
Open
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
7 changes: 7 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1616,6 +1616,13 @@ public function testBug15003(): void
$this->assertNoErrors($errors);
}

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

/**
* @param string[]|null $allAnalysedFiles
* @return list<Error>
Expand Down
84 changes: 84 additions & 0 deletions tests/PHPStan/Analyser/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