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:
parent
f458959af5
commit
95bf7f835b
@ -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()
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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 = [];
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -500,7 +500,7 @@ class CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$method_ids[] = $method_id;
|
||||
$method_ids[] = '$' . $method_id;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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')]
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user