1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Merge pull request #9681 from robchett/no-seal-methods_and_no-seal-propeties

Add support for @psalm-no-seal-properties and @psalm-no-seal-methods
This commit is contained in:
orklah 2023-05-02 19:20:34 +02:00 committed by GitHub
commit a5effd2d2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 117 additions and 22 deletions

View File

@ -202,9 +202,10 @@ takesFoo(getFoo());
This provides the same, but for `false`. Psalm uses this internally for functions like `preg_replace`, which can return false if the given input has encoding errors, but where 99.9% of the time the function operates as expected.
### `@psalm-seal-properties`
### `@psalm-seal-properties`, `@psalm-no-seal-properties`
If you have a magic property getter/setter, you can use `@psalm-seal-properties` to instruct Psalm to disallow getting and setting any properties not contained in a list of `@property` (or `@property-read`/`@property-write`) annotations.
This is automatically enabled with the configuration option `sealAllProperties` and can be disabled for a class with `@psalm-no-seal-properties`
```php
<?php
@ -226,6 +227,29 @@ $a = new A();
$a->bar = 5; // this call fails
```
### `@psalm-seal-methods`, `@psalm-no-seal-methods`
If you have a magic method caller, you can use `@psalm-seal-methods` to instruct Psalm to disallow calling any methods not contained in a list of `@method` annotations.
This is automatically enabled with the configuration option `sealAllMethods` and can be disabled for a class with `@psalm-no-seal-methods`
```php
<?php
/**
* @method foo(): string
* @psalm-seal-methods
*/
class A {
public function __call(string $name, array $args) {
if ($name === "foo") {
return "hello";
}
}
}
$a = new A();
$b = $a->bar(); // this call fails
```
### `@psalm-internal`
Used to mark a class, property or function as internal to a given namespace. Psalm treats this slightly differently to

View File

@ -24,6 +24,7 @@ final class DocComment
'assert', 'assert-if-true', 'assert-if-false', 'suppress',
'ignore-nullable-return', 'override-property-visibility',
'override-method-visibility', 'seal-properties', 'seal-methods',
'no-seal-properties', 'no-seal-methods',
'ignore-falsable-return', 'variadic', 'pure',
'ignore-variable-method', 'ignore-variable-property', 'internal',
'taint-sink', 'taint-source', 'assert-untainted', 'scope-this',

View File

@ -1087,7 +1087,7 @@ class InstancePropertyAssignmentAnalyzer
* If we have an explicit list of all allowed magic properties on the class, and we're
* not in that list, fall through
*/
if (!$var_id || !$class_storage->sealed_properties) {
if (!$var_id || !$class_storage->hasSealedProperties($codebase->config)) {
if (!$context->collect_initializations && !$context->collect_mutations) {
self::taintProperty(
$statements_analyzer,

View File

@ -570,7 +570,7 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
case '__set':
// If `@psalm-seal-properties` is set, the property must be defined with
// a `@property` annotation
if (($class_storage->sealed_properties || $codebase->config->seal_all_properties)
if (($class_storage->hasSealedProperties($codebase->config))
&& !isset($class_storage->pseudo_property_set_types['$' . $prop_name])
) {
IssueBuffer::maybeAdd(
@ -668,7 +668,7 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
case '__get':
// If `@psalm-seal-properties` is set, the property must be defined with
// a `@property` annotation
if (($class_storage->sealed_properties || $codebase->config->seal_all_properties)
if (($class_storage->hasSealedProperties($codebase->config))
&& !isset($class_storage->pseudo_property_get_types['$' . $prop_name])
) {
IssueBuffer::maybeAdd(

View File

@ -190,7 +190,7 @@ class MissingMethodCallHandler
$context,
);
if ($class_storage->sealed_methods || $config->seal_all_methods) {
if ($class_storage->hasSealedMethods($config)) {
$result->non_existent_magic_method_ids[] = $method_id->__toString();
return null;

View File

@ -713,7 +713,7 @@ class AtomicPropertyFetchAnalyzer
* If we have an explicit list of all allowed magic properties on the class, and we're
* not in that list, fall through
*/
if (!($class_storage->sealed_properties || $codebase->config->seal_all_properties)
if (!($class_storage->hasSealedProperties($codebase->config))
&& !$override_property_visibility
) {
return false;

View File

@ -920,8 +920,8 @@ class Populator
$fq_class_name = $storage->name;
$fq_class_name_lc = strtolower($fq_class_name);
if ($parent_storage->sealed_methods) {
$storage->sealed_methods = true;
if ($parent_storage->sealed_methods !== null) {
$storage->sealed_methods = $parent_storage->sealed_methods;
}
// register where they appear (can never be in a trait)
@ -1032,8 +1032,8 @@ class Populator
ClassLikeStorage $storage,
ClassLikeStorage $parent_storage
): void {
if ($parent_storage->sealed_properties) {
$storage->sealed_properties = true;
if ($parent_storage->sealed_properties !== null) {
$storage->sealed_properties = $parent_storage->sealed_properties;
}
// register where they appear (can never be in a trait)

View File

@ -241,10 +241,16 @@ class ClassLikeDocblockParser
if (isset($parsed_docblock->tags['psalm-seal-properties'])) {
$info->sealed_properties = true;
}
if (isset($parsed_docblock->tags['psalm-no-seal-properties'])) {
$info->sealed_properties = false;
}
if (isset($parsed_docblock->tags['psalm-seal-methods'])) {
$info->sealed_methods = true;
}
if (isset($parsed_docblock->tags['psalm-no-seal-methods'])) {
$info->sealed_methods = false;
}
if (isset($parsed_docblock->tags['psalm-immutable'])
|| isset($parsed_docblock->tags['psalm-mutation-free'])
@ -296,6 +302,9 @@ class ClassLikeDocblockParser
}
if (isset($parsed_docblock->combined_tags['method'])) {
if ($info->sealed_methods === null) {
$info->sealed_methods = true;
}
foreach ($parsed_docblock->combined_tags['method'] as $offset => $method_entry) {
$method_entry = preg_replace('/[ \t]+/', ' ', trim($method_entry));
@ -481,6 +490,13 @@ class ClassLikeDocblockParser
$info->public_api = isset($parsed_docblock->tags['psalm-api']) || isset($parsed_docblock->tags['api']);
if (isset($parsed_docblock->tags['property'])
&& $codebase->config->docblock_property_types_seal_properties
&& $info->sealed_properties === null
) {
$info->sealed_properties = true;
}
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property');
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'psalm-property');
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property-read');

View File

@ -577,10 +577,6 @@ class ClassLikeNodeScanner
);
}
}
if ($this->config->docblock_property_types_seal_properties) {
$storage->sealed_properties = true;
}
}
foreach ($docblock_info->methods as $method) {
@ -607,8 +603,6 @@ class ClassLikeNodeScanner
$lc_method_name,
);
}
$storage->sealed_methods = true;
}

View File

@ -63,9 +63,9 @@ class ClassLikeDocblockComment
*/
public array $methods = [];
public bool $sealed_properties = false;
public ?bool $sealed_properties = null;
public bool $sealed_methods = false;
public ?bool $sealed_methods = null;
public bool $override_property_visibility = false;

View File

@ -4,6 +4,7 @@ namespace Psalm\Storage;
use Psalm\Aliases;
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TypeAlias\ClassTypeAlias;
@ -66,14 +67,14 @@ final class ClassLikeStorage implements HasAttributesInterface
public $mixin_declaring_fqcln;
/**
* @var bool
* @var ?bool
*/
public $sealed_properties = false;
public $sealed_properties = null;
/**
* @var bool
* @var ?bool
*/
public $sealed_methods = false;
public $sealed_methods = null;
/**
* @var bool
@ -500,4 +501,14 @@ final class ClassLikeStorage implements HasAttributesInterface
return $type_params;
}
public function hasSealedProperties(Config $config): bool
{
return $this->sealed_properties ?? $config->seal_all_properties;
}
public function hasSealedMethods(Config $config): bool
{
return $this->sealed_methods ?? $config->seal_all_methods;
}
}

View File

@ -1174,6 +1174,31 @@ class MagicMethodAnnotationTest extends TestCase
$this->analyzeFile('somefile.php', new Context());
}
public function testNoSealAllMethods(): void
{
Config::getInstance()->seal_all_methods = true;
$this->addFile(
'somefile.php',
'<?php
/** @psalm-no-seal-properties */
class A {
public function __call(string $method, array $args) {}
}
class B extends A {}
$b = new B();
$b->foo();
',
);
$error_message = 'UndefinedMagicMethod';
$this->expectException(CodeException::class);
$this->expectExceptionMessage($error_message);
$this->analyzeFile('somefile.php', new Context());
}
public function testSealAllMethodsWithFoo(): void
{
Config::getInstance()->seal_all_methods = true;

View File

@ -1213,4 +1213,28 @@ class MagicPropertyTest extends TestCase
$this->expectExceptionMessage($error_message);
$this->analyzeFile('somefile.php', new Context());
}
public function testNoSealAllProperties(): void
{
Config::getInstance()->seal_all_properties = true;
Config::getInstance()->seal_all_methods = true;
$this->addFile(
'somefile.php',
'<?php
/** @psalm-no-seal-properties */
class A {
public function __get(string $name) {}
}
class B extends A {}
$b = new B();
/** @var string $result */
$result = $b->foo;
',
);
$this->analyzeFile('somefile.php', new Context());
}
}