Skip to content
Closed
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
40 changes: 40 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -6108,6 +6108,46 @@
$conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
}

if (
$assignedExpr instanceof FuncCall
&& $assignedExpr->name instanceof Name
&& in_array($assignedExpr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
&& count($assignedExpr->getArgs()) >= 1
) {
$arrayArg = $assignedExpr->getArgs()[0]->value;
$arrayType = $scope->getType($arrayArg);
$nonNullType = TypeCombinator::removeNull($type);
if (
$arrayArg instanceof Variable
&& is_string($arrayArg->name)
&& $arrayType->isArray()->yes()

Check warning on line 6123 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $arrayArg instanceof Variable && is_string($arrayArg->name) - && $arrayType->isArray()->yes() + && !$arrayType->isArray()->no() && !$arrayType->isIterableAtLeastOnce()->yes() && !$nonNullType instanceof NeverType && !$type->equals($nonNullType)

Check warning on line 6123 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $arrayArg instanceof Variable && is_string($arrayArg->name) - && $arrayType->isArray()->yes() + && !$arrayType->isArray()->no() && !$arrayType->isIterableAtLeastOnce()->yes() && !$nonNullType instanceof NeverType && !$type->equals($nonNullType)
&& !$arrayType->isIterableAtLeastOnce()->yes()
&& !$nonNullType instanceof NeverType
&& !$type->equals($nonNullType)
) {
$narrowedArrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());

$arrayExprString = '$' . $arrayArg->name;
$arrayHolder = new ConditionalExpressionHolder([
'$' . $var->name => ExpressionTypeHolder::createYes(new Variable($var->name), $nonNullType),
], ExpressionTypeHolder::createYes(
$arrayArg,
$narrowedArrayType,
));
$conditionalExpressions[$arrayExprString][$arrayHolder->getKey()] = $arrayHolder;

$dimFetch = new ArrayDimFetch($arrayArg, $var);
$dimFetchExprString = sprintf('$%s[$%s]', $arrayArg->name, $var->name);
$dimFetchHolder = new ConditionalExpressionHolder([
'$' . $var->name => ExpressionTypeHolder::createYes(new Variable($var->name), $nonNullType),
], ExpressionTypeHolder::createYes(
$dimFetch,
$narrowedArrayType->getIterableValueType(),
));
$conditionalExpressions[$dimFetchExprString][$dimFetchHolder->getKey()] = $dimFetchHolder;
}
}

$this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage);
$scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes());
foreach ($conditionalExpressions as $exprString => $holders) {
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-13546.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function mixedLast($mixed): void
function firstInCondition(array $array)
{
if (($key = array_key_first($array)) !== null) {
assertType('list<string>', $array); // could be 'non-empty-list<string>'
assertType('non-empty-list<string>', $array);
return $array[$key];
}
assertType('list<string>', $array);
Expand Down
40 changes: 40 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14081.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types = 1);

namespace Bug14081Nsrt;

use function PHPStan\Testing\assertType;

/** @param list<string> $list */
function firstWithNullCheck(array $list): void
{
$key = array_key_first($list);
if ($key !== null) {
assertType('non-empty-list<string>', $list);
assertType('int<0, max>', $key);
assertType('string', $list[$key]);
}
}

/** @param list<string> $list */
function lastWithNullCheck(array $list): void
{
$key = array_key_last($list);
if ($key !== null) {
assertType('non-empty-list<string>', $list);
assertType('int<0, max>', $key);
assertType('string', $list[$key]);
}
}

/** @param array<string, int> $map */
function firstOnMapWithNullCheck(array $map): void
{
$key = array_key_first($map);
if ($key !== null) {
assertType('non-empty-array<string, int>', $map);
assertType('string', $key);
assertType('int', $map[$key]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1145,4 +1145,10 @@ public function testBug11276(): void
$this->analyse([__DIR__ . '/data/bug-11276.php'], []);
}

public function testBug14081(): void
{
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
$this->analyse([__DIR__ . '/data/bug-14081.php'], []);
}

}
73 changes: 73 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-14081.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types = 1);

namespace Bug14081;

class HelloWorld
{
/**
* @param list<string> $list
*/
public function firstWithNullCheck(array $list): string
{
$key = array_key_first($list);
if ($key !== null) {
return $list[$key];
}

return 'nothing';
}

/**
* @param list<string> $list
*/
public function lastWithNullCheck(array $list): string
{
$key = array_key_last($list);
if ($key !== null) {
return $list[$key];
}

return 'nothing';
}

/**
* @param array<string, int> $map
*/
public function firstOnMapWithNullCheck(array $map): int
{
$key = array_key_first($map);
if ($key !== null) {
return $map[$key];
}

return 0;
}

/**
* @param array<string, int> $map
*/
public function lastOnMapWithNullCheck(array $map): int
{
$key = array_key_last($map);
if ($key !== null) {
return $map[$key];
}

return 0;
}

/**
* @param list<string> $list
*/
public function nullCheckReversed(array $list): string
{
$key = array_key_first($list);
if (null !== $key) {
return $list[$key];
}

return 'nothing';
}
}
Loading