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 composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,8 @@ services:
class: PHPStan\Type\Doctrine\Collection\IsEmptyTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.methodTypeSpecifyingExtension

-
class: PHPStan\Rules\Gedmo\PropertiesExtension
tags:
- phpstan.properties.readWriteExtension
15 changes: 15 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
103 changes: 103 additions & 0 deletions src/Rules/Gedmo/PropertiesExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Gedmo;

use Doctrine\Common\Annotations\AnnotationReader;
use Gedmo\Mapping\Annotation as Gedmo;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use function class_exists;
use function get_class;
use function in_array;

class PropertiesExtension implements ReadWritePropertiesExtension
{

private const GEDMO_WRITE_CLASSLIST = [
Gedmo\Blameable::class,
Gedmo\IpTraceable::class,
Gedmo\Locale::class,
Gedmo\Language::class,
Gedmo\Slug::class,
Gedmo\SortablePosition::class,
Gedmo\Timestampable::class,
Gedmo\TreeLeft::class,
Gedmo\TreeLevel::class,
Gedmo\TreeParent::class,
Gedmo\TreePath::class,
Gedmo\TreePathHash::class,
Gedmo\TreeRight::class,
Gedmo\TreeRoot::class,
Gedmo\UploadableFileMimeType::class,
Gedmo\UploadableFileName::class,
Gedmo\UploadableFilePath::class,
Gedmo\UploadableFileSize::class,
];

private const GEDMO_READ_CLASSLIST = [
Gedmo\Locale::class,
Gedmo\Language::class,
];

/** @var AnnotationReader|null */
private $annotationReader;

/** @var ObjectMetadataResolver */
private $objectMetadataResolver;

public function __construct(ObjectMetadataResolver $objectMetadataResolver)
{
$this->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<class-string> $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;
}

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

namespace PHPStan\Rules\Properties;

use PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule;
use PHPStan\Rules\Gedmo\PropertiesExtension;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<UnusedPrivatePropertyRule>
*/
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
]);
}

}
73 changes: 73 additions & 0 deletions tests/Rules/Properties/MissingGedmoPropertyAssignRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Properties;

use Iterator;
use PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule;
use PHPStan\Rules\Gedmo\PropertiesExtension;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<UnusedPrivatePropertyRule>
*/
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<mixed[]>
*/
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',
],
],
];
}

}
11 changes: 11 additions & 0 deletions tests/Rules/Properties/data/gedmo-property-assign-non-entity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php // lint >= 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
}
17 changes: 17 additions & 0 deletions tests/Rules/Properties/data/gedmo-property-assign-phpdoc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php // lint >= 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
}
13 changes: 13 additions & 0 deletions tests/Rules/Properties/data/gedmo-property-assign.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php // lint >= 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
}
38 changes: 38 additions & 0 deletions tests/Rules/Properties/entity-manager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\DoctrineProvider;

$config = new Configuration();
$config->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
);