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: 23 additions & 10 deletions src/Analyser/IssetabilityDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Expr\Variable;
Expand Down Expand Up @@ -98,16 +99,28 @@
throw new ShouldNotHappenException();
}

// A rule-facing resolution re-prices untracked links on the asking
// scope: the stored results were priced under the coalesce walk's
// ensured non-nullability (NonNullabilityHelper), so their own types
// have null already stripped - folding the isset verdict from them
// would be circular. Engine-side folds keep the stored reads (their
// scopes track the ensured links; re-pricing there would walk).
$reprocessVar = $reprocessUntrackedLinks && !$scope->hasExpressionType($varResult->getExpr())->yes();
$varType = $reprocessVar
? ($useNativeTypes ? $scope->doNotTreatPhpDocTypesAsCertain()->getNativeType($varResult->getExpr()) : $scope->getType($varResult->getExpr()))
: $varResult->getTypeOnScope($scope, $useNativeTypes);
// The stored results of the chain links were priced under the
// isset/empty/?? walk's ensured non-nullability (NonNullabilityHelper):
// a link deviced non-null also narrows the fetches it is read from,
// so their own types have null already stripped - folding the verdict
// from them would be circular. A fetch the asking scope does not track
// reads its state there instead, derived from its receiver's state
// without a walk. Any other untracked link is re-priced on the asking
// scope by a rule-facing resolution; engine-side folds keep its stored
// read (re-pricing there would walk).
$varExpr = $varResult->getExpr();
$isVarTracked = $scope->hasExpressionType($varExpr)->yes();

Check warning on line 112 in src/Analyser/IssetabilityDescriptor.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // scope by a rule-facing resolution; engine-side folds keep its stored // read (re-pricing there would walk). $varExpr = $varResult->getExpr(); - $isVarTracked = $scope->hasExpressionType($varExpr)->yes(); + $isVarTracked = !$scope->hasExpressionType($varExpr)->no(); if ( !$isVarTracked && !$varResult->containsNullsafe()

Check warning on line 112 in src/Analyser/IssetabilityDescriptor.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // scope by a rule-facing resolution; engine-side folds keep its stored // read (re-pricing there would walk). $varExpr = $varResult->getExpr(); - $isVarTracked = $scope->hasExpressionType($varExpr)->yes(); + $isVarTracked = !$scope->hasExpressionType($varExpr)->no(); if ( !$isVarTracked && !$varResult->containsNullsafe()
if (
!$isVarTracked
&& !$varResult->containsNullsafe()
&& ($varExpr instanceof ArrayDimFetch || $varExpr instanceof PropertyFetch || $varExpr instanceof StaticPropertyFetch)
) {
$varType = ($useNativeTypes ? $scope->doNotTreatPhpDocTypesAsCertain() : $scope)->getStateType($varExpr);
} elseif ($reprocessUntrackedLinks && !$isVarTracked) {
$varType = $useNativeTypes ? $scope->doNotTreatPhpDocTypesAsCertain()->getNativeType($varExpr) : $scope->getType($varExpr);
} else {
$varType = $varResult->getTypeOnScope($scope, $useNativeTypes);
}
$dimType = $dimResult->getTypeOnScope($scope, $useNativeTypes);
$hasOffsetValue = $varType->hasOffsetValueType($dimType);
$valueType = $hasOffsetValue->no() ? new NeverType() : $varType->getOffsetValueType($dimType);
Expand Down
24 changes: 24 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15722.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);

namespace Bug15722;

use function PHPStan\Testing\assertType;

/** @param array{r: array{a: array{b: int}|null}} $p */
function nested(array $p): bool
{
$x = $p['r']['a']['b'] ?? null;
assertType('array{r: array{a: array{b: int}|null}}', $p);
assertType('int|null', $x);

return isset($p['r']['a']);
}

/** @param array{r: array{a: array{b: int}|null}} $p */
function ternary(array $p): ?int
{
assertType('bool', isset($p['r']['a']['b']));
assertType('int|null', isset($p['r']['a']['b']) ? $p['r']['a']['b'] : null);

return isset($p['r']['a']['b']) ? $p['r']['a']['b'] : null;
}
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Variables/IssetRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,13 @@ public function testBug14421(): void
$this->analyse([__DIR__ . '/data/bug-14421.php'], []);
}

public function testBug15722(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/bug-15722.php'], []);
}

public function testPregMatchOffsetCaptureWithoutTreatPhpDocTypesAsCertain(): void
{
$this->treatPhpDocTypesAsCertain = false;
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,11 @@ public function testBug12780(): void
$this->analyse([__DIR__ . '/data/bug-12780.php'], []);
}

public function testBug15722(): void
{
$this->analyse([__DIR__ . '/data/bug-15722.php'], []);
}

#[RequiresPhp('>= 8.2.0')]
public function testReadonlyPropertyOnNullableReceiver(): void
{
Expand Down
27 changes: 27 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-15722.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace Bug15722Rules;

/** @param array{r: array{a: array{b: int}|null}} $p */
function nested(array $p): bool
{
$x = $p['r']['a']['b'] ?? null;

return isset($p['r']['a']);
}

class Foo
{
/** @var array{a: array{b: int}|null} */
private array $data = ['a' => null];

public function get(): ?int
{
return $this->data['a']['b'] ?? null;
}

public function has(): bool
{
return isset($this->data['a']['b']);
}
}
56 changes: 44 additions & 12 deletions turbo-ext/src/IssetabilityDescriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ zv::Val newNeverType()
return true;
}

/* $expr instanceof ArrayDimFetch || PropertyFetch || StaticPropertyFetch;
* false = pending exception */
[[nodiscard]] bool isFetchLink(zval *expr, bool &out)
{
out = false;
if (Z_TYPE_P(expr) != IS_OBJECT) return true;
for (int classIdx : { PT_CLASS_ARRAY_DIM_FETCH, PT_CLASS_PROPERTY_FETCH, PT_CLASS_STATIC_PROPERTY_FETCH }) {
zend_class_entry *ce = pt_class(classIdx);
if (UNEXPECTED(ce == NULL)) return false;
if (instanceof_function(Z_OBJCE_P(expr), ce)) {
out = true;
return true;
}
}
return true;
}

} // namespace

namespace phpstanturbo {
Expand Down Expand Up @@ -216,20 +233,35 @@ class IssetabilityDescriptor
return zv::Val();
}

bool reprocessVar = false;
if (reprocessUntrackedLinks) {
zv::Val varExprHold;
zval *varExpr = pt_expression_result_expr(varResult, varExprHold);
if (UNEXPECTED(varExpr == NULL)) return zv::Val();
zend_long isTracked = pt_mutating_scope_has_expression_type(Z_OBJ_P(scope), varExpr);
if (UNEXPECTED(isTracked < 0)) return zv::Val();
reprocessVar = isTracked != PT_TRI_YES;
zv::Val varExprHold;
zval *varExpr = pt_expression_result_expr(varResult, varExprHold);
if (UNEXPECTED(varExpr == NULL)) return zv::Val();
zend_long isVarTracked = pt_mutating_scope_has_expression_type(Z_OBJ_P(scope), varExpr);
if (UNEXPECTED(isVarTracked < 0)) return zv::Val();
bool readsState = false;
if (isVarTracked != PT_TRI_YES) {
bool containsNullsafe;
if (UNEXPECTED(!pt_expression_result_contains_nullsafe(varResult, containsNullsafe))) return zv::Val();
if (!containsNullsafe) {
bool isFetch;
if (UNEXPECTED(!isFetchLink(varExpr, isFetch))) return zv::Val();
readsState = isFetch;
}
}
zv::Val varType;
if (reprocessVar) {
zv::Val varExprHold;
zval *varExpr = pt_expression_result_expr(varResult, varExprHold);
if (UNEXPECTED(varExpr == NULL)) return zv::Val();
if (readsState) {
if (useNativeTypes) {
zv::Val nativeScope = pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(scope));
if (UNEXPECTED(nativeScope.isUndef())) return zv::Val();
if (UNEXPECTED(Z_TYPE_P(nativeScope.raw()) != IS_OBJECT)) {
zend_throw_error(NULL, "Call to a member function getStateType() on %s", zend_zval_value_name(nativeScope.raw()));
return zv::Val();
}
varType = pt_mutating_scope_get_state_type(Z_OBJ_P(nativeScope.raw()), Z_OBJ_P(varExpr));
} else {
varType = pt_mutating_scope_get_state_type(Z_OBJ_P(scope), Z_OBJ_P(varExpr));
}
} else if (reprocessUntrackedLinks && isVarTracked != PT_TRI_YES) {
if (useNativeTypes) {
zv::Val nativeScope = pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(scope));
if (UNEXPECTED(nativeScope.isUndef())) return zv::Val();
Expand Down
Loading