diff --git a/extension.neon b/extension.neon index 7d3d61c7..e49796c6 100644 --- a/extension.neon +++ b/extension.neon @@ -310,3 +310,9 @@ services: tags: [phpstan.doctrine.typeDescriptor] arguments: uuidTypeName: Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType + + # Doctrine Collection + - + class: PHPStan\Type\Doctrine\Collection\FirstTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/src/Type/Doctrine/Collection/FirstTypeSpecifyingExtension.php b/src/Type/Doctrine/Collection/FirstTypeSpecifyingExtension.php new file mode 100644 index 00000000..e0ca8495 --- /dev/null +++ b/src/Type/Doctrine/Collection/FirstTypeSpecifyingExtension.php @@ -0,0 +1,62 @@ +getDeclaringClass()->getName() === self::COLLECTION_CLASS + || $methodReflection->getDeclaringClass()->isSubclassOf(self::COLLECTION_CLASS) + ) + && $methodReflection->getName() === self::IS_EMPTY_METHOD_NAME; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->create( + new MethodCall($node->var, self::FIRST_METHOD_NAME), + new ConstantBooleanType(false), + $context + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/tests/Type/Doctrine/Collection/FirstTypeSpecifyingExtensionTest.php b/tests/Type/Doctrine/Collection/FirstTypeSpecifyingExtensionTest.php new file mode 100644 index 00000000..8eeea9ae --- /dev/null +++ b/tests/Type/Doctrine/Collection/FirstTypeSpecifyingExtensionTest.php @@ -0,0 +1,46 @@ + + */ +class FirstTypeSpecifyingExtensionTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): Rule + { + return new VariableTypeReportingRule(); + } + + /** + * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] + */ + protected function getMethodTypeSpecifyingExtensions(): array + { + return [ + new FirstTypeSpecifyingExtension(), + ]; + } + + public function testExtension(): void + { + $this->analyse([__DIR__ . '/data/collection.php'], [ + [ + 'Variable $entityOrFalse is: MyEntity|false', + 18, + ], + [ + 'Variable $false is: false', + 22, + ], + [ + 'Variable $entity is: MyEntity', + 27, + ], + ]); + } + +} diff --git a/tests/Type/Doctrine/Collection/VariableTypeReportingRule.php b/tests/Type/Doctrine/Collection/VariableTypeReportingRule.php new file mode 100644 index 00000000..54b675c4 --- /dev/null +++ b/tests/Type/Doctrine/Collection/VariableTypeReportingRule.php @@ -0,0 +1,41 @@ + + */ +class VariableTypeReportingRule implements \PHPStan\Rules\Rule +{ + + public function getNodeType(): string + { + return Node\Expr\Variable::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!is_string($node->name)) { + return []; + } + if (!$scope->isInFirstLevelStatement()) { + return []; + }; + + if ($scope->isInExpressionAssign($node)) { + return []; + } + + return [ + sprintf( + 'Variable $%s is: %s', + $node->name, + $scope->getType($node)->describe(\PHPStan\Type\VerbosityLevel::value()) + ), + ]; + } + +} diff --git a/tests/Type/Doctrine/Collection/data/collection.php b/tests/Type/Doctrine/Collection/data/collection.php new file mode 100644 index 00000000..4ced3056 --- /dev/null +++ b/tests/Type/Doctrine/Collection/data/collection.php @@ -0,0 +1,28 @@ + $collection + */ +$collection = new ArrayCollection(); + +$entityOrFalse = $collection->first(); +$entityOrFalse; + +if ($collection->isEmpty()) { + $false = $collection->first(); + $false; +} + +if (!$collection->isEmpty()) { + $entity = $collection->first(); + $entity; +}