diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index 8ba537930d..d504a260de 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -21,6 +21,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\TypeCombinator; use function count; +use function in_array; class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -38,7 +39,7 @@ public function isFunctionSupported( TypeSpecifierContext $context, ): bool { - return $functionReflection->getName() === 'array_key_exists' + return in_array($functionReflection->getName(), ['array_key_exists', 'key_exists'], true) && !$context->null(); } diff --git a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php index 628dfa7be6..d28f3c5075 100644 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php @@ -39,6 +39,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo { return in_array($functionReflection->getName(), [ 'array_key_exists', + 'key_exists', 'in_array', 'is_numeric', 'is_int', diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 1c2b7a69c6..b4ebdfdf5a 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1011,6 +1011,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6170.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key-exists.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/key-exists.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7909.php'); if (PHP_VERSION_ID >= 80000) { diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index f1791cb923..052906f97f 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -954,6 +954,54 @@ public function dataCondition(): array '$array' => '~hasOffset(\'foo\')', ], ], + [ + new Expr\BinaryOp\BooleanOr( + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('foo')), + new Arg(new Variable('array')), + ]), + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('bar')), + new Arg(new Variable('array')), + ]), + ), + [ + '$array' => 'array', + ], + [ + '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', + ], + ], + [ + new BooleanNot(new Expr\BinaryOp\BooleanOr( + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('foo')), + new Arg(new Variable('array')), + ]), + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('bar')), + new Arg(new Variable('array')), + ]), + )), + [ + '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', + ], + [ + '$array' => 'array', + ], + ], + [ + new FuncCall(new Name('key_exists'), [ + new Arg(new String_('foo')), + new Arg(new Variable('array')), + ]), + [ + '$array' => 'array&hasOffset(\'foo\')', + ], + [ + '$array' => '~hasOffset(\'foo\')', + ], + ], [ new FuncCall(new Name('is_subclass_of'), [ new Arg(new Variable('string')), diff --git a/tests/PHPStan/Analyser/data/key-exists.php b/tests/PHPStan/Analyser/data/key-exists.php new file mode 100644 index 0000000000..678bc368a9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/key-exists.php @@ -0,0 +1,29 @@ + $a + * @return void + */ + public function doFoo(array $a, string $key): void + { + assertType('false', key_exists(2, $a)); + assertType('bool', key_exists('foo', $a)); + assertType('false', key_exists('2', $a)); + + $a = ['foo' => 2, 3 => 'bar']; + assertType('true', key_exists('foo', $a)); + assertType('true', key_exists('3', $a)); + assertType('false', key_exists(4, $a)); + + $empty = []; + assertType('false', key_exists('foo', $empty)); + assertType('false', key_exists($key, $empty)); + } +}