Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Truthy isset($arr[$k]) should narrow $k #3453

Merged
merged 19 commits into from
Sep 22, 2024
54 changes: 54 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,60 @@ public function specifyTypesInCondition(
$rootExpr,
),
);
} else {
$varType = $scope->getType($var->var);
if ($varType->isArray()->yes() && !$varType->isIterableAtLeastOnce()->no()) {
$varIterableKeyType = $varType->getIterableKeyType();

if ($varIterableKeyType->isConstantScalarValue()->yes()) {
$narrowedKey = TypeCombinator::union(
$varIterableKeyType,
TypeCombinator::remove($varIterableKeyType->toString(), new ConstantStringType('')),
);

if (!$varType->hasOffsetValueType(new ConstantIntegerType(0))->no()) {
$narrowedKey = TypeCombinator::union(
$narrowedKey,
new ConstantBooleanType(false),
);
}

if (!$varType->hasOffsetValueType(new ConstantIntegerType(1))->no()) {
$narrowedKey = TypeCombinator::union(
$narrowedKey,
new ConstantBooleanType(true),
);
}

if (!$varType->hasOffsetValueType(new ConstantStringType(''))->no()) {
$narrowedKey = TypeCombinator::addNull($narrowedKey);
}

if (!$varIterableKeyType->isNumericString()->no() || !$varIterableKeyType->isInteger()->no()) {
$narrowedKey = TypeCombinator::union($narrowedKey, new FloatType());
}
} else {
$narrowedKey = new MixedType(
false,
new UnionType([
new ArrayType(new MixedType(), new MixedType()),
new ObjectWithoutClassType(),
new ResourceType(),
]),
);
}

$types = $types->unionWith(
$this->create(
$var->dim,
$narrowedKey,
$context,
false,
$scope,
$rootExpr,
),
);
}
}
}

Expand Down
214 changes: 214 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11716.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
<?php // lint >= 8.0

namespace Bug11716;

use function PHPStan\Testing\assertType;

class TypeExpression
{
/**
* @return '&'|'|'
*/
public function parse(string $glue): string
{
$seenGlues = ['|' => false, '&' => false];

assertType("array{|: false, &: false}", $seenGlues);

if ($glue !== '') {
assertType('non-empty-string', $glue);

\assert(isset($seenGlues[$glue]));
$seenGlues[$glue] = true;

assertType("'&'|'|'", $glue);
assertType('array{|: bool, &: bool}', $seenGlues);
} else {
assertType("''", $glue);
}

assertType("''|'&'|'|'", $glue);
assertType("array{|: bool, &: bool}", $seenGlues);

return array_key_first($seenGlues);
}
}

/**
* @param array<int, string> $arr
*/
function narrowKey($mixed, string $s, int $i, array $generalArr, array $arr): void {
if (isset($generalArr[$mixed])) {
assertType('mixed~(array|object|resource)', $mixed);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);

if (isset($generalArr[$i])) {
assertType('int', $i);
} else {
assertType('int', $i);
}
assertType('int', $i);

if (isset($generalArr[$s])) {
assertType('string', $s);
} else {
assertType('string', $s);
}
assertType('string', $s);

if (isset($arr[$mixed])) {
assertType('mixed~(array|object|resource)', $mixed);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);

if (isset($arr[$i])) {
assertType('int', $i);
} else {
assertType('int', $i);
}
assertType('int', $i);

if (isset($arr[$s])) {
assertType('string', $s);
} else {
assertType('string', $s);
}
assertType('string', $s);
}

/**
* @param array<int, array<string, float>> $arr
*/
function multiDim($mixed, $mixed2, array $arr) {
if (isset($arr[$mixed])) {
assertType('mixed~(array|object|resource)', $mixed);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);

if (isset($arr[$mixed]) && isset($arr[$mixed][$mixed2])) {
assertType('mixed~(array|object|resource)', $mixed);
assertType('mixed~(array|object|resource)', $mixed2);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);

if (isset($arr[$mixed][$mixed2])) {
assertType('mixed~(array|object|resource)', $mixed);
assertType('mixed~(array|object|resource)', $mixed2);
} else {
assertType('mixed', $mixed);
assertType('mixed', $mixed2);
}
assertType('mixed', $mixed);
assertType('mixed', $mixed2);
}

/**
* @param array<int, string> $arr
*/
function emptyArrr($mixed, array $arr)
{
if (count($arr) !== 0) {
return;
}

assertType('array{}', $arr);
if (isset($arr[$mixed])) {
assertType('mixed', $mixed);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);
}

function emptyString($mixed)
{
// see https://3v4l.org/XHZdr
$arr = ['' => 1, 'a' => 2];
if (isset($arr[$mixed])) {
assertType("''|'a'|null", $mixed);
} else {
assertType('mixed', $mixed); // could be mixed~(''|'a'|null)
}
assertType('mixed', $mixed);
}

function numericString($mixed, int $i, string $s)
{
$arr = ['1' => 1, '2' => 2];
if (isset($arr[$mixed])) {
assertType("1|2|'1'|'2'|float|true", $mixed);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);

$arr = ['0' => 1, '2' => 2];
if (isset($arr[$mixed])) {
assertType("0|2|'0'|'2'|float|false", $mixed);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);

$arr = ['1' => 1, '2' => 2];
if (isset($arr[$i])) {
assertType("1|2", $i);
} else {
assertType('int', $i);
}
assertType('int', $i);

$arr = ['1' => 1, '2' => 2, 3 => 3];
if (isset($arr[$s])) {
assertType("'1'|'2'|'3'", $s);
} else {
assertType('string', $s);
}
assertType('string', $s);

$arr = ['1' => 1, '2' => 2, 3 => 3];
if (isset($arr[substr($s, 10)])) {
assertType("string", $s);
assertType("'1'|'2'|'3'", substr($s, 10));
} else {
assertType('string', $s);
}
assertType('string', $s);
}

function intKeys($mixed)
{
$arr = [1 => 1, 2 => 2];
if (isset($arr[$mixed])) {
assertType("1|2|'1'|'2'|float|true", $mixed);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);

$arr = [0 => 0, 1 => 1, 2 => 2];
if (isset($arr[$mixed])) {
assertType("0|1|2|'0'|'1'|'2'|bool|float", $mixed);
} else {
assertType('mixed', $mixed);
}
assertType('mixed', $mixed);
}

function arrayAccess(\ArrayAccess $arr, $mixed) {
if (isset($arr[$mixed])) {
assertType("mixed", $mixed);
} else {
assertType('mixed', $mixed);
staabm marked this conversation as resolved.
Show resolved Hide resolved
}
assertType('mixed', $mixed);
}
40 changes: 40 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-8559.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Bug8559;

use function PHPStan\Testing\assertType;

class X
{
const KEYS = ['a' => 1, 'b' => 2];

/**
* @phpstan-assert key-of<self::KEYS> $key
* @return value-of<self::KEYS>
*/
public static function get(string $key): int
{
assert(isset(self::KEYS[$key]));
assertType("'a'|'b'", $key);
return self::KEYS[$key];
}

/**
* @phpstan-assert key-of<self::KEYS> $key
* @return value-of<self::KEYS>
*/
public static function get2(string $key): int
{
assert(in_array($key, array_keys(self::KEYS), true));
assertType("'a'|'b'", $key);
return self::KEYS[$key];
}
}

$key = 'x';
$v = X::get($key);
assertType("*NEVER*", $key);

$key = 'a';
$v = X::get($key);
assertType("'a'", $key);
Loading