1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Add new config: sealAllMethods (#3578)

* Add new config: sealAllMethods

* Add some more tests

* Fix codesniffer issue with preg_quote

* Fix missing method in test

Co-authored-by: Olle <noemail>
This commit is contained in:
Olle Härstedt 2020-06-16 04:36:42 +02:00 committed by GitHub
parent 03e9649d49
commit e1cc27f7a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 2 deletions

View File

@ -70,6 +70,7 @@
<xs:attribute name="usePhpDocMethodsWithoutMagicCall" type="xs:boolean" default="false" />
<xs:attribute name="usePhpDocPropertiesWithoutMagicCall" type="xs:boolean" default="false" />
<xs:attribute name="skipChecksOnUnresolvableIncludes" type="xs:boolean" default="true" />
<xs:attribute name="sealAllMethods" type="xs:boolean" default="false" />
</xs:complexType>
<xs:complexType name="ProjectFilesType">

View File

@ -286,6 +286,16 @@ When `true`, Psalm will skip checking classes, variables and functions after it
For backwards compatibility, this defaults to `true`, but if you do not rely on dynamically generated includes to cause classes otherwise unknown to Psalm to come into existence, it's recommended you set this to `false` in order to reliably detect errors that would be fatal to PHP at runtime.
#### sealAllMethods
```xml
<psalm
sealAllMethods="[bool]"
>
```
When `true`, Psalm will treat all classes as if they had sealed methods, meaning that if you implement the magic method `__call`, you also have to add `@method` for each magic method. Defaults to false.
### Running Psalm
#### autoloader

View File

@ -299,6 +299,11 @@ class Config
*/
public $skip_checks_on_unresolvable_includes = true;
/**
* @var bool
*/
public $seal_all_methods = false;
/**
* @var bool
*/
@ -785,7 +790,8 @@ class Config
'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist',
'reportMixedIssues' => 'show_mixed_issues',
'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes'
'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes',
'sealAllMethods' => 'seal_all_methods'
];
foreach ($booleanAttributes as $xmlName => $internalName) {

View File

@ -422,6 +422,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
$method_id,
$class_storage,
$context,
$config,
$all_intersection_return_type,
$result
);

View File

@ -20,6 +20,7 @@ class MissingMethodCallHandler
MethodIdentifier $method_id,
\Psalm\Storage\ClassLikeStorage $class_storage,
Context $context,
\Psalm\Config $config,
?Type\Union $all_intersection_return_type,
AtomicMethodCallAnalysisResult $result
) : ?AtomicCallContext {
@ -92,7 +93,7 @@ class MissingMethodCallHandler
$context
);
if ($class_storage->sealed_methods) {
if ($class_storage->sealed_methods || $config->seal_all_methods) {
$result->non_existent_magic_method_ids[] = $method_id;
return null;

View File

@ -789,4 +789,131 @@ class MagicMethodAnnotationTest extends TestCase
],
];
}
/**
* @return void
*/
public function testSealAllMethodsWithoutFoo()
{
Config::getInstance()->seal_all_methods = true;
$this->addFile(
'somefile.php',
'<?php
class A {
public function __call(string $method, array $args) {}
}
class B extends A {}
$b = new B();
$b->foo();
'
);
$error_message = 'UndefinedMagicMethod';
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessage($error_message);
$this->analyzeFile('somefile.php', new Context());
}
/**
* @return void
*/
public function testSealAllMethodsWithFoo()
{
Config::getInstance()->seal_all_methods = true;
$this->addFile(
'somefile.php',
'<?php
class A {
public function __call(string $method, array $args) {}
public function foo(): void {}
}
class B extends A {}
$b = new B();
$b->foo();
'
);
$this->analyzeFile('somefile.php', new Context());
}
/**
* @return void
*/
public function testSealAllMethodsWithFooInSubclass()
{
Config::getInstance()->seal_all_methods = true;
$this->addFile(
'somefile.php',
'<?php
class A {
public function __call(string $method, array $args) {}
}
class B extends A {
public function foo(): void {}
}
$b = new B();
$b->foo();
'
);
$this->analyzeFile('somefile.php', new Context());
}
/**
* @return void
*/
public function testSealAllMethodsWithFooAnnotated()
{
Config::getInstance()->seal_all_methods = true;
$this->addFile(
'somefile.php',
'<?php
/** @method foo(): int */
class A {
public function __call(string $method, array $args) {}
}
class B extends A {}
$b = new B();
$b->foo();
'
);
$this->analyzeFile('somefile.php', new Context());
}
/**
* @return void
*/
public function testSealAllMethodsSetToFalse()
{
Config::getInstance()->seal_all_methods = false;
$this->addFile(
'somefile.php',
'<?php
class A {
public function __call(string $method, array $args) {}
}
class B extends A {}
$b = new B();
$b->foo();
'
);
$this->analyzeFile('somefile.php', new Context());
}
}