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
13 changes: 13 additions & 0 deletions src/Analyser/StmtHandler/IfHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public function processStmt(
$entryScope = $scope;
$flowBranches = [];
$elseFlow = null;
$deadBranchScopes = [];
$condResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments()));
$dependencies[] = $condResult->getDependencies();
$nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage);
Expand Down Expand Up @@ -84,6 +85,8 @@ public function processStmt(
$endStatements[] = new InternalEndStatementResult($stmt, $branchScopeStatementResult);
}
$hasYield = $branchScopeStatementResult->hasYield() || $hasYield;
} else {
$deadBranchScopes[] = $branchScopeStatementResult->getScope();
}

$scope = $condResult->getFalseyScope();
Expand Down Expand Up @@ -119,6 +122,8 @@ public function processStmt(
$endStatements[] = new InternalEndStatementResult($elseif, $branchScopeStatementResult);
}
$hasYield = $hasYield || $branchScopeStatementResult->hasYield();
} else {
$deadBranchScopes[] = $branchScopeStatementResult->getScope();
}

if (
Expand Down Expand Up @@ -156,13 +161,21 @@ public function processStmt(
$endStatements[] = new InternalEndStatementResult($stmt->else, $branchScopeStatementResult);
}
$hasYield = $hasYield || $branchScopeStatementResult->hasYield();
} else {
$deadBranchScopes[] = $branchScopeStatementResult->getScope();
}
}

if ($finalScope === null) {
$finalScope = $scope;
}

// a branch the condition rules out is still walked and its rules still
// see its calls - their template arguments are resolved from it too
foreach ($deadBranchScopes as $deadBranchScope) {
$finalScope = $finalScope->addTemplateArgumentConstraints($deadBranchScope->getTemplateArgumentConstraints());
}

if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
$endStatements[] = new InternalEndStatementResult($stmt, new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: $throwPoints, impurePoints: $impurePoints));
}
Expand Down
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
* @extends RuleTestCase<YieldTypeRule>
Expand Down Expand Up @@ -87,4 +88,10 @@ public function testYieldOversizedSelfRejection(): void
$this->analyse([__DIR__ . '/data/bug-yield-oversized-self-rejection.php'], []);
}

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

}
56 changes: 56 additions & 0 deletions tests/PHPStan/Rules/Generators/data/bug-15476.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug15476Generators;

/**
* @template TKey of array-key
* @template TValue
*/
final class Box
{
}

interface Factory
{
/**
* @template TKey of array-key
* @template TValue
* @param iterable<TKey, TValue> $value
* @return Box<TKey, TValue>
*/
public function box(iterable $value = []): Box;
}

final class Item
{
}

final class Example
{
public function __construct(private Factory $factory)
{
}

/** @return \Generator<int, Box<int, Item>> */
public function deadBranch(Item $item): \Generator
{
if ($item === null) {
yield $this->factory->box();
}

yield $this->factory->box([$item]);
}

/** @return \Generator<int, Box<int, Item>> */
public function deadElse(Item $item): \Generator
{
if ($item !== null) {
yield $this->factory->box([$item]);
} else {
yield $this->factory->box();
}
}

}
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1419,4 +1419,10 @@ public function testBug13325(): void
$this->analyse([__DIR__ . '/data/bug-13325.php'], []);
}

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

}
102 changes: 102 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-15476.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug15476;

/**
* @template TKey of array-key
* @template TValue
*/
final class Box
{
}

interface Factory
{
/**
* @template TKey of array-key
* @template TValue
* @param iterable<TKey, TValue> $value
* @return Box<TKey, TValue>
*/
public function box(iterable $value = []): Box;
}

final class Item
{
}

final class Example
{
public function __construct(private Factory $factory)
{
}

/** @return Box<int, Item> */
public function deadBranch(Item $item): Box
{
if ($item === null) {
return $this->factory->box();
}

return $this->factory->box([$item]);
}

/** @return Box<int, Item> */
public function liveBranch(?Item $item): Box
{
if ($item === null) {
return $this->factory->box();
}

return $this->factory->box([$item]);
}

/** @return Box<int, Item> */
public function deadElseif(Item $item, bool $b): Box
{
if ($b) {
return $this->factory->box([$item]);
} elseif ($item === null) {
return $this->factory->box();
}

return $this->factory->box([$item]);
}

/** @return Box<int, Item> */
public function deadElse(Item $item): Box
{
if ($item !== null) {
return $this->factory->box([$item]);
} else {
return $this->factory->box();
}
}

/** @return Box<int, Item> */
public function deadElseAfterAlwaysTrueElseif(Item $item, bool $b): Box
{
if ($b) {
return $this->factory->box([$item]);
} elseif ($item !== null) {
return $this->factory->box([$item]);
} else {
return $this->factory->box();
}
}

/** @return Box<int, Item> */
public function deadNestedBranch(Item $item, bool $b): Box
{
if ($b) {
if ($item === null) {
return $this->factory->box();
}
}

return $this->factory->box([$item]);
}

}
27 changes: 27 additions & 0 deletions turbo-ext/src/IfHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ void pushFlowBranch(zv::Arr &flowBranches, zv::Val conditionFlow, zval *branchFl
flowBranches.push(zv::Ref(branchFlow));
}

/* $deadBranchScopes[] = $branchScopeStatementResult->getScope(); false = pending exception */
[[nodiscard]] bool pushDeadBranchScope(zv::Arr &deadBranchScopes, zval *branchResult)
{
zv::Val hold;
zval *branchScope = pt_internal_statement_result_scope(branchResult, hold);
if (UNEXPECTED(branchScope == NULL)) return false;
deadBranchScopes.push(zv::Ref(branchScope));
return true;
}

} // namespace

namespace phpstanturbo {
Expand Down Expand Up @@ -137,6 +147,7 @@ class IfHandler
zv::Arr dependencies = zv::Arr::empty();
zval *entryScope = scope;
zv::Arr flowBranches = zv::Arr::empty();
zv::Arr deadBranchScopes = zv::Arr::empty();
zv::Val elseFlow = zv::Val::null();

zval *cond = ptsh::readNodeProperty(pt_ih_cond_site, stmt, PT_LC("cond"));
Expand Down Expand Up @@ -200,6 +211,8 @@ class IfHandler
bool branchHasYield;
if (UNEXPECTED(!pt_internal_statement_result_has_yield(branchResult.raw(), branchHasYield))) return zv::Val();
hasYield = branchHasYield || hasYield;
} else if (UNEXPECTED(!pushDeadBranchScope(deadBranchScopes, branchResult.raw()))) {
return zv::Val();
}
}

Expand Down Expand Up @@ -258,6 +271,8 @@ class IfHandler
pushFlowBranch(flowBranches, std::move(conditionFlow), branchFlow);
if (!ifAlwaysTrue && !lastElseIfConditionIsTrue && elseIfConditionType.isTrue != PT_TRI_NO) {
if (UNEXPECTED(!mergeBranch(branchResult.raw(), exitPoints, throwPoints, impurePoints, finalScope, alwaysTerminating, endStatements, elseif, pt_ih_elseif_stmts_site, hasYield))) return zv::Val();
} else if (UNEXPECTED(!pushDeadBranchScope(deadBranchScopes, branchResult.raw()))) {
return zv::Val();
}

if (elseIfConditionType.isTrue == PT_TRI_YES) {
Expand Down Expand Up @@ -309,13 +324,25 @@ class IfHandler
if (UNEXPECTED(elseNode == NULL)) return zv::Val();
elseHold = zv::Val::copyOf(zv::Ref(elseNode));
if (UNEXPECTED(!mergeBranch(branchResult.raw(), exitPoints, throwPoints, impurePoints, finalScope, alwaysTerminating, endStatements, elseHold.raw(), pt_ih_else_stmts_site, hasYield))) return zv::Val();
} else if (UNEXPECTED(!pushDeadBranchScope(deadBranchScopes, branchResult.raw()))) {
return zv::Val();
}
}

if (finalScope.isNull()) {
finalScope = zv::Val::copyOf(zv::Ref(scopeValue.raw()));
}

// a branch the condition rules out is still walked and its rules still
// see its calls - their template arguments are resolved from it too
for (auto entry : zv::TableRef(deadBranchScopes.table())) {
zv::Val constraints = pt_mutating_scope_get_template_argument_constraints(Z_OBJ_P(entry.value().raw()));
if (UNEXPECTED(constraints.isUndef())) return zv::Val();
zv::Val constrained = pt_mutating_scope_add_template_argument_constraints(Z_OBJ_P(finalScope.raw()), constraints.raw());
if (UNEXPECTED(constrained.isUndef())) return zv::Val();
finalScope = std::move(constrained);
}

elseNode = ptsh::readNodeProperty(pt_ih_else_site, stmt, PT_LC("else"));
if (UNEXPECTED(elseNode == NULL)) return zv::Val();
if (Z_TYPE_P(elseNode) == IS_NULL && !ifAlwaysTrue && !lastElseIfConditionIsTrue) {
Expand Down
Loading