1
0
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:
Matthew Brown 2020-03-28 17:18:21 -04:00
parent 47c1470e3b
commit de6aee32d1
7 changed files with 148 additions and 33 deletions

View File

@ -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,

View File

@ -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 = []

View File

@ -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,

View File

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

View File

@ -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;

View File

@ -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;'
],
];
}

View File

@ -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;'
],
];
}