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:
parent
99549871b6
commit
067104e170
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -589,7 +589,7 @@ class ArrayAssignmentAnalyzer
|
||||
);
|
||||
|
||||
$current_type->replaceTemplateTypesWithArgTypes(
|
||||
$template_result->generic_params,
|
||||
$template_result,
|
||||
$codebase
|
||||
);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -456,7 +456,8 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
|
||||
);
|
||||
|
||||
$mapping_return_type->replaceTemplateTypesWithArgTypes(
|
||||
$template_result->generic_params
|
||||
$template_result,
|
||||
$codebase
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user