mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Fix #3035 - improve templating for property assignments
This commit is contained in:
parent
47c1470e3b
commit
de6aee32d1
@ -7,6 +7,7 @@ use PhpParser\Node\Stmt\PropertyProperty;
|
||||
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\PropertyFetchAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Analyzer\TypeAnalyzer;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
@ -600,12 +601,12 @@ class PropertyAssignmentAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
|
||||
$declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
|
||||
|
||||
$property_storage = null;
|
||||
|
||||
if (isset($class_storage->properties[$prop_name])) {
|
||||
$property_storage = $class_storage->properties[$prop_name];
|
||||
if (isset($declaring_class_storage->properties[$prop_name])) {
|
||||
$property_storage = $declaring_class_storage->properties[$prop_name];
|
||||
|
||||
if ($property_storage->deprecated) {
|
||||
if (IssueBuffer::accepts(
|
||||
@ -683,7 +684,7 @@ class PropertyAssignmentAnalyzer
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($class_storage->mutation_free) {
|
||||
} elseif ($declaring_class_storage->mutation_free) {
|
||||
$visitor = new \Psalm\Internal\TypeVisitor\ImmutablePropertyAssignmentVisitor(
|
||||
$statements_analyzer,
|
||||
$stmt
|
||||
@ -725,7 +726,7 @@ class PropertyAssignmentAnalyzer
|
||||
$class_property_type,
|
||||
$fq_class_name,
|
||||
$lhs_type_part,
|
||||
$class_storage->parent_class
|
||||
$declaring_class_storage->parent_class
|
||||
);
|
||||
|
||||
$class_property_type = \Psalm\Internal\Codebase\Methods::localizeType(
|
||||
@ -735,6 +736,18 @@ class PropertyAssignmentAnalyzer
|
||||
$declaring_property_class
|
||||
);
|
||||
|
||||
if ($lhs_type_part instanceof Type\Atomic\TGenericObject) {
|
||||
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
|
||||
|
||||
$class_property_type = PropertyFetchAnalyzer::localizePropertyType(
|
||||
$codebase,
|
||||
$class_property_type,
|
||||
$lhs_type_part,
|
||||
$class_storage,
|
||||
$declaring_class_storage
|
||||
);
|
||||
}
|
||||
|
||||
$assignment_value_type = \Psalm\Internal\Codebase\Methods::localizeType(
|
||||
$codebase,
|
||||
$assignment_value_type,
|
||||
|
@ -2079,7 +2079,7 @@ class CallAnalyzer
|
||||
* @return array<string, array<string, array{Type\Union}>>
|
||||
* @param array<string, non-empty-array<string, array{Type\Union}>> $existing_template_types
|
||||
*/
|
||||
private static function getTemplateTypesForCall(
|
||||
public static function getTemplateTypesForCall(
|
||||
ClassLikeStorage $declaring_class_storage = null,
|
||||
ClassLikeStorage $calling_class_storage = null,
|
||||
array $existing_template_types = []
|
||||
|
@ -6,6 +6,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\CodeLocation;
|
||||
@ -28,6 +29,7 @@ use Psalm\Issue\UndefinedThisPropertyFetch;
|
||||
use Psalm\Issue\UninitializedProperty;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNull;
|
||||
@ -825,34 +827,15 @@ class PropertyFetchAnalyzer
|
||||
);
|
||||
|
||||
if ($lhs_type_part instanceof TGenericObject) {
|
||||
if ($class_storage->template_types) {
|
||||
$class_template_params = [];
|
||||
|
||||
$reversed_class_template_types = array_reverse(array_keys($class_storage->template_types));
|
||||
|
||||
$provided_type_param_count = count($lhs_type_part->type_params);
|
||||
|
||||
foreach ($reversed_class_template_types as $i => $type_name) {
|
||||
if (isset($lhs_type_part->type_params[$provided_type_param_count - 1 - $i])) {
|
||||
$class_template_params[$type_name][$declaring_class_storage->name] = [
|
||||
$lhs_type_part->type_params[$provided_type_param_count - 1 - $i],
|
||||
0
|
||||
];
|
||||
} else {
|
||||
$class_template_params[$type_name][$declaring_class_storage->name] = [
|
||||
Type::getMixed(),
|
||||
0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$class_property_type->replaceTemplateTypesWithArgTypes(
|
||||
$class_template_params,
|
||||
$codebase
|
||||
$class_property_type = self::localizePropertyType(
|
||||
$codebase,
|
||||
$class_property_type,
|
||||
$lhs_type_part,
|
||||
$class_storage,
|
||||
$declaring_class_storage
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self::processTaints($statements_analyzer, $stmt, $class_property_type, $property_id);
|
||||
|
||||
@ -919,6 +902,47 @@ class PropertyFetchAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
public static function localizePropertyType(
|
||||
\Psalm\Codebase $codebase,
|
||||
Type\Union $class_property_type,
|
||||
TGenericObject $lhs_type_part,
|
||||
ClassLikeStorage $calling_class_storage,
|
||||
ClassLikeStorage $declaring_class_storage
|
||||
) : Type\Union {
|
||||
$template_types = CallAnalyzer::getTemplateTypesForCall(
|
||||
$declaring_class_storage,
|
||||
$calling_class_storage,
|
||||
$calling_class_storage->template_types ?: []
|
||||
);
|
||||
|
||||
if ($template_types) {
|
||||
$reversed_class_template_types = array_reverse(array_keys($template_types));
|
||||
|
||||
$provided_type_param_count = count($lhs_type_part->type_params);
|
||||
|
||||
foreach ($reversed_class_template_types as $i => $type_name) {
|
||||
if (isset($lhs_type_part->type_params[$provided_type_param_count - 1 - $i])) {
|
||||
$template_types[$type_name][$declaring_class_storage->name] = [
|
||||
$lhs_type_part->type_params[$provided_type_param_count - 1 - $i],
|
||||
0
|
||||
];
|
||||
} else {
|
||||
$template_types[$type_name][$declaring_class_storage->name] = [
|
||||
Type::getMixed(),
|
||||
0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$class_property_type->replaceTemplateTypesWithArgTypes(
|
||||
$template_types,
|
||||
$codebase
|
||||
);
|
||||
}
|
||||
|
||||
return $class_property_type;
|
||||
}
|
||||
|
||||
private static function processTaints(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\PropertyFetch $stmt,
|
||||
|
@ -1201,6 +1201,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
$storage->has_docblock_issues = true;
|
||||
}
|
||||
} else {
|
||||
/** @psalm-suppress PropertyTypeCoercion due to a Psalm bug */
|
||||
$storage->template_types[$template_name][$fq_classlike_name] = [Type::getMixed()];
|
||||
}
|
||||
|
||||
|
@ -327,7 +327,7 @@ class ClassLikeStorage
|
||||
public $overridden_property_ids = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, array{Type\Union}>>|null
|
||||
* @var array<string, non-empty-array<string, array{Type\Union}>>|null
|
||||
*/
|
||||
public $template_types;
|
||||
|
||||
|
@ -3507,6 +3507,60 @@ class ClassTemplateExtendsTest extends TestCase
|
||||
}
|
||||
}'
|
||||
],
|
||||
'setInheritedTemplatedPropertyOutsideClass' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template TValue as scalar
|
||||
*/
|
||||
class Watcher {
|
||||
/**
|
||||
* @psalm-var TValue
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @psalm-param TValue $value
|
||||
*/
|
||||
public function __construct($value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @extends Watcher<int> */
|
||||
class IntWatcher extends Watcher {}
|
||||
|
||||
$watcher = new IntWatcher(0);
|
||||
$watcher->value = 10;'
|
||||
],
|
||||
'setRetemplatedPropertyOutsideClass' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template TValue as scalar
|
||||
*/
|
||||
class Watcher {
|
||||
/**
|
||||
* @psalm-var TValue
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @psalm-param TValue $value
|
||||
*/
|
||||
public function __construct($value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T as scalar
|
||||
* @extends Watcher<T>
|
||||
*/
|
||||
class Watcher2 extends Watcher {}
|
||||
|
||||
/** @psalm-var Watcher2<int> $watcher */
|
||||
$watcher = new Watcher2(0);
|
||||
$watcher->value = 10;'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -2527,6 +2527,29 @@ class ClassTemplateTest extends TestCase
|
||||
|
||||
if (null !== $a->filter) {}'
|
||||
],
|
||||
'setTemplatedPropertyOutsideClass' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template TValue as scalar
|
||||
*/
|
||||
class Watcher {
|
||||
/**
|
||||
* @psalm-var TValue
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @psalm-param TValue $value
|
||||
*/
|
||||
public function __construct($value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @psalm-var Watcher<int> $watcher */
|
||||
$watcher = new Watcher(0);
|
||||
$watcher->value = 0;'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user