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
15 changes: 14 additions & 1 deletion src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,20 @@
return IsSuperTypeOfResult::createYes();
}

return IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types));
$result = IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types));
if ($result->no() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) {

Check warning on line 313 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $result = IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); - if ($result->no() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) { + if ($result->no() && !$this->isList()->no() && $otherType->isArray()->yes() && $otherType->isList()->no()) { // `[]` is a list, so a list and a possibly-empty array whose keys // cannot form a list are at worst `maybe`, never `no`. $emptyArray = new ConstantArrayType([], []);

Check warning on line 313 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $result = IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); - if ($result->no() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) { + if ($result->no() && $this->isList()->yes() && !$otherType->isArray()->no() && $otherType->isList()->no()) { // `[]` is a list, so a list and a possibly-empty array whose keys // cannot form a list are at worst `maybe`, never `no`. $emptyArray = new ConstantArrayType([], []);

Check warning on line 313 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $result = IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); - if ($result->no() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) { + if (!$result->yes() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) { // `[]` is a list, so a list and a possibly-empty array whose keys // cannot form a list are at worst `maybe`, never `no`. $emptyArray = new ConstantArrayType([], []);

Check warning on line 313 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $result = IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); - if ($result->no() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) { + if (!$result->yes() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) { // `[]` is a list, so a list and a possibly-empty array whose keys // cannot form a list are at worst `maybe`, never `no`. $emptyArray = new ConstantArrayType([], []);

Check warning on line 313 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $result = IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); - if ($result->no() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) { + if ($result->no() && !$this->isList()->no() && $otherType->isArray()->yes() && $otherType->isList()->no()) { // `[]` is a list, so a list and a possibly-empty array whose keys // cannot form a list are at worst `maybe`, never `no`. $emptyArray = new ConstantArrayType([], []);

Check warning on line 313 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $result = IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types)); - if ($result->no() && $this->isList()->yes() && $otherType->isArray()->yes() && $otherType->isList()->no()) { + if ($result->no() && $this->isList()->yes() && !$otherType->isArray()->no() && $otherType->isList()->no()) { // `[]` is a list, so a list and a possibly-empty array whose keys // cannot form a list are at worst `maybe`, never `no`. $emptyArray = new ConstantArrayType([], []);
// `[]` is a list, so a list and a possibly-empty array whose keys
// cannot form a list are at worst `maybe`, never `no`.
$emptyArray = new ConstantArrayType([], []);
if (
IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($emptyArray), $this->types))->yes()

Check warning on line 318 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // cannot form a list are at worst `maybe`, never `no`. $emptyArray = new ConstantArrayType([], []); if ( - IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($emptyArray), $this->types))->yes() + !IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($emptyArray), $this->types))->no() && $otherType->isSuperTypeOf($emptyArray)->yes() ) { return IsSuperTypeOfResult::createMaybe();

Check warning on line 318 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // cannot form a list are at worst `maybe`, never `no`. $emptyArray = new ConstantArrayType([], []); if ( - IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($emptyArray), $this->types))->yes() + !IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($emptyArray), $this->types))->no() && $otherType->isSuperTypeOf($emptyArray)->yes() ) { return IsSuperTypeOfResult::createMaybe();
&& $otherType->isSuperTypeOf($emptyArray)->yes()

Check warning on line 319 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $emptyArray = new ConstantArrayType([], []); if ( IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($emptyArray), $this->types))->yes() - && $otherType->isSuperTypeOf($emptyArray)->yes() + && !$otherType->isSuperTypeOf($emptyArray)->no() ) { return IsSuperTypeOfResult::createMaybe(); }

Check warning on line 319 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $emptyArray = new ConstantArrayType([], []); if ( IsSuperTypeOfResult::createYes()->and(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($emptyArray), $this->types))->yes() - && $otherType->isSuperTypeOf($emptyArray)->yes() + && !$otherType->isSuperTypeOf($emptyArray)->no() ) { return IsSuperTypeOfResult::createMaybe(); }
) {
return IsSuperTypeOfResult::createMaybe();
}
}

return $result;
}

public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
Expand Down
13 changes: 13 additions & 0 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2374,6 +2374,19 @@ public static function doIntersect(Type ...$types): Type
}

if ($isSuperTypeA->no()) {
$emptyArray = new ConstantArrayType([], []);
if (
$types[$i]->isArray()->yes()
&& $types[$j]->isArray()->yes()
&& ($types[$i]->isList()->no() || $types[$j]->isList()->no())
&& count(array_filter($types, static fn (Type $type): bool => $type instanceof AccessoryArrayListType)) > 0
&& $types[$i]->isSuperTypeOf($emptyArray)->yes()
&& $types[$j]->isSuperTypeOf($emptyArray)->yes()
) {
// `[]` is the only list a possibly-empty array whose keys cannot form a list admits
return self::intersect($emptyArray, ...$types);
}

return new NeverType(reason: $isSuperTypeA->getReasons()[0] ?? null);
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-14177.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public function testUnsetInt(array $a, array $b, array $c, int $int): void
unset($a[$int]);
unset($b[$int]);
assertType('bool', array_is_list($a));
assertType('false', array_is_list($b));
assertType('bool', array_is_list($b));
}

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

declare(strict_types = 1);

namespace Bug15450;

use function PHPStan\Testing\assertType;

/** @param array<string, mixed> $v */
function isList(array $v): void
{
if (array_is_list($v)) {
assertType('array{}', $v);
} else {
assertType('non-empty-array<string, mixed>', $v);
}
}

/** @param array<string, mixed> $v */
function isEmpty(array $v): void
{
if ($v === []) {
assertType('array{}', $v);
}
}

/** @param non-empty-array<string, mixed> $v */
function isNonEmptyList(array $v): void
{
if (array_is_list($v)) {
assertType('*NEVER*', $v);
} else {
assertType('non-empty-array<string, mixed>', $v);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -389,11 +389,6 @@ public function testBug2714(): void
public function testArrayIsList(): void
{
$this->analyse([__DIR__ . '/data/array-is-list.php'], [
[
'Call to function array_is_list() with array<string, int> will always evaluate to false.',
13,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function array_is_list() with array{foo: \'bar\', bar: \'baz\'} will always evaluate to false.',
40,
Expand All @@ -402,6 +397,11 @@ public function testArrayIsList(): void
'Call to function array_is_list() with array{0: \'foo\', foo: \'bar\', bar: \'baz\'} will always evaluate to false.',
44,
],
[
'Call to function array_is_list() with non-empty-array<string, int> will always evaluate to false.',
54,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
]);
}

Expand Down
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/array-is-list.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,14 @@ public function doBax()
// Fine
}
}

/**
* @param non-empty-array<string, int> $nonEmptyStringKeyedArray
*/
public function doQux(array $nonEmptyStringKeyedArray)
{
if (array_is_list($nonEmptyStringKeyedArray)) {

}
}
}
48 changes: 46 additions & 2 deletions turbo-ext/src/IntersectionType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ static zv::Val integerRange(NullableLong min, NullableLong max)
* new BooleanType() / new ObjectWithoutClassType() / new ClassStringType() */
static zv::Val constantInteger(zend_long value) { return pt_type_new_constant_integer(value); }

/* new ConstantArrayType([], []) */
static zv::Val emptyConstantArray()
{
zval empty, result;
ZVAL_EMPTY_ARRAY(&empty);
if (UNEXPECTED(!pt_constant_array_type_new(&result, &empty, &empty))) return zv::Val();
return zv::Val::adopt(result);
}

static zv::Val stringType()
{
return pt_val_of<pt_string_type_new>();
Expand Down Expand Up @@ -577,7 +586,8 @@ class IntersectionType
}

/* yes for an equal intersection or a never, else yes and'ed with every
* member's isSuperTypeOf(); UNDEF = pending exception */
* member's isSuperTypeOf() — at worst maybe for a list and a non-list
* array when both sides admit `[]`; UNDEF = pending exception */
zv::Val isSuperTypeOf(zval *otherType) const
{
if (zv::Ref(otherType).instanceOf(pt_ce_intersection_type)) {
Expand All @@ -591,7 +601,41 @@ class IntersectionType
if (UNEXPECTED(results.isUndef())) return zv::Val();
zv::Val yes = pt_type_is_super_type_of_result(PT_TRI_YES);
if (UNEXPECTED(yes.isUndef())) return zv::Val();
return pt_is_super_type_of_result_spread(Z_OBJ_P(yes.raw()), true, zv::ArrRef(results.raw()).table());
zv::Val result = pt_is_super_type_of_result_spread(Z_OBJ_P(yes.raw()), true, zv::ArrRef(results.raw()).table());
if (UNEXPECTED(result.isUndef())) return zv::Val();
zend_long value = pt_type_result_trinary(result.raw());
if (UNEXPECTED(value < 0)) return zv::Val();
if (value != PT_TRI_NO) return result;

zend_long isList = thisTrinary(PT_LC("islist"), &IntersectionType::isList);
if (UNEXPECTED(isList < 0)) return zv::Val();
if (isList != PT_TRI_YES) return result;
zend_long otherIsArray = pt_type_call_trinary(Z_OBJ_P(otherType), PT_LC("isarray"), 0, NULL);
if (UNEXPECTED(otherIsArray < 0)) return zv::Val();
if (otherIsArray != PT_TRI_YES) return result;
zend_long otherIsList = pt_type_op_trinary(Z_OBJ_P(otherType), PT_OP_IS_LIST, 0, NULL);
if (UNEXPECTED(otherIsList < 0)) return zv::Val();
if (otherIsList != PT_TRI_NO) return result;

/* `[]` is a list, so a list and a possibly-empty array whose keys
* cannot form a list are at worst `maybe`, never `no` */
zv::Val emptyArray = emptyConstantArray();
if (UNEXPECTED(emptyArray.isUndef())) return zv::Val();
zv::Val emptyResults = mapCall(PT_LC("issupertypeof"), 1, emptyArray.raw());
if (UNEXPECTED(emptyResults.isUndef())) return zv::Val();
zv::Val admitsEmpty = pt_is_super_type_of_result_spread(Z_OBJ_P(yes.raw()), true, zv::ArrRef(emptyResults.raw()).table());
if (UNEXPECTED(admitsEmpty.isUndef())) return zv::Val();
zend_long admitsEmptyValue = pt_type_result_trinary(admitsEmpty.raw());
if (UNEXPECTED(admitsEmptyValue < 0)) return zv::Val();
if (admitsEmptyValue != PT_TRI_YES) return result;

zv::Val otherAdmitsEmpty = pt_type_call(Z_OBJ_P(otherType), PT_LC("issupertypeof"), 1, emptyArray.raw());
if (UNEXPECTED(otherAdmitsEmpty.isUndef())) return zv::Val();
zend_long otherAdmitsEmptyValue = pt_type_result_trinary(otherAdmitsEmpty.raw());
if (UNEXPECTED(otherAdmitsEmptyValue < 0)) return zv::Val();
if (otherAdmitsEmptyValue != PT_TRI_YES) return result;

return pt_type_is_super_type_of_result(PT_TRI_MAYBE);
}

/* $otherType->isSuperTypeOf($this) for a compound other type, else
Expand Down
52 changes: 52 additions & 0 deletions turbo-ext/src/TypeCombinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,14 @@ static zv::Val oversizedArrayType()
return adopted(pt_oversized_array_type_new(&raw), raw);
}

/* new ConstantArrayType([], []) */
static zv::Val emptyConstantArrayType()
{
zval empty, raw;
ZVAL_EMPTY_ARRAY(&empty);
return adopted(pt_constant_array_type_new(&raw, &empty, &empty), raw);
}

static zv::Val accessoryLowercaseStringType()
{
zval raw;
Expand Down Expand Up @@ -3111,6 +3119,46 @@ class TypeCombinator
return call(type, PT_LC("issupertypeof"), 1, other);
}

/* `[]` is the only list a possibly-empty array whose keys cannot form a
* list admits: new ConstantArrayType([], []) when $a->isArray()->yes()
* && $b->isArray()->yes() && ($a->isList()->no() || $b->isList()->no())
* && $types holds an AccessoryArrayListType and both $a and $b are super
* types of it, else null; UNDEF = pending exception */
static zv::Val onlyEmptyListAdmitted(TypeList &types, zval *a, zval *b)
{
zend_long isArray = callTrinary(a, PT_LC("isarray"));
PT_FAIL_IF_NEG(isArray);
if (isArray != PT_TRI_YES) return zv::Val::null();
isArray = callTrinary(b, PT_LC("isarray"));
PT_FAIL_IF_NEG(isArray);
if (isArray != PT_TRI_YES) return zv::Val::null();
zend_long isList = callTrinary(a, PT_LC("islist"));
PT_FAIL_IF_NEG(isList);
if (isList != PT_TRI_NO) {
isList = callTrinary(b, PT_LC("islist"));
PT_FAIL_IF_NEG(isList);
if (isList != PT_TRI_NO) return zv::Val::null();
}
bool hasList = false;
for (zv::Val &type : types) {
if (isInstance(type.raw(), pt_ce_accessory_array_list_type)) {
hasList = true;
break;
}
}
if (!hasList) return zv::Val::null();

zv::Val emptyArray = emptyConstantArrayType();
PT_FAIL_IF_UNDEF(emptyArray);
zend_long admits = callResultTrinary(a, PT_LC("issupertypeof"), 1, emptyArray.raw());
PT_FAIL_IF_NEG(admits);
if (admits != PT_TRI_YES) return zv::Val::null();
admits = callResultTrinary(b, PT_LC("issupertypeof"), 1, emptyArray.raw());
PT_FAIL_IF_NEG(admits);
if (admits != PT_TRI_YES) return zv::Val::null();
return emptyArray;
}

/* the SubtractableType arm of doIntersect()'s reduction: whether the
* subtractable's type without its subtracted type is a super type of
* $other; -1 = pending exception */
Expand Down Expand Up @@ -3570,6 +3618,10 @@ class TypeCombinator
}

if (isSuperTypeAValue == PT_TRI_NO) {
zv::Val emptyArray = onlyEmptyListAdmitted(types, types[i].raw(), types[j].raw());
PT_FAIL_IF_UNDEF(emptyArray);
if (!emptyArray.isNull()) return intersectWith(emptyArray.raw(), types);

zv::Val reasons = call(isSuperTypeA.raw(), PT_LC("getreasons"));
PT_FAIL_IF_UNDEF(reasons);
zval *first = indexOf(reasons.raw(), 0);
Expand Down
Loading