Skip to content
53 changes: 33 additions & 20 deletions src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,33 +187,46 @@
}

if ($isNeedleSupertype->maybe() || $isNeedleSupertype->yes()) {
$needleFiniteTypes = $needleType->getFiniteTypes();
if ($needleFiniteTypes === []) {
return null;
}

foreach ($haystackArrayTypes as $haystackArrayType) {
if ($haystackArrayType instanceof ConstantArrayType) {
foreach ($haystackArrayType->getValueTypes() as $i => $haystackArrayValueType) {
if ($haystackArrayType->isOptionalKey($i)) {
continue;
}
$guaranteedValueTypes = [];
if (!($haystackArrayType instanceof ConstantArrayType)) {
// A general array cannot guarantee any specific value is present.
return null;
}

$haystackArrayValueConstantScalarTypes = $haystackArrayValueType->getConstantScalarTypes();
if (count($haystackArrayValueConstantScalarTypes) > 1) {
continue;
}
foreach ($haystackArrayType->getValueTypes() as $i => $haystackArrayValueType) {
if ($haystackArrayType->isOptionalKey($i)) {
continue;
}

foreach ($haystackArrayValueConstantScalarTypes as $constantScalarType) {
if ($constantScalarType->isSuperTypeOf($needleType)->yes()) {
continue 3;
}
}
$haystackArrayValueFiniteTypes = $haystackArrayValueType->getFiniteTypes();
if (count($haystackArrayValueFiniteTypes) !== 1) {
continue;
}
} else {
foreach ($haystackArrayType->getIterableValueType()->getConstantScalarTypes() as $constantScalarType) {
if ($constantScalarType->isSuperTypeOf($needleType)->yes()) {
continue 2;

$guaranteedValueTypes[] = $haystackArrayValueFiniteTypes[0];
}

// in_array() is only guaranteed true when every possible needle value
// is guaranteed to be present in this haystack variant.
foreach ($needleFiniteTypes as $needleFiniteType) {
$found = false;
foreach ($guaranteedValueTypes as $guaranteedValueType) {
if ($guaranteedValueType->isSuperTypeOf($needleFiniteType)->yes()) {

Check warning on line 220 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ foreach ($needleFiniteTypes as $needleFiniteType) { $found = false; foreach ($guaranteedValueTypes as $guaranteedValueType) { - if ($guaranteedValueType->isSuperTypeOf($needleFiniteType)->yes()) { + if (!$guaranteedValueType->isSuperTypeOf($needleFiniteType)->no()) { $found = true; break; }

Check warning on line 220 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ foreach ($needleFiniteTypes as $needleFiniteType) { $found = false; foreach ($guaranteedValueTypes as $guaranteedValueType) { - if ($guaranteedValueType->isSuperTypeOf($needleFiniteType)->yes()) { + if ($needleFiniteType->isSuperTypeOf($guaranteedValueType)->yes()) { $found = true; break; }

Check warning on line 220 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ foreach ($needleFiniteTypes as $needleFiniteType) { $found = false; foreach ($guaranteedValueTypes as $guaranteedValueType) { - if ($guaranteedValueType->isSuperTypeOf($needleFiniteType)->yes()) { + if (!$guaranteedValueType->isSuperTypeOf($needleFiniteType)->no()) { $found = true; break; }

Check warning on line 220 in src/Rules/Comparison/ImpossibleCheckTypeHelper.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ foreach ($needleFiniteTypes as $needleFiniteType) { $found = false; foreach ($guaranteedValueTypes as $guaranteedValueType) { - if ($guaranteedValueType->isSuperTypeOf($needleFiniteType)->yes()) { + if ($needleFiniteType->isSuperTypeOf($guaranteedValueType)->yes()) { $found = true; break; }
$found = true;
break;
}
}
}

return null;
if (!$found) {
return null;
}
}
}
}

Expand Down
4 changes: 1 addition & 3 deletions src/Rules/Functions/ParameterCastableToStringRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array

if (in_array($functionName, $checkAllArgsFunctions, true)) {
$argsToCheck = $origArgs;
} elseif (in_array($functionName, $checkFirstArgFunctions, true)) {
} else {
$normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);

if ($normalizedFuncCall === null) {
Expand All @@ -86,8 +86,6 @@ public function processNode(Node $node, Scope $scope): array
return [];
}
$argsToCheck = [0 => $normalizedArgs[0]];
} else {
return [];
}

$errors = [];
Expand Down
12 changes: 12 additions & 0 deletions src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n

if ($arrayExpr instanceof Array_) {
$types = null;
$combinedMultipleItems = false;
foreach ($arrayExpr->items as $item) {
if ($item->unpack) {
$types = null;
Expand All @@ -89,10 +90,21 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
continue;
}

$combinedMultipleItems = true;
$types = $context->true() ? $types->normalize($scope)->intersectWith($itemTypes->normalize($scope)) : $types->unionWith($itemTypes);
}

if ($types !== null) {
if ($combinedMultipleItems) {
// Each per-item SpecifiedTypes carries its own "$needle === $item"
// as its root expression. Once the comparisons of multiple items are
// combined, that per-item root expression no longer describes the whole
// in_array() call, so it must be cleared. Leaving it set lets callers
// such as ImpossibleCheckTypeHelper read the type of a single item's
// comparison and draw a wrong conclusion about the whole call.
$types = $types->setRootExpr(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need setRootExpr ?

}

return $types;
}
}
Expand Down
114 changes: 114 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14873.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug14873;

use function PHPStan\Testing\assertType;
use function in_array;

enum Suit: string
{

case Hearts = 'H';
case Spades = 'S';
case Clubs = 'C';

}

class HelloWorld
{

/**
* @param 'a'|'b'|'c' $full
* @param 'a'|'b' $subset
* @param 'a'|'x' $partial
*/
public function variableHaystack(string $full, string $subset, string $partial): void
{
$a = ['a', 'b', 'c'];

assertType('true', in_array($full, $a, true));
assertType('true', in_array($subset, $a, true));
assertType('bool', in_array($partial, $a, true));
}

/**
* @param 'a'|'b'|'c' $full
* @param 'a'|'b' $subset
* @param 'a'|'x' $partial
*/
public function literalHaystack(string $full, string $subset, string $partial): void
{
assertType('true', in_array($full, ['a', 'b', 'c'], true));
assertType('bool', in_array($full, ['a', 'b', 'c'], false)); // non-strict
assertType('true', in_array($subset, ['a', 'b', 'c'], true));
assertType('bool', in_array($partial, ['a', 'b', 'c'], true));
assertType('false', in_array($subset, ['x', 'y'], true));

$fullOrEmpty = rand(0,1) ? $full : [];
assertType('bool', in_array($fullOrEmpty, ['a', 'b', 'c'], true));
}

/**
* @param 1|2 $full
* @param 1 $subset
*/
public function integers(int $full, int $subset): void
{
$a = [1, 2];

assertType('true', in_array($full, $a, true));
assertType('true', in_array($subset, $a, true));
assertType('true', in_array($full, [1, 2, 3], true));
}

/**
* @param Suit::Hearts|Suit::Spades $subset
*/
public function enums(Suit $subset): void
{
$a = [Suit::Hearts, Suit::Spades, Suit::Clubs];

assertType('true', in_array($subset, $a, true));
assertType('true', in_array($subset, [Suit::Hearts, Suit::Spades, Suit::Clubs], true));
assertType('bool', in_array($subset, [Suit::Hearts], true));
}

/**
* Plain objects do not have a finite set of possible values, so in_array()
* must not be reported as always-true even when the needle's class matches
* every haystack value type.
*/
public function objects(Article $article, ?Article $a, ?Article $b): void
{
$haystack = [$a, $b];

assertType('bool', in_array($article, $haystack, true));
assertType('bool', in_array($article, [$a, $b], true));
}

/**
* A general (non-constant) array does not guarantee that any particular value
* is present, so a subset needle must not be reported as always-true. Only a
* non-empty array whose values all share a single finite type guarantees that
* value's presence.
*
* @param 1|2 $needle
* @param array<int, 1|2> $maybeEmpty
* @param non-empty-array<int, 1|2> $nonEmptyMulti
* @param non-empty-array<int, 1> $nonEmptySingle
*/
public function generalArrays(int $needle, array $maybeEmpty, array $nonEmptyMulti, array $nonEmptySingle): void
{
assertType('bool', in_array($needle, $maybeEmpty, true));
assertType('bool', in_array($needle, $nonEmptyMulti, true));
assertType('true', in_array(1, $nonEmptySingle, true));
}

}

class Article
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ public function testImpossibleCheckTypeFunctionCall(): void
'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'baz\', \'lorem\'} and true will always evaluate to false.',
255,
],
[
'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.',
259,
],
[
'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.',
263,
Expand Down Expand Up @@ -1094,6 +1098,25 @@ public function testBug12412(): void
$this->analyse([__DIR__ . '/data/bug-12412.php'], []);
}

public function testBug14873(): void
{
$tipText = '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%</>.';

$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-14873.php'], [
[
'Call to function in_array() with arguments \'a\'|\'b\'|\'c\', array{\'a\', \'b\', \'c\'} and true will always evaluate to true.',
17,
$tipText,
],
[
'Call to function in_array() with arguments \'a\'|\'b\', array{\'a\', \'b\', \'c\'} and true will always evaluate to true.',
21,
$tipText,
],
]);
}

public function testBug2730(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
50 changes: 50 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-14873.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1);

namespace Bug14873Rule;

class HelloWorld
{

/**
* @param 'a'|'b'|'c' $full
* @param 'a'|'b' $subset
* @param 'a'|'x' $partial
*/
public function sayHello(string $full, string $subset, string $partial): void
{
$a = ['a', 'b', 'c'];

if (in_array($full, $a, true)) {
echo 'full';
}

if (in_array($subset, $a, true)) {
echo 'subset';
}

if (in_array($partial, $a, true)) {
echo 'partial';
}
}

/**
* A general (non-constant) array does not guarantee that any particular value
* is present, so a subset needle must not be reported as always-true - even
* when every finite needle value appears in the array's value type.
*
* @param 1|2 $needle
* @param array<int, 1|2> $maybeEmpty
* @param non-empty-array<int, 1|2> $nonEmptyMulti
*/
public function generalArrays(int $needle, array $maybeEmpty, array $nonEmptyMulti): void
{
if (in_array($needle, $maybeEmpty, true)) {
echo 'maybeEmpty';
}

if (in_array($needle, $nonEmptyMulti, true)) {
echo 'nonEmptyMulti';
}
}

}
Loading