diff --git a/README.md b/README.md index 0a035f4..b36b377 100644 --- a/README.md +++ b/README.md @@ -219,12 +219,19 @@ class NoNativeReturnTypehint { - Ensures immutability of all public properties by enforcing `readonly` modifier - No modifier needed for readonly classes in PHP 8.2 - Does nothing if PHP version does not support readonly properties (PHP 8.0 and below) +- Can be configured to exclude properties with a default value ```php class EnforceReadonlyPublicPropertyRule { public int $foo; // fails, no readonly modifier public readonly int $bar; } ``` +```neon +parameters: + shipmonkRules: + enforceReadonlyPublicProperty: + excludePropertyWithDefaultValue: true # defaults to false +``` ### forbidArithmeticOperationOnNonNumber - Disallows using [arithmetic operators](https://www.php.net/manual/en/language.operators.arithmetic.php) with non-numeric types (only `float`, `int` and `BcMath\Number` is allowed) diff --git a/rules.neon b/rules.neon index 687dbf3..c23929d 100644 --- a/rules.neon +++ b/rules.neon @@ -22,6 +22,7 @@ parameters: enabled: %shipmonkRules.enableAllRules% enforceReadonlyPublicProperty: enabled: %shipmonkRules.enableAllRules% + excludePropertyWithDefaultValue: false forbidArithmeticOperationOnNonNumber: enabled: %shipmonkRules.enableAllRules% allowNumericString: false @@ -121,6 +122,7 @@ parametersSchema: ]) enforceReadonlyPublicProperty: structure([ enabled: bool() + excludePropertyWithDefaultValue: bool() ]) forbidArithmeticOperationOnNonNumber: structure([ enabled: bool() @@ -325,6 +327,8 @@ services: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - class: ShipMonk\PHPStan\Rule\EnforceReadonlyPublicPropertyRule + arguments: + excludePropertyWithDefaultValue: %shipmonkRules.enforceReadonlyPublicProperty.excludePropertyWithDefaultValue% - class: ShipMonk\PHPStan\Rule\ForbidArithmeticOperationOnNonNumberRule arguments: diff --git a/src/Rule/EnforceReadonlyPublicPropertyRule.php b/src/Rule/EnforceReadonlyPublicPropertyRule.php index b6b77dc..daf0aa3 100644 --- a/src/Rule/EnforceReadonlyPublicPropertyRule.php +++ b/src/Rule/EnforceReadonlyPublicPropertyRule.php @@ -16,10 +16,16 @@ class EnforceReadonlyPublicPropertyRule implements Rule { + private bool $excludePropertyWithDefaultValue; + private PhpVersion $phpVersion; - public function __construct(PhpVersion $phpVersion) + public function __construct( + bool $excludePropertyWithDefaultValue, + PhpVersion $phpVersion + ) { + $this->excludePropertyWithDefaultValue = $excludePropertyWithDefaultValue; $this->phpVersion = $phpVersion; } @@ -41,7 +47,16 @@ public function processNode( return []; } - if (!$node->isPublic() || $node->isReadOnly() || $node->hasHooks() || $node->isPrivateSet() || $node->isProtectedSet() || $node->isStatic()) { + if ( + !$node->isPublic() + || $node->isReadOnly() + || $node->hasHooks() + || $node->isPrivateSet() + || $node->isProtectedSet() + || $node->isStatic() + || $node->getNativeType() === null + || ($this->excludePropertyWithDefaultValue && $node->getDefault() !== null) + ) { return []; } diff --git a/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php b/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php index 6b21839..c4f9b2b 100644 --- a/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php +++ b/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php @@ -2,6 +2,7 @@ namespace ShipMonk\PHPStan\Rule; +use LogicException; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use ShipMonk\PHPStan\RuleTestCase; @@ -13,12 +14,24 @@ class EnforceReadonlyPublicPropertyRuleTest extends RuleTestCase { + private ?bool $excludePropertyWithDefaultValue = null; + private ?PhpVersion $phpVersion = null; protected function getRule(): Rule { - self::assertNotNull($this->phpVersion); - return new EnforceReadonlyPublicPropertyRule($this->phpVersion); + if ($this->excludePropertyWithDefaultValue === null) { + throw new LogicException('excludePropertyWithDefaultValue must be set'); + } + + if ($this->phpVersion === null) { + throw new LogicException('phpVersion must be set'); + } + + return new EnforceReadonlyPublicPropertyRule( + $this->excludePropertyWithDefaultValue, + $this->phpVersion, + ); } public function testPhp84(): void @@ -27,22 +40,32 @@ public function testPhp84(): void self::markTestSkipped('PHP7 parser fails with property hooks'); } + $this->excludePropertyWithDefaultValue = false; $this->phpVersion = $this->createPhpVersion(80_400); $this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/code-84.php'); } public function testPhp81(): void { + $this->excludePropertyWithDefaultValue = false; $this->phpVersion = $this->createPhpVersion(80_100); $this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/code-81.php'); } public function testPhp80(): void { + $this->excludePropertyWithDefaultValue = false; $this->phpVersion = $this->createPhpVersion(80_000); $this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/code-80.php'); } + public function testExcludePropertyWithDefaultValue(): void + { + $this->excludePropertyWithDefaultValue = true; + $this->phpVersion = $this->createPhpVersion(80_100); + $this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/exclude-property-with-default-value.php'); + } + private function createPhpVersion(int $version): PhpVersion { return new PhpVersion($version); diff --git a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-81.php b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-81.php index 23cde24..a3f326c 100644 --- a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-81.php +++ b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-81.php @@ -14,6 +14,10 @@ trait MyTrait { public static string $static; + public int $default = 42; // error: Public property `default` not marked as readonly. + + public $untyped; + } class MyClass { @@ -30,6 +34,10 @@ class MyClass { public static string $static; + public int $quux = 7; // error: Public property `quux` not marked as readonly. + + public $quuz; + } readonly class MyReadonlyClass { diff --git a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php index 43d4e27..c6bf9c9 100644 --- a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php +++ b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php @@ -16,6 +16,10 @@ trait MyTrait { public static string $static; + public int $default = 42; // error: Public property `default` not marked as readonly. + + public $untyped; + } class MyClass { @@ -34,6 +38,10 @@ class MyClass { public static string $static; + public int $quux = 7; // error: Public property `quux` not marked as readonly. + + public $quuz; + } readonly class MyReadonlyClass { diff --git a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/exclude-property-with-default-value.php b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/exclude-property-with-default-value.php new file mode 100644 index 0000000..06c4749 --- /dev/null +++ b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/exclude-property-with-default-value.php @@ -0,0 +1,22 @@ +