Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve inferring the "final" static type when calling static methods inside a different class #8249

Merged
merged 7 commits into from Jul 14, 2022
Expand Up @@ -35,6 +35,7 @@
use Psalm\Type\Union;

use function array_map;
use function assert;
use function count;
use function explode;
use function in_array;
Expand Down Expand Up @@ -551,6 +552,14 @@ private static function getMethodReturnType(
) {
$static_type = $context->self;
$context_final = $codebase->classlike_storage_provider->get($context->self)->final;
} elseif ($context->calling_method_id !== null) {
// differentiate between these cases:
// 1. "static" comes from the CALLED static method - use $fq_class_name.
// 2. "static" in return type comes from return type of the
// method CALLING the currently analyzed static method - use $context->self.
$static_type = self::hasStaticInType($return_type_candidate)
? $fq_class_name
: $context->self;
} else {
$static_type = $fq_class_name;
}
Expand Down Expand Up @@ -613,4 +622,29 @@ private static function getMethodReturnType(

return $return_type_candidate;
}

/**
* Dumb way to determine whether a type contains "static" somewhere inside.
*/
private static function hasStaticInType(Type\TypeNode $type): bool
{
assert($type instanceof Atomic || $type instanceof Union);
$union_parts = $type instanceof Union
? $type->getAtomicTypes()
: [ $type ];

foreach ($union_parts as $atomic_type) {
if ($atomic_type instanceof TNamedObject && $atomic_type->value === 'static') {
return true;
}

foreach ($atomic_type->getChildNodes() as $child_type) {
if (self::hasStaticInType($child_type)) {
return true;
}
}
}

return false;
}
AndrolGenhald marked this conversation as resolved.
Show resolved Hide resolved
}
125 changes: 125 additions & 0 deletions tests/Template/Issue8200Test.php
@@ -0,0 +1,125 @@
<?php

namespace Psalm\Tests\Template;

use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;

class Issue8200Test extends TestCase
{
use ValidCodeAnalysisTestTrait;

/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/
public function providerValidCodeParse(): iterable
{
return [
'return TemplatedClass<static>' => [
'<?php

/**
* @template-covariant A
* @psalm-immutable
*/
final class Maybe
{
/**
* @param null|A $value
*/
public function __construct(private $value = null) {}

/**
* @template B
* @param B $value
* @return Maybe<B>
*
* @psalm-pure
*/
public static function just($value): self
{
return new self($value);
}
}

abstract class Test
{
final private function __construct() {}

/** @return Maybe<static> */
final public static function create(): Maybe
{
return Maybe::just(new static());
}
}',
],
'return list<static>' => [
'<?php

final class Lister
{
/**
* @template B
* @param B $value
* @return list<B>
*
* @psalm-pure
*/
public static function mklist($value): array
{
return [ $value ];
}
}

abstract class Test
{
final private function __construct() {}

/** @return list<static> */
final public static function create(): array
{
return Lister::mklist(new static());
}
}',
],
'use TemplatedClass<static> as an intermediate variable inside a method' => [
'<?php

/**
* @template-covariant A
* @psalm-immutable
*/
final class Maybe
{
/**
* @param A $value
*/
public function __construct(public $value) {}

/**
* @template B
* @param B $value
* @return Maybe<B>
*
* @psalm-pure
*/
public static function just($value): self
{
return new self($value);
}
}

abstract class Test
{
final private function __construct() {}

final public static function create(): static
{
$maybe = Maybe::just(new static());
return $maybe->value;
}
}',
]
];
}
}