Skip to content

Commit

Permalink
Add new config: sealAllMethods (#3578)
Browse files Browse the repository at this point in the history
* Add new config: sealAllMethods

* Add some more tests

* Fix codesniffer issue with preg_quote

* Fix missing method in test

Co-authored-by: Olle <noemail>
  • Loading branch information
olleharstedt committed Jun 16, 2020
1 parent 03e9649 commit e1cc27f
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 2 deletions.
1 change: 1 addition & 0 deletions config.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
10 changes: 10 additions & 0 deletions docs/running_psalm/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/Psalm/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ class Config
*/
public $skip_checks_on_unresolvable_includes = true;

/**
* @var bool
*/
public $seal_all_methods = false;

/**
* @var bool
*/
Expand Down Expand Up @@ -785,7 +790,8 @@ private static function fromXmlAndPaths(string $base_dir, string $file_contents,
'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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ public static function analyze(
$method_id,
$class_storage,
$context,
$config,
$all_intersection_return_type,
$result
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static function handleMagicMethod(
MethodIdentifier $method_id,
\Psalm\Storage\ClassLikeStorage $class_storage,
Context $context,
\Psalm\Config $config,
?Type\Union $all_intersection_return_type,
AtomicMethodCallAnalysisResult $result
) : ?AtomicCallContext {
Expand Down Expand Up @@ -92,7 +93,7 @@ public static function handleMagicMethod(
$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;
Expand Down
127 changes: 127 additions & 0 deletions tests/MagicMethodAnnotationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -789,4 +789,131 @@ class B extends A {}
],
];
}

/**
* @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());
}
}

0 comments on commit e1cc27f

Please sign in to comment.