1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Fix #3084 - keep track of upper and lower bounds of inferred template types

This commit is contained in:
Brown 2020-04-07 00:13:56 -04:00
parent 99549871b6
commit 067104e170
34 changed files with 348 additions and 195 deletions

View File

@ -907,10 +907,10 @@ class MethodComparator
}
}
$template_result = new \Psalm\Internal\Type\TemplateResult($template_types, []);
$template_result = new \Psalm\Internal\Type\TemplateResult([], $template_types);
$templated_type->replaceTemplateTypesWithArgTypes(
$template_result->template_types,
$template_result,
$codebase
);
}

View File

@ -518,7 +518,8 @@ class ForeachAnalyzer
} else {
$intersection_value_type = Type::intersectUnionTypes(
$intersection_value_type,
$value_type_part
$value_type_part,
$codebase
) ?: Type::getMixed();
}
@ -527,7 +528,8 @@ class ForeachAnalyzer
} else {
$intersection_key_type = Type::intersectUnionTypes(
$intersection_key_type,
$key_type_part
$key_type_part,
$codebase
) ?: Type::getMixed();
}
}

View File

@ -589,7 +589,7 @@ class ArrayAssignmentAnalyzer
);
$current_type->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$template_result,
$codebase
);

View File

@ -205,7 +205,8 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
} else {
$all_intersection_return_type = Type::intersectUnionTypes(
$all_intersection_return_type,
$intersection_result->return_type
$intersection_result->return_type,
$codebase
) ?: Type::getMixed();
}
}
@ -521,7 +522,8 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
$result,
$return_type_candidate,
$all_intersection_return_type,
$method_name_lc
$method_name_lc,
$codebase
);
return;
@ -601,7 +603,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
);
}
$class_template_params = $template_result->generic_params;
$class_template_params = $template_result->upper_bounds;
if ($method_storage->assertions) {
self::applyAssertionsToContext(
@ -725,7 +727,8 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
$result,
$return_type_candidate,
$all_intersection_return_type,
$method_name_lc
$method_name_lc,
$codebase
);
}
@ -733,13 +736,15 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
AtomicMethodCallAnalysisResult $result,
?Type\Union $return_type_candidate,
?Type\Union $all_intersection_return_type,
string $method_name
string $method_name,
Codebase $codebase
) : void {
if ($return_type_candidate) {
if ($all_intersection_return_type) {
$return_type_candidate = Type::intersectUnionTypes(
$all_intersection_return_type,
$return_type_candidate
$return_type_candidate,
$codebase
) ?: Type::getMixed();
}

View File

@ -4,6 +4,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
use PhpParser;
use Psalm\Internal\Analyzer\FunctionAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssertionFinder;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TypeAnalyzer;
@ -14,6 +15,7 @@ use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Issue\DeprecatedFunction;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\MixedFunctionCall;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\InvalidFunctionCall;
use Psalm\Issue\ImpureFunctionCall;
use Psalm\Issue\NullFunctionCall;
@ -50,7 +52,7 @@ use function explode;
/**
* @internal
*/
class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer
class FunctionCallAnalyzer extends CallAnalyzer
{
/**
* @param StatementsAnalyzer $statements_analyzer
@ -336,6 +338,13 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio
// fall through
}
CallAnalyzer::checkTemplateResult(
$statements_analyzer,
$template_result,
$code_location,
$function_id
);
if ($function_name instanceof PhpParser\Node\Name && $function_id) {
$stmt_type = self::getFunctionCallReturnType(
$statements_analyzer,
@ -416,7 +425,7 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio
);
if ($function_storage) {
$generic_params = $template_result ? $template_result->generic_params : [];
$generic_params = $template_result ? $template_result->upper_bounds : [];
if ($function_storage->assertions && $function_name instanceof PhpParser\Node\Name) {
self::applyAssertionsToContext(
@ -887,8 +896,8 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio
if (!$in_call_map || $is_stubbed) {
if ($function_storage && $function_storage->template_types) {
foreach ($function_storage->template_types as $template_name => $_) {
if (!isset($template_result->generic_params[$template_name])) {
$template_result->generic_params[$template_name] = [
if (!isset($template_result->upper_bounds[$template_name])) {
$template_result->upper_bounds[$template_name] = [
'fn-' . $function_id => [Type::getEmpty(), 0]
];
}
@ -906,7 +915,7 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio
if ($function_storage && $function_storage->return_type) {
$return_type = clone $function_storage->return_type;
if ($template_result->generic_params && $function_storage->template_types) {
if ($template_result->upper_bounds && $function_storage->template_types) {
$return_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$return_type,
@ -916,7 +925,7 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio
);
$return_type->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$template_result,
$codebase
);
}

View File

@ -82,16 +82,16 @@ class MethodCallReturnTypeFetcher
$class_storage = $codebase->methods->getClassLikeStorageForMethod($method_id);
if (CallMap::inCallMap((string) $call_map_id)) {
if (($template_result->generic_params || $class_storage->stubbed)
if (($template_result->upper_bounds || $class_storage->stubbed)
&& isset($class_storage->methods[$method_id->method_name])
&& ($method_storage = $class_storage->methods[$method_id->method_name])
&& $method_storage->return_type
) {
$return_type_candidate = clone $method_storage->return_type;
if ($template_result->generic_params) {
if ($template_result->upper_bounds) {
$return_type_candidate->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$template_result,
$codebase
);
}
@ -135,19 +135,19 @@ class MethodCallReturnTypeFetcher
foreach ($bindable_template_types as $template_type) {
if ($template_type->defining_class !== $fq_class_name
&& !isset(
$template_result->generic_params
$template_result->upper_bounds
[$template_type->param_name]
[$template_type->defining_class]
)
) {
$template_result->generic_params[$template_type->param_name] = [
$template_result->upper_bounds[$template_type->param_name] = [
($template_type->defining_class) => [Type::getEmpty(), 0]
];
}
}
}
if ($template_result->generic_params) {
if ($template_result->upper_bounds) {
$return_type_candidate = ExpressionAnalyzer::fleshOutType(
$codebase,
$return_type_candidate,
@ -157,7 +157,7 @@ class MethodCallReturnTypeFetcher
);
$return_type_candidate->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$template_result,
$codebase
);
}

View File

@ -67,7 +67,8 @@ class MissingMethodCallHandler
if ($all_intersection_return_type) {
$return_type_candidate = Type::intersectUnionTypes(
$all_intersection_return_type,
$return_type_candidate
$return_type_candidate,
$codebase
) ?: Type::getMixed();
}
@ -76,7 +77,8 @@ class MissingMethodCallHandler
} else {
$result->return_type = Type::combineUnionTypes(
$return_type_candidate,
$result->return_type
$result->return_type,
$codebase
);
}
@ -181,7 +183,8 @@ class MissingMethodCallHandler
if ($all_intersection_return_type) {
$return_type_candidate = Type::intersectUnionTypes(
$all_intersection_return_type,
$return_type_candidate
$return_type_candidate,
$codebase
) ?: Type::getMixed();
}

View File

@ -488,15 +488,15 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
: $fq_class_name;
foreach ($storage->template_types as $template_name => $base_type) {
if (isset($template_result->generic_params[$template_name][$fq_class_name])) {
if (isset($template_result->upper_bounds[$template_name][$fq_class_name])) {
$generic_param_type
= $template_result->generic_params[$template_name][$fq_class_name][0];
} elseif ($storage->template_type_extends && $template_result->generic_params) {
= $template_result->upper_bounds[$template_name][$fq_class_name][0];
} 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->generic_params
$template_result->upper_bounds
);
} else {
$generic_param_type = array_values($base_type)[0][0];

View File

@ -912,11 +912,11 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
foreach ($bindable_template_types as $template_type) {
if (!isset(
$template_result->generic_params
$template_result->upper_bounds
[$template_type->param_name]
[$template_type->defining_class]
)) {
$template_result->generic_params[$template_type->param_name] = [
$template_result->upper_bounds[$template_type->param_name] = [
($template_type->defining_class) => [Type::getEmpty(), 0]
];
}
@ -937,7 +937,7 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
$static_type = $fq_class_name;
}
if ($template_result->generic_params) {
if ($template_result->upper_bounds) {
$return_type_candidate = ExpressionAnalyzer::fleshOutType(
$codebase,
$return_type_candidate,
@ -947,7 +947,7 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
);
$return_type_candidate->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$template_result,
$codebase
);
}
@ -1033,7 +1033,7 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
}
}
$generic_params = $template_result->generic_params;
$generic_params = $template_result->upper_bounds;
if ($method_storage->assertions) {
self::applyAssertionsToContext(

View File

@ -347,6 +347,15 @@ class CallAnalyzer
return false;
}
if ($class_template_result) {
self::checkTemplateResult(
$statements_analyzer,
$class_template_result,
$code_location,
strtolower((string) $method_id)
);
}
return null;
}
@ -461,7 +470,7 @@ class CallAnalyzer
if (($arg->value instanceof PhpParser\Node\Expr\Closure
|| $arg->value instanceof PhpParser\Node\Expr\ArrowFunction)
&& $template_result
&& $template_result->generic_params
&& $template_result->upper_bounds
&& $param
&& $param->type
&& !$arg->value->getDocComment()
@ -471,7 +480,7 @@ class CallAnalyzer
) {
$function_like_params = [];
foreach ($template_result->generic_params as $template_name => $_) {
foreach ($template_result->upper_bounds as $template_name => $_) {
$function_like_params[] = new \Psalm\Storage\FunctionLikeParameter(
'function',
false,
@ -496,7 +505,7 @@ class CallAnalyzer
}
$replace_template_result = new \Psalm\Internal\Type\TemplateResult(
$template_result->generic_params,
$template_result->upper_bounds,
[]
);
@ -512,7 +521,7 @@ class CallAnalyzer
);
$replaced_type->replaceTemplateTypesWithArgTypes(
$replace_template_result->generic_params,
$replace_template_result,
$codebase
);
@ -606,12 +615,12 @@ class CallAnalyzer
'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
);
if ($replace_template_result->generic_params) {
if ($replace_template_result->upper_bounds) {
if (!$template_result) {
$template_result = new TemplateResult([], []);
}
$template_result->generic_params += $replace_template_result->generic_params;
$template_result->upper_bounds += $replace_template_result->upper_bounds;
}
}
@ -1311,7 +1320,7 @@ class CallAnalyzer
$template_result = null;
$class_generic_params = $class_template_result
? $class_template_result->generic_params
? $class_template_result->upper_bounds
: [];
if ($function_storage) {
@ -1360,7 +1369,7 @@ class CallAnalyzer
);
if (!$class_template_result) {
$template_result->generic_params = [];
$template_result->upper_bounds = [];
}
}
}
@ -1759,9 +1768,10 @@ class CallAnalyzer
'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
);
if ($template_result->generic_params) {
if ($template_result->upper_bounds) {
$original_by_ref_type->replaceTemplateTypesWithArgTypes(
$template_result->generic_params
$template_result,
$codebase
);
$by_ref_type = $original_by_ref_type;
@ -1781,9 +1791,10 @@ class CallAnalyzer
'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
);
if ($template_result->generic_params) {
if ($template_result->upper_bounds) {
$original_by_ref_out_type->replaceTemplateTypesWithArgTypes(
$template_result->generic_params
$template_result,
$codebase
);
$by_ref_out_type = $original_by_ref_out_type;
@ -1931,11 +1942,17 @@ class CallAnalyzer
foreach ($bindable_template_params as $template_type) {
if (!isset(
$template_result->generic_params
$template_result->upper_bounds
[$template_type->param_name]
[$template_type->defining_class]
)) {
$template_result->generic_params[$template_type->param_name][$template_type->defining_class] = [
)
&& !isset(
$template_result->lower_bounds
[$template_type->param_name]
[$template_type->defining_class]
)
) {
$template_result->upper_bounds[$template_type->param_name][$template_type->defining_class] = [
clone $template_type->as,
0
];
@ -2512,7 +2529,8 @@ class CallAnalyzer
);
$closure_type->return_type->replaceTemplateTypesWithArgTypes(
$template_result->generic_params
$template_result,
$codebase
);
}
@ -3807,4 +3825,43 @@ class CallAnalyzer
$context->vars_in_scope = $op_vars_in_scope;
}
}
public static function checkTemplateResult(
StatementsAnalyzer $statements_analyzer,
TemplateResult $template_result,
CodeLocation $code_location,
?string $function_id
) : 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 => list($lower_bound_type)) {
if (isset($template_result->upper_bounds[$template_name][$defining_id])) {
$upper_bound_type = $template_result->upper_bounds[$template_name][$defining_id][0];
if (!TypeAnalyzer::isContainedBy(
$statements_analyzer->getCodebase(),
$upper_bound_type,
$lower_bound_type
)) {
if (IssueBuffer::accepts(
new InvalidArgument(
'Could not reconcile upper and lower bounds '
. $upper_bound_type->getId() . ' and '
. $lower_bound_type->getId() . ' for template param '
. $template_name,
$code_location,
$function_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// continue
}
}
} else {
$template_result->upper_bounds[$template_name][$defining_id][0] = clone $lower_bound_type;
}
}
}
}
}
}

View File

@ -793,7 +793,7 @@ class ArrayFetchAnalyzer
$expected_value_param_get = clone $type->value_param;
$expected_value_param_get->replaceTemplateTypesWithArgTypes(
$template_result_get->generic_params,
$template_result_get,
$codebase
);
@ -801,7 +801,7 @@ class ArrayFetchAnalyzer
$expected_value_param_set = clone $type->value_param;
$replacement_type->replaceTemplateTypesWithArgTypes(
$template_result_set->generic_params,
$template_result_set,
$codebase
);

View File

@ -9,6 +9,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\Type\TemplateResult;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\DeprecatedProperty;
@ -935,7 +936,7 @@ class PropertyFetchAnalyzer
}
$class_property_type->replaceTemplateTypesWithArgTypes(
$template_types,
new TemplateResult([], $template_types),
$codebase
);
}

View File

@ -17,6 +17,7 @@ use Psalm\Exception\DocblockParseException;
use Psalm\Internal\Analyzer\TypeComparisonResult;
use Psalm\Internal\Taint\Sink;
use Psalm\Internal\Taint\Source;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Issue\FalsableReturnStatement;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\InvalidReturnStatement;
@ -243,7 +244,8 @@ class ReturnAnalyzer
$local_return_type = clone $local_return_type;
$local_return_type->replaceTemplateTypesWithArgTypes(
$found_generic_params
new TemplateResult([], $found_generic_params),
$codebase
);
}
}

View File

@ -4,6 +4,7 @@ namespace Psalm\Internal\Analyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Codebase;
use Psalm\Internal\Codebase\CallMap;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Type;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TObjectWithProperties;
@ -2158,7 +2159,8 @@ class TypeAnalyzer
$new_input_param = clone $new_input_param;
$new_input_param->replaceTemplateTypesWithArgTypes(
$replacement_templates
new TemplateResult([], $replacement_templates),
$codebase
);
$new_input_params[] = $new_input_param;

View File

@ -762,7 +762,8 @@ class Methods
if (!$old_contained_by_new && !$new_contained_by_old) {
$attempted_intersection = Type::intersectUnionTypes(
$candidate_type,
$overridden_return_type
$overridden_return_type,
$source_analyzer->getCodebase()
);
if ($attempted_intersection) {

View File

@ -456,7 +456,8 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
);
$mapping_return_type->replaceTemplateTypesWithArgTypes(
$template_result->generic_params
$template_result,
$codebase
);
}

View File

@ -12,6 +12,7 @@ use Psalm\CodeLocation;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TraitAnalyzer;
use Psalm\Internal\Analyzer\TypeAnalyzer;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Issue\DocblockTypeContradiction;
use Psalm\Issue\ParadoxicalCondition;
use Psalm\Issue\PsalmInternalError;
@ -2579,7 +2580,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
if ($template_type_map) {
$new_param->replaceTemplateTypesWithArgTypes(
$template_type_map,
new TemplateResult([], $template_type_map),
$codebase
);
}

View File

@ -14,15 +14,20 @@ class TemplateResult
/**
* @var array<string, array<string, array{0: Union, 1?: int, 2?: ?int}>>
*/
public $generic_params;
public $upper_bounds;
/**
* @var array<string, array<string, array{0: Union, 1?: int, 2?: ?int}>>
*/
public $lower_bounds = [];
/**
* @param array<string, array<string, array{0: Union}>> $template_types
* @param array<string, array<string, array{0: Union, 1?: int, 2?: ?int}>> $generic_params
* @param array<string, array<string, array{0: Union, 1?: int, 2?: ?int}>> $upper_bounds
*/
public function __construct(array $template_types, array $generic_params)
public function __construct(array $template_types, array $upper_bounds)
{
$this->template_types = $template_types;
$this->generic_params = $generic_params;
$this->upper_bounds = $upper_bounds;
}
}

View File

@ -153,13 +153,13 @@ class UnionTemplateHandler
$include_first = true;
if (isset($template_result->template_types[$atomic_type->array_param_name][$atomic_type->defining_class])
&& !empty($template_result->generic_params[$atomic_type->offset_param_name])
&& !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];
$offset_template_type
= array_values(
$template_result->generic_params[$atomic_type->offset_param_name]
$template_result->upper_bounds[$atomic_type->offset_param_name]
)[0][0];
if ($array_template_type->isSingle()
@ -454,7 +454,7 @@ class UnionTemplateHandler
$atomic_types[] = clone $key_type_atomic;
}
$template_result->generic_params[$atomic_type->param_name][$atomic_type->defining_class][0]
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class][0]
= clone $key_type;
}
}
@ -510,12 +510,16 @@ class UnionTemplateHandler
$generic_param->removeType('null');
}
if ($add_upper_bound) {
return array_values($generic_param->getAtomicTypes());
}
$generic_param->setFromDocblock();
if (isset(
$template_result->generic_params[$param_name_key][$atomic_type->defining_class][0]
$template_result->upper_bounds[$param_name_key][$atomic_type->defining_class][0]
)) {
$existing_generic_param = $template_result->generic_params
$existing_generic_param = $template_result->upper_bounds
[$param_name_key]
[$atomic_type->defining_class];
@ -528,7 +532,7 @@ class UnionTemplateHandler
if ($existing_depth === $depth || $input_arg_offset !== $existing_arg_offset) {
$generic_param = \Psalm\Type::combineUnionTypes(
$template_result->generic_params
$template_result->upper_bounds
[$param_name_key]
[$atomic_type->defining_class]
[0],
@ -538,7 +542,7 @@ class UnionTemplateHandler
}
}
$template_result->generic_params[$param_name_key][$atomic_type->defining_class] = [
$template_result->upper_bounds[$param_name_key][$atomic_type->defining_class] = [
$generic_param,
$depth,
$input_arg_offset
@ -573,8 +577,19 @@ class UnionTemplateHandler
}
}
$template_result->template_types[$param_name_key][$atomic_type->defining_class][0]
= $generic_param;
if (isset($template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0])) {
$intersection_type = \Psalm\Type::intersectUnionTypes(
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0],
$generic_param,
$codebase
);
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0]
= $intersection_type ?: \Psalm\Type::getMixed();
} else {
$template_result->lower_bounds[$param_name_key][$atomic_type->defining_class][0]
= $generic_param;
}
}
}
@ -639,16 +654,16 @@ class UnionTemplateHandler
}
if ($generic_param) {
if (isset($template_result->generic_params[$atomic_type->param_name][$atomic_type->defining_class])) {
$template_result->generic_params[$atomic_type->param_name][$atomic_type->defining_class] = [
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] = [
\Psalm\Type::combineUnionTypes(
$generic_param,
$template_result->generic_params[$atomic_type->param_name][$atomic_type->defining_class][0]
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class][0]
),
$depth
];
} else {
$template_result->generic_params[$atomic_type->param_name][$atomic_type->defining_class] = [
$template_result->upper_bounds[$atomic_type->param_name][$atomic_type->defining_class] = [
$generic_param,
$depth,
$input_arg_offset

View File

@ -20,6 +20,7 @@ use function preg_match;
use function preg_quote;
use function preg_replace;
use Psalm\Exception\TypeParseTreeException;
use Psalm\Internal\Analyzer\TypeAnalyzer;
use Psalm\Internal\Type\ParseTree;
use Psalm\Internal\Type\TypeCombination;
use Psalm\Storage\FunctionLikeParameter;
@ -1730,7 +1731,8 @@ abstract class Type
*/
public static function intersectUnionTypes(
Union $type_1,
Union $type_2
Union $type_2,
Codebase $codebase
) {
$intersection_performed = false;
@ -1760,6 +1762,28 @@ abstract class Type
foreach ($combined_type->getAtomicTypes() as $t1_key => $type_1_atomic) {
foreach ($type_2->getAtomicTypes() as $t2_key => $type_2_atomic) {
if ($type_1_atomic instanceof TNamedObject
&& $type_2_atomic instanceof TNamedObject
) {
if (TypeAnalyzer::isAtomicContainedBy(
$codebase,
$type_2_atomic,
$type_1_atomic,
)) {
$combined_type->removeType($t1_key);
$combined_type->addType(clone $type_2_atomic);
$intersection_performed = true;
} elseif (TypeAnalyzer::isAtomicContainedBy(
$codebase,
$type_1_atomic,
$type_2_atomic
)) {
$combined_type->removeType($t2_key);
$combined_type->addType(clone $type_1_atomic);
$intersection_performed = true;
}
}
if (($type_1_atomic instanceof TIterable
|| $type_1_atomic instanceof TNamedObject
|| $type_1_atomic instanceof TTemplateParam

View File

@ -621,13 +621,10 @@ abstract class Atomic implements TypeNode
return $this;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
// do nothing
}

View File

@ -260,25 +260,22 @@ trait CallableTrait
return $callable;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
if ($this->params) {
foreach ($this->params as $param) {
if (!$param->type) {
continue;
}
$param->type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$param->type->replaceTemplateTypesWithArgTypes($template_result, $codebase);
}
}
if ($this->return_type) {
$this->return_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$this->return_type->replaceTemplateTypesWithArgTypes($template_result, $codebase);
}
}

View File

@ -216,15 +216,12 @@ trait GenericTrait
return $atomic;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
foreach ($this->type_params as $offset => $type_param) {
$type_param->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$type_param->replaceTemplateTypesWithArgTypes($template_result, $codebase);
if ($this instanceof Atomic\TArray && $offset === 0 && $type_param->isMixed()) {
$this->type_params[0] = \Psalm\Type::getArrayKey();
@ -236,7 +233,7 @@ trait GenericTrait
}
if ($this instanceof TGenericObject || $this instanceof TIterable) {
$this->replaceIntersectionTemplateTypesWithArgTypes($template_types, $codebase);
$this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase);
}
}
}

View File

@ -5,6 +5,7 @@ use function array_map;
use function implode;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Internal\Type\TemplateResult;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Atomic;
@ -71,11 +72,10 @@ trait HasIntersectionTrait
return $this->extra_types;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*/
public function replaceIntersectionTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase) : void
{
public function replaceIntersectionTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
if (!$this->extra_types) {
return;
}
@ -84,9 +84,10 @@ trait HasIntersectionTrait
foreach ($this->extra_types as $extra_type) {
if ($extra_type instanceof TTemplateParam
&& isset($template_types[$extra_type->param_name][$extra_type->defining_class])
&& isset($template_result->upper_bounds[$extra_type->param_name][$extra_type->defining_class])
) {
$template_type = clone $template_types[$extra_type->param_name][$extra_type->defining_class][0];
$template_type = clone $template_result->upper_bounds
[$extra_type->param_name][$extra_type->defining_class][0];
foreach ($template_type->getAtomicTypes() as $template_type_part) {
if ($template_type_part instanceof TNamedObject) {
@ -96,7 +97,7 @@ trait HasIntersectionTrait
}
}
} else {
$extra_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase);
$new_types[$extra_type->getKey()] = $extra_type;
}
}

View File

@ -344,16 +344,13 @@ class ObjectLike extends \Psalm\Type\Atomic
return $object_like;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
foreach ($this->properties as $property) {
$property->replaceTemplateTypesWithArgTypes(
$template_types,
$template_result,
$codebase
);
}

View File

@ -199,14 +199,11 @@ class TClassStringMap extends \Psalm\Type\Atomic
return $map;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
$this->value_param->replaceTemplateTypesWithArgTypes($template_types);
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
$this->value_param->replaceTemplateTypesWithArgTypes($template_result, $codebase);
}
public function getChildNodes() : array

View File

@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic;
use function implode;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Internal\Type\TemplateResult;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Union;
@ -154,15 +155,12 @@ class TConditional extends \Psalm\Type\Atomic
return false;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
$this->conditional_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$this->if_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$this->else_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
$this->conditional_type->replaceTemplateTypesWithArgTypes($template_result, $codebase);
$this->if_type->replaceTemplateTypesWithArgTypes($template_result, $codebase);
$this->else_type->replaceTemplateTypesWithArgTypes($template_result, $codebase);
}
}

View File

@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic;
use function count;
use function implode;
use Psalm\CodeLocation;
use Psalm\Internal\Type\TemplateResult;
use Psalm\StatementsSource;
use Psalm\Type\Atomic;
use function substr;

View File

@ -169,14 +169,11 @@ class TList extends \Psalm\Type\Atomic
return $list;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
$this->type_param->replaceTemplateTypesWithArgTypes($template_types);
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
$this->type_param->replaceTemplateTypesWithArgTypes($template_result, $codebase);
}
/**

View File

@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic;
use function implode;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Internal\Type\TemplateResult;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Atomic;
@ -129,14 +130,11 @@ class TNamedObject extends Atomic
return $this->value !== 'static';
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
$this->replaceIntersectionTemplateTypesWithArgTypes($template_types, $codebase);
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
$this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase);
}
public function getChildNodes() : array

View File

@ -277,16 +277,13 @@ class TObjectWithProperties extends TObject
return $object_like;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?\Psalm\Codebase $codebase)
{
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
foreach ($this->properties as $property) {
$property->replaceTemplateTypesWithArgTypes(
$template_types,
$template_result,
$codebase
);
}

View File

@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic;
use function implode;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Internal\Type\TemplateResult;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Union;
@ -144,13 +145,10 @@ class TTemplateParam extends \Psalm\Type\Atomic
return false;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
$this->replaceIntersectionTemplateTypesWithArgTypes($template_types, $codebase);
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
$this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase);
}
}

View File

@ -12,6 +12,7 @@ use function is_string;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Internal\Analyzer\TypeAnalyzer;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Internal\Type\TypeCombination;
use Psalm\StatementsSource;
use Psalm\Storage\FileStorage;
@ -1125,27 +1126,26 @@ class Union implements TypeNode
$this->id = null;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, Codebase $codebase = null)
{
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
) : void {
$keys_to_unset = [];
$new_types = [];
$is_mixed = false;
$found_generic_params = $template_result->upper_bounds ?: [];
foreach ($this->types as $key => $atomic_type) {
$atomic_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase);
if ($atomic_type instanceof Type\Atomic\TTemplateParam) {
$template_type = null;
$traversed_type = \Psalm\Internal\Type\UnionTemplateHandler::getRootTemplateType(
$template_types,
$found_generic_params,
$atomic_type->param_name,
$atomic_type->defining_class
);
@ -1183,7 +1183,7 @@ class Union implements TypeNode
}
}
} elseif ($codebase) {
foreach ($template_types as $template_type_map) {
foreach ($found_generic_params as $template_type_map) {
foreach ($template_type_map as $template_class => $_) {
if (substr($template_class, 0, 3) === 'fn-') {
continue;
@ -1199,10 +1199,11 @@ class Union implements TypeNode
$param_map = $classlike_storage->template_type_extends[$defining_class];
if (isset($param_map[$key])
&& isset($template_types[(string) $param_map[$key]][$template_class])
&& isset($found_generic_params[(string) $param_map[$key]][$template_class])
) {
$template_type
= clone $template_types[(string) $param_map[$key]][$template_class][0];
= clone $found_generic_params
[(string) $param_map[$key]][$template_class][0];
}
}
}
@ -1224,8 +1225,8 @@ class Union implements TypeNode
}
}
} elseif ($atomic_type instanceof Type\Atomic\TTemplateParamClass) {
$template_type = isset($template_types[$atomic_type->param_name][$atomic_type->defining_class])
? clone $template_types[$atomic_type->param_name][$atomic_type->defining_class][0]
$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]
: null;
$class_template_type = null;
@ -1263,14 +1264,14 @@ class Union implements TypeNode
$template_type = null;
if (isset($template_types[$atomic_type->array_param_name][$atomic_type->defining_class])
&& !empty($template_types[$atomic_type->offset_param_name])
if (isset($found_generic_params[$atomic_type->array_param_name][$atomic_type->defining_class])
&& !empty($found_generic_params[$atomic_type->offset_param_name])
) {
$array_template_type
= $template_types[$atomic_type->array_param_name][$atomic_type->defining_class][0];
= $found_generic_params[$atomic_type->array_param_name][$atomic_type->defining_class][0];
$offset_template_type
= array_values(
$template_types[$atomic_type->offset_param_name]
$found_generic_params[$atomic_type->offset_param_name]
)[0][0];
if ($array_template_type->isSingle()
@ -1305,8 +1306,8 @@ class Union implements TypeNode
} elseif ($atomic_type instanceof Type\Atomic\TConditional
&& $codebase
) {
$template_type = isset($template_types[$atomic_type->param_name][$atomic_type->defining_class])
? clone $template_types[$atomic_type->param_name][$atomic_type->defining_class][0]
$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]
: null;
$class_template_type = null;

View File

@ -1187,6 +1187,29 @@ class FunctionTemplateTest extends TestCase
useFooAndBar(decorateWithFoo(decorateWithBar($input)));
}'
],
'bottomTypeInClosureShouldNotBind' => [
'<?php
/**
* @template T
* @param class-string<T> $className
* @param Closure(T):void $outmaker
* @return T
*/
function createProxy(
string $className,
Closure $outmaker
) : object {
$t = new $className();
$outmaker($t);
return $t;
}
class A {
public function bar() : void {}
}
createProxy(A::class, function(object $o):void {})->bar();'
],
];
}
@ -1354,7 +1377,7 @@ class FunctionTemplateTest extends TestCase
}
apply(function(int $_i) : void {}, "hello");',
'error_message' => 'InvalidScalarArgument',
'error_message' => 'InvalidArgument',
],
'bindFirstTemplatedClosureParameterTypeCoercion' => [
'<?php
@ -1373,7 +1396,7 @@ class FunctionTemplateTest extends TestCase
class AChild extends A {}
apply(function(AChild $_i) : void {}, new A());',
'error_message' => 'ArgumentTypeCoercion',
'error_message' => 'InvalidArgument',
],
'callableDoesNotReturnItself' => [
@ -1395,7 +1418,7 @@ class FunctionTemplateTest extends TestCase
function takesReturnTCallable(callable $s) {}
takesReturnTCallable($b);',
'error_message' => 'InvalidScalarArgument',
'error_message' => 'InvalidArgument',
],
'multipleArgConstraintWithMoreRestrictiveFirstArg' => [
'<?php
@ -1418,7 +1441,7 @@ class FunctionTemplateTest extends TestCase
function(A $_a) : void {},
new A()
);',
'error_message' => 'ArgumentTypeCoercion',
'error_message' => 'InvalidArgument',
],
'multipleArgConstraintWithMoreRestrictiveSecondArg' => [
'<?php
@ -1441,7 +1464,7 @@ class FunctionTemplateTest extends TestCase
function(AChild $_a) : void {},
new A()
);',
'error_message' => 'ArgumentTypeCoercion',
'error_message' => 'InvalidArgument',
],
'multipleArgConstraintWithLessRestrictiveThirdArg' => [
'<?php
@ -1464,7 +1487,7 @@ class FunctionTemplateTest extends TestCase
function(AChild $_a) : void {},
new A()
);',
'error_message' => 'ArgumentTypeCoercion',
'error_message' => 'InvalidArgument',
],
'possiblyInvalidArgumentWithUnionFirstArg' => [
'<?php
@ -1670,6 +1693,32 @@ class FunctionTemplateTest extends TestCase
}',
'error_message' => 'InvalidReturnStatement',
],
'bottomTypeInClosureShouldClash' => [
'<?php
/**
* @template T
* @param class-string<T> $className
* @param Closure(T):void $outmaker
* @return T
*/
function createProxy(
string $className,
Closure $outmaker
) : object {
$t = new $className();
$outmaker($t);
return $t;
}
class A {
public function bar() : void {}
}
class B {}
createProxy(A::class, function(B $o):void {})->bar();',
'error_message' => 'InvalidArgument'
],
];
}
}