mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Merge pull request #9444 from weirdan/readonly-classes
This commit is contained in:
commit
864b25150e
@ -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",
|
||||
|
@ -2360,6 +2360,18 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
|
@ -42,11 +42,13 @@ use Psalm\Issue\ConstantDeclarationInTrait;
|
||||
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;
|
||||
@ -256,6 +258,7 @@ class ClassLikeNodeScanner
|
||||
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);
|
||||
|
||||
@ -765,6 +768,14 @@ class ClassLikeNodeScanner
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@ -1586,10 +1597,22 @@ class ClassLikeNodeScanner
|
||||
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(
|
||||
|
@ -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;
|
||||
|
@ -1248,6 +1248,51 @@ class ClassTest extends TestCase
|
||||
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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user