Skip to content

Commit fa4054b

Browse files
authored
enforceReadonlyPublicProperty: skip untyped props + excludePropertyWithDefaultValue option (#334)
1 parent 10a5543 commit fa4054b

File tree

7 files changed

+91
-4
lines changed

7 files changed

+91
-4
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,19 @@ class NoNativeReturnTypehint {
219219
- Ensures immutability of all public properties by enforcing `readonly` modifier
220220
- No modifier needed for readonly classes in PHP 8.2
221221
- Does nothing if PHP version does not support readonly properties (PHP 8.0 and below)
222+
- Can be configured to exclude properties with a default value
222223
```php
223224
class EnforceReadonlyPublicPropertyRule {
224225
public int $foo; // fails, no readonly modifier
225226
public readonly int $bar;
226227
}
227228
```
229+
```neon
230+
parameters:
231+
shipmonkRules:
232+
enforceReadonlyPublicProperty:
233+
excludePropertyWithDefaultValue: true # defaults to false
234+
```
228235

229236
### forbidArithmeticOperationOnNonNumber
230237
- 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)

rules.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ parameters:
2222
enabled: %shipmonkRules.enableAllRules%
2323
enforceReadonlyPublicProperty:
2424
enabled: %shipmonkRules.enableAllRules%
25+
excludePropertyWithDefaultValue: false
2526
forbidArithmeticOperationOnNonNumber:
2627
enabled: %shipmonkRules.enableAllRules%
2728
allowNumericString: false
@@ -121,6 +122,7 @@ parametersSchema:
121122
])
122123
enforceReadonlyPublicProperty: structure([
123124
enabled: bool()
125+
excludePropertyWithDefaultValue: bool()
124126
])
125127
forbidArithmeticOperationOnNonNumber: structure([
126128
enabled: bool()
@@ -325,6 +327,8 @@ services:
325327
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
326328
-
327329
class: ShipMonk\PHPStan\Rule\EnforceReadonlyPublicPropertyRule
330+
arguments:
331+
excludePropertyWithDefaultValue: %shipmonkRules.enforceReadonlyPublicProperty.excludePropertyWithDefaultValue%
328332
-
329333
class: ShipMonk\PHPStan\Rule\ForbidArithmeticOperationOnNonNumberRule
330334
arguments:

src/Rule/EnforceReadonlyPublicPropertyRule.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616
class EnforceReadonlyPublicPropertyRule implements Rule
1717
{
1818

19+
private bool $excludePropertyWithDefaultValue;
20+
1921
private PhpVersion $phpVersion;
2022

21-
public function __construct(PhpVersion $phpVersion)
23+
public function __construct(
24+
bool $excludePropertyWithDefaultValue,
25+
PhpVersion $phpVersion
26+
)
2227
{
28+
$this->excludePropertyWithDefaultValue = $excludePropertyWithDefaultValue;
2329
$this->phpVersion = $phpVersion;
2430
}
2531

@@ -41,7 +47,16 @@ public function processNode(
4147
return [];
4248
}
4349

44-
if (!$node->isPublic() || $node->isReadOnly() || $node->hasHooks() || $node->isPrivateSet() || $node->isProtectedSet() || $node->isStatic()) {
50+
if (
51+
!$node->isPublic()
52+
|| $node->isReadOnly()
53+
|| $node->hasHooks()
54+
|| $node->isPrivateSet()
55+
|| $node->isProtectedSet()
56+
|| $node->isStatic()
57+
|| $node->getNativeType() === null
58+
|| ($this->excludePropertyWithDefaultValue && $node->getDefault() !== null)
59+
) {
4560
return [];
4661
}
4762

tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ShipMonk\PHPStan\Rule;
44

5+
use LogicException;
56
use PHPStan\Php\PhpVersion;
67
use PHPStan\Rules\Rule;
78
use ShipMonk\PHPStan\RuleTestCase;
@@ -13,12 +14,24 @@
1314
class EnforceReadonlyPublicPropertyRuleTest extends RuleTestCase
1415
{
1516

17+
private ?bool $excludePropertyWithDefaultValue = null;
18+
1619
private ?PhpVersion $phpVersion = null;
1720

1821
protected function getRule(): Rule
1922
{
20-
self::assertNotNull($this->phpVersion);
21-
return new EnforceReadonlyPublicPropertyRule($this->phpVersion);
23+
if ($this->excludePropertyWithDefaultValue === null) {
24+
throw new LogicException('excludePropertyWithDefaultValue must be set');
25+
}
26+
27+
if ($this->phpVersion === null) {
28+
throw new LogicException('phpVersion must be set');
29+
}
30+
31+
return new EnforceReadonlyPublicPropertyRule(
32+
$this->excludePropertyWithDefaultValue,
33+
$this->phpVersion,
34+
);
2235
}
2336

2437
public function testPhp84(): void
@@ -27,22 +40,32 @@ public function testPhp84(): void
2740
self::markTestSkipped('PHP7 parser fails with property hooks');
2841
}
2942

43+
$this->excludePropertyWithDefaultValue = false;
3044
$this->phpVersion = $this->createPhpVersion(80_400);
3145
$this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/code-84.php');
3246
}
3347

3448
public function testPhp81(): void
3549
{
50+
$this->excludePropertyWithDefaultValue = false;
3651
$this->phpVersion = $this->createPhpVersion(80_100);
3752
$this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/code-81.php');
3853
}
3954

4055
public function testPhp80(): void
4156
{
57+
$this->excludePropertyWithDefaultValue = false;
4258
$this->phpVersion = $this->createPhpVersion(80_000);
4359
$this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/code-80.php');
4460
}
4561

62+
public function testExcludePropertyWithDefaultValue(): void
63+
{
64+
$this->excludePropertyWithDefaultValue = true;
65+
$this->phpVersion = $this->createPhpVersion(80_100);
66+
$this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/exclude-property-with-default-value.php');
67+
}
68+
4669
private function createPhpVersion(int $version): PhpVersion
4770
{
4871
return new PhpVersion($version);

tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-81.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ trait MyTrait {
1414

1515
public static string $static;
1616

17+
public int $default = 42; // error: Public property `default` not marked as readonly.
18+
19+
public $untyped;
20+
1721
}
1822

1923
class MyClass {
@@ -30,6 +34,10 @@ class MyClass {
3034

3135
public static string $static;
3236

37+
public int $quux = 7; // error: Public property `quux` not marked as readonly.
38+
39+
public $quuz;
40+
3341
}
3442

3543
readonly class MyReadonlyClass {

tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ trait MyTrait {
1616

1717
public static string $static;
1818

19+
public int $default = 42; // error: Public property `default` not marked as readonly.
20+
21+
public $untyped;
22+
1923
}
2024

2125
class MyClass {
@@ -34,6 +38,10 @@ class MyClass {
3438

3539
public static string $static;
3640

41+
public int $quux = 7; // error: Public property `quux` not marked as readonly.
42+
43+
public $quuz;
44+
3745
}
3846

3947
readonly class MyReadonlyClass {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace EnforceReadonlyPublicPropertyRuleExcludePropertyWithDefaultValue;
4+
5+
trait MyTrait {
6+
7+
public ?string $public; // error: Public property `public` not marked as readonly.
8+
9+
public int $default = 42;
10+
11+
}
12+
13+
class MyClass {
14+
15+
use MyTrait;
16+
17+
public ?int $foo; // error: Public property `foo` not marked as readonly.
18+
19+
public int $quux = 7;
20+
21+
}
22+

0 commit comments

Comments
 (0)