diff --git a/conf/config.neon b/conf/config.neon index fb534ed239..15a8482007 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1637,6 +1637,11 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\Php\ClassImplementsFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\DefineConstantTypeSpecifyingExtension tags: diff --git a/resources/functionMap.php b/resources/functionMap.php index 0b25c6784b..4efa675198 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -948,9 +948,9 @@ 'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'positive-int', 'ending='=>'string'], 'class_alias' => ['bool', 'user_class_name'=>'string', 'alias_name'=>'string', 'autoload='=>'bool'], 'class_exists' => ['bool', 'classname'=>'string', 'autoload='=>'bool'], -'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], +'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], 'class_parents' => ['array|false', 'instance'=>'object|string', 'autoload='=>'bool'], -'class_uses' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], +'class_uses' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], 'classkit_import' => ['array', 'filename'=>'string'], 'classkit_method_add' => ['bool', 'classname'=>'string', 'methodname'=>'string', 'args'=>'string', 'code'=>'string', 'flags='=>'int'], 'classkit_method_copy' => ['bool', 'dclass'=>'string', 'dmethod'=>'string', 'sclass'=>'string', 'smethod='=>'string'], diff --git a/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..879e4d3e67 --- /dev/null +++ b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php @@ -0,0 +1,61 @@ +getName(), + ['class_implements', 'class_uses', 'class_parents'], + true, + ); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + $autoload = !isset($functionCall->getArgs()[1]) + || $scope->getType($functionCall->getArgs()[1]->value)->equals(new ConstantBooleanType(true)); + + $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($firstArgType); + + $objectOrClassString = (new UnionType([new ObjectWithoutClassType(), new ClassStringType()])); + if ( + $autoload && $objectOrClassString->isSuperTypeOf($firstArgType)->yes() + || $isObject->yes() + ) { + return TypeCombinator::remove( + ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(), + new ConstantBooleanType(false), + ); + } + + if ($isObject->no() && $firstArgType->isClassStringType()->no()) { + return new ConstantBooleanType(false); + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index eb53bed1ef..b1eb3f399f 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -492,6 +492,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/class-implements.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3379.php'); diff --git a/tests/PHPStan/Analyser/data/class-implements.php b/tests/PHPStan/Analyser/data/class-implements.php new file mode 100644 index 0000000000..76689ec8cd --- /dev/null +++ b/tests/PHPStan/Analyser/data/class-implements.php @@ -0,0 +1,128 @@ +', class_implements($object)); + assertType('array', class_implements($objectOrClassString)); + assertType('array|false', class_implements($objectOrString)); + assertType('array', class_implements($classString)); + assertType('array|false', class_implements($className)); + assertType('false', class_implements('thisIsNotAClass')); + + assertType('array', class_implements($object, true)); + assertType('array', class_implements($objectOrClassString, true)); + assertType('array|false', class_implements($objectOrString, true)); + assertType('array', class_implements($classString, true)); + assertType('array|false', class_implements($className, true)); + assertType('false', class_implements('thisIsNotAClass', true)); + + assertType('array', class_implements($object, false)); + assertType('array|false', class_implements($objectOrClassString, false)); + assertType('array|false', class_implements($objectOrString, false)); + assertType('array|false', class_implements($classString, false)); + assertType('array|false', class_implements($className, false)); + assertType('false', class_implements('thisIsNotAClass', false)); + + assertType('array', class_implements($object, $bool)); + assertType('array|false', class_implements($objectOrClassString, $bool)); + assertType('array|false', class_implements($objectOrString, $bool)); + assertType('array|false', class_implements($classString, $bool)); + assertType('array|false', class_implements($className, $bool)); + assertType('false', class_implements('thisIsNotAClass', $bool)); + + assertType('array', class_implements($object, $mixed)); + assertType('array|false', class_implements($objectOrClassString, $mixed)); + assertType('array|false', class_implements($objectOrString, $mixed)); + assertType('array|false', class_implements($classString, $mixed)); + assertType('array|false', class_implements($className, $mixed)); + assertType('false', class_implements('thisIsNotAClass', $mixed)); + + assertType('array', class_uses($object)); + assertType('array', class_uses($objectOrClassString)); + assertType('array|false', class_uses($objectOrString)); + assertType('array', class_uses($classString)); + assertType('array|false', class_uses($className)); + assertType('false', class_uses('thisIsNotAClass')); + + assertType('array', class_uses($object, true)); + assertType('array', class_uses($objectOrClassString, true)); + assertType('array|false', class_uses($objectOrString, true)); + assertType('array', class_uses($classString, true)); + assertType('array|false', class_uses($className, true)); + assertType('false', class_uses('thisIsNotAClass', true)); + + assertType('array', class_uses($object, false)); + assertType('array|false', class_uses($objectOrClassString, false)); + assertType('array|false', class_uses($objectOrString, false)); + assertType('array|false', class_uses($classString, false)); + assertType('array|false', class_uses($className, false)); + assertType('false', class_uses('thisIsNotAClass', false)); + + assertType('array', class_uses($object, $bool)); + assertType('array|false', class_uses($objectOrClassString, $bool)); + assertType('array|false', class_uses($objectOrString, $bool)); + assertType('array|false', class_uses($classString, $bool)); + assertType('array|false', class_uses($className, $bool)); + assertType('false', class_uses('thisIsNotAClass', $bool)); + + assertType('array', class_uses($object, $mixed)); + assertType('array|false', class_uses($objectOrClassString, $mixed)); + assertType('array|false', class_uses($objectOrString, $mixed)); + assertType('array|false', class_uses($classString, $mixed)); + assertType('array|false', class_uses($className, $mixed)); + assertType('false', class_uses('thisIsNotAClass', $mixed)); + + assertType('array', class_parents($object)); + assertType('array', class_parents($objectOrClassString)); + assertType('array|false', class_parents($objectOrString)); + assertType('array', class_parents($classString)); + assertType('array|false', class_parents($className)); + assertType('false', class_parents('thisIsNotAClass', $className)); + + assertType('array', class_parents($object, true)); + assertType('array', class_parents($objectOrClassString, true)); + assertType('array|false', class_parents($objectOrString, true)); + assertType('array', class_parents($classString, true)); + assertType('array|false', class_parents($className, true)); + assertType('false', class_parents('thisIsNotAClass', true)); + + assertType('array', class_parents($object, false)); + assertType('array|false', class_parents($objectOrClassString, false)); + assertType('array|false', class_parents($objectOrString, false)); + assertType('array|false', class_parents($classString, false)); + assertType('array|false', class_parents($className, false)); + assertType('false', class_parents('thisIsNotAClass', false)); + + assertType('array', class_parents($object, $bool)); + assertType('array|false', class_parents($objectOrClassString, $bool)); + assertType('array|false', class_parents($objectOrString, $bool)); + assertType('array|false', class_parents($classString, $bool)); + assertType('array|false', class_parents($className, $bool)); + assertType('false', class_parents('thisIsNotAClass', $bool)); + + assertType('array', class_parents($object, $mixed)); + assertType('array|false', class_parents($objectOrClassString, $mixed)); + assertType('array|false', class_parents($objectOrString, $mixed)); + assertType('array|false', class_parents($classString, $mixed)); + assertType('array|false', class_parents($className, $mixed)); + assertType('false', class_parents('thisIsNotAClass', $mixed)); + } + +} diff --git a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php index 19ddcbd783..b6abc55a97 100644 --- a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php @@ -75,4 +75,9 @@ public function testBug6564(): void $this->analyse([__DIR__ . '/data/bug-6564.php'], []); } + public function testBug4335(): void + { + $this->analyse([__DIR__ . '/data/bug-4335.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4335.php b/tests/PHPStan/Rules/Arrays/data/bug-4335.php new file mode 100644 index 0000000000..0824514d15 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4335.php @@ -0,0 +1,19 @@ + $v) { + var_dump($k, $v); + } + foreach (class_parents($this) as $k => $v) { + var_dump($k, $v); + } + foreach (class_uses($this) as $k => $v) { + var_dump($k, $v); + } + } +}