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
112 changes: 73 additions & 39 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -848,29 +848,62 @@
$this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
);
} elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
// The array might be empty here, so we cannot register
// $arr[$key] unconditionally. Attach a conditional holder
// that fires once the user narrows $key to non-null
// (e.g. `if ($key !== null)`), giving the deep-write
// path the same shape information `isset($arr[$key])`
// would have provided.
$keyType = $scope->getType($expr->expr);
$nonNullKeyType = TypeCombinator::removeNull($keyType);
if (!$nonNullKeyType instanceof NeverType && !$keyType->isNull()->yes()) {
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
$dimFetchString = $this->exprPrinter->printExpr($dimFetch);
$keyExprString = $this->exprPrinter->printExpr($expr->var);

$holder = new ConditionalExpressionHolder(
[$keyExprString => ExpressionTypeHolder::createYes($expr->var, $nonNullKeyType)],
ExpressionTypeHolder::createYes($dimFetch, $arrayType->getIterableValueType()),
if (!$nonNullKeyType instanceof NeverType) {
$specifiedTypes = $specifiedTypes->unionWith(
$this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $arrayType, $nonNullKeyType),
);
}
}
}
}

// infer $arr[$key] after $key = array_search($needle, $arr) or $key = array_find_key($arr, $callback)
if (
$expr->expr instanceof FuncCall
&& $expr->expr->name instanceof Name
&& count($expr->expr->getArgs()) >= 2
) {
$funcName = $expr->expr->name->toLowerString();
$arrayArgIndex = null;
$sentinelType = null;
$narrowToNonEmpty = false;

if ($funcName === 'array_search') {
$arrayArgIndex = 1;
$sentinelType = new ConstantBooleanType(false);
} elseif ($funcName === 'array_find_key') {
$arrayArgIndex = 0;
$sentinelType = new NullType();
$narrowToNonEmpty = true;
}

if ($arrayArgIndex !== null && $sentinelType !== null) {

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, ubuntu-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.4)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.5)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, ubuntu-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, windows-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, windows-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, windows-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 882 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.
$arrayArg = $expr->expr->getArgs()[$arrayArgIndex]->value;
$arrayType = $scope->getType($arrayArg);

if ($arrayType->isArray()->yes()) {
if ($context->true()) {
if ($narrowToNonEmpty) {
$specifiedTypes = $specifiedTypes->unionWith(
$this->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope),
);
}

Check failure on line 892 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

Check failure on line 892 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, windows-latest)

Strict comparison using !== between PHPStan\Type\Constant\ConstantBooleanType|PHPStan\Type\NullType and null will always evaluate to true.

$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);

$specifiedTypes = $specifiedTypes->unionWith(
(new SpecifiedTypes([], []))->setNewConditionalExpressionHolders([
$dimFetchString => [$holder->getKey() => $holder],
]),
$this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
);
} elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
$keyType = $scope->getType($expr->expr);
$narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType);
if (!$narrowedKeyType instanceof NeverType) {
$specifiedTypes = $specifiedTypes->unionWith(
$this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $arrayType, $narrowedKeyType),
);
}
}
}
}
Expand Down Expand Up @@ -933,27 +966,6 @@
return $specifiedTypes;
}

if ($context->true()) {
// infer $arr[$key] after $key = array_search($needle, $arr)
if (
$expr->expr instanceof FuncCall
&& $expr->expr->name instanceof Name
&& $expr->expr->name->toLowerString() === 'array_search'
&& count($expr->expr->getArgs()) >= 2
) {
$arrayArg = $expr->expr->getArgs()[1]->value;
$arrayType = $scope->getType($arrayArg);

if ($arrayType->isArray()->yes()) {
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
$iterableValueType = $arrayType->getIterableValueType();

return $specifiedTypes->unionWith(
$this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope),
);
}
}
}
return $specifiedTypes;
} elseif (
$expr instanceof Expr\Isset_
Expand Down Expand Up @@ -2392,6 +2404,27 @@
return $types;
}

private function createArrayDimFetchConditionalExpressionHolder(
Expr\Variable $keyVar,
Expr $arrayArg,
Type $arrayType,
Type $narrowedKeyType,
): SpecifiedTypes
{
$dimFetch = new ArrayDimFetch($arrayArg, $keyVar);
$dimFetchString = $this->exprPrinter->printExpr($dimFetch);
$keyExprString = $this->exprPrinter->printExpr($keyVar);

$holder = new ConditionalExpressionHolder(
[$keyExprString => ExpressionTypeHolder::createYes($keyVar, $narrowedKeyType)],
ExpressionTypeHolder::createYes($dimFetch, $arrayType->getIterableValueType()),
);

return (new SpecifiedTypes([], []))->setNewConditionalExpressionHolders([
$dimFetchString => [$holder->getKey() => $holder],
]);
}

private function createForExpr(
Expr $expr,
Type $type,
Expand Down Expand Up @@ -3016,10 +3049,11 @@

// array_key_first($a) !== null
// array_key_last($a) !== null
// array_find_key($a, $cb) !== null
if (
$unwrappedLeftExpr instanceof FuncCall
&& $unwrappedLeftExpr->name instanceof Name
&& in_array($unwrappedLeftExpr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
&& in_array($unwrappedLeftExpr->name->toLowerString(), ['array_key_first', 'array_key_last', 'array_find_key'], true)
&& isset($unwrappedLeftExpr->getArgs()[0])
&& $rightType->isNull()->yes()
) {
Expand Down
30 changes: 30 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array-find-key-existing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php // lint >= 8.4

declare(strict_types=1);

namespace ArrayFindKeyExisting;

use function PHPStan\Testing\assertType;

/**
* @param list<string> $list
*/
function arrayFindKeyNotNull(array $list, string $s): void
{
$key = array_find_key($list, fn (string $v) => $v === $s);
if ($key !== null) {
assertType('non-empty-list<string>', $list);
assertType('string', $list[$key]);
}
}

/**
* @param array<string, int> $map
*/
function arrayFindKeyStringKey(array $map): void
{
$key = array_find_key($map, fn (int $v) => $v > 10);
if ($key !== null) {
assertType('int', $map[$key]);
}
}
39 changes: 39 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array-search-existing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types=1);

namespace ArraySearchExisting;

use function PHPStan\Testing\assertType;

/**
* @param list<string> $list
*/
function arraySearchNotFalse(array $list, string $s): void
{
$key = array_search($s, $list);
if ($key !== false) {
assertType('non-empty-list<string>', $list);
assertType('string', $list[$key]);
}
}

/**
* @param array<string, int> $map
*/
function arraySearchStringKey(array $map, int $needle): void
{
$key = array_search($needle, $map);
if ($key !== false) {
assertType('int', $map[$key]);
}
}

/**
* @param list<string> $list
*/
function arraySearchDeepWrite(array $list, string $s): void
{
$key = array_search($s, $list);
if ($key !== false) {
assertType('string', $list[$key]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -873,12 +873,7 @@ public function testArrayDimFetchAfterArraySearch(): void
{
$this->reportPossiblyNonexistentGeneralArrayOffset = true;

$this->analyse([__DIR__ . '/data/array-dim-after-array-search.php'], [
[
'Offset int|string might not exist on non-empty-array.',
20,
],
]);
$this->analyse([__DIR__ . '/data/array-dim-after-array-search.php'], []);
}

public function testArrayDimFetchOnArrayKeyFirsOrLastOrCount(): void
Expand Down Expand Up @@ -1310,4 +1305,11 @@ public function testBug11218(): void
$this->analyse([__DIR__ . '/data/bug-11218.php'], []);
}

public function testBug14537(): void
{
$this->reportPossiblyNonexistentGeneralArrayOffset = true;

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

}
71 changes: 71 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-14537.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php // lint >= 8.4

declare(strict_types=1);

namespace Bug14537;

/**
* @param list<string> $list
*/
function arraySearchNotFalse(array $list, string $s): void
{
$key = array_search($s, $list);
if ($key !== false) {
echo $list[$key];
}
}

/**
* @param array<string, int> $map
*/
function arraySearchStringKey(array $map, int $needle): void
{
$key = array_search($needle, $map);
if ($key !== false) {
echo $map[$key];
}
}

/**
* @param list<string> $list
*/
function arraySearchReversedComparison(array $list, string $s): void
{
$key = array_search($s, $list);
if (false !== $key) {
echo $list[$key];
}
}

/**
* @param list<string> $list
*/
function arrayFindKeyNotNull(array $list, string $s): void
{
$key = array_find_key($list, fn (string $v) => $v === $s);
if ($key !== null) {
echo $list[$key];
}
}

/**
* @param array<string, int> $map
*/
function arrayFindKeyStringKey(array $map): void
{
$key = array_find_key($map, fn (int $v) => $v > 10);
if ($key !== null) {
echo $map[$key];
}
}

/**
* @param list<string> $list
*/
function arrayFindKeyReversedComparison(array $list, string $s): void
{
$key = array_find_key($list, fn (string $v) => $v === $s);
if (null !== $key) {
echo $list[$key];
}
}
Loading