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

Merge pull request #10157 from boesing/bugfix/assertions-from-inherited-docblock

This commit is contained in:
Bruce Weirdan 2023-08-28 12:04:47 +02:00 committed by GitHub
commit 3814fbb231
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 241 additions and 4 deletions

View File

@ -684,6 +684,8 @@ final class Codebase
/**
* Check whether a class/interface exists
*
* @psalm-assert-if-true class-string|interface-string|enum-string $fq_class_name
*/
public function classOrInterfaceOrEnumExists(
string $fq_class_name,

View File

@ -15,6 +15,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TraitAnalyzer;
use Psalm\Internal\Codebase\AssertionsFromInheritanceResolver;
use Psalm\Internal\Codebase\InternalCallMapHandler;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\MethodIdentifier;
@ -415,11 +416,14 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
}
}
if ($method_storage->assertions) {
$assertionsResolver = new AssertionsFromInheritanceResolver($codebase);
$assertions = $assertionsResolver->resolve($method_storage, $class_storage);
if ($assertions) {
self::applyAssertionsToContext(
$stmt_name,
ExpressionIdentifier::getExtendedVarId($stmt->var, null, $statements_analyzer),
$method_storage->assertions,
$assertions,
$args,
$template_result,
$context,

View File

@ -15,6 +15,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodCallProhibit
use Psalm\Internal\Analyzer\Statements\Expression\Call\StaticCallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Codebase\AssertionsFromInheritanceResolver;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
@ -317,11 +318,17 @@ class ExistingAtomicStaticCallAnalyzer
}
}
if ($method_storage->assertions) {
$assertionsResolver = new AssertionsFromInheritanceResolver($codebase);
$assertions = $assertionsResolver->resolve(
$method_storage,
$class_storage,
);
if ($assertions) {
CallAnalyzer::applyAssertionsToContext(
$stmt_name,
null,
$method_storage->assertions,
$assertions,
$stmt->getArgs(),
$template_result,
$context,

View File

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Psalm\Internal\Codebase;
use Psalm\Codebase;
use Psalm\Storage\Assertion\IsType;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\MethodStorage;
use Psalm\Storage\Possibilities;
use Psalm\Type\Atomic\TTemplateParam;
use function array_filter;
use function array_map;
use function array_merge;
use function array_values;
use function reset;
use function strtolower;
/**
* @internal
*/
final class AssertionsFromInheritanceResolver
{
private Codebase $codebase;
public function __construct(
Codebase $codebase
) {
$this->codebase = $codebase;
}
/**
* @return array<int,Possibilities>
*/
public function resolve(
MethodStorage $method_storage,
ClassLikeStorage $called_class
): array {
$method_name_lc = strtolower($method_storage->cased_name ?? '');
$assertions = $method_storage->assertions;
$inherited_classes_and_interfaces = array_values(array_filter(array_merge(
$called_class->parent_classes,
$called_class->class_implements,
), fn(string $classOrInterface) => $this->codebase->classOrInterfaceOrEnumExists($classOrInterface)));
foreach ($inherited_classes_and_interfaces as $potential_assertion_providing_class) {
$potential_assertion_providing_classlike_storage = $this->codebase->classlike_storage_provider->get(
$potential_assertion_providing_class,
);
if (!isset($potential_assertion_providing_classlike_storage->methods[$method_name_lc])) {
continue;
}
$potential_assertion_providing_method_storage = $potential_assertion_providing_classlike_storage
->methods[$method_name_lc];
/**
* Since the inheritance does not provide its own assertions, we have to detect those
* from inherited classes
*/
$assertions += array_map(
fn(Possibilities $possibilities) => $this->modifyAssertionsForInheritance(
$possibilities,
$this->codebase,
$called_class,
$inherited_classes_and_interfaces,
),
$potential_assertion_providing_method_storage->assertions,
);
}
return $assertions;
}
/**
* In case the called class is either implementing or extending a class/interface which does also has the
* template we are searching for, we assume that the called method has the same assertions.
*
* @param list<class-string> $potential_assertion_providing_classes
*/
private function modifyAssertionsForInheritance(
Possibilities $possibilities,
Codebase $codebase,
ClassLikeStorage $called_class,
array $potential_assertion_providing_classes
): Possibilities {
$replacement = new Possibilities($possibilities->var_id, []);
$extended_params = $called_class->template_extended_params;
foreach ($possibilities->rule as $assertion) {
if (!$assertion instanceof IsType
|| !$assertion->type instanceof TTemplateParam) {
$replacement->rule[] = $assertion;
continue;
}
/** Called class does not extend the template parameter */
$extended_templates = $called_class->template_extended_params;
if (!isset($extended_templates[$assertion->type->defining_class][$assertion->type->param_name])) {
$replacement->rule[] = $assertion;
continue;
}
foreach ($potential_assertion_providing_classes as $potential_assertion_providing_class) {
if (!isset($extended_params[$potential_assertion_providing_class][$assertion->type->param_name])) {
continue;
}
if (!$codebase->classlike_storage_provider->has($potential_assertion_providing_class)) {
continue;
}
$potential_assertion_providing_classlike_storage = $codebase->classlike_storage_provider->get(
$potential_assertion_providing_class,
);
if (!isset(
$potential_assertion_providing_classlike_storage->template_types[$assertion->type->param_name],
)) {
continue;
}
$replacement->rule[] = new IsType(new TTemplateParam(
$assertion->type->param_name,
reset(
$potential_assertion_providing_classlike_storage->template_types[$assertion->type->param_name],
),
$potential_assertion_providing_class,
));
continue 2;
}
$replacement->rule[] = $assertion;
}
return $replacement;
}
}

View File

@ -2883,6 +2883,90 @@ class AssertAnnotationTest extends TestCase
'$iterable===' => 'non-empty-list<string>',
],
],
'assertFromInheritedDocBlock' => [
'code' => '<?php
namespace Namespace1 {
/** @template InstanceType */
interface PluginManagerInterface
{
/** @return InstanceType */
public function get(): mixed;
/** @psalm-assert InstanceType $value */
public function validate(mixed $value): void;
}
/**
* @template InstanceType
* @template-implements PluginManagerInterface<InstanceType>
*/
abstract class AbstractPluginManager implements PluginManagerInterface
{
/** @param InstanceType $value */
public function __construct(private readonly mixed $value)
{}
/** {@inheritDoc} */
public function get(): mixed
{
return $this->value;
}
}
/**
* @template InstanceType of object
* @template-extends AbstractPluginManager<InstanceType>
*/
abstract class AbstractSingleInstancePluginManager extends AbstractPluginManager
{
/**
* An object type that the created instance must be instanced of
*
* @var class-string<InstanceType>
*/
protected string $instanceOf;
/** {@inheritDoc} */
public function get(): object
{
return parent::get();
}
/** {@inheritDoc} */
public function validate(mixed $value): void
{
}
}
}
namespace Namespace2 {
use Namespace1\AbstractSingleInstancePluginManager;
use stdClass;
/** @template-extends AbstractSingleInstancePluginManager<stdClass> */
final class Qoo extends AbstractSingleInstancePluginManager
{
/** @var class-string<stdClass> */
protected string $instanceOf = stdClass::class;
}
}
namespace {
$baz = new \Namespace2\Qoo(new stdClass);
/** @var mixed $object */
$object = null;
$baz->validate($object);
}
',
'assertions' => [
'$object===' => 'stdClass',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
];
}