Skip to content

Commit

Permalink
Readonly classes cannot be combined with #[AllowDynamicProperties]
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Dec 19, 2024
1 parent 5dfc583 commit 05dfb04
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ lint:
--exclude tests/PHPStan/Rules/Classes/data/extends-readonly-class.php \
--exclude tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php \
--exclude tests/PHPStan/Rules/Classes/data/bug-11592.php \
--exclude tests/PHPStan/Rules/Classes/data/bug-12281.php \
--exclude tests/PHPStan/Rules/Traits/data/bug-12281.php \
src tests

cs:
Expand Down
1 change: 1 addition & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ rules:
- PHPStan\Rules\Properties\ReadOnlyPropertyRule
- PHPStan\Rules\Traits\ConflictingTraitConstantsRule
- PHPStan\Rules\Traits\ConstantsInTraitsRule
- PHPStan\Rules\Traits\TraitAttributesRule
- PHPStan\Rules\Types\InvalidTypesInUnionRule
- PHPStan\Rules\Variables\UnsetRule
- PHPStan\Rules\Whitespace\FileWhitespaceRule
Expand Down
32 changes: 31 additions & 1 deletion src/Rules/Classes/ClassAttributesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use PHPStan\Node\InClassNode;
use PHPStan\Rules\AttributesCheck;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function count;
use function sprintf;

/**
* @implements Rule<InClassNode>
Expand All @@ -28,12 +31,39 @@ public function processNode(Node $node, Scope $scope): array
{
$classLikeNode = $node->getOriginalNode();

return $this->attributesCheck->check(
$errors = $this->attributesCheck->check(
$scope,
$classLikeNode->attrGroups,
Attribute::TARGET_CLASS,
'class',
);

$classReflection = $node->getClassReflection();
if (
$classReflection->isReadOnly()
|| $classReflection->isEnum()
|| $classReflection->isInterface()
) {
$typeName = 'readonly class';
$identifier = 'class.allowDynamicPropertiesReadonly';
if ($classReflection->isEnum()) {
$typeName = 'enum';
$identifier = 'enum.allowDynamicProperties';
}
if ($classReflection->isInterface()) {
$typeName = 'interface';
$identifier = 'interface.allowDynamicProperties';
}

if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) {
$errors[] = RuleErrorBuilder::message(sprintf('Attribute class AllowDynamicProperties cannot be used with %s.', $typeName))
->identifier($identifier)
->nonIgnorable()
->build();
}
}

return $errors;
}

}
63 changes: 63 additions & 0 deletions src/Rules/Traits/TraitAttributesRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Traits;

use Attribute;
use PhpParser\Node;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\AttributesCheck;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use function count;

/**
* @implements Rule<Node\Stmt\Trait_>
*/
final class TraitAttributesRule implements Rule
{

public function __construct(
private AttributesCheck $attributesCheck,
private ReflectionProvider $reflectionProvider,
)
{
}

public function getNodeType(): string
{
return Node\Stmt\Trait_::class;
}

public function processNode(Node $node, Scope $scope): array
{
$traitName = $node->namespacedName;
if ($traitName === null) {
return [];
}

if (!$this->reflectionProvider->hasClass($traitName->toString())) {
return [];
}

$errors = $this->attributesCheck->check(
$scope,
$node->attrGroups,
Attribute::TARGET_CLASS,
'class',
);

$classReflection = $this->reflectionProvider->getClass($traitName->toString());
if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) {
$errors[] = RuleErrorBuilder::message('Attribute class AllowDynamicProperties cannot be used with trait.')
->identifier('class.allowDynamicPropertiesTrait')
->nonIgnorable()
->build();
}

return $errors;
}

}
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,28 @@ public function testBug12011(): void
]);
}

public function testBug12281(): void
{
if (PHP_VERSION_ID < 80200) {
$this->markTestSkipped('Test requires PHP 8.2.');
}

$this->checkExplicitMixed = true;
$this->checkImplicitMixed = true;
$this->analyse([__DIR__ . '/data/bug-12281.php'], [
[
'Attribute class AllowDynamicProperties cannot be used with readonly class.',
05,
],
[
'Attribute class AllowDynamicProperties cannot be used with enum.',
12,
],
[
'Attribute class AllowDynamicProperties cannot be used with interface.',
15,
],
]);
}

}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Classes/data/bug-12281.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php // lint >= 8.2

namespace Bug12281;

#[\AllowDynamicProperties]
readonly class BlogData { /* … */ }

/** @readonly */
#[\AllowDynamicProperties]
class BlogDataPhpdoc { /* … */ }

#[\AllowDynamicProperties]
enum BlogDataEnum { /* … */ }

#[\AllowDynamicProperties]
interface BlogDataInterface { /* … */ }
100 changes: 100 additions & 0 deletions tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Traits;

use PHPStan\Php\PhpVersion;
use PHPStan\Rules\AttributesCheck;
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\Classes\ClassAttributesRule;
use PHPStan\Rules\ClassForbiddenNameCheck;
use PHPStan\Rules\ClassNameCheck;
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\NullsafeCheck;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Rules\Traits\TraitAttributesRule;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<TraitAttributesRule>
*/
class TraitAttributesRuleTest extends RuleTestCase
{

private bool $checkExplicitMixed = false;

private bool $checkImplicitMixed = false;

protected function getRule(): Rule
{
$reflectionProvider = $this->createReflectionProvider();
return new TraitAttributesRule(
new AttributesCheck(
$reflectionProvider,
new FunctionCallParametersCheck(
new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false),
new NullsafeCheck(),
new PhpVersion(80000),
new UnresolvableTypeHelper(),
new PropertyReflectionFinder(),
true,
true,
true,
true,
true,
),
new ClassNameCheck(
new ClassCaseSensitivityCheck($reflectionProvider, false),
new ClassForbiddenNameCheck(self::getContainer()),
),
true,
),
$reflectionProvider,
);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/trait-attributes.php'], [
[
'Attribute class TraitAttributes\AbstractAttribute is abstract.',
8,
],
[
'Attribute class TraitAttributes\MyTargettedAttribute does not have the class target.',
20,
],
]);
}

public function testBug12011(): void
{
if (PHP_VERSION_ID < 80300) {
$this->markTestSkipped('Test requires PHP 8.3.');
}

$this->checkExplicitMixed = true;
$this->checkImplicitMixed = true;

$this->analyse([__DIR__ . '/data/bug-12011.php'], [
[
'Parameter #1 $name of attribute class Bug12011Trait\Table constructor expects string|null, int given.',
8,
],
]);
}

public function testBug12281(): void
{
$this->analyse([__DIR__ . '/data/bug-12281.php'], [
[
'Attribute class AllowDynamicProperties cannot be used with trait.',
11,
],
]);
}

}
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Traits/data/bug-12011.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php // lint >= 8.3

namespace Bug12011Trait;

use Attribute;


#[Table(self::TABLE_NAME)]
trait MyTrait
{
private const int TABLE_NAME = 'table';
}

class X {
use MyTrait;
}

#[Attribute(Attribute::TARGET_CLASS)]
final class Table
{
public function __construct(
public readonly string|null $name = null,
public readonly string|null $schema = null,
) {
}
}
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Traits/data/bug-12281.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php // lint >= 8.2

namespace Bug12281Traits;

#[\AllowDynamicProperties]
enum BlogDataEnum { /* … */ } // reported by ClassAttributesRule

#[\AllowDynamicProperties]
interface BlogDataInterface { /* … */ } // reported by ClassAttributesRule

#[\AllowDynamicProperties]
trait BlogDataTrait { /* … */ }
21 changes: 21 additions & 0 deletions tests/PHPStan/Rules/Traits/data/trait-attributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace TraitAttributes;

#[\Attribute]
abstract class AbstractAttribute {}

#[AbstractAttribute]
trait MyTrait {}

#[\Attribute]
class MyAttribute {}

#[MyAttribute]
trait MyTrait2 {}

#[\Attribute(\Attribute::TARGET_PROPERTY)]
class MyTargettedAttribute {}

#[MyTargettedAttribute]
trait MyTrait3 {}

0 comments on commit 05dfb04

Please sign in to comment.