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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ parameters:
enabled: %shipmonkRules.enableAllRules%
enforceReadonlyPublicProperty:
enabled: %shipmonkRules.enableAllRules%
excludePropertyWithDefaultValue: false
forbidArithmeticOperationOnNonNumber:
enabled: %shipmonkRules.enableAllRules%
allowNumericString: false
Expand Down Expand Up @@ -121,6 +122,7 @@ parametersSchema:
])
enforceReadonlyPublicProperty: structure([
enabled: bool()
excludePropertyWithDefaultValue: bool()
])
forbidArithmeticOperationOnNonNumber: structure([
enabled: bool()
Expand Down Expand Up @@ -325,6 +327,8 @@ services:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
-
class: ShipMonk\PHPStan\Rule\EnforceReadonlyPublicPropertyRule
arguments:
excludePropertyWithDefaultValue: %shipmonkRules.enforceReadonlyPublicProperty.excludePropertyWithDefaultValue%
-
class: ShipMonk\PHPStan\Rule\ForbidArithmeticOperationOnNonNumberRule
arguments:
Expand Down
19 changes: 17 additions & 2 deletions src/Rule/EnforceReadonlyPublicPropertyRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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 [];
}

Expand Down
27 changes: 25 additions & 2 deletions tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ShipMonk\PHPStan\Rule;

use LogicException;
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use ShipMonk\PHPStan\RuleTestCase;
Expand All @@ -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
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-81.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace EnforceReadonlyPublicPropertyRuleExcludePropertyWithDefaultValue;

trait MyTrait {

public ?string $public; // error: Public property `public` not marked as readonly.

public int $default = 42;

}

class MyClass {

use MyTrait;

public ?int $foo; // error: Public property `foo` not marked as readonly.

public int $quux = 7;

}