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:
commit
a5effd2d2d
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user