Skip to content
Merged
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
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ parameters:
checkDynamicConstantNameValues: true
unusedLabel: true
newOnNonObject: true
finiteTypesInHaystack: true
7 changes: 7 additions & 0 deletions conf/config.level4.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ conditionalTags:
phpstan.rules.rule: %exceptions.check.tooWideThrowType%
PHPStan\Rules\Keywords\UnusedLabelRule:
phpstan.rules.rule: %featureToggles.unusedLabel%
PHPStan\Rules\Comparison\ImpossibleInArrayHaystackFiniteTypesRule:
phpstan.rules.rule: %featureToggles.finiteTypesInHaystack%

parameters:
checkAdvancedIsset: true
Expand All @@ -35,3 +37,8 @@ services:

-
class: PHPStan\Rules\Keywords\UnusedLabelRule

-
class: PHPStan\Rules\Comparison\ImpossibleInArrayHaystackFiniteTypesRule
arguments:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ parameters:
checkDynamicConstantNameValues: false
unusedLabel: false
newOnNonObject: false
finiteTypesInHaystack: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ parametersSchema:
checkDynamicConstantNameValues: bool()
unusedLabel: bool()
newOnNonObject: bool()
finiteTypesInHaystack: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
141 changes: 141 additions & 0 deletions src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Comparison;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function array_key_exists;
use function count;
use function max;
use function sprintf;
use function strtolower;

/**
* Reports finite-typed values in a constant array haystack passed to in_array(),
* array_search() or array_keys() that can never be the needle, using
* Type::getConstantArrays() and Type::getFiniteTypes() instead of inspecting the
* AST of the array literal.
*
* @implements Rule<FuncCall>
*/
final class ImpossibleInArrayHaystackFiniteTypesRule implements Rule
{

/** Argument positions of the needle and the haystack per supported function. */
private const FUNCTIONS = [
'in_array' => ['needle' => 0, 'haystack' => 1],
'array_search' => ['needle' => 0, 'haystack' => 1],
'array_keys' => ['needle' => 1, 'haystack' => 0],
];

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private bool $treatPhpDocTypesAsCertain,
)
{
}

public function getNodeType(): string
{
return FuncCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Name) {
return [];
}

$functionName = strtolower((string) $node->name);
if (!array_key_exists($functionName, self::FUNCTIONS)) {
return [];
}

$needleArg = self::FUNCTIONS[$functionName]['needle'];
$haystackArg = self::FUNCTIONS[$functionName]['haystack'];

$args = $node->getArgs();
if (count($args) <= max($needleArg, $haystackArg)) {
return [];
}

$needleType = $this->treatPhpDocTypesAsCertain ? $scope->getType($args[$needleArg]->value) : $scope->getNativeType($args[$needleArg]->value);
$haystackType = $this->treatPhpDocTypesAsCertain ? $scope->getType($args[$haystackArg]->value) : $scope->getNativeType($args[$haystackArg]->value);

$constantArrays = $haystackType->getConstantArrays();
if (count($constantArrays) === 0) {
return [];
}

$isStrict = count($args) >= 3
? ($this->treatPhpDocTypesAsCertain ? $scope->getType($args[2]->value) : $scope->getNativeType($args[2]->value))->isTrue()
: TrinaryLogic::createNo();

$deadValueTypes = [];
$anyPossibleMatch = false;
foreach ($constantArrays as $constantArray) {
foreach ($constantArray->getValueTypes() as $valueType) {
if (!$this->canNeverMatch($needleType, $valueType, $isStrict)) {
$anyPossibleMatch = true;
continue;
}

if (count($valueType->getFiniteTypes()) === 0) {
continue;
}

$deadValueTypes[$valueType->describe(VerbosityLevel::precise())] = $valueType;
}
}

// When no haystack value can ever match, the whole in_array() call is
// impossible and reported by ImpossibleCheckTypeFunctionCallRule instead.
// array_search() has no such companion rule, so keep reporting there.
if (!$anyPossibleMatch && $functionName === 'in_array') {
return [];
}

$verb = $isStrict->no() ? 'equal to' : 'identical to';

$errors = [];
foreach ($deadValueTypes as $valueType) {
$errors[] = $this->buildError($valueType, $needleType, $functionName, $verb);
}

return $errors;
}

private function canNeverMatch(Type $needleType, Type $valueType, TrinaryLogic $isStrict): bool
{
if ($isStrict->yes()) {
return $this->initializerExprTypeResolver->resolveIdenticalType($needleType, $valueType)->type->isFalse()->yes();
}

if ($isStrict->no()) {
return $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType)->type->isFalse()->yes();
}

return $this->initializerExprTypeResolver->resolveIdenticalType($needleType, $valueType)->type->isFalse()->yes()
&& $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType)->type->isFalse()->yes();
}

private function buildError(Type $valueType, Type $needleType, string $functionName, string $verb): IdentifierRuleError
{
return RuleErrorBuilder::message(sprintf(
'Value %s in the haystack passed to %s() can never be %s the needle type %s.',
$valueType->describe(VerbosityLevel::precise()),
$functionName,
$verb,
$needleType->describe(VerbosityLevel::precise()),
))->identifier('function.impossibleHaystackValue')->build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Comparison;

use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
* @extends RuleTestCase<ImpossibleInArrayHaystackFiniteTypesRule>
*/
class ImpossibleInArrayHaystackFiniteTypesRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new ImpossibleInArrayHaystackFiniteTypesRule(
self::getContainer()->getByType(InitializerExprTypeResolver::class),
true,
);
}

#[RequiresPhp('>= 8.1.0')]
public function testRule(): void
{
$this->analyse([__DIR__ . '/data/impossible-in-array-finite-types.php'], [
[
'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to in_array() can never be identical to the needle type int.',
19,
],
[
'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to in_array() can never be equal to the needle type int.',
26,
],
[
'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to array_search() can never be identical to the needle type int.',
33,
],
[
'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to array_keys() can never be identical to the needle type int.',
38,
],
[
'Value ImpossibleInArrayFiniteTypes\Foo::TWO in the haystack passed to in_array() can never be identical to the needle type ImpossibleInArrayFiniteTypes\Foo::ONE.',
48,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace ImpossibleInArrayFiniteTypes;

enum Foo
{

case ONE;
case TWO;
case THREE;

}

function reportedFiniteValueStrict(int $i): void
{
// Foo::ONE can never be an int, but 1 and 2 can.
if (in_array($i, [Foo::ONE, 1, 2], true)) {
echo 'yes';
}
}

function reportedFiniteValueLoose(int $i): void
{
if (in_array($i, [Foo::ONE, 1, 2])) {
echo 'yes';
}
}

function reportedArraySearch(int $i): void
{
array_search($i, [Foo::ONE, 1, 2], true);
}

function reportedArrayKeys(int $i): void
{
array_keys([Foo::ONE, 1, 2], $i, true);
}

function reportedEnumNeedle(Foo $foo): void
{
if ($foo !== Foo::ONE) {
return;
}

// Foo::TWO can never be Foo::ONE (finite value in haystack), but Foo::ONE can.
if (in_array($foo, [Foo::ONE, Foo::TWO], true)) {
echo 'yes';
}
}

function noErrorEverythingMatches(int $i): void
{
if (in_array($i, [1, 2, 3], true)) {
echo 'yes';
}
}

function noErrorMixedNeedle(mixed $i): void
{
if (in_array($i, [Foo::ONE, 1, 2], true)) {
echo 'yes';
}
}

function noErrorWholeCallImpossible(int $i): void
{
// Whole call is impossible - reported by ImpossibleCheckTypeFunctionCallRule instead.
if (in_array($i, [Foo::ONE, Foo::TWO], true)) {
echo 'yes';
}
}

function noErrorNonConstantHaystack(int $i, array $haystack): void
{
if (in_array($i, $haystack, true)) {
echo 'yes';
}
}

/**
* @param int|string $i
*/
function noErrorUnionNeedleMatches(int|string $i): void
{
if (in_array($i, ['a', 'b', 1], true)) {
echo 'yes';
}
}
Loading