1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Improve handling of array_map, faking out calls where nececssary

This commit is contained in:
Brown 2020-06-25 13:05:34 -04:00
parent f458959af5
commit 95bf7f835b
12 changed files with 281 additions and 252 deletions

View File

@ -124,16 +124,11 @@ class ReturnTypeAnalyzer
$inferred_yield_types = [];
$ignore_nullable_issues = false;
$ignore_falsable_issues = false;
$inferred_return_type_parts = ReturnTypeCollector::getReturnTypes(
$codebase,
$type_provider,
$function_stmts,
$inferred_yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues,
true
);
@ -153,10 +148,10 @@ class ReturnTypeAnalyzer
) {
// only add null if we have a return statement elsewhere and it wasn't void
foreach ($inferred_return_type_parts as $inferred_return_type_part) {
if (!$inferred_return_type_part instanceof Type\Atomic\TVoid) {
if (!$inferred_return_type_part->isVoid()) {
$atomic_null = new Type\Atomic\TNull();
$atomic_null->from_docblock = true;
$inferred_return_type_parts[] = $atomic_null;
$inferred_return_type_parts[] = new Type\Union([$atomic_null]);
break;
}
}
@ -213,9 +208,11 @@ class ReturnTypeAnalyzer
}
$inferred_return_type = $inferred_return_type_parts
? TypeCombination::combineTypes($inferred_return_type_parts)
? \Psalm\Type::combineUnionTypeArray($inferred_return_type_parts, $codebase)
: Type::getVoid();
$inferred_yield_type = $inferred_yield_types ? TypeCombination::combineTypes($inferred_yield_types) : null;
$inferred_yield_type = $inferred_yield_types
? \Psalm\Type::combineUnionTypeArray($inferred_yield_types, $codebase)
: null;
if ($inferred_yield_type) {
$inferred_return_type = $inferred_yield_type;
@ -233,7 +230,7 @@ class ReturnTypeAnalyzer
&& !$inferred_yield_types
) {
foreach ($inferred_return_type_parts as $inferred_return_type_part) {
if ($inferred_return_type_part instanceof Type\Atomic\TVoid) {
if ($inferred_return_type_part->isVoid()) {
$unsafe_return_type = true;
}
}
@ -592,7 +589,7 @@ class ReturnTypeAnalyzer
}
}
if (!$ignore_nullable_issues
if (!$inferred_return_type->ignore_nullable_issues
&& $inferred_return_type->isNullable()
&& !$declared_return_type->isNullable()
&& !$declared_return_type->hasTemplate()
@ -631,7 +628,7 @@ class ReturnTypeAnalyzer
}
}
if (!$ignore_falsable_issues
if (!$inferred_return_type->ignore_falsable_issues
&& $inferred_return_type->isFalsable()
&& !$declared_return_type->isFalsable()
&& !$declared_return_type->hasBool()

View File

@ -17,20 +17,16 @@ class ReturnTypeCollector
* Gets the return types from a list of statements
*
* @param array<PhpParser\Node> $stmts
* @param list<Type\Atomic> $yield_types
* @param bool $ignore_nullable_issues
* @param bool $ignore_falsable_issues
* @param list<Type\Union> $yield_types
* @param bool $collapse_types
*
* @return list<Type\Atomic> a list of return types
* @return list<Type\Union> a list of return types
*/
public static function getReturnTypes(
\Psalm\Codebase $codebase,
\Psalm\Internal\Provider\NodeDataProvider $nodes,
array $stmts,
array &$yield_types,
bool &$ignore_nullable_issues = false,
bool &$ignore_falsable_issues = false,
bool $collapse_types = false
) {
$return_types = [];
@ -43,19 +39,11 @@ class ReturnTypeCollector
}
if (!$stmt->expr) {
$return_types[] = new Atomic\TVoid();
$return_types[] = Type::getVoid();
} elseif ($stmt_type = $nodes->getType($stmt)) {
$return_types = array_merge(array_values($stmt_type->getAtomicTypes()), $return_types);
if ($stmt_type->ignore_nullable_issues) {
$ignore_nullable_issues = true;
}
if ($stmt_type->ignore_falsable_issues) {
$ignore_falsable_issues = true;
}
$return_types[] = $stmt_type;
} else {
$return_types[] = new Atomic\TMixed();
$return_types[] = Type::getMixed();
}
break;
@ -86,9 +74,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
[$stmt->expr->expr],
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Expression
@ -107,9 +93,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$stmt->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
@ -120,9 +104,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$elseif->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
}
@ -134,9 +116,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$stmt->else->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
}
@ -147,9 +127,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$stmt->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
@ -160,9 +138,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$catch->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
}
@ -174,9 +150,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$stmt->finally->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
}
@ -187,9 +161,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$stmt->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
@ -199,9 +171,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$stmt->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
} elseif ($stmt instanceof PhpParser\Node\Stmt\While_) {
@ -212,9 +182,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$stmt->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) {
@ -224,9 +192,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$stmt->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
@ -237,9 +203,7 @@ class ReturnTypeCollector
$codebase,
$nodes,
$case->stmts,
$yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues
$yield_types
)
);
}
@ -253,7 +217,9 @@ class ReturnTypeCollector
$key_type = null;
$value_type = null;
foreach ($yield_types as $type) {
$yield_type = Type::combineUnionTypeArray($yield_types, null);
foreach ($yield_type->getAtomicTypes() as $type) {
if ($type instanceof Type\Atomic\ObjectLike) {
$type = $type->getGenericArrayType();
}
@ -290,15 +256,17 @@ class ReturnTypeCollector
}
$yield_types = [
new Atomic\TGenericObject(
'Generator',
[
$key_type ?: Type::getMixed(),
$value_type ?: Type::getMixed(),
Type::getMixed(),
$return_types ? new Type\Union($return_types) : Type::getVoid()
]
),
new Type\Union([
new Atomic\TGenericObject(
'Generator',
[
$key_type ?: Type::getMixed(),
$value_type ?: Type::getMixed(),
Type::getMixed(),
$return_types ? Type::combineUnionTypeArray($return_types, null) : Type::getVoid()
]
),
])
];
}
}
@ -309,7 +277,7 @@ class ReturnTypeCollector
/**
* @param PhpParser\Node\Expr $stmt
*
* @return list<Atomic>
* @return list<Type\Union>
*/
protected static function getYieldTypeFromExpression(
PhpParser\Node\Expr $stmt,
@ -335,16 +303,16 @@ class ReturnTypeCollector
]
);
return [$generator_type];
return [new Type\Union([$generator_type])];
}
return [new Atomic\TMixed()];
return [Type::getMixed()];
} elseif ($stmt instanceof PhpParser\Node\Expr\YieldFrom) {
if ($stmt_expr_type = $nodes->getType($stmt->expr)) {
return array_values($stmt_expr_type->getAtomicTypes());
return [$stmt_expr_type];
}
return [new Atomic\TMixed()];
return [Type::getMixed()];
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
return array_merge(
self::getYieldTypeFromExpression($stmt->left, $nodes),

View File

@ -602,28 +602,23 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
$closure_yield_types = [];
$ignore_nullable_issues = false;
$ignore_falsable_issues = false;
$closure_return_types = ReturnTypeCollector::getReturnTypes(
$codebase,
$type_provider,
$function_stmts,
$closure_yield_types,
$ignore_nullable_issues,
$ignore_falsable_issues,
true
);
$closure_return_type = $closure_return_types
? \Psalm\Internal\Type\TypeCombination::combineTypes(
? \Psalm\Type::combineUnionTypeArray(
$closure_return_types,
$codebase
)
: null;
$closure_yield_type = $closure_yield_types
? \Psalm\Internal\Type\TypeCombination::combineTypes(
? \Psalm\Type::combineUnionTypeArray(
$closure_yield_types,
$codebase
)

View File

@ -841,6 +841,10 @@ class ArgumentAnalyzer
foreach ($function_ids as $function_id) {
if (strpos($function_id, '::') !== false) {
if ($function_id[0] === '$') {
$function_id = \substr($function_id, 1);
}
$function_id_parts = explode('&', $function_id);
$non_existent_method_ids = [];

View File

@ -7,6 +7,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TypeAnalyzer;
use Psalm\Internal\Codebase\InternalCallMapHandler;
@ -240,6 +241,16 @@ class ArgumentsAnalyzer
&& !$replaced_type_part->params[$closure_param_offset]->type->hasTemplate()
) {
if ($param_storage->type) {
if ($method_id === 'array_map' || $method_id === 'array_filter') {
ArrayFetchAnalyzer::taintArrayFetch(
$statements_analyzer,
$args[1 - $argument_offset]->value,
null,
$param_storage->type,
Type::getMixed()
);
}
if ($param_storage->type !== $param_storage->signature_type) {
continue;
}
@ -256,6 +267,16 @@ class ArgumentsAnalyzer
}
$param_storage->type = $replaced_type_part->params[$closure_param_offset]->type;
if ($method_id === 'array_map' || $method_id === 'array_filter') {
ArrayFetchAnalyzer::taintArrayFetch(
$statements_analyzer,
$args[1 - $argument_offset]->value,
null,
$param_storage->type,
Type::getMixed()
);
}
}
}
}

View File

@ -501,6 +501,10 @@ class ArrayFunctionArgumentsAnalyzer
$function_id = strtolower($function_id);
if (strpos($function_id, '::') !== false) {
if ($function_id[0] === '$') {
$function_id = \substr($function_id, 1);
}
$function_id_parts = explode('&', $function_id);
foreach ($function_id_parts as $function_id_part) {
@ -529,7 +533,7 @@ class ArrayFunctionArgumentsAnalyzer
$function_id_part = new \Psalm\Internal\MethodIdentifier(
$callable_fq_class_name,
$method_name
strtolower($method_name)
);
try {

View File

@ -500,7 +500,7 @@ class CallAnalyzer
}
}
$method_ids[] = $method_id;
$method_ids[] = '$' . $method_id;
}
}

View File

@ -68,12 +68,16 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
return Type::getArray();
}
$array_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
$array_arg = $call_args[1] ?? null;
if (!$array_arg) {
return Type::getArray();
}
$array_arg_atomic_type = null;
$array_arg_type = null;
if ($array_arg && ($array_arg_union_type = $statements_source->node_data->getType($array_arg))) {
if ($array_arg_union_type = $statements_source->node_data->getType($array_arg->value)) {
$arg_types = $array_arg_union_type->getAtomicTypes();
if (isset($arg_types['array'])) {
@ -117,6 +121,7 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
$mapping_function_ids,
$context,
$function_call_arg,
$array_arg,
$array_arg_type
);
}
@ -229,7 +234,7 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
private static function executeFakeCall(
\Psalm\Internal\Analyzer\StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\StaticCall $fake_method_call,
PhpParser\Node\Expr $fake_call,
Context $context
) : ?Type\Union {
$old_data_provider = $statements_analyzer->node_data;
@ -242,15 +247,35 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
}
if (!in_array('MixedArrayOffset', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['MixedArrayOffset']);
}
$was_inside_call = $context->inside_call;
$context->inside_call = true;
\Psalm\Internal\Analyzer\Statements\Expression\Call\StaticCallAnalyzer::analyze(
$statements_analyzer,
$fake_method_call,
$context
);
if ($fake_call instanceof PhpParser\Node\Expr\StaticCall) {
\Psalm\Internal\Analyzer\Statements\Expression\Call\StaticCallAnalyzer::analyze(
$statements_analyzer,
$fake_call,
$context
);
} elseif ($fake_call instanceof PhpParser\Node\Expr\MethodCall) {
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
$statements_analyzer,
$fake_call,
$context
);
} elseif ($fake_call instanceof PhpParser\Node\Expr\FuncCall) {
\Psalm\Internal\Analyzer\Statements\Expression\Call\FunctionCallAnalyzer::analyze(
$statements_analyzer,
$fake_call,
$context
);
} else {
throw new \UnexpectedValueException('UnrecognizedCall');
}
$context->inside_call = $was_inside_call;
@ -258,7 +283,11 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
}
$return_type = $statements_analyzer->node_data->getType($fake_method_call) ?: null;
if (!in_array('MixedArrayOffset', $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues(['MixedArrayOffset']);
}
$return_type = $statements_analyzer->node_data->getType($fake_call) ?: null;
$statements_analyzer->node_data = $old_data_provider;
@ -273,149 +302,152 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
array $mapping_function_ids,
Context $context,
PhpParser\Node\Arg $function_call_arg,
PhpParser\Node\Arg $array_arg,
?\Psalm\Internal\Type\ArrayType $array_arg_type
) : Type\Union {
$call_map = InternalCallMapHandler::getCallMap();
$mapping_return_type = null;
$closure_param_type = null;
$codebase = $statements_source->getCodebase();
foreach ($mapping_function_ids as $mapping_function_id) {
$mapping_function_id = strtolower($mapping_function_id);
$mapping_function_id_parts = explode('&', $mapping_function_id);
$function_id_return_type = null;
foreach ($mapping_function_id_parts as $mapping_function_id_part) {
if (isset($call_map[$mapping_function_id_part][0])) {
if ($call_map[$mapping_function_id_part][0]) {
$mapped_function_return =
Type::parseString($call_map[$mapping_function_id_part][0]);
if (strpos($mapping_function_id_part, '::') !== false) {
$is_instance = false;
if ($function_id_return_type) {
$function_id_return_type = Type::combineUnionTypes(
$function_id_return_type,
$mapped_function_return
);
} else {
$function_id_return_type = $mapped_function_return;
}
if ($mapping_function_id_part[0] === '$') {
$mapping_function_id_part = \substr($mapping_function_id_part, 1);
$is_instance = true;
}
} else {
if (strpos($mapping_function_id_part, '::') !== false) {
$method_id_parts = explode('::', $mapping_function_id_part);
$callable_fq_class_name = $method_id_parts[0];
if (in_array($callable_fq_class_name, ['self', 'static', 'parent'], true)) {
continue;
}
$method_id_parts = explode('::', $mapping_function_id_part);
[$callable_fq_class_name, $callable_method_name] = $method_id_parts;
if (!$codebase->classlikes->classOrInterfaceExists($callable_fq_class_name)) {
continue;
}
$class_storage = $codebase->classlike_storage_provider->get($callable_fq_class_name);
$method_id = new \Psalm\Internal\MethodIdentifier(
$callable_fq_class_name,
$method_id_parts[1]
if ($is_instance) {
$fake_method_call = new PhpParser\Node\Expr\MethodCall(
new PhpParser\Node\Expr\Variable(
'__fake_method_call_var__',
$function_call_arg->getAttributes()
),
new PhpParser\Node\Identifier(
$callable_method_name,
$function_call_arg->getAttributes()
),
[
new PhpParser\Node\Arg(
new PhpParser\Node\Expr\ArrayDimFetch(
$array_arg->value,
new PhpParser\Node\Expr\Variable(
'__fake_offset_var__',
$array_arg->value->getAttributes()
),
$array_arg->value->getAttributes()
),
false,
false,
$array_arg->getAttributes()
)
],
$function_call_arg->getAttributes()
);
if (!$codebase->methods->methodExists(
$method_id,
!$context->collect_initializations
&& !$context->collect_mutations
? $context->calling_method_id
: null,
$codebase->collect_locations
? new CodeLocation(
$statements_source,
$function_call_arg->value
) : null,
null,
$statements_source->getFilePath()
)) {
continue;
}
$context->vars_in_scope['$__fake_offset_var__'] = Type::getMixed();
$context->vars_in_scope['$__fake_method_call_var__'] = new Type\Union([
new Type\Atomic\TNamedObject($callable_fq_class_name)
]);
$params = $codebase->methods->getMethodParams(
$method_id,
$statements_source
);
if (isset($params[0]->type)) {
$closure_param_type = $params[0]->type;
}
$self_class = 'self';
$return_type = $codebase->methods->getMethodReturnType(
new \Psalm\Internal\MethodIdentifier(...$method_id_parts),
$self_class
) ?: Type::getMixed();
$static_class = $self_class;
if ($self_class !== 'self') {
$static_class = $class_storage->name;
}
$return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,
$return_type,
$self_class,
$static_class,
$class_storage->parent_class
);
if ($function_id_return_type) {
$function_id_return_type = Type::combineUnionTypes(
$function_id_return_type,
$return_type
);
} else {
$function_id_return_type = $return_type;
}
} else {
if (!$mapping_function_id_part
|| !$codebase->functions->functionExists(
$statements_source,
$mapping_function_id_part
)
) {
$function_id_return_type = Type::getMixed();
continue;
}
$function_storage = $codebase->functions->getStorage(
$fake_method_return_type = self::executeFakeCall(
$statements_source,
$mapping_function_id_part
$fake_method_call,
$context
);
if (isset($function_storage->params[0]->type)) {
$closure_param_type = $function_storage->params[0]->type;
}
unset(
$context->vars_in_scope['$__fake_offset_var__'],
$context->vars_in_scope['$__method_call_var__']
);
} else {
$fake_method_call = new PhpParser\Node\Expr\StaticCall(
new PhpParser\Node\Name\FullyQualified(
$callable_fq_class_name,
$function_call_arg->getAttributes()
),
new PhpParser\Node\Identifier(
$callable_method_name,
$function_call_arg->getAttributes()
),
[
new PhpParser\Node\Arg(
new PhpParser\Node\Expr\ArrayDimFetch(
$array_arg->value,
new PhpParser\Node\Expr\Variable(
'__fake_offset_var__',
$array_arg->value->getAttributes()
),
$array_arg->value->getAttributes()
),
false,
false,
$array_arg->getAttributes()
)
],
$function_call_arg->getAttributes()
);
$return_type = $function_storage->return_type ?: Type::getMixed();
$context->vars_in_scope['$__fake_offset_var__'] = Type::getMixed();
if ($function_id_return_type) {
$function_id_return_type = Type::combineUnionTypes(
$function_id_return_type,
$return_type
);
} else {
$function_id_return_type = $return_type;
}
$fake_method_return_type = self::executeFakeCall(
$statements_source,
$fake_method_call,
$context
);
unset($context->vars_in_scope['$__fake_offset_var__']);
}
$function_id_return_type = $fake_method_return_type ?: Type::getMixed();
} else {
$fake_function_call = new PhpParser\Node\Expr\FuncCall(
new PhpParser\Node\Name\FullyQualified(
$mapping_function_id_part,
$function_call_arg->getAttributes()
),
[
new PhpParser\Node\Arg(
new PhpParser\Node\Expr\ArrayDimFetch(
$array_arg->value,
new PhpParser\Node\Expr\Variable(
'__fake_offset_var__',
$array_arg->value->getAttributes()
),
$array_arg->value->getAttributes()
),
false,
false,
$array_arg->getAttributes()
)
],
$function_call_arg->getAttributes()
);
$context->vars_in_scope['$__fake_offset_var__'] = Type::getMixed();
$fake_function_return_type = self::executeFakeCall(
$statements_source,
$fake_function_call,
$context
);
unset($context->vars_in_scope['$__fake_offset_var__']);
$function_id_return_type = $fake_function_return_type ?: Type::getMixed();
}
}
if ($function_id_return_type === null) {
$mapping_return_type = Type::getMixed();
} elseif (!$mapping_return_type) {
if (!$mapping_return_type) {
$mapping_return_type = $function_id_return_type;
} else {
$mapping_return_type = Type::combineUnionTypes(
@ -426,42 +458,6 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
}
}
if ($closure_param_type
&& $mapping_return_type->hasTemplate()
&& $array_arg_type
) {
$mapping_return_type = clone $mapping_return_type;
$template_types = [];
foreach ($closure_param_type->getTemplateTypes() as $template_type) {
$template_types[$template_type->param_name] = [
($template_type->defining_class) => [$template_type->as]
];
}
$template_result = new \Psalm\Internal\Type\TemplateResult(
$template_types,
[]
);
\Psalm\Internal\Type\UnionTemplateHandler::replaceTemplateTypesWithStandins(
$closure_param_type,
$template_result,
$codebase,
$statements_source,
$array_arg_type->value,
0,
$context->self,
$context->calling_method_id ?: $context->calling_function_id
);
$mapping_return_type->replaceTemplateTypesWithArgTypes(
$template_result,
$codebase
);
}
return $mapping_return_type;
}
}

View File

@ -193,7 +193,7 @@ class ArrayReduceReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturn
$call_map = InternalCallMapHandler::getCallMap();
foreach ($mapping_function_ids as $mapping_function_id) {
$mapping_function_id = strtolower($mapping_function_id);
$mapping_function_id = $mapping_function_id;
$mapping_function_id_parts = explode('&', $mapping_function_id);
@ -214,6 +214,10 @@ class ArrayReduceReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturn
}
} elseif ($mapping_function_id_part) {
if (strpos($mapping_function_id_part, '::') !== false) {
if ($mapping_function_id_part[0] === '$') {
$mapping_function_id_part = \substr($mapping_function_id_part, 1);
}
list($callable_fq_class_name, $method_name) = explode('::', $mapping_function_id_part);
if (in_array($callable_fq_class_name, ['self', 'static', 'parent'], true)) {
@ -222,7 +226,7 @@ class ArrayReduceReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturn
$method_id = new \Psalm\Internal\MethodIdentifier(
$callable_fq_class_name,
$method_name
strtolower($method_name)
);
if (!$codebase->methods->methodExists(

View File

@ -437,6 +437,20 @@ abstract class Type
return new Union([new TResource]);
}
/**
* @param non-empty-list<Type\Union> $union_types
*/
public static function combineUnionTypeArray(array $union_types, ?Codebase $codebase) : Type\Union
{
$first_type = array_pop($union_types);
foreach ($union_types as $type) {
$first_type = self::combineUnionTypes($first_type, $type, $codebase);
}
return $first_type;
}
/**
* Combines two union types into one
*

View File

@ -939,9 +939,10 @@ class PluginTest extends \Psalm\Tests\TestCase
);
$mock = $this->getMockBuilder(\stdClass::class)->setMethods(['check'])->getMock();
$mock->expects($this->exactly(3))
$mock->expects($this->exactly(4))
->method('check')
->withConsecutive(
[$this->equalTo('b')],
[$this->equalTo('array_map')],
[$this->equalTo('fopen')],
[$this->equalTo('a')]

View File

@ -1292,6 +1292,31 @@ class TaintTest extends TestCase
echo $a[0]["a"];',
'error_message' => 'TaintedInput',
],
'taintThroughArrayMapExplicitClosure' => [
'<?php
$get = array_map(function($str) { return trim($str);}, $_GET);
echo $get["test"];',
'error_message' => 'TaintedInput',
],
'taintThroughArrayMapExplicitTypedClosure' => [
'<?php
$get = array_map(function(string $str) : string { return trim($str);}, $_GET);
echo $get["test"];',
'error_message' => 'TaintedInput',
],
'taintThroughArrayMapExplicitArrowFunction' => [
'<?php
$get = array_map(fn($str) => trim($str), $_GET);
echo $get["test"];',
'error_message' => 'TaintedInput',
],
'taintThroughArrayMapImplicitFunctionCall' => [
'<?php
$a = ["test" => $_GET["name"]];
$get = array_map("trim", $a);
echo $get["test"];',
'error_message' => 'TaintedInput',
],
];
}
}