1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Turn template bound tuples into object

Ref #4714
This commit is contained in:
Matt Brown 2020-11-27 11:43:23 -05:00 committed by Daniil Gentili
parent 45d058c2dd
commit 9089f77176
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
45 changed files with 392 additions and 334 deletions

View File

@ -2046,7 +2046,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$template_params[] = new Type\Union([
new Type\Atomic\TTemplateParam(
$param_name,
\reset($template_map)[0],
\reset($template_map),
$key
)
]);
@ -2212,8 +2212,8 @@ class ClassAnalyzer extends ClassLikeAnalyzer
}
}
if (!$template_type[0]->isMixed()) {
$template_type_copy = clone $template_type[0];
if (!$template_type->isMixed()) {
$template_type_copy = clone $template_type;
$template_result = new \Psalm\Internal\Type\TemplateResult(
$previous_extended ?: [],
@ -2244,12 +2244,12 @@ class ClassAnalyzer extends ClassLikeAnalyzer
}
} else {
$previous_extended[$template_name] = [
$declaring_class => [$extended_type]
$declaring_class => $extended_type
];
}
} else {
$previous_extended[$template_name] = [
$declaring_class => [$extended_type]
$declaring_class => $extended_type
];
}
}

View File

@ -455,7 +455,7 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer
}
/**
* @return array<string, array<string, array{Type\Union}>>|null
* @return array<string, array<string, Type\Union>>|null
*/
public function getTemplateTypeMap(): ?array
{

View File

@ -35,7 +35,7 @@ class CommentAnalyzer
public const TYPE_REGEX = '(\??\\\?[\(\)A-Za-z0-9_&\<\.=,\>\[\]\-\{\}:|?\\\\]*|\$[a-zA-Z_0-9_]+)';
/**
* @param array<string, array<string, array{Type\Union}>>|null $template_type_map
* @param array<string, array<string, Type\Union>>|null $template_type_map
* @param array<string, TypeAlias> $type_aliases
*
* @throws DocblockParseException if there was a problem parsing the docblock
@ -62,7 +62,7 @@ class CommentAnalyzer
}
/**
* @param array<string, array<string, array{Type\Union}>>|null $template_type_map
* @param array<string, array<string, Type\Union>>|null $template_type_map
* @param array<string, TypeAlias> $type_aliases
*
* @return list<VarDocblockComment>

View File

@ -603,7 +603,7 @@ class FileAnalyzer extends SourceAnalyzer
}
/**
* @return array<string, array<string, array{Type\Union}>>|null
* @return array<string, array<string, Type\Union>>|null
*/
public function getTemplateTypeMap(): ?array
{

View File

@ -224,7 +224,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
$template_params[] = new Type\Union([
new Type\Atomic\TTemplateParam(
$param_name,
\reset($template_map)[0],
\reset($template_map),
$key
)
]);
@ -1874,7 +1874,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
}
/**
* @return array<string, array<string, array{Type\Union}>>|null
* @return array<string, array<string, Type\Union>>|null
*/
public function getTemplateTypeMap(): ?array
{

View File

@ -1064,7 +1064,7 @@ class MethodComparator
}
}
$template_types[$key][$base_class_name] = [$mapped_type];
$template_types[$key][$base_class_name] = $mapped_type;
}
}

View File

@ -166,7 +166,7 @@ abstract class SourceAnalyzer implements StatementsSource
}
/**
* @return array<string, array<string, array{Type\Union}>>|null
* @return array<string, array<string, Type\Union>>|null
*/
public function getTemplateTypeMap(): ?array
{

View File

@ -1005,13 +1005,9 @@ class ForeachAnalyzer
? $iterator_atomic_type->type_params
: array_values(
array_map(
/** @param array<string, array{0:Type\Union}> $arr */
/** @param array<string, Type\Union> $arr */
function (array $arr) use ($iterator_atomic_type) : Type\Union {
if (isset($arr[$iterator_atomic_type->value])) {
return $arr[$iterator_atomic_type->value][0];
}
return Type::getMixed();
return $arr[$iterator_atomic_type->value] ?? Type::getMixed();
},
$generic_storage->template_types
)
@ -1096,7 +1092,7 @@ class ForeachAnalyzer
/**
* @param array<string, array<int|string, Type\Union>> $template_type_extends
* @param array<string, array<string, array{Type\Union}>> $class_template_types
* @param array<string, array<string, Type\Union>> $class_template_types
* @param array<int, Type\Union> $calling_type_params
*/
private static function getExtendedType(

View File

@ -508,17 +508,15 @@ class ArrayAssignmentAnalyzer
[],
[
$offset_type_part->param_name => [
$offset_type_part->defining_class => [
new Type\Union([
new Type\Atomic\TTemplateParam(
$class_string_map->param_name,
$offset_type_part->as_type
? new Type\Union([$offset_type_part->as_type])
: Type::getObject(),
'class-string-map'
)
])
]
$offset_type_part->defining_class => new Type\Union([
new Type\Atomic\TTemplateParam(
$class_string_map->param_name,
$offset_type_part->as_type
? new Type\Union([$offset_type_part->as_type])
: Type::getObject(),
'class-string-map'
)
])
]
]
);

View File

@ -16,6 +16,7 @@ use Psalm\Internal\DataFlow\TaintSink;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TemplateBound;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Internal\Type\UnionTemplateHandler;
use Psalm\CodeLocation;
@ -50,7 +51,7 @@ use function count;
class ArgumentAnalyzer
{
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $class_generic_params
* @param array<string, array<string, Type\Union>> $class_generic_params
* @return false|null
*/
public static function checkArgumentMatches(
@ -190,9 +191,7 @@ class ArgumentAnalyzer
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $class_generic_params
* @param array<string, array<string, array{Type\Union, 1?:int}>> $generic_params
* @param array<string, array<string, array{Type\Union}>> $template_types
* @param array<string, array<string, Type\Union>> $class_generic_params
* @return false|null
*/
private static function checkFunctionLikeTypeMatches(
@ -317,17 +316,17 @@ class ArgumentAnalyzer
[$template_type->param_name]
[$template_type->defining_class]
)) {
$template_result->upper_bounds[$template_type->param_name][$template_type->defining_class] = [
clone $template_result->lower_bounds
[$template_type->param_name]
[$template_type->defining_class][0],
0
];
$template_result->upper_bounds[$template_type->param_name][$template_type->defining_class]
= new TemplateBound(
clone $template_result->lower_bounds
[$template_type->param_name]
[$template_type->defining_class]->type
);
} else {
$template_result->upper_bounds[$template_type->param_name][$template_type->defining_class] = [
clone $template_type->as,
0
];
$template_result->upper_bounds[$template_type->param_name][$template_type->defining_class]
= new TemplateBound(
clone $template_type->as
);
}
}
}

View File

@ -39,6 +39,7 @@ use function count;
use function in_array;
use function array_reverse;
use function is_string;
use function array_map;
/**
* @internal
@ -48,7 +49,6 @@ class ArgumentsAnalyzer
/**
* @param list<PhpParser\Node\Arg> $args
* @param array<int, FunctionLikeParameter>|null $function_params
* @param array<string, array<string, array{Type\Union, 1?:int}>>|null $generic_params
*
* @return false|null
*/
@ -246,7 +246,7 @@ class ArgumentsAnalyzer
])
]);
$template_types = ['ArrayValue' . $argument_offset => [$method_id => [Type::getMixed()]]];
$template_types = ['ArrayValue' . $argument_offset => [$method_id => Type::getMixed()]];
$replace_template_result = new \Psalm\Internal\Type\TemplateResult(
$template_types,
@ -323,7 +323,17 @@ class ArgumentsAnalyzer
}
$replace_template_result = new \Psalm\Internal\Type\TemplateResult(
$template_result->upper_bounds,
array_map(
function ($template_map) {
return array_map(
function ($bound) {
return $bound->type;
},
$template_map
);
},
$template_result->upper_bounds
),
[]
);
@ -543,9 +553,15 @@ class ArgumentsAnalyzer
$template_result = null;
$class_generic_params = $class_template_result
? $class_template_result->upper_bounds
: [];
$class_generic_params = [];
if ($class_template_result) {
foreach ($class_template_result->upper_bounds as $template_name => $type_map) {
foreach ($type_map as $class => $bound) {
$class_generic_params[$template_name][$class] = clone $bound->type;
}
}
}
if ($function_storage) {
$template_types = CallAnalyzer::getTemplateTypesForCall(
@ -612,12 +628,6 @@ class ArgumentsAnalyzer
}
}
foreach ($class_generic_params as $template_name => $type_map) {
foreach ($type_map as $class => $type) {
$class_generic_params[$template_name][$class][0] = clone $type[0];
}
}
$function_param_count = count($function_params);
if (count($function_params) > count($args) && !$has_packed_var) {

View File

@ -826,7 +826,7 @@ class ArrayFunctionArgumentsAnalyzer
foreach ($closure_param_type->getTemplateTypes() as $template_type) {
$template_result->template_types[$template_type->param_name] = [
($template_type->defining_class) => [$template_type->as]
($template_type->defining_class) => $template_type->as
];
}

View File

@ -17,7 +17,7 @@ class ClassTemplateParamCollector
{
/**
* @param lowercase-string $method_name
* @return array<string, array<string, array{Type\Union, 1?:int}>>|null
* @return array<string, array<string, Type\Union>>|null
*/
public static function collect(
Codebase $codebase,
@ -94,9 +94,8 @@ class ClassTemplateParamCollector
foreach ($static_class_storage->template_types as $type_name => $_) {
if (isset($lhs_type_part->type_params[$i])) {
if ($lhs_var_id !== '$this' || $static_fq_class_name !== $static_class_storage->name) {
$class_template_params[$type_name][$static_class_storage->name] = [
$lhs_type_part->type_params[$i]
];
$class_template_params[$type_name][$static_class_storage->name]
= $lhs_type_part->type_params[$i];
}
}
@ -175,46 +174,41 @@ class ClassTemplateParamCollector
}
if ($lhs_var_id !== '$this' || $static_fq_class_name !== $class_storage->name) {
$class_template_params[$type_name][$class_storage->name] = [
$output_type_extends ?: Type::getMixed()
];
$class_template_params[$type_name][$class_storage->name]
= $output_type_extends ?: Type::getMixed();
}
}
if (($lhs_var_id !== '$this' || $static_fq_class_name !== $class_storage->name)
&& !isset($class_template_params[$type_name])
) {
$class_template_params[$type_name] = [
$class_storage->name => [Type::getMixed()]
];
$class_template_params[$type_name] = [$class_storage->name => Type::getMixed()];
}
}
}
foreach ($template_types as $type_name => $type_map) {
foreach ($type_map as [$type]) {
foreach ($type_map as $type) {
foreach ($candidate_class_storages as $candidate_class_storage) {
if ($candidate_class_storage !== $static_class_storage
&& isset($e[$candidate_class_storage->name][$type_name])
&& !isset($class_template_params[$type_name][$candidate_class_storage->name])
) {
$class_template_params[$type_name][$candidate_class_storage->name] = [
new Type\Union(
self::expandType(
$codebase,
$e[$candidate_class_storage->name][$type_name],
$e,
$static_class_storage->name,
$static_class_storage->template_types
)
$class_template_params[$type_name][$candidate_class_storage->name] = new Type\Union(
self::expandType(
$codebase,
$e[$candidate_class_storage->name][$type_name],
$e,
$static_class_storage->name,
$static_class_storage->template_types
)
];
);
}
}
if ($lhs_var_id !== '$this') {
if (!isset($class_template_params[$type_name])) {
$class_template_params[$type_name][$class_storage->name] = [$type];
$class_template_params[$type_name][$class_storage->name] = $type;
}
}
}

View File

@ -53,6 +53,7 @@ use function is_string;
use function array_map;
use function extension_loaded;
use function strpos;
use Psalm\Internal\Type\TemplateBound;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Storage\FunctionLikeParameter;
use function explode;
@ -439,7 +440,7 @@ class FunctionCallAnalyzer extends CallAnalyzer
);
if ($function_storage) {
$generic_params = $template_result ? $template_result->upper_bounds : [];
$inferred_upper_bouunds = $template_result ? $template_result->upper_bounds : [];
if ($function_storage->assertions && $function_name instanceof PhpParser\Node\Name) {
self::applyAssertionsToContext(
@ -447,7 +448,7 @@ class FunctionCallAnalyzer extends CallAnalyzer
null,
$function_storage->assertions,
$stmt->args,
$generic_params,
$inferred_upper_bouunds,
$context,
$statements_analyzer
);
@ -457,8 +458,8 @@ class FunctionCallAnalyzer extends CallAnalyzer
$statements_analyzer->node_data->setIfTrueAssertions(
$stmt,
array_map(
function (Assertion $assertion) use ($generic_params) : Assertion {
return $assertion->getUntemplatedCopy($generic_params ?: [], null);
function (Assertion $assertion) use ($inferred_upper_bouunds) : Assertion {
return $assertion->getUntemplatedCopy($inferred_upper_bouunds ?: [], null);
},
$function_storage->if_true_assertions
)
@ -469,8 +470,8 @@ class FunctionCallAnalyzer extends CallAnalyzer
$statements_analyzer->node_data->setIfFalseAssertions(
$stmt,
array_map(
function (Assertion $assertion) use ($generic_params) : Assertion {
return $assertion->getUntemplatedCopy($generic_params ?: [], null);
function (Assertion $assertion) use ($inferred_upper_bouunds) : Assertion {
return $assertion->getUntemplatedCopy($inferred_upper_bouunds ?: [], null);
},
$function_storage->if_false_assertions
)
@ -980,15 +981,21 @@ class FunctionCallAnalyzer extends CallAnalyzer
if (!isset($template_result->upper_bounds[$template_name])) {
if ($template_name === 'TFunctionArgCount') {
$template_result->upper_bounds[$template_name] = [
'fn-' . $function_id => [Type::getInt(false, count($stmt->args)), 0]
'fn-' . $function_id => new TemplateBound(
Type::getInt(false, count($stmt->args))
)
];
} elseif ($template_name === 'TPhpMajorVersion') {
$template_result->upper_bounds[$template_name] = [
'fn-' . $function_id => [Type::getInt(false, $codebase->php_major_version), 0]
'fn-' . $function_id => new TemplateBound(
Type::getInt(false, $codebase->php_major_version)
)
];
} else {
$template_result->upper_bounds[$template_name] = [
'fn-' . $function_id => [Type::getEmpty(), 0]
'fn-' . $function_id => new TemplateBound(
Type::getEmpty()
)
];
}
}

View File

@ -9,6 +9,7 @@ use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TemplateBound;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Type;
use Psalm\Type\Atomic\TGenericObject;
@ -432,21 +433,19 @@ class MethodCallReturnTypeFetcher
) {
if ($template_type->param_name === 'TFunctionArgCount') {
$template_result->upper_bounds[$template_type->param_name] = [
'fn-' . strtolower((string) $method_id) => [
Type::getInt(false, $arg_count),
0
]
'fn-' . strtolower((string) $method_id) => new TemplateBound(
Type::getInt(false, $arg_count)
)
];
} elseif ($template_type->param_name === 'TPhpMajorVersion') {
$template_result->upper_bounds[$template_type->param_name] = [
'fn-' . strtolower((string) $method_id) => [
Type::getInt(false, $codebase->php_major_version),
0
]
'fn-' . strtolower((string) $method_id) => new TemplateBound(
Type::getInt(false, $codebase->php_major_version)
)
];
} else {
$template_result->upper_bounds[$template_type->param_name] = [
($template_type->defining_class) => [Type::getEmpty(), 0]
($template_type->defining_class) => new TemplateBound(Type::getEmpty())
];
}
}

View File

@ -29,6 +29,7 @@ use function strtolower;
use function implode;
use function array_values;
use function is_string;
use function array_map;
/**
* @internal
@ -628,19 +629,29 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
foreach ($storage->template_types as $template_name => $base_type) {
if (isset($template_result->upper_bounds[$template_name][$fq_class_name])) {
$generic_param_type
= $template_result->upper_bounds[$template_name][$fq_class_name][0];
= $template_result->upper_bounds[$template_name][$fq_class_name]->type;
} elseif ($storage->template_type_extends && $template_result->upper_bounds) {
$generic_param_type = self::getGenericParamForOffset(
$declaring_fq_class_name,
$template_name,
$storage->template_type_extends,
$template_result->upper_bounds
array_map(
function ($type_map) {
return array_map(
function ($bound) {
return $bound->type;
},
$type_map
);
},
$template_result->upper_bounds
)
);
} else {
if ($fq_class_name === 'SplObjectStorage') {
$generic_param_type = Type::getEmpty();
} else {
$generic_param_type = array_values($base_type)[0][0];
$generic_param_type = array_values($base_type)[0];
}
}

View File

@ -10,6 +10,7 @@ use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TemplateBound;
use Psalm\Issue\AbstractMethodCall;
use Psalm\Issue\ImpureMethodCall;
use Psalm\IssueBuffer;
@ -145,7 +146,7 @@ class ExistingAtomicStaticCallAnalyzer
}
if (isset($found_generic_params[$type_key][$template_fq_class_name])) {
$found_generic_params[$type_key][$template_fq_class_name][0] = clone $extended_type;
$found_generic_params[$type_key][$template_fq_class_name] = clone $extended_type;
continue;
}
@ -153,13 +154,11 @@ class ExistingAtomicStaticCallAnalyzer
if ($t instanceof Type\Atomic\TTemplateParam
&& isset($found_generic_params[$t->param_name][$t->defining_class])
) {
$found_generic_params[$type_key][$template_fq_class_name] = [
$found_generic_params[$t->param_name][$t->defining_class][0]
];
$found_generic_params[$type_key][$template_fq_class_name]
= $found_generic_params[$t->param_name][$t->defining_class];
} else {
$found_generic_params[$type_key][$template_fq_class_name] = [
clone $extended_type
];
$found_generic_params[$type_key][$template_fq_class_name]
= clone $extended_type;
break;
}
}
@ -245,21 +244,19 @@ class ExistingAtomicStaticCallAnalyzer
)) {
if ($template_type->param_name === 'TFunctionArgCount') {
$template_result->upper_bounds[$template_type->param_name] = [
'fn-' . strtolower((string) $method_id) => [
Type::getInt(false, count($stmt->args)),
0
]
'fn-' . strtolower((string) $method_id) => new TemplateBound(
Type::getInt(false, count($stmt->args))
)
];
} elseif ($template_type->param_name === 'TPhpMajorVersion') {
$template_result->upper_bounds[$template_type->param_name] = [
'fn-' . strtolower((string) $method_id) => [
'fn-' . strtolower((string) $method_id) => new TemplateBound(
Type::getInt(false, $codebase->php_major_version),
0
]
)
];
} else {
$template_result->upper_bounds[$template_type->param_name] = [
($template_type->defining_class) => [Type::getEmpty(), 0]
($template_type->defining_class) => new TemplateBound(Type::getEmpty())
];
}
}

View File

@ -7,6 +7,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Type\TemplateBound;
use Psalm\Internal\Type\TemplateResult;
use Psalm\CodeLocation;
use Psalm\Context;
@ -30,6 +31,7 @@ use function str_replace;
use function is_int;
use function substr;
use function array_merge;
use function array_map;
/**
* @internal
@ -339,8 +341,8 @@ class CallAnalyzer
}
/**
* @return array<string, array<string, array{Type\Union}>>
* @param array<string, non-empty-array<string, array{Type\Union}>> $existing_template_types
* @return array<string, array<string, Type\Union>>
* @param array<string, non-empty-array<string, Type\Union>> $existing_template_types
*/
public static function getTemplateTypesForCall(
\Psalm\Codebase $codebase,
@ -383,14 +385,14 @@ class CallAnalyzer
}
}
$template_types[$template_name][$declaring_class_storage->name] = [$output_type];
$template_types[$template_name][$declaring_class_storage->name] = $output_type;
}
}
}
} elseif ($declaring_class_storage->template_types) {
foreach ($declaring_class_storage->template_types as $template_name => $type_map) {
foreach ($type_map as $key => [$type]) {
$template_types[$template_name][$key] = [$type];
foreach ($type_map as $key => $type) {
$template_types[$template_name][$key] = $type;
}
}
}
@ -398,9 +400,9 @@ class CallAnalyzer
foreach ($template_types as $key => $type_map) {
foreach ($type_map as $class => $type) {
$template_types[$key][$class][0] = \Psalm\Internal\Type\TypeExpander::expandUnion(
$template_types[$key][$class] = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,
$type[0],
$type,
$appearing_class_name,
$calling_class_storage ? $calling_class_storage->name : null,
null,
@ -416,7 +418,7 @@ class CallAnalyzer
/**
* @param array<string, array<int|string, Type\Union>> $template_type_extends
* @param array<string, array<string, array{Type\Union}>> $found_generic_params
* @param array<string, array<string, Type\Union>> $found_generic_params
*/
public static function getGenericParamForOffset(
string $fq_class_name,
@ -436,7 +438,7 @@ class CallAnalyzer
}
}
return $found_generic_params[$template_name][$fq_class_name][0];
return $found_generic_params[$template_name][$fq_class_name];
}
foreach ($template_type_extends as $type_map) {
@ -619,7 +621,7 @@ class CallAnalyzer
* @param \Psalm\Storage\Assertion[] $assertions
* @param string $thisName
* @param list<PhpParser\Node\Arg> $args
* @param array<string, array<string, array{Type\Union}>> $template_type_map,
* @param array<string, array<string, TemplateBound>> $inferred_upper_bouunds,
*
*/
public static function applyAssertionsToContext(
@ -627,7 +629,7 @@ class CallAnalyzer
?string $thisName,
array $assertions,
array $args,
array $template_type_map,
array $inferred_upper_bouunds,
Context $context,
StatementsAnalyzer $statements_analyzer
): void {
@ -677,13 +679,13 @@ class CallAnalyzer
$rule = substr($rule, 1);
}
if (isset($template_type_map[$rule])) {
foreach ($template_type_map[$rule] as $template_map) {
if ($template_map[0]->hasMixed()) {
if (isset($inferred_upper_bouunds[$rule])) {
foreach ($inferred_upper_bouunds[$rule] as $template_map) {
if ($template_map->type->hasMixed()) {
continue 2;
}
$replacement_atomic_types = $template_map[0]->getAtomicTypes();
$replacement_atomic_types = $template_map->type->getAtomicTypes();
if (count($replacement_atomic_types) > 1) {
continue 2;
@ -791,17 +793,27 @@ class CallAnalyzer
}
if ($type_assertions) {
$template_type_map = array_map(
function ($type_map) {
return array_map(
function ($bound) {
return $bound->type;
},
$type_map
);
},
$inferred_upper_bouunds
);
foreach (($statements_analyzer->getTemplateTypeMap() ?: []) as $template_name => $map) {
foreach ($map as $ref => [$type]) {
$template_type_map[$template_name][$ref] = [
new Type\Union([
new Type\Atomic\TTemplateParam(
$template_name,
$type,
$ref
)
])
];
foreach ($map as $ref => $type) {
$template_type_map[$template_name][$ref] = new Type\Union([
new Type\Atomic\TTemplateParam(
$template_name,
$type,
$ref
)
]);
}
}
@ -874,21 +886,21 @@ class CallAnalyzer
) : void {
if ($template_result->upper_bounds && $template_result->lower_bounds) {
foreach ($template_result->lower_bounds as $template_name => $defining_map) {
foreach ($defining_map as $defining_id => [$lower_bound_type]) {
foreach ($defining_map as $defining_id => $lower_bound) {
if (isset($template_result->upper_bounds[$template_name][$defining_id])) {
$upper_bound_type = $template_result->upper_bounds[$template_name][$defining_id][0];
$upper_bound = $template_result->upper_bounds[$template_name][$defining_id];
$union_comparison_result = new \Psalm\Internal\Type\Comparator\TypeComparisonResult();
if (count($template_result->lower_bounds_unintersectable_types) > 1) {
[$upper_bound_type, $lower_bound_type]
[$upper_bound->type, $lower_bound->type]
= $template_result->lower_bounds_unintersectable_types;
}
if (!UnionTypeComparator::isContainedBy(
$statements_analyzer->getCodebase(),
$upper_bound_type,
$lower_bound_type,
$upper_bound->type,
$lower_bound->type,
false,
false,
$union_comparison_result
@ -897,8 +909,8 @@ class CallAnalyzer
if ($union_comparison_result->type_coerced_from_mixed) {
if (IssueBuffer::accepts(
new MixedArgumentTypeCoercion(
'Type ' . $upper_bound_type->getId() . ' should be a subtype of '
. $lower_bound_type->getId(),
'Type ' . $upper_bound->type->getId() . ' should be a subtype of '
. $lower_bound->type->getId(),
$code_location,
$function_id
),
@ -909,8 +921,8 @@ class CallAnalyzer
} else {
if (IssueBuffer::accepts(
new ArgumentTypeCoercion(
'Type ' . $upper_bound_type->getId() . ' should be a subtype of '
. $lower_bound_type->getId(),
'Type ' . $upper_bound->type->getId() . ' should be a subtype of '
. $lower_bound->type->getId(),
$code_location,
$function_id
),
@ -922,8 +934,8 @@ class CallAnalyzer
} elseif ($union_comparison_result->scalar_type_match_found) {
if (IssueBuffer::accepts(
new InvalidScalarArgument(
'Type ' . $upper_bound_type->getId() . ' should be a subtype of '
. $lower_bound_type->getId(),
'Type ' . $upper_bound->type->getId() . ' should be a subtype of '
. $lower_bound->type->getId(),
$code_location,
$function_id
),
@ -934,8 +946,8 @@ class CallAnalyzer
} else {
if (IssueBuffer::accepts(
new InvalidArgument(
'Type ' . $upper_bound_type->getId() . ' should be a subtype of '
. $lower_bound_type->getId(),
'Type ' . $upper_bound->type->getId() . ' should be a subtype of '
. $lower_bound->type->getId(),
$code_location,
$function_id
),
@ -946,7 +958,9 @@ class CallAnalyzer
}
}
} else {
$template_result->upper_bounds[$template_name][$defining_id][0] = clone $lower_bound_type;
$template_result->upper_bounds[$template_name][$defining_id] = new TemplateBound(
clone $lower_bound->type
);
}
}
}

View File

@ -837,17 +837,15 @@ class ArrayFetchAnalyzer
[],
[
$type->param_name => [
'class-string-map' => [
new Type\Union([
new TTemplateParam(
$offset_type_part->param_name,
$offset_type_part->as_type
? new Type\Union([$offset_type_part->as_type])
: Type::getObject(),
$offset_type_part->defining_class
)
])
]
'class-string-map' => new Type\Union([
new TTemplateParam(
$offset_type_part->param_name,
$offset_type_part->as_type
? new Type\Union([$offset_type_part->as_type])
: Type::getObject(),
$offset_type_part->defining_class
)
])
]
]
);
@ -856,17 +854,15 @@ class ArrayFetchAnalyzer
[],
[
$offset_type_part->param_name => [
$offset_type_part->defining_class => [
new Type\Union([
new TTemplateParam(
$type->param_name,
$type->as_type
? new Type\Union([$type->as_type])
: Type::getObject(),
'class-string-map'
)
])
]
$offset_type_part->defining_class => new Type\Union([
new TTemplateParam(
$type->param_name,
$type->as_type
? new Type\Union([$type->as_type])
: Type::getObject(),
'class-string-map'
)
])
]
]
);
@ -875,12 +871,10 @@ class ArrayFetchAnalyzer
[],
[
$type->param_name => [
'class-string-map' => [
new Type\Union([
$offset_type_part->as_type
?: new Type\Atomic\TObject()
])
]
'class-string-map' => new Type\Union([
$offset_type_part->as_type
?: new Type\Atomic\TObject()
])
]
]
);

View File

@ -299,7 +299,7 @@ class AtomicPropertyFetchAnalyzer
$type_params = [];
foreach ($class_storage->template_types as $type_map) {
$type_params[] = clone array_values($type_map)[0][0];
$type_params[] = clone array_values($type_map)[0];
}
$lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params);
@ -434,7 +434,7 @@ class AtomicPropertyFetchAnalyzer
$type_params = [];
foreach ($class_storage->template_types as $type_map) {
$type_params[] = clone array_values($type_map)[0][0];
$type_params[] = clone array_values($type_map)[0];
}
$lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params);
@ -625,7 +625,7 @@ class AtomicPropertyFetchAnalyzer
$type_params = [];
foreach ($declaring_class_storage->template_types as $type_map) {
$type_params[] = clone array_values($type_map)[0][0];
$type_params[] = clone array_values($type_map)[0];
}
$lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params);
@ -716,10 +716,7 @@ class AtomicPropertyFetchAnalyzer
$i++;
if ($i === $param_offset) {
$template_types[$calling_param_name][$calling_class_storage->name] = [
$lhs_param_type,
0
];
$template_types[$calling_param_name][$calling_class_storage->name] = $lhs_param_type;
break;
}
}
@ -747,10 +744,8 @@ class AtomicPropertyFetchAnalyzer
}
if ($position !== false && isset($lhs_type_part->type_params[$position])) {
$template_types[$type_name][$declaring_class_storage->name] = [
$lhs_type_part->type_params[$position],
0
];
$template_types[$type_name][$declaring_class_storage->name]
= $lhs_type_part->type_params[$position];
}
}
}

View File

@ -421,7 +421,7 @@ class Populator
foreach ($trait_storage->template_types as $template_name => $template_type_map) {
foreach ($template_type_map as $template_type) {
$default_param = clone $template_type[0];
$default_param = clone $template_type;
$default_param->from_docblock = false;
$storage->template_type_extends[$trait_storage->name][$template_name]
= $default_param;
@ -533,7 +533,7 @@ class Populator
foreach ($parent_storage->template_types as $template_name => $template_type_map) {
foreach ($template_type_map as $template_type) {
$default_param = clone $template_type[0];
$default_param = clone $template_type;
$default_param->from_docblock = false;
$storage->template_type_extends[$parent_storage->name][$template_name]
= $default_param;
@ -674,7 +674,7 @@ class Populator
foreach ($parent_interface_storage->template_types as $template_name => $template_type_map) {
foreach ($template_type_map as $template_type) {
$default_param = clone $template_type[0];
$default_param = clone $template_type;
$default_param->from_docblock = false;
$storage->template_type_extends[$parent_interface_storage->name][$template_name]
= $default_param;
@ -771,7 +771,7 @@ class Populator
foreach ($implemented_interface_storage->template_types as $template_name => $template_type_map) {
foreach ($template_type_map as $template_type) {
$default_param = clone $template_type[0];
$default_param = clone $template_type;
$default_param->from_docblock = false;
$storage->template_type_extends[$implemented_interface_storage->name][$template_name]
= $default_param;

View File

@ -170,8 +170,8 @@ class Reflection
if ($class_name_lower === 'generator') {
$storage->template_types = [
'TKey' => ['Generator' => [Type::getMixed()]],
'TValue' => ['Generator' => [Type::getMixed()]],
'TKey' => ['Generator' => Type::getMixed()],
'TValue' => ['Generator' => Type::getMixed()],
];
}

View File

@ -79,7 +79,7 @@ class ClassLikeNodeScanner
private $classlike_type_aliases = [];
/**
* @var array<string, array<string, array{Type\Union}>>
* @var array<string, array<string, Type\Union>>
*/
public $class_template_types = [];
@ -334,7 +334,7 @@ class ClassLikeNodeScanner
}
$storage->template_types[$template_name] = [
$fq_classlike_name => [$template_type],
$fq_classlike_name => $template_type,
];
} else {
$storage->docblock_issues[] = new InvalidDocblock(
@ -344,7 +344,7 @@ class ClassLikeNodeScanner
}
} else {
/** @psalm-suppress PropertyTypeCoercion due to a Psalm bug */
$storage->template_types[$template_name][$fq_classlike_name] = [Type::getMixed()];
$storage->template_types[$template_name][$fq_classlike_name] = Type::getMixed();
}
$storage->template_covariants[$i] = $template_map[3];

View File

@ -35,7 +35,7 @@ use function strlen;
class FunctionLikeDocblockScanner
{
/**
* @param array<string, non-empty-array<string, array{Type\Union}>> $existing_function_template_types
* @param array<string, non-empty-array<string, Type\Union>> $existing_function_template_types
* @param array<string, TypeAlias> $type_aliases
*/
public static function addDocblockInfo(
@ -217,7 +217,7 @@ class FunctionLikeDocblockScanner
);
} else {
$storage->template_types[$template_name] = [
'fn-' . strtolower($cased_function_id) => [$template_type],
'fn-' . strtolower($cased_function_id) => $template_type,
];
}
@ -615,7 +615,7 @@ class FunctionLikeDocblockScanner
if (isset($template_types[$template_typeof['template_type']])) {
foreach ($template_types[$template_typeof['template_type']] as $class => $map) {
$template_type = $map[0];
$template_type = $map;
$template_class = $class;
}
}
@ -753,11 +753,11 @@ class FunctionLikeDocblockScanner
}
/**
* @param array<string, array<string, array{Type\Union}>> $template_types
* @param array<string, array<string, Type\Union>> $template_types
* @param array<string, TypeAlias>|null $type_aliases
* @param array<string, array<string, array{Type\Union}>> $function_template_types
* @param array<string, array<string, Type\Union>> $function_template_types
*
* @return array{array<int, array{0: string, 1: int, 2?: string}>, array<string, array<string, array{Type\Union}>>}
* @return array{array<int, array{0: string, 1: int, 2?: string}>, array<string, array<string, Type\Union>>}
*/
private static function getConditionalSanitizedTypeTokens(
string $docblock_return_type,
@ -796,9 +796,7 @@ class FunctionLikeDocblockScanner
: Type::getMixed();
$storage->template_types[$template_name] = [
$template_function_id => [
$template_as_type
],
$template_function_id => $template_as_type,
];
$function_template_types[$template_name]
@ -834,9 +832,7 @@ class FunctionLikeDocblockScanner
$template_name = 'TFunctionArgCount';
$storage->template_types[$template_name] = [
$template_function_id => [
Type::getInt()
],
$template_function_id => Type::getInt(),
];
$function_template_types[$template_name]
@ -849,9 +845,7 @@ class FunctionLikeDocblockScanner
$template_name = 'TPhpMajorVersion';
$storage->template_types[$template_name] = [
$template_function_id => [
Type::getInt()
],
$template_function_id => Type::getInt(),
];
$function_template_types[$template_name]
@ -865,8 +859,8 @@ class FunctionLikeDocblockScanner
}
/**
* @param array<string, array<string, array{Type\Union}>> $class_template_types
* @param array<string, array<string, array{Type\Union}>> $function_template_types
* @param array<string, array<string, Type\Union>> $class_template_types
* @param array<string, array<string, Type\Union>> $function_template_types
* @param array<string, TypeAlias> $type_aliases
* @return non-empty-list<string>|null
*/
@ -964,8 +958,8 @@ class FunctionLikeDocblockScanner
}
/**
* @param array<string, array<string, array{Type\Union}>> $class_template_types
* @param array<string, array<string, array{Type\Union}>> $function_template_types
* @param array<string, array<string, Type\Union>> $class_template_types
* @param array<string, array<string, Type\Union>> $function_template_types
* @param array<string, TypeAlias> $type_aliases
* @param array<int, array{type:string,name:string,line_number:int,start:int,end:int}> $docblock_params
*/
@ -1096,9 +1090,9 @@ class FunctionLikeDocblockScanner
if ($storage->template_types) {
foreach ($storage->template_types as $t => $type_map) {
foreach ($type_map as $obj => [$type]) {
foreach ($type_map as $obj => $type) {
if ($type->isMixed() && $docblock_param['type'] === 'class-string<' . $t . '>') {
$storage->template_types[$t][$obj] = [Type::getObject()];
$storage->template_types[$t][$obj] = Type::getObject();
if (isset($function_template_types[$t])) {
$function_template_types[$t][$obj] = $storage->template_types[$t][$obj];

View File

@ -70,7 +70,7 @@ class FunctionLikeNodeScanner
private $classlike_storage;
/**
* @var array<string, non-empty-array<string, array{Type\Union}>>
* @var array<string, non-empty-array<string, Type\Union>>
*/
private $existing_function_template_types;
@ -90,7 +90,7 @@ class FunctionLikeNodeScanner
public $storage;
/**
* @param array<string, non-empty-array<string, array{Type\Union}>> $existing_function_template_types
* @param array<string, non-empty-array<string, Type\Union>> $existing_function_template_types
* @param array<string, TypeAlias> $type_aliases
*/
public function __construct(

View File

@ -31,7 +31,7 @@ class ClassLikeStubGenerator
$template_offset = 0;
foreach ($storage->template_types ?: [] as $template_name => $map) {
$type = array_values($map)[0][0];
$type = array_values($map)[0];
$key = isset($storage->template_covariants[$template_offset]) ? 'template-covariant' : 'template';
@ -226,7 +226,7 @@ class ClassLikeStubGenerator
$docblock = new ParsedDocblock('', []);
foreach ($method_storage->template_types ?: [] as $template_name => $map) {
$type = array_values($map)[0][0];
$type = array_values($map)[0];
$docblock->tags['template'][] = $template_name . ' as ' . $type->toNamespacedString(
$namespace_name,

View File

@ -178,7 +178,7 @@ class StubsGenerator
$docblock = new ParsedDocblock('', []);
foreach ($function_storage->template_types ?: [] as $template_name => $map) {
$type = array_values($map)[0][0];
$type = array_values($map)[0];
$docblock->tags['template'][] = $template_name . ' as ' . $type->toNamespacedString(
$namespace_name,

View File

@ -43,7 +43,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
* - notEmpty(Object|false) => Object
*
* @param string[] $suppressed_issues
* @param array<string, array<string, array{Type\Union}>> $template_type_map
* @param array<string, array<string, Type\Union>> $template_type_map
* @param-out 0|1|2 $failed_reconciliation
*/
public static function reconcile(
@ -362,7 +362,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
/**
* @param 0|1|2 $failed_reconciliation
* @param string[] $suppressed_issues
* @param array<string, array<string, array{Type\Union}>> $template_type_map
* @param array<string, array<string, Type\Union>> $template_type_map
* @param-out 0|1|2 $failed_reconciliation
*/
private static function refine(
@ -671,7 +671,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
/**
* @param array<string, array<string, array{0:Type\Union, 1?: int}>> $template_type_map
* @param array<string, array<string, Type\Union>> $template_type_map
*/
private static function filterTypeWithAnother(
Codebase $codebase,

View File

@ -26,7 +26,7 @@ use function substr;
class NegatedAssertionReconciler extends Reconciler
{
/**
* @param array<string, array<string, array{Type\Union}>> $template_type_map
* @param array<string, array<string, Type\Union>> $template_type_map
* @param string[] $suppressed_issues
* @param 0|1|2 $failed_reconciliation
*

View File

@ -30,7 +30,7 @@ use function substr;
class SimpleNegatedAssertionReconciler extends Reconciler
{
/**
* @param array<string, array<string, array{Type\Union}>> $template_type_map
* @param array<string, array<string, Type\Union>> $template_type_map
* @param string[] $suppressed_issues
* @param 0|1|2 $failed_reconciliation
*

View File

@ -0,0 +1,40 @@
<?php
namespace Psalm\Internal\Type;
use Psalm\Type\Union;
class TemplateBound
{
/**
* @var Union
*/
public $type;
/**
* This is the depth at which the template appears in a given type.
*
* In the type Foo<T, Bar<T, array<T>>> the type T appears at three different depths.
*
* The shallowest-appearance of the template takes prominence when inferring the type of T.
*
* @var ?int
*/
public $appearance_depth;
/**
* The argument offset where this template was set
*
* In the type Foo<T, string, T> the type appears at argument offsets 0 and 2
*
* @var ?int
*/
public $arg_offset;
public function __construct(Union $type, ?int $appearance_depth = null, ?int $arg_offset = null)
{
$this->type = $type;
$this->appearance_depth = $appearance_depth;
$this->arg_offset = $arg_offset;
}
}

View File

@ -3,21 +3,22 @@
namespace Psalm\Internal\Type;
use Psalm\Type\Union;
use function array_map;
class TemplateResult
{
/**
* @var array<string, array<string, array{0: Union}>>
* @var array<string, array<string, Union>>
*/
public $template_types;
/**
* @var array<string, array<string, array{0: Union, 1?: int, 2?: ?int}>>
* @var array<string, array<string, TemplateBound>>
*/
public $upper_bounds;
/**
* @var array<string, array<string, array{0: Union, 1?: int, 2?: ?int}>>
* @var array<string, array<string, TemplateBound>>
*/
public $lower_bounds = [];
@ -27,12 +28,23 @@ class TemplateResult
public $lower_bounds_unintersectable_types = [];
/**
* @param array<string, array<string, array{0: Union}>> $template_types
* @param array<string, array<string, array{0: Union, 1?: int, 2?: ?int}>> $upper_bounds
* @param array<string, array<string, Union>> $template_types
* @param array<string, array<string, Union>> $upper_bounds
*/
public function __construct(array $template_types, array $upper_bounds)
{
$this->template_types = $template_types;
$this->upper_bounds = $upper_bounds;
$this->upper_bounds = array_map(
function ($type_map) {
return array_map(
function ($type) {
return new TemplateBound($type);
},
$type_map
);
},
$upper_bounds
);
}
}

View File

@ -58,7 +58,7 @@ class TypeParser
*
* @param list<array{0: string, 1: int, 2?: string}> $type_tokens
* @param array{int,int}|null $php_version
* @param array<string, array<string, array{Union}>> $template_type_map
* @param array<string, array<string, Union>> $template_type_map
* @param array<string, TypeAlias> $type_aliases
*
*/
@ -110,7 +110,7 @@ class TypeParser
/**
* @param array{int,int}|null $php_version
* @param array<string, array<string, array{Union}>> $template_type_map
* @param array<string, array<string, Union>> $template_type_map
* @param array<string, TypeAlias> $type_aliases
*
* @return Atomic|Union
@ -140,9 +140,9 @@ class TypeParser
&& $i === 0
) {
if ($tree_type instanceof TTemplateParam) {
$template_type_map[$tree_type->param_name] = ['class-string-map' => [$tree_type->as]];
$template_type_map[$tree_type->param_name] = ['class-string-map' => $tree_type->as];
} elseif ($tree_type instanceof TNamedObject) {
$template_type_map[$tree_type->value] = ['class-string-map' => [\Psalm\Type::getObject()]];
$template_type_map[$tree_type->value] = ['class-string-map' => \Psalm\Type::getObject()];
}
}
@ -234,7 +234,7 @@ class TypeParser
return self::getGenericParamClass(
$class_name,
$template_type_map[$class_name][$first_class][0],
$template_type_map[$class_name][$first_class],
$first_class
);
}
@ -299,7 +299,7 @@ class TypeParser
return new Atomic\TTemplateKeyOf(
$param_name,
$defining_class,
$template_type_map[$param_name][$defining_class][0]
$template_type_map[$param_name][$defining_class]
);
}
@ -886,9 +886,9 @@ class TypeParser
if (!$offset_defining_class
&& isset($offset_template_data[''])
&& $offset_template_data[''][0]->isSingle()
&& $offset_template_data['']->isSingle()
) {
$offset_template_type = array_values($offset_template_data[''][0]->getAtomicTypes())[0];
$offset_template_type = array_values($offset_template_data['']->getAtomicTypes())[0];
if ($offset_template_type instanceof Atomic\TTemplateKeyOf) {
$offset_defining_class = $offset_template_type->defining_class;
@ -970,7 +970,7 @@ class TypeParser
return new Atomic\TConditional(
$template_param_name,
$first_class,
$template_type_map[$template_param_name][$first_class][0],
$template_type_map[$template_param_name][$first_class],
$conditional_type,
$if_type,
$else_type
@ -993,7 +993,7 @@ class TypeParser
return self::getGenericParamClass(
$fq_classlike_name,
$template_type_map[$fq_classlike_name][$first_class][0],
$template_type_map[$fq_classlike_name][$first_class],
$first_class
);
}

View File

@ -155,11 +155,11 @@ class UnionTemplateHandler
&& !empty($template_result->upper_bounds[$atomic_type->offset_param_name])
) {
$array_template_type
= $template_result->template_types[$atomic_type->array_param_name][$atomic_type->defining_class][0];
= $template_result->template_types[$atomic_type->array_param_name][$atomic_type->defining_class];
$offset_template_type
= array_values(
$template_result->upper_bounds[$atomic_type->offset_param_name]
)[0][0];
)[0]->type;
if ($array_template_type->isSingle()
&& $offset_template_type->isSingle()
@ -204,7 +204,7 @@ class UnionTemplateHandler
if (isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class])) {
$template_type
= $template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class][0];
= $template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class];
if ($template_type->isSingle()) {
$template_type = array_values($template_type->getAtomicTypes())[0];
@ -473,8 +473,7 @@ class UnionTemplateHandler
) : array {
$template_type = $template_result->template_types
[$atomic_type->param_name]
[$atomic_type->defining_class]
[0];
[$atomic_type->defining_class];
if ($template_type->getId() === $key) {
return array_values($template_type->getAtomicTypes());
@ -561,9 +560,9 @@ class UnionTemplateHandler
// @codingStandardsIgnoreStart
if ($replacement_atomic_type instanceof Atomic\TTemplateKeyOf
&& isset($template_result->template_types[$replacement_atomic_type->param_name][$replacement_atomic_type->defining_class][0])
&& isset($template_result->template_types[$replacement_atomic_type->param_name][$replacement_atomic_type->defining_class])
) {
$keyed_template = $template_result->template_types[$replacement_atomic_type->param_name][$replacement_atomic_type->defining_class][0];
$keyed_template = $template_result->template_types[$replacement_atomic_type->param_name][$replacement_atomic_type->defining_class];
if ($keyed_template->isSingle()) {
$keyed_template = array_values($keyed_template->getAtomicTypes())[0];
@ -587,7 +586,7 @@ class UnionTemplateHandler
$atomic_types[] = clone $key_type_atomic;
}
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class][0]
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]->type
= clone $key_type;
}
}
@ -674,14 +673,14 @@ class UnionTemplateHandler
$generic_param->setFromDocblock();
if (isset(
$template_result->upper_bounds[$param_name_key][$atomic_type->defining_class][0]
$template_result->upper_bounds[$param_name_key][$atomic_type->defining_class]
)) {
$existing_generic_param = $template_result->upper_bounds
[$param_name_key]
[$atomic_type->defining_class];
$existing_depth = $existing_generic_param[1] ?? -1;
$existing_arg_offset = $existing_generic_param[2] ?? $input_arg_offset;
$existing_depth = $existing_generic_param->appearance_depth ?? -1;
$existing_arg_offset = $existing_generic_param->arg_offset ?? $input_arg_offset;
if ($existing_depth > $depth && $input_arg_offset === $existing_arg_offset) {
return $atomic_types ?: [$atomic_type];
@ -691,19 +690,18 @@ class UnionTemplateHandler
$generic_param = \Psalm\Type::combineUnionTypes(
$template_result->upper_bounds
[$param_name_key]
[$atomic_type->defining_class]
[0],
[$atomic_type->defining_class]->type,
$generic_param,
$codebase
);
}
}
$template_result->upper_bounds[$param_name_key][$atomic_type->defining_class] = [
$template_result->upper_bounds[$param_name_key][$atomic_type->defining_class] = new TemplateBound(
$generic_param,
$depth,
$input_arg_offset
];
);
}
foreach ($atomic_types as $atomic_type) {
@ -744,18 +742,18 @@ class UnionTemplateHandler
}
}
if (isset($template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0])) {
if (isset($template_result->lower_bounds[$param_name_key][$atomic_type->defining_class])) {
if (!UnionTypeComparator::isContainedBy(
$codebase,
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0],
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class]->type,
$generic_param
) || !UnionTypeComparator::isContainedBy(
$codebase,
$generic_param,
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0]
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class]->type
)) {
$intersection_type = \Psalm\Type::intersectUnionTypes(
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0],
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class]->type,
$generic_param,
$codebase
);
@ -764,19 +762,20 @@ class UnionTemplateHandler
}
if ($intersection_type) {
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0]
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class]->type
= $intersection_type;
} else {
$template_result->lower_bounds_unintersectable_types[]
= $template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0];
= $template_result->lower_bounds[$param_name_key][$atomic_type->defining_class]->type;
$template_result->lower_bounds_unintersectable_types[] = $generic_param;
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0]
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class]->type
= \Psalm\Type::getMixed();
}
} else {
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0]
= $generic_param;
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class] = new TemplateBound(
$generic_param
);
}
}
}
@ -845,19 +844,19 @@ class UnionTemplateHandler
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] = [
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class] = new TemplateBound(
\Psalm\Type::combineUnionTypes(
$generic_param,
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class][0]
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]->type
),
$depth
];
);
} else {
$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(
$generic_param,
$depth,
$input_arg_offset
];
);
}
}
}
@ -866,29 +865,27 @@ class UnionTemplateHandler
}
/**
* @param array<string, array<string, array{Union, 1?:int}>> $template_types
*
* @return array{Union, 1?:int}|null
* @param array<string, array<string, TemplateBound>> $template_types
*/
public static function getRootTemplateType(
array $template_types,
string $param_name,
string $defining_class,
array $visited_classes = []
) : ?array {
) : ?Union {
if (isset($visited_classes[$defining_class])) {
return null;
}
if (isset($template_types[$param_name][$defining_class])) {
$mapped_type = $template_types[$param_name][$defining_class][0];
$mapped_type = $template_types[$param_name][$defining_class]->type;
$mapped_type_atomic_types = array_values($mapped_type->getAtomicTypes());
if (count($mapped_type_atomic_types) > 1
|| !$mapped_type_atomic_types[0] instanceof Atomic\TTemplateParam
) {
return $template_types[$param_name][$defining_class];
return $template_types[$param_name][$defining_class]->type;
}
$first_template = $mapped_type_atomic_types[0];
@ -898,7 +895,7 @@ class UnionTemplateHandler
$first_template->param_name,
$first_template->defining_class,
$visited_classes + [$defining_class => true]
) ?? $template_types[$param_name][$defining_class];
) ?? $template_types[$param_name][$defining_class]->type;
}
return null;
@ -942,7 +939,7 @@ class UnionTemplateHandler
break;
}
$replacement_templates[$template_name][$input_type_part->value] = [$input_type_params[$i]];
$replacement_templates[$template_name][$input_type_part->value] = $input_type_params[$i];
$i++;
}

View File

@ -250,7 +250,7 @@ class TypeChecker extends NodeVisitor
&& empty($expected_param_covariants[$i]);
if (isset(\array_values($expected_type_params)[$i])) {
$expected_type_param = \reset(\array_values($expected_type_params)[$i])[0];
$expected_type_param = \reset(\array_values($expected_type_params)[$i]);
$expected_type_param = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,

View File

@ -22,7 +22,7 @@ interface StatementsSource extends FileSource
public function getParentFQCLN(): ?string;
/**
* @return array<string, array<string, array{Type\Union}>>|null
* @return array<string, array<string, Type\Union>>|null
*/
public function getTemplateTypeMap(): ?array;

View File

@ -28,9 +28,9 @@ class Assertion
}
/**
* @param array<string, array<string, array{0:\Psalm\Type\Union}>> $template_type_map
* @param array<string, array<string, \Psalm\Internal\Type\TemplateBound>> $inferred_upper_bounds
*/
public function getUntemplatedCopy(array $template_type_map, ?string $this_var_id) : self
public function getUntemplatedCopy(array $inferred_upper_bounds, ?string $this_var_id) : self
{
return new Assertion(
\is_string($this->var_id) && $this_var_id
@ -42,20 +42,20 @@ class Assertion
*
* @return array{0: string}
*/
function (array $rules) use ($template_type_map) : array {
function (array $rules) use ($inferred_upper_bounds) : array {
$first_rule = $rules[0];
if ($template_type_map) {
if ($inferred_upper_bounds) {
$rule_tokens = \Psalm\Internal\Type\TypeTokenizer::tokenize($first_rule);
$substitute = false;
foreach ($rule_tokens as &$rule_token) {
if (isset($template_type_map[$rule_token[0]])) {
foreach ($template_type_map[$rule_token[0]] as [$type]) {
if (isset($inferred_upper_bounds[$rule_token[0]])) {
foreach ($inferred_upper_bounds[$rule_token[0]] as $bound) {
$substitute = true;
$first_type = \array_values($type->getAtomicTypes())[0];
$first_type = \array_values($bound->type->getAtomicTypes())[0];
if ($first_type instanceof \Psalm\Type\Atomic\TTemplateParam) {
$rule_token[0] = $first_type->param_name;

View File

@ -292,7 +292,7 @@ class ClassLikeStorage
public $overridden_property_ids = [];
/**
* @var array<string, non-empty-array<string, array{Type\Union}>>|null
* @var array<string, non-empty-array<string, Type\Union>>|null
*/
public $template_types;

View File

@ -102,7 +102,7 @@ abstract class FunctionLikeStorage
public $global_types = [];
/**
* @var array<string, non-empty-array<string, array{Type\Union}>>|null
* @var array<string, non-empty-array<string, Type\Union>>|null
*/
public $template_types;

View File

@ -54,7 +54,7 @@ abstract class Type
* Parses a string type representation
*
* @param array{int,int}|null $php_version
* @param array<string, array<string, array{Type\Union}>> $template_type_map
* @param array<string, array<string, Type\Union>> $template_type_map
*/
public static function parseString(
string $type_string,

View File

@ -89,7 +89,7 @@ abstract class Atomic implements TypeNode
/**
* @param array{int,int}|null $php_version
* @param array<string, array<string, array{Union}>> $template_type_map
* @param array<string, array<string, Union>> $template_type_map
* @param array<string, TypeAlias> $type_aliases
*/
public static function create(
@ -265,7 +265,7 @@ abstract class Atomic implements TypeNode
return new TTemplateParam(
$value,
$template_type_map[$value][$first_class][0],
$template_type_map[$value][$first_class],
$first_class
);
}

View File

@ -85,7 +85,7 @@ trait HasIntersectionTrait
&& isset($template_result->upper_bounds[$extra_type->param_name][$extra_type->defining_class])
) {
$template_type = clone $template_result->upper_bounds
[$extra_type->param_name][$extra_type->defining_class][0];
[$extra_type->param_name][$extra_type->defining_class]->type;
foreach ($template_type->getAtomicTypes() as $template_type_part) {
if ($template_type_part instanceof TNamedObject) {

View File

@ -50,7 +50,7 @@ class Reconciler
* @param array<string, Type\Union> $existing_types
* @param array<string, bool> $changed_var_ids
* @param array<string, bool> $referenced_var_ids
* @param array<string, array<string, array{Type\Union}>> $template_type_map
* @param array<string, array<string, Type\Union>> $template_type_map
*
* @return array<string, Type\Union>
*/

View File

@ -1035,7 +1035,7 @@ class Union implements TypeNode
$is_mixed = false;
$found_generic_params = $template_result->upper_bounds ?: [];
$inferred_upper_bounds = $template_result->upper_bounds ?: [];
foreach ($this->types as $key => $atomic_type) {
$atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase);
@ -1044,13 +1044,13 @@ class Union implements TypeNode
$template_type = null;
$traversed_type = \Psalm\Internal\Type\UnionTemplateHandler::getRootTemplateType(
$found_generic_params,
$inferred_upper_bounds,
$atomic_type->param_name,
$atomic_type->defining_class
);
if ($traversed_type) {
$template_type = $traversed_type[0];
$template_type = $traversed_type;
if (!$atomic_type->as->isMixed() && $template_type->isMixed()) {
$template_type = clone $atomic_type->as;
@ -1082,7 +1082,7 @@ class Union implements TypeNode
}
}
} elseif ($codebase) {
foreach ($found_generic_params as $template_type_map) {
foreach ($inferred_upper_bounds as $template_type_map) {
foreach ($template_type_map as $template_class => $_) {
if (substr($template_class, 0, 3) === 'fn-') {
continue;
@ -1098,11 +1098,12 @@ class Union implements TypeNode
$param_map = $classlike_storage->template_type_extends[$defining_class];
if (isset($param_map[$key])
&& isset($found_generic_params[(string) $param_map[$key]][$template_class])
&& isset($inferred_upper_bounds[(string) $param_map[$key]][$template_class])
) {
$template_name = (string) $param_map[$key];
$template_type
= clone $found_generic_params
[(string) $param_map[$key]][$template_class][0];
= clone $inferred_upper_bounds[$template_name][$template_class]->type;
}
}
}
@ -1124,8 +1125,8 @@ class Union implements TypeNode
}
}
} elseif ($atomic_type instanceof Type\Atomic\TTemplateParamClass) {
$template_type = isset($found_generic_params[$atomic_type->param_name][$atomic_type->defining_class])
? clone $found_generic_params[$atomic_type->param_name][$atomic_type->defining_class][0]
$template_type = isset($inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class])
? clone $inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]->type
: null;
$class_template_type = null;
@ -1163,15 +1164,15 @@ class Union implements TypeNode
$template_type = null;
if (isset($found_generic_params[$atomic_type->array_param_name][$atomic_type->defining_class])
&& !empty($found_generic_params[$atomic_type->offset_param_name])
if (isset($inferred_upper_bounds[$atomic_type->array_param_name][$atomic_type->defining_class])
&& !empty($inferred_upper_bounds[$atomic_type->offset_param_name])
) {
$array_template_type
= $found_generic_params[$atomic_type->array_param_name][$atomic_type->defining_class][0];
= $inferred_upper_bounds[$atomic_type->array_param_name][$atomic_type->defining_class]->type;
$offset_template_type
= array_values(
$found_generic_params[$atomic_type->offset_param_name]
)[0][0];
$inferred_upper_bounds[$atomic_type->offset_param_name]
)[0]->type;
if ($array_template_type->isSingle()
&& $offset_template_type->isSingle()
@ -1205,8 +1206,8 @@ class Union implements TypeNode
} elseif ($atomic_type instanceof Type\Atomic\TConditional
&& $codebase
) {
$template_type = isset($found_generic_params[$atomic_type->param_name][$atomic_type->defining_class])
? clone $found_generic_params[$atomic_type->param_name][$atomic_type->defining_class][0]
$template_type = isset($inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class])
? clone $inferred_upper_bounds[$atomic_type->param_name][$atomic_type->defining_class]->type
: null;
$class_template_type = null;

View File

@ -492,7 +492,7 @@ class TypeParseTest extends TestCase
{
$this->assertSame(
'(T is string ? string : int)',
(string) Type::parseString('(T is string ? string : int)', null, ['T' => ['' => [Type::getArray()]]])
(string) Type::parseString('(T is string ? string : int)', null, ['T' => ['' => Type::getArray()]])
);
}
@ -500,7 +500,7 @@ class TypeParseTest extends TestCase
{
$this->assertSame(
'(T is string|true ? int|string : int)',
(string) Type::parseString('(T is "hello"|true ? string|int : int)', null, ['T' => ['' => [Type::getArray()]]])
(string) Type::parseString('(T is "hello"|true ? string|int : int)', null, ['T' => ['' => Type::getArray()]])
);
}
@ -508,7 +508,7 @@ class TypeParseTest extends TestCase
{
$this->assertSame(
'(T is array{a: string} ? string : int)',
(string) Type::parseString('(T is array{a: string} ? string : int)', null, ['T' => ['' => [Type::getArray()]]])
(string) Type::parseString('(T is array{a: string} ? string : int)', null, ['T' => ['' => Type::getArray()]])
);
}
@ -516,7 +516,7 @@ class TypeParseTest extends TestCase
{
$this->assertSame(
'(T is array<array-key, string> ? string : int)',
(string) Type::parseString('(T is array<string> ? string : int)', null, ['T' => ['' => [Type::getArray()]]])
(string) Type::parseString('(T is array<string> ? string : int)', null, ['T' => ['' => Type::getArray()]])
);
}
@ -524,7 +524,7 @@ class TypeParseTest extends TestCase
{
$this->assertSame(
'(T is A&B ? string : int)',
(string) Type::parseString('(T is A&B ? string : int)', null, ['T' => ['' => [Type::getArray()]]])
(string) Type::parseString('(T is A&B ? string : int)', null, ['T' => ['' => Type::getArray()]])
);
}
@ -532,21 +532,21 @@ class TypeParseTest extends TestCase
{
$this->assertSame(
'(T is string ? string : int)',
(string) Type::parseString('(T is string?string:int)', null, ['T' => ['' => [Type::getArray()]]])
(string) Type::parseString('(T is string?string:int)', null, ['T' => ['' => Type::getArray()]])
);
}
public function testConditionalTypeWithCallableElseBool(): void
{
$this->expectException(\Psalm\Exception\TypeParseTreeException::class);
Type::parseString('(T is string ? callable() : bool)', null, ['T' => ['' => [Type::getArray()]]]);
Type::parseString('(T is string ? callable() : bool)', null, ['T' => ['' => Type::getArray()]]);
}
public function testConditionalTypeWithCallableReturningBoolElseBool(): void
{
$this->assertSame(
'(T is string ? callable():bool : bool)',
(string) Type::parseString('(T is string ? (callable() : bool) : bool)', null, ['T' => ['' => [Type::getArray()]]])
(string) Type::parseString('(T is string ? (callable() : bool) : bool)', null, ['T' => ['' => Type::getArray()]])
);
}
@ -557,7 +557,7 @@ class TypeParseTest extends TestCase
(string) Type::parseString(
'(T is string ? string : array<string, string>)',
null,
['T' => ['' => [Type::getArray()]]]
['T' => ['' => Type::getArray()]]
)
);
}
@ -569,7 +569,7 @@ class TypeParseTest extends TestCase
(string) Type::parseString(
'(T is string ? (callable(string, string):string) : (callable(mixed...):mixed))',
null,
['T' => ['' => [Type::getArray()]]]
['T' => ['' => Type::getArray()]]
)
);
}
@ -581,7 +581,7 @@ class TypeParseTest extends TestCase
(string) Type::parseString(
'(T is string ? callable(string, string):string : callable(mixed...):mixed)',
null,
['T' => ['' => [Type::getArray()]]]
['T' => ['' => Type::getArray()]]
)
);
}
@ -742,7 +742,7 @@ class TypeParseTest extends TestCase
{
$this->assertSame(
'key-of<T>',
(string)Type::parseString('key-of<T>', null, ['T' => ['' => [Type::getArray()]]])
(string)Type::parseString('key-of<T>', null, ['T' => ['' => Type::getArray()]])
);
}
@ -754,10 +754,10 @@ class TypeParseTest extends TestCase
'T[K]',
null,
[
'T' => ['' => [Type::getArray()]],
'K' => ['' => [new Type\Union([
'T' => ['' => Type::getArray()],
'K' => ['' => new Type\Union([
new Type\Atomic\TTemplateKeyOf('T', 'fn-foo', Type::getMixed())
])]],
])],
]
)
);