From 5afea15a536b60ff4d9cd7c6a2d010c544c00641 Mon Sep 17 00:00:00 2001 From: Jeroen Thora Date: Sun, 14 Aug 2022 11:10:18 +0200 Subject: [PATCH] Add ReadWritePropertiesExtension for Gedmo annotations/attributes --- composer.json | 1 + extension.neon | 5 + phpstan-baseline.neon | 15 +++ src/Rules/Gedmo/PropertiesExtension.php | 103 ++++++++++++++++++ ...ingGedmoByPhpDocPropertyAssignRuleTest.php | 46 ++++++++ .../MissingGedmoPropertyAssignRuleTest.php | 73 +++++++++++++ .../data/gedmo-property-assign-non-entity.php | 11 ++ .../data/gedmo-property-assign-phpdoc.php | 17 +++ .../Properties/data/gedmo-property-assign.php | 13 +++ tests/Rules/Properties/entity-manager.php | 38 +++++++ 10 files changed, 322 insertions(+) create mode 100644 src/Rules/Gedmo/PropertiesExtension.php create mode 100644 tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php create mode 100644 tests/Rules/Properties/MissingGedmoPropertyAssignRuleTest.php create mode 100644 tests/Rules/Properties/data/gedmo-property-assign-non-entity.php create mode 100644 tests/Rules/Properties/data/gedmo-property-assign-phpdoc.php create mode 100644 tests/Rules/Properties/data/gedmo-property-assign.php create mode 100644 tests/Rules/Properties/entity-manager.php diff --git a/composer.json b/composer.json index 76f850ff..17169aed 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "doctrine/mongodb-odm": "^1.3 || ^2.1", "doctrine/orm": "^2.11.0", "doctrine/persistence": "^1.3.8 || ^2.2.1", + "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", diff --git a/extension.neon b/extension.neon index 2c9f4ad2..53636e7e 100644 --- a/extension.neon +++ b/extension.neon @@ -371,3 +371,8 @@ services: class: PHPStan\Type\Doctrine\Collection\IsEmptyTypeSpecifyingExtension tags: - phpstan.typeSpecifier.methodTypeSpecifyingExtension + + - + class: PHPStan\Rules\Gedmo\PropertiesExtension + tags: + - phpstan.properties.readWriteExtension diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fa594564..517ef707 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,6 +5,11 @@ parameters: count: 1 path: src/Doctrine/Mapping/ClassMetadataFactory.php + - + message: "#^Call to method getProperty\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" + count: 1 + path: src/Rules/Gedmo/PropertiesExtension.php + - message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" count: 1 @@ -20,6 +25,16 @@ parameters: count: 1 path: tests/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php + - + message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php + + - + message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/Properties/MissingGedmoPropertyAssignRuleTest.php + - message: "#^Accessing PHPStan\\\\Rules\\\\Properties\\\\MissingReadOnlyByPhpDocPropertyAssignRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" count: 1 diff --git a/src/Rules/Gedmo/PropertiesExtension.php b/src/Rules/Gedmo/PropertiesExtension.php new file mode 100644 index 00000000..829f60f3 --- /dev/null +++ b/src/Rules/Gedmo/PropertiesExtension.php @@ -0,0 +1,103 @@ +annotationReader = class_exists(AnnotationReader::class) ? new AnnotationReader() : null; + $this->objectMetadataResolver = $objectMetadataResolver; + } + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return $this->isGedmoAnnotationOrAttribute($property, $propertyName, self::GEDMO_READ_CLASSLIST); + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isGedmoAnnotationOrAttribute($property, $propertyName, self::GEDMO_WRITE_CLASSLIST); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + /** + * @param array $classList + */ + private function isGedmoAnnotationOrAttribute(PropertyReflection $property, string $propertyName, array $classList): bool + { + if ($this->annotationReader === null) { + return false; + } + + $classReflection = $property->getDeclaringClass(); + if ($this->objectMetadataResolver->isTransient($classReflection->getName())) { + return false; + } + + $propertyReflection = $classReflection->getNativeReflection()->getProperty($propertyName); + + $annotations = $this->annotationReader->getPropertyAnnotations($propertyReflection); + foreach ($annotations as $annotation) { + if (in_array(get_class($annotation), $classList, true)) { + return true; + } + } + + $attributes = $propertyReflection->getAttributes(); + foreach ($attributes as $attribute) { + if (in_array($attribute->getName(), $classList, true)) { + return true; + } + } + + return false; + } + +} diff --git a/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php b/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php new file mode 100644 index 00000000..d4b8e0da --- /dev/null +++ b/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php @@ -0,0 +1,46 @@ + + */ +class MissingGedmoByPhpDocPropertyAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(UnusedPrivatePropertyRule::class); + } + + protected function getReadWritePropertiesExtensions(): array + { + return [ + new PropertiesExtension(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php')), + ]; + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/../../../extension.neon']; + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/gedmo-property-assign-phpdoc.php'], [ + // No errors expected + ]); + } + +} diff --git a/tests/Rules/Properties/MissingGedmoPropertyAssignRuleTest.php b/tests/Rules/Properties/MissingGedmoPropertyAssignRuleTest.php new file mode 100644 index 00000000..d91685e0 --- /dev/null +++ b/tests/Rules/Properties/MissingGedmoPropertyAssignRuleTest.php @@ -0,0 +1,73 @@ + + */ +class MissingGedmoPropertyAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(UnusedPrivatePropertyRule::class); + } + + protected function getReadWritePropertiesExtensions(): array + { + return [ + new PropertiesExtension(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php')), + ]; + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/../../../extension.neon']; + } + + /** + * @dataProvider ruleProvider + * @param mixed[] $expectedErrors + */ + public function testRule(string $file, array $expectedErrors): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([$file], $expectedErrors); + } + + /** + * @return Iterator + */ + public function ruleProvider(): Iterator + { + yield 'entity with gedmo attributes' => [ + __DIR__ . '/data/gedmo-property-assign.php', + [ + // No errors expected + ], + ]; + + yield 'non-entity with gedmo attributes' => [ + __DIR__ . '/data/gedmo-property-assign-non-entity.php', + [ + [ + 'Property MissingGedmoWrittenPropertyAssign\NonEntityWithAGemdoLocaleField::$locale is unused.', + 10, + 'See: https://phpstan.org/developing-extensions/always-read-written-properties', + ], + ], + ]; + } + +} diff --git a/tests/Rules/Properties/data/gedmo-property-assign-non-entity.php b/tests/Rules/Properties/data/gedmo-property-assign-non-entity.php new file mode 100644 index 00000000..7805de3c --- /dev/null +++ b/tests/Rules/Properties/data/gedmo-property-assign-non-entity.php @@ -0,0 +1,11 @@ += 8.1 + +namespace MissingGedmoWrittenPropertyAssign; + +use Gedmo\Mapping\Annotation as Gedmo; + +class NonEntityWithAGemdoLocaleField +{ + #[Gedmo\Locale] + private string $locale; // ok, locale is written and read by gedmo listeners +} diff --git a/tests/Rules/Properties/data/gedmo-property-assign-phpdoc.php b/tests/Rules/Properties/data/gedmo-property-assign-phpdoc.php new file mode 100644 index 00000000..95303b17 --- /dev/null +++ b/tests/Rules/Properties/data/gedmo-property-assign-phpdoc.php @@ -0,0 +1,17 @@ += 7.4 + +namespace MissingGedmoWrittenPropertyAssignPhpDoc; + +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * @ORM\Entity + */ +class EntityWithAPhpDocGemdoLocaleField +{ + /** + * @Gedmo\Locale + */ + private string $locale; // ok, locale is written and read by gedmo listeners +} diff --git a/tests/Rules/Properties/data/gedmo-property-assign.php b/tests/Rules/Properties/data/gedmo-property-assign.php new file mode 100644 index 00000000..f4899bc0 --- /dev/null +++ b/tests/Rules/Properties/data/gedmo-property-assign.php @@ -0,0 +1,13 @@ += 8.1 + +namespace MissingGedmoWrittenPropertyAssign; + +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; + +#[ORM\Entity] +class EntityWithAGemdoLocaleField +{ + #[Gedmo\Locale] + private string $locale; // ok, locale is written and read by gedmo listeners +} diff --git a/tests/Rules/Properties/entity-manager.php b/tests/Rules/Properties/entity-manager.php new file mode 100644 index 00000000..e484217b --- /dev/null +++ b/tests/Rules/Properties/entity-manager.php @@ -0,0 +1,38 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('PHPstan\Doctrine\OrmProxies'); +$config->setMetadataCacheImpl(new DoctrineProvider(new ArrayAdapter())); + +$metadataDriver = new MappingDriverChain(); +$metadataDriver->addDriver(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'] +), 'PHPStan\\Rules\\Doctrine\\ORM\\'); + +if (PHP_VERSION_ID >= 80100) { + $metadataDriver->addDriver( + new AttributeDriver([__DIR__ . '/data']), + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + ); +} + +$config->setMetadataDriverImpl($metadataDriver); + +return EntityManager::create( + [ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ], + $config +);