Skip to content

Commit

Permalink
Support for readonly classes
Browse files Browse the repository at this point in the history
  • Loading branch information
weirdan committed Mar 3, 2023
1 parent b1b2010 commit 36913b1
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 2 deletions.
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -33,7 +33,7 @@
"felixfbecker/language-server-protocol": "^1.5.2",
"fidry/cpu-core-counter": "^0.4.1 || ^0.5.1",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"nikic/php-parser": "^4.13",
"nikic/php-parser": "^4.14",
"sebastian/diff": "^4.0 || ^5.0",
"spatie/array-to-xml": "^2.17.0 || ^3.0",
"symfony/console": "^4.1.6 || ^5.0 || ^6.0",
Expand Down
12 changes: 12 additions & 0 deletions src/Psalm/Internal/Analyzer/ClassAnalyzer.php
Expand Up @@ -2360,6 +2360,18 @@ private function checkParentClass(
);
}

if ($parent_class_storage->readonly && !$storage->readonly) {
IssueBuffer::maybeAdd(
new InvalidExtendClass(
'Non-readonly class ' . $fq_class_name . ' may not inherit from '
. 'readonly class ' . $parent_fq_class_name,
$code_location,
$fq_class_name,
),
$storage->suppressed_issues + $this->getSuppressedIssues(),
);
}

if ($parent_class_storage->deprecated) {
IssueBuffer::maybeAdd(
new DeprecatedClass(
Expand Down
25 changes: 24 additions & 1 deletion src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
Expand Up @@ -42,11 +42,13 @@
use Psalm\Issue\DuplicateClass;
use Psalm\Issue\DuplicateConstant;
use Psalm\Issue\DuplicateEnumCase;
use Psalm\Issue\InvalidAttribute;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\InvalidEnumBackingType;
use Psalm\Issue\InvalidEnumCaseValue;
use Psalm\Issue\InvalidTypeImport;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\MissingPropertyType;
use Psalm\Issue\ParseError;
use Psalm\IssueBuffer;
use Psalm\Storage\AttributeStorage;
Expand Down Expand Up @@ -256,6 +258,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
if ($node instanceof PhpParser\Node\Stmt\Class_) {
$storage->abstract = $node->isAbstract();
$storage->final = $node->isFinal();
$storage->readonly = $node->isReadonly();

$this->codebase->classlikes->addFullyQualifiedClassName($fq_classlike_name, $this->file_path);

Expand Down Expand Up @@ -765,6 +768,14 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
$storage->external_mutation_free = true;
}

if ($attribute->fq_class_name === 'AllowDynamicProperties' && $storage->readonly) {
IssueBuffer::maybeAdd(new InvalidAttribute(
'Readonly classes cannot have dynamic properties',
new CodeLocation($this->file_scanner, $attr, null, true),
));
continue;
}

$storage->attributes[] = $attribute;
}
}
Expand Down Expand Up @@ -1586,10 +1597,22 @@ private function visitPropertyDeclaration(
if (count($property_storage->internal) === 0 && $var_comment && $var_comment->internal) {
$property_storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)];
}
$property_storage->readonly = $stmt->isReadonly() || ($var_comment && $var_comment->readonly);
$property_storage->readonly = $storage->readonly
|| $stmt->isReadonly()
|| ($var_comment && $var_comment->readonly);
$property_storage->allow_private_mutation = $var_comment ? $var_comment->allow_private_mutation : false;
$property_storage->description = $var_comment ? $var_comment->description : null;

if (!$signature_type && $storage->readonly) {
IssueBuffer::maybeAdd(
new MissingPropertyType(
'Properties of readonly classes must have a type',
new CodeLocation($this->file_scanner, $stmt, null, true),
$fq_classlike_name . '::$' . $property->name->name,
),
);
}

if (!$signature_type && !$doc_var_group_type) {
if ($property->default) {
$property_storage->suggested_type = SimpleTypeInferer::infer(
Expand Down
2 changes: 2 additions & 0 deletions src/Psalm/Storage/ClassLikeStorage.php
Expand Up @@ -470,6 +470,8 @@ final class ClassLikeStorage implements HasAttributesInterface

public bool $public_api = false;

public bool $readonly = false;

public function __construct(string $name)
{
$this->name = $name;
Expand Down
45 changes: 45 additions & 0 deletions tests/ClassTest.php
Expand Up @@ -1248,6 +1248,51 @@ final private function baz(): void {}
PHP,
'error_message' => 'PrivateFinalMethod',
],
'readonlyClass' => [
'code' => <<<'PHP'
<?php
readonly class Foo {
public int $a = 22;
}
$foo = new Foo;
$foo->a = 33;
PHP,
'error_message' => 'InaccessibleProperty',
'ignored_issues' => [],
'php_version' => '8.2',
],
'readonlyClassRequiresTypedProperties' => [
'code' => <<<'PHP'
<?php
readonly class Foo {
/** @var int */
public $a = 22;
}
PHP,
'error_message' => 'MissingPropertyType',
'ignored_issues' => [],
'php_version' => '8.2',
],
'readonlyClassCannotHaveDynamicProperties' => [
'code' => <<<'PHP'
<?php
#[AllowDynamicProperties]
readonly class Foo {}
PHP,
'error_message' => 'InvalidAttribute',
'ignored_issues' => [],
'php_version' => '8.2',
],
'readonlyClassesCannotBeExtendedByNonReadonlyOnes' => [
'code' => <<<'PHP'
<?php
readonly class Foo {}
class Bar extends Foo {}
PHP,
'error_message' => 'InvalidExtendClass',
'ignored_issues' => [],
'php_version' => '8.2',
],
];
}
}

0 comments on commit 36913b1

Please sign in to comment.