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
33 changes: 33 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -4486,12 +4486,45 @@ private static function findMergedCallCandidates(array $differingExpressionKeys,
if (!$expr instanceof FuncCall && !$expr instanceof MethodCall && !$expr instanceof Expr\StaticCall) {
continue;
}
if (!self::hasNarrowableArgument($expr)) {
continue;
}
$candidates[$exprString] = $holder;
}

return $candidates;
}

/**
* Whether the call has an argument that later narrowing can change - only then
* can its remembered entry go stale. Any other call keeps its entry even when
* it is uninformative: merging with a branch that lacks the entry would leave a
* Maybe holder instead, which derives no conditional guards and is intersected
* as if it held the call's type when a conditional expression narrows the call.
*/
private static function hasNarrowableArgument(FuncCall|MethodCall|Expr\StaticCall $call): bool
{
foreach ($call->args as $arg) {
if (!$arg instanceof Arg) {
continue;
}
$value = $arg->value;
if ($value instanceof Scalar && !$value instanceof Scalar\InterpolatedString) {
continue;
}
if ($value instanceof ConstFetch) {
continue;
}
if ($value instanceof ClassConstFetch && $value->class instanceof Name && $value->name instanceof Identifier) {
continue;
}

return true;
}

return false;
}

/**
* Called on the merged scope without the candidates: keys of candidates whose
* merged type (and native type) is exactly what the call resolves to here.
Expand Down
106 changes: 106 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15727.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php declare(strict_types = 1);

namespace Bug15727;

use PHPStan\TrinaryLogic;
use function PHPStan\Testing\assertType;
use function PHPStan\Testing\assertVariableCertainty;

function isMulti(): bool
{
if (defined('MULTI')) {
return (bool) MULTI;
}

return false;
}

function can(string $cap): bool
{
return $cap === 'update';
}

function getS(): string
{
return (string) rand();
}

function variableAssignedUnderOr(): void
{
if (isMulti()) {
echo 'network';
}

if (!isMulti() || can('update')) {
$updateData = 1;
}

if (!isMulti()) {
assertVariableCertainty(TrinaryLogic::createYes(), $updateData);
echo $updateData;
}
}

function callNarrowedAfterIfWithoutElse(): void
{
$client = null;
if (getS()) {
$client = 1;
}

if (getS() && $client !== null) {
echo 1;
}

assertType('string', getS());
}

class Foo
{

public function isMulti(): bool
{
return true;
}

public static function isMultiStatic(): bool
{
return true;
}

public function can(string $cap): bool
{
return $cap === 'update';
}

public function methodCall(): void
{
if ($this->isMulti()) {
echo 'network';
}

if (!$this->isMulti() || $this->can(self::class)) {
$updateData = 1;
}

if (!$this->isMulti()) {
assertVariableCertainty(TrinaryLogic::createYes(), $updateData);
}
}

public function staticCall(): void
{
if (self::isMultiStatic()) {
echo 'network';
}

if (!self::isMultiStatic() || $this->can('update')) {
$updateData = 1;
}

if (!self::isMultiStatic()) {
assertVariableCertainty(TrinaryLogic::createYes(), $updateData);
}
}

}
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,13 @@ public function testBug14418(): void
$this->analyse([__DIR__ . '/data/bug-14418.php'], []);
}

public function testBug15727(): void
{
$this->cliArgumentsVariablesRegistered = true;
$this->checkMaybeUndefinedVariables = true;
$this->analyse([__DIR__ . '/data/bug-15727.php'], []);
}

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

namespace Bug15727Rule;

function isMulti(): bool
{
if (defined('MULTI')) {
return (bool) MULTI;
}

return false;
}

function can(string $cap): bool
{
return $cap === 'update';
}

function variableAssignedUnderOr(): void
{
if (isMulti()) {
echo 'network';
}

if (!isMulti() || can('update')) {
$updateData = 1;
}

if (!isMulti()) {
echo $updateData;
}
}
44 changes: 44 additions & 0 deletions turbo-ext/src/MutatingScope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8513,6 +8513,9 @@ class MutatingScope
if (isCall) break;
}
if (!isCall) continue;
bool narrowable;
if (UNEXPECTED(!hasNarrowableArgument(expr.ref().asObject(), narrowable))) return zv::Val();
if (!narrowable) continue;
candidates.separate();
Z_TRY_ADDREF_P(holder);
if (key != NULL) {
Expand All @@ -8524,6 +8527,47 @@ class MutatingScope
return zv::Val(std::move(candidates));
}

/* private static (twin hasNarrowableArgument()): an Arg in $call->args
* whose value is not a Scalar other than an InterpolatedString, a
* ConstFetch, or a ClassConstFetch of a Name and an Identifier; false =
* pending exception */
[[nodiscard]] static bool hasNarrowableArgument(zend_object *call, bool &out)
{
out = false;
zv::Ref args = nodeProp(call, PT_LC("args"));
if (UNEXPECTED(args.raw() == NULL)) return false;
for (auto entry : zv::ArrRef(args.deref().raw())) {
zv::Ref arg = entry.value().deref();
bool is;
if (UNEXPECTED(!isInstance(arg, PT_CLASS_ARG, is))) return false;
if (!is) continue;
zv::Ref valueSlot = nodeProp(arg.asObject(), PT_LC("value"));
if (UNEXPECTED(valueSlot.raw() == NULL)) return false;
zv::Ref value = valueSlot.deref();
if (UNEXPECTED(!isInstance(value, PT_CLASS_SCALAR, is))) return false;
if (is) {
if (UNEXPECTED(!isInstance(value, PT_CLASS_INTERPOLATED_STRING, is))) return false;
if (!is) continue;
} else {
if (UNEXPECTED(!isInstance(value, PT_CLASS_CONST_FETCH, is))) return false;
if (is) continue;
if (UNEXPECTED(!isInstance(value, PT_CLASS_CLASS_CONST_FETCH, is))) return false;
if (is) {
zv::Ref classSlot = nodeProp(value.asObject(), PT_LC("class"));
if (UNEXPECTED(classSlot.raw() == NULL)) return false;
bool classIsName;
if (UNEXPECTED(!isInstance(classSlot.deref(), PT_CLASS_NAME, classIsName))) return false;
bool nameIsIdentifier = false;
if (classIsName && UNEXPECTED(!nodeNameIs(value.asObject(), PT_CLASS_IDENTIFIER, nameIsIdentifier))) return false;
if (nameIsIdentifier) continue;
}
}
out = true;
return true;
}
return true;
}

/* private (twin findUninformativeMergedCalls(), called on the merged scope
* without the candidates): keys of candidates whose merged type (and
* native type) is exactly what the call resolves to there */
Expand Down
Loading
Loading