Skip to content

Commit

Permalink
Fix #5290 - improve inference of nested class-string template types
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed Feb 27, 2021
1 parent 216e500 commit 44f8d71
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 10 deletions.
60 changes: 51 additions & 9 deletions src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,16 @@ private static function handleAtomicStandin(
$atomic_type,
$input_type,
$input_arg_offset,
$calling_class,
$calling_function,
$template_result,
$codebase,
$statements_analyzer,
$replace,
$add_upper_bound,
$depth,
$was_single
$was_single,
$had_template
);
}
}
Expand Down Expand Up @@ -806,12 +813,17 @@ public static function handleTemplateParamClassStandin(
Atomic\TTemplateParamClass $atomic_type,
?Union $input_type,
?int $input_arg_offset,
?string $calling_class,
?string $calling_function,
TemplateResult $template_result,
?Codebase $codebase,
?StatementsAnalyzer $statements_analyzer,
bool $replace,
bool $add_upper_bound,
int $depth,
bool $was_single
bool $was_single,
bool &$had_template
) : array {
$class_string = new Atomic\TClassString($atomic_type->as, $atomic_type->as_type);

$atomic_types = [];

if ($input_type && !$template_result->readonly) {
Expand Down Expand Up @@ -856,6 +868,34 @@ public static function handleTemplateParamClassStandin(
$generic_param = \Psalm\Type::getMixed();
}

if ($atomic_type->as_type) {
// sometimes templated class-strings can contain nested templates
// in the as type that need to be resolved as well.
$as_type_union = self::replace(
new Union([$atomic_type->as_type]),
$template_result,
$codebase,
$statements_analyzer,
$generic_param,
$input_arg_offset,
$calling_class,
$calling_function,
$replace,
$add_upper_bound,
$depth + 1
);

$as_type_union_types = $as_type_union->getAtomicTypes();

$first = \reset($as_type_union_types);

if (count($as_type_union_types) === 1 && $first instanceof Atomic\TNamedObject) {
$atomic_type->as_type = $first;
} else {
$atomic_type->as_type = null;
}
}

if ($generic_param) {
if (isset($template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class])) {
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class] = new TemplateBound(
Expand All @@ -878,18 +918,20 @@ public static function handleTemplateParamClassStandin(
[$atomic_type->param_name]
[$atomic_type->defining_class];

foreach ($template_type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof Atomic\TNamedObject) {
foreach ($template_type->getAtomicTypes() as $template_atomic_type) {
if ($template_atomic_type instanceof Atomic\TNamedObject) {
$atomic_types[] = new Atomic\TClassString(
$atomic_type->value,
$atomic_type
$template_atomic_type->value,
$template_atomic_type
);
} elseif ($atomic_type instanceof Atomic\TObject) {
} elseif ($template_atomic_type instanceof Atomic\TObject) {
$atomic_types[] = new Atomic\TClassString();
}
}
}

$class_string = new Atomic\TClassString($atomic_type->as, $atomic_type->as_type);

if (!$atomic_types) {
$atomic_types[] = $class_string;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Psalm/Type/Atomic/TTemplateParamClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public function __toString(): string

public function getId(bool $nested = false): string
{
return 'class-string<' . $this->param_name . ':' . $this->defining_class . ' as ' . $this->as . '>';
return 'class-string<' . $this->param_name . ':' . $this->defining_class
. ' as ' . ($this->as_type ? $this->as_type->getId() : $this->as) . '>';
}

public function getAssertionString(bool $exact = false): string
Expand Down
29 changes: 29 additions & 0 deletions tests/Template/NestedClassTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ function unwrapGeneric(Wrapper $wrapper) {
return $wrapper->unwrap();
}'
],
'unwrapFromTemplatedClassString' => [
'<?php
/**
* @template TInner
*/
interface Wrapper {
/** @return TInner */
public function unwrap();
}
/**
* @extends Wrapper<string>
*/
interface StringWrapper extends Wrapper {}
/**
* @template TInner
* @template TWrapper of Wrapper<TInner>
*
* @param class-string<TWrapper> $class
* @return TInner
*/
function load(string $class) {
$package = new $class();
return $package->unwrap();
}
$result = load(StringWrapper::class);'
],
];
}

Expand Down

0 comments on commit 44f8d71

Please sign in to comment.