mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Merge pull request #7259 from klimick/infer-this-context-for-psalm-if-this-is
This commit is contained in:
commit
376d2a389e
@ -24,6 +24,9 @@ use Psalm\Internal\PhpVisitor\NodeCounterVisitor;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||
use Psalm\Internal\Type\TypeExpander;
|
||||
use Psalm\Issue\InvalidDocblockParamName;
|
||||
use Psalm\Issue\InvalidParamDefault;
|
||||
@ -64,6 +67,7 @@ use function count;
|
||||
use function end;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function mb_strpos;
|
||||
use function md5;
|
||||
use function microtime;
|
||||
use function reset;
|
||||
@ -1808,10 +1812,30 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
} else {
|
||||
$this_object_type = new TNamedObject($context->self);
|
||||
}
|
||||
|
||||
|
||||
$this_object_type->was_static = !$storage->final;
|
||||
|
||||
$context->vars_in_scope['$this'] = new Union([$this_object_type]);
|
||||
if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) {
|
||||
$template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []);
|
||||
|
||||
TemplateStandinTypeReplacer::replace(
|
||||
new Union([$this_object_type]),
|
||||
$template_result,
|
||||
$codebase,
|
||||
null,
|
||||
$this->storage->if_this_is_type
|
||||
);
|
||||
|
||||
foreach ($context->vars_in_scope as $var_name => $var_type) {
|
||||
if (0 === mb_strpos($var_name, '$this->')) {
|
||||
TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase);
|
||||
}
|
||||
}
|
||||
|
||||
$context->vars_in_scope['$this'] = $this->storage->if_this_is_type;
|
||||
} else {
|
||||
$context->vars_in_scope['$this'] = new Union([$this_object_type]);
|
||||
}
|
||||
|
||||
if ($codebase->taint_flow_graph
|
||||
&& $storage->specialize_call
|
||||
|
@ -20,7 +20,9 @@ use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||
use Psalm\Internal\Type\TypeExpander;
|
||||
use Psalm\Issue\IfThisIsMismatch;
|
||||
use Psalm\Issue\InvalidPropertyAssignmentValue;
|
||||
@ -188,7 +190,32 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
||||
|
||||
try {
|
||||
$method_storage = $codebase->methods->getStorage($declaring_method_id ?? $method_id);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$method_storage = null;
|
||||
}
|
||||
|
||||
$method_template_params = [];
|
||||
|
||||
if ($method_storage && $method_storage->if_this_is_type) {
|
||||
$method_template_result = new TemplateResult($method_storage->template_types ?: [], []);
|
||||
|
||||
TemplateStandinTypeReplacer::replace(
|
||||
clone $method_storage->if_this_is_type,
|
||||
$method_template_result,
|
||||
$codebase,
|
||||
null,
|
||||
new Union([$lhs_type_part])
|
||||
);
|
||||
|
||||
$method_template_params = $method_template_result->lower_bounds;
|
||||
}
|
||||
|
||||
$template_result = new TemplateResult([], $class_template_params ?: []);
|
||||
$template_result->lower_bounds += $method_template_params;
|
||||
|
||||
if ($codebase->store_node_types
|
||||
&& !$context->collect_initializations
|
||||
@ -215,8 +242,6 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
||||
|
||||
$return_type_candidate = MethodCallReturnTypeFetcher::fetch(
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
@ -264,30 +289,23 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$method_storage = $codebase->methods->getStorage($declaring_method_id ?? $method_id);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$method_storage = null;
|
||||
}
|
||||
|
||||
if ($method_storage) {
|
||||
$class_type = new Union([$lhs_type_part]);
|
||||
if ($method_storage->if_this_is_type) {
|
||||
$class_type = new Union([$lhs_type_part]);
|
||||
$if_this_is_type = clone $method_storage->if_this_is_type;
|
||||
|
||||
if ($method_storage->if_this_is_type
|
||||
&& !UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$class_type,
|
||||
$method_storage->if_this_is_type
|
||||
)
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new IfThisIsMismatch(
|
||||
'Class type must be ' . $method_storage->if_this_is_type->getId()
|
||||
. ' current type ' . $class_type->getId(),
|
||||
new CodeLocation($source, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
);
|
||||
TemplateInferredTypeReplacer::replace($if_this_is_type, $template_result, $codebase);
|
||||
|
||||
if (!UnionTypeComparator::isContainedBy($codebase, $class_type, $if_this_is_type)) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new IfThisIsMismatch(
|
||||
'Class type must be ' . $method_storage->if_this_is_type->getId()
|
||||
. ' current type ' . $class_type->getId(),
|
||||
new CodeLocation($source, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($method_storage->self_out_type && $lhs_var_id) {
|
||||
|
@ -134,6 +134,136 @@ class IfThisIsTest extends TestCase
|
||||
$app->start();
|
||||
'
|
||||
],
|
||||
'ifThisIsChangeThisTypeInsideMethod' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
final class Option
|
||||
{
|
||||
/**
|
||||
* @return T|null
|
||||
*/
|
||||
public function unwrap()
|
||||
{
|
||||
throw new RuntimeException("???");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
final class ArrayList
|
||||
{
|
||||
/** @var list<T> */
|
||||
private $items;
|
||||
|
||||
/**
|
||||
* @param list<T> $items
|
||||
*/
|
||||
public function __construct(array $items)
|
||||
{
|
||||
$this->items = $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-if-this-is ArrayList<Option<int>>
|
||||
* @return ArrayList<int>
|
||||
*/
|
||||
public function compact(): ArrayList
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
$value = $item->unwrap();
|
||||
|
||||
if (null !== $value) {
|
||||
$values[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return new self($values);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ArrayList<Option<int>> $list */
|
||||
$list = new ArrayList([]);
|
||||
$numbers = $list->compact();
|
||||
',
|
||||
'assertions' => [
|
||||
'$numbers' => 'ArrayList<int>'
|
||||
],
|
||||
],
|
||||
'ifThisIsResolveTemplateParams' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
final class Option
|
||||
{
|
||||
/** @return T|null */
|
||||
public function unwrap() { throw new RuntimeException("???"); }
|
||||
}
|
||||
|
||||
/**
|
||||
* @template L
|
||||
* @template R
|
||||
*/
|
||||
final class Either
|
||||
{
|
||||
/** @return R|null */
|
||||
public function unwrap() { throw new RuntimeException("???"); }
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
final class ArrayList
|
||||
{
|
||||
/** @var list<T> */
|
||||
private $items;
|
||||
|
||||
/**
|
||||
* @param list<T> $items
|
||||
*/
|
||||
public function __construct(array $items)
|
||||
{
|
||||
$this->items = $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template A
|
||||
* @template B
|
||||
* @template TOption of Option<A>
|
||||
* @template TEither of Either<mixed, B>
|
||||
*
|
||||
* @psalm-if-this-is ArrayList<TOption|TEither>
|
||||
* @return ArrayList<A|B>
|
||||
*/
|
||||
public function compact(): ArrayList
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
$value = $item->unwrap();
|
||||
|
||||
if (null !== $value) {
|
||||
$values[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return new self($values);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ArrayList<Either<Exception, int>|Option<int>> $list */
|
||||
$list = new ArrayList([]);
|
||||
$numbers = $list->compact();
|
||||
',
|
||||
'assertions' => [
|
||||
'$numbers' => 'ArrayList<int>'
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -221,6 +351,32 @@ class IfThisIsTest extends TestCase
|
||||
',
|
||||
'error_message' => 'IfThisIsMismatch'
|
||||
],
|
||||
'failWithInvalidTemplateConstraint' => [
|
||||
'<?php
|
||||
/** @template T */
|
||||
final class Option { }
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
final class ArrayList
|
||||
{
|
||||
/**
|
||||
* @template A
|
||||
* @psalm-if-this-is ArrayList<Option<A>>
|
||||
* @return ArrayList<A>
|
||||
*/
|
||||
public function compact(): ArrayList
|
||||
{
|
||||
throw new RuntimeException("???");
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ArrayList<int> $list */
|
||||
$list = new ArrayList();
|
||||
$numbers = $list->compact();',
|
||||
'error_message' => 'IfThisIsMismatch'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user