1
0
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:
Bruce Weirdan 2022-01-02 05:31:24 +02:00 committed by GitHub
commit 376d2a389e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 224 additions and 26 deletions

View File

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

View File

@ -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) {

View File

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