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