Skip to content

Commit

Permalink
Merge pull request #7259 from klimick/infer-this-context-for-psalm-if…
Browse files Browse the repository at this point in the history
…-this-is
  • Loading branch information
weirdan committed Jan 2, 2022
2 parents 6f4707a + 0f69483 commit 376d2a3
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 27 deletions.
28 changes: 26 additions & 2 deletions src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
Expand Up @@ -24,6 +24,9 @@
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;
Expand Down Expand Up @@ -64,6 +67,7 @@
use function end;
use function in_array;
use function is_string;
use function mb_strpos;
use function md5;
use function microtime;
use function reset;
Expand Down Expand Up @@ -1808,10 +1812,30 @@ private function getFunctionInformation(
} 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
Expand Down
Expand Up @@ -20,7 +20,9 @@
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;
Expand Down Expand Up @@ -188,7 +190,32 @@ public static function analyze(
}
}

$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
Expand All @@ -215,8 +242,6 @@ public static function analyze(
return Type::getMixed();
}

$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);

$return_type_candidate = MethodCallReturnTypeFetcher::fetch(
$statements_analyzer,
$codebase,
Expand Down Expand Up @@ -264,30 +289,23 @@ public static function analyze(
}
}

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
&& !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()
);
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;

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) {
Expand Down
156 changes: 156 additions & 0 deletions tests/IfThisIsTest.php
Expand Up @@ -134,6 +134,136 @@ public function start(): void
$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>'
],
],
];
}

Expand Down Expand Up @@ -221,6 +351,32 @@ public function freeze()
',
'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'
],
];
}
}

0 comments on commit 376d2a3

Please sign in to comment.