1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-10 06:58:41 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
Bruce Weirdan 5758d31161
Apply literal number separator rector
I opted for customary 3-digit groups, except version id, where 2-digit
groups are used to match the version id encoding.
2022-01-05 01:00:05 +02:00

1632 lines
64 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\ClassLikeNameOptions;
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
use Psalm\Internal\Analyzer\MethodAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CastAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TraitAnalyzer;
use Psalm\Internal\Codebase\ConstantTypeResolver;
use Psalm\Internal\Codebase\InternalCallMapHandler;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Type\TemplateBound;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
use Psalm\Internal\Type\TypeExpander;
use Psalm\Issue\ArgumentTypeCoercion;
use Psalm\Issue\ImplicitToStringCast;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\InvalidLiteralArgument;
use Psalm\Issue\InvalidScalarArgument;
use Psalm\Issue\MixedArgument;
use Psalm\Issue\MixedArgumentTypeCoercion;
use Psalm\Issue\NamedArgumentNotAllowed;
use Psalm\Issue\NoValue;
use Psalm\Issue\NullArgument;
use Psalm\Issue\PossiblyFalseArgument;
use Psalm\Issue\PossiblyInvalidArgument;
use Psalm\Issue\PossiblyNullArgument;
use Psalm\IssueBuffer;
use Psalm\Node\VirtualArg;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TClassStringMap;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
use function array_merge;
use function count;
use function explode;
use function in_array;
use function ord;
use function preg_split;
use function reset;
use function strpos;
use function strtolower;
use function substr;
use const PREG_SPLIT_NO_EMPTY;
/**
* @internal
*/
class ArgumentAnalyzer
{
/**
* @param array<string, array<string, Union>> $class_generic_params
* @return false|null
*/
public static function checkArgumentMatches(
StatementsAnalyzer $statements_analyzer,
?string $cased_method_id,
?MethodIdentifier $method_id,
?string $self_fq_class_name,
?string $static_fq_class_name,
CodeLocation $function_call_location,
?FunctionLikeParameter $function_param,
int $argument_offset,
int $unpacked_argument_offset,
bool $allow_named_args,
PhpParser\Node\Arg $arg,
?Union $arg_value_type,
Context $context,
array $class_generic_params,
?TemplateResult $template_result,
bool $specialize_taint,
bool $in_call_map
): ?bool {
$codebase = $statements_analyzer->getCodebase();
if (!$arg_value_type) {
if ($function_param && !$function_param->by_ref) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
$param_type = $function_param->type;
if ($function_param->is_variadic
&& $param_type
&& $param_type->hasArray()
) {
/**
* @psalm-suppress PossiblyUndefinedStringArrayOffset
* @var TList|TArray
*/
$array_type = $param_type->getAtomicTypes()['array'];
if ($array_type instanceof TList) {
$param_type = $array_type->type_param;
} else {
$param_type = $array_type->type_params[1];
}
}
if ($param_type && !$param_type->hasMixed()) {
IssueBuffer::maybeAdd(
new MixedArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
. ' cannot be mixed, expecting ' . $param_type,
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
}
return null;
}
if (!$function_param) {
return null;
}
if ($function_param->expect_variable
&& $arg_value_type->isSingleStringLiteral()
&& !$arg->value instanceof PhpParser\Node\Scalar\MagicConst
&& !$arg->value instanceof PhpParser\Node\Expr\ConstFetch
&& !$arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
) {
$values = preg_split('//u', $arg_value_type->getSingleStringLiteral()->value, -1, PREG_SPLIT_NO_EMPTY);
if ($values !== false) {
$prev_ord = 0;
$gt_count = 0;
foreach ($values as $value) {
$ord = ord($value);
if ($ord > $prev_ord) {
$gt_count++;
}
$prev_ord = $ord;
}
if (count($values) < 12 || ($gt_count / count($values)) < 0.8) {
IssueBuffer::maybeAdd(
new InvalidLiteralArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
. ' expects a non-literal value, ' . $arg_value_type->getId() . ' provided',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
}
}
if (self::checkFunctionLikeTypeMatches(
$statements_analyzer,
$codebase,
$cased_method_id,
$method_id,
$self_fq_class_name,
$static_fq_class_name,
$function_call_location,
$function_param,
$allow_named_args,
$arg_value_type,
$argument_offset,
$unpacked_argument_offset,
$arg,
$context,
$class_generic_params,
$template_result,
$specialize_taint,
$in_call_map
) === false) {
return false;
}
return null;
}
/**
* @param array<string, array<string, Union>> $class_generic_params
* @return false|null
*/
private static function checkFunctionLikeTypeMatches(
StatementsAnalyzer $statements_analyzer,
Codebase $codebase,
?string $cased_method_id,
?MethodIdentifier $method_id,
?string $self_fq_class_name,
?string $static_fq_class_name,
CodeLocation $function_call_location,
FunctionLikeParameter $function_param,
bool $allow_named_args,
Union $arg_type,
int $argument_offset,
int $unpacked_argument_offset,
PhpParser\Node\Arg $arg,
Context $context,
?array $class_generic_params,
?TemplateResult $template_result,
bool $specialize_taint,
bool $in_call_map
): ?bool {
if (!$function_param->type) {
if (!$codebase->infer_types_from_usage && !$statements_analyzer->data_flow_graph) {
return null;
}
$param_type = Type::getMixed();
} else {
$param_type = clone $function_param->type;
}
$bindable_template_params = [];
if ($template_result) {
$bindable_template_params = $param_type->getTemplateTypes();
}
$parent_class = null;
$classlike_storage = null;
$static_classlike_storage = null;
if ($self_fq_class_name) {
$classlike_storage = $codebase->classlike_storage_provider->get($self_fq_class_name);
$parent_class = $classlike_storage->parent_class;
$static_classlike_storage = $classlike_storage;
if ($static_fq_class_name && $static_fq_class_name !== $self_fq_class_name) {
$static_classlike_storage = $codebase->classlike_storage_provider->get($static_fq_class_name);
}
}
$param_type = TypeExpander::expandUnion(
$codebase,
$param_type,
$classlike_storage->name ?? null,
$static_classlike_storage->name ?? null,
$parent_class,
true,
false,
$static_classlike_storage->final ?? false,
true
);
if ($class_generic_params) {
// here we're replacing the param types and arg types with the bound
// class template params.
//
// For example, if we're operating on a class Foo with params TKey and TValue,
// and we're calling a method "add(TKey $key, TValue $value)" on an instance
// of that class where we know that TKey is int and TValue is string, then we
// want to substitute the expected parameters so it's as if we were actually
// calling "add(int $key, string $value)"
$readonly_template_result = new TemplateResult($class_generic_params, []);
// This flag ensures that the template results will never be written to
// It also supercedes the `$add_lower_bounds` flag so that closure params
// dont get overwritten
$readonly_template_result->readonly = true;
$arg_value_type = $statements_analyzer->node_data->getType($arg->value);
$param_type = TemplateStandinTypeReplacer::replace(
$param_type,
$readonly_template_result,
$codebase,
$statements_analyzer,
$arg_value_type,
$argument_offset,
$context->self,
$context->calling_function_id ?: $context->calling_method_id
);
$arg_type = TemplateStandinTypeReplacer::replace(
$arg_type,
$readonly_template_result,
$codebase,
$statements_analyzer,
$arg_value_type,
$argument_offset,
$context->self,
$context->calling_function_id ?: $context->calling_method_id
);
}
if ($template_result && $template_result->template_types) {
$arg_type_param = $arg_type;
if ($arg->unpack) {
$arg_type_param = null;
foreach ($arg_type->getAtomicTypes() as $arg_atomic_type) {
if ($arg_atomic_type instanceof TArray
|| $arg_atomic_type instanceof TList
|| $arg_atomic_type instanceof TKeyedArray
) {
if ($arg_atomic_type instanceof TKeyedArray) {
$arg_type_param = $arg_atomic_type->getGenericValueType();
} elseif ($arg_atomic_type instanceof TList) {
$arg_type_param = $arg_atomic_type->type_param;
} else {
$arg_type_param = $arg_atomic_type->type_params[1];
}
} elseif ($arg_atomic_type instanceof TIterable) {
$arg_type_param = $arg_atomic_type->type_params[1];
} elseif ($arg_atomic_type instanceof TNamedObject) {
ForeachAnalyzer::getKeyValueParamsForTraversableObject(
$arg_atomic_type,
$codebase,
$key_type,
$arg_type_param
);
}
}
if (!$arg_type_param) {
$arg_type_param = Type::getMixed();
$arg_type_param->parent_nodes = $arg_type->parent_nodes;
}
}
$param_type = TemplateStandinTypeReplacer::replace(
$param_type,
$template_result,
$codebase,
$statements_analyzer,
$arg_type_param,
$argument_offset,
!$statements_analyzer->isStatic()
&& (!$method_id || $method_id->method_name !== '__construct')
? $context->self
: null,
$context->calling_method_id ?: $context->calling_function_id
);
foreach ($bindable_template_params as $template_type) {
if (!isset(
$template_result->lower_bounds
[$template_type->param_name]
[$template_type->defining_class]
)) {
if (isset(
$template_result->upper_bounds
[$template_type->param_name]
[$template_type->defining_class]
)) {
$template_result->lower_bounds[$template_type->param_name][$template_type->defining_class] = [
new TemplateBound(
clone $template_result->upper_bounds
[$template_type->param_name]
[$template_type->defining_class]->type
)
];
} else {
$template_result->lower_bounds[$template_type->param_name][$template_type->defining_class] = [
new TemplateBound(
clone $template_type->as
)
];
}
}
}
$param_type = TypeExpander::expandUnion(
$codebase,
$param_type,
$classlike_storage->name ?? null,
$static_classlike_storage->name ?? null,
$parent_class,
true,
false,
$static_classlike_storage->final ?? false,
true
);
}
$fleshed_out_signature_type = $function_param->signature_type
? TypeExpander::expandUnion(
$codebase,
$function_param->signature_type,
$classlike_storage->name ?? null,
$static_classlike_storage->name ?? null,
$parent_class
)
: null;
$unpacked_atomic_array = null;
if ($arg->unpack) {
if ($arg_type->hasMixed()) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
IssueBuffer::maybeAdd(
new MixedArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
. ' cannot unpack ' . $arg_type->getId() . ', expecting iterable',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
if ($cased_method_id) {
$arg_location = new CodeLocation($statements_analyzer->getSource(), $arg->value);
self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$method_id,
$argument_offset,
$arg_location,
$function_call_location,
$function_param,
$arg_type,
$arg->value,
$context,
$specialize_taint
);
}
return null;
}
if ($arg_type->hasArray()) {
/**
* @psalm-suppress PossiblyUndefinedStringArrayOffset
* @var TArray|TList|TKeyedArray|TClassStringMap
*/
$unpacked_atomic_array = $arg_type->getAtomicTypes()['array'];
$arg_key_allowed = true;
if ($unpacked_atomic_array instanceof TKeyedArray) {
if (!$allow_named_args && !$unpacked_atomic_array->getGenericKeyType()->isInt()) {
$arg_key_allowed = false;
}
if ($function_param->is_variadic) {
$arg_type = $unpacked_atomic_array->getGenericValueType();
} elseif ($codebase->analysis_php_version_id >= 8_00_00
&& $allow_named_args
&& isset($unpacked_atomic_array->properties[$function_param->name])
) {
$arg_type = clone $unpacked_atomic_array->properties[$function_param->name];
} elseif ($unpacked_atomic_array->is_list
&& isset($unpacked_atomic_array->properties[$unpacked_argument_offset])
) {
$arg_type = clone $unpacked_atomic_array->properties[$unpacked_argument_offset];
} elseif ($function_param->is_optional && $function_param->default_type) {
if ($function_param->default_type instanceof Union) {
$arg_type = $function_param->default_type;
} else {
$arg_type_atomic = ConstantTypeResolver::resolve(
$codebase->classlikes,
$function_param->default_type,
$statements_analyzer
);
$arg_type = new Union([$arg_type_atomic]);
}
} else {
$arg_type = Type::getMixed();
}
} elseif ($unpacked_atomic_array instanceof TList) {
$arg_type = $unpacked_atomic_array->type_param;
} elseif ($unpacked_atomic_array instanceof TClassStringMap) {
$arg_type = Type::getMixed();
} else {
if (!$allow_named_args && !$unpacked_atomic_array->type_params[0]->isInt()) {
$arg_key_allowed = false;
}
$arg_type = $unpacked_atomic_array->type_params[1];
}
if (!$arg_key_allowed) {
IssueBuffer::maybeAdd(
new NamedArgumentNotAllowed(
'Method ' . $cased_method_id
. ' called with named unpacked array ' . $unpacked_atomic_array->getId()
. ' (array with string keys)',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
} else {
$non_iterable = false;
$invalid_key = false;
$invalid_string_key = false;
$possibly_matches = false;
foreach ($arg_type->getAtomicTypes() as $atomic_type) {
if (!$atomic_type->isIterable($codebase)) {
$non_iterable = true;
} else {
$key_type = $codebase->getKeyValueParamsForTraversableObject($atomic_type)[0];
if (!UnionTypeComparator::isContainedBy(
$codebase,
$key_type,
Type::getArrayKey()
)) {
$invalid_key = true;
continue;
}
if (($codebase->analysis_php_version_id < 8_00_00 || !$allow_named_args)
&& !$key_type->isInt()
) {
$invalid_string_key = true;
continue;
}
$possibly_matches = true;
}
}
$issue_type = $possibly_matches ? PossiblyInvalidArgument::class : InvalidArgument::class;
if ($non_iterable) {
IssueBuffer::maybeAdd(
new $issue_type(
'Tried to unpack non-iterable ' . $arg_type->getId(),
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
if ($invalid_key) {
IssueBuffer::maybeAdd(
new $issue_type(
'Method ' . $cased_method_id
. ' called with unpacked iterable ' . $arg_type->getId()
. ' with invalid key (must be '
. ($codebase->analysis_php_version_id < 8_00_00 ? 'int' : 'int|string') . ')',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
if ($invalid_string_key) {
if ($codebase->analysis_php_version_id < 8_00_00) {
IssueBuffer::maybeAdd(
new $issue_type(
'String keys not supported in unpacked arguments',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
} else {
IssueBuffer::maybeAdd(
new NamedArgumentNotAllowed(
'Method ' . $cased_method_id
. ' called with named unpacked iterable ' . $arg_type->getId()
. ' (iterable with string keys)',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
}
return null;
}
} else {
if (!$allow_named_args && $arg->name !== null) {
IssueBuffer::maybeAdd(
new NamedArgumentNotAllowed(
'Method ' . $cased_method_id. ' called with named argument ' . $arg->name->name,
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
}
// bypass verifying argument types when collecting initialisations,
// because the argument locations are not reliable (file names normally differ)
// See https://github.com/vimeo/psalm/issues/5662
if ($arg instanceof VirtualArg
&& $context->collect_initializations
) {
return null;
}
if (self::verifyType(
$statements_analyzer,
$arg_type,
$param_type,
$fleshed_out_signature_type,
$cased_method_id,
$method_id,
$argument_offset,
new CodeLocation($statements_analyzer->getSource(), $arg->value),
$arg->value,
$context,
$function_param,
$arg->unpack,
$unpacked_atomic_array,
$specialize_taint,
$in_call_map,
$function_call_location
) === false) {
return false;
}
return null;
}
/**
* @param TKeyedArray|TArray|TList|TClassStringMap|null $unpacked_atomic_array
* @return null|false
* @psalm-suppress ComplexMethod
*/
public static function verifyType(
StatementsAnalyzer $statements_analyzer,
Union $input_type,
Union $param_type,
?Union $signature_param_type,
?string $cased_method_id,
?MethodIdentifier $method_id,
int $argument_offset,
CodeLocation $arg_location,
PhpParser\Node\Expr $input_expr,
Context $context,
FunctionLikeParameter $function_param,
bool $unpack,
?Atomic $unpacked_atomic_array,
bool $specialize_taint,
bool $in_call_map,
CodeLocation $function_call_location
): ?bool {
$codebase = $statements_analyzer->getCodebase();
if ($param_type->hasMixed()) {
if ($codebase->infer_types_from_usage
&& !$input_type->hasMixed()
&& !$param_type->from_docblock
&& !$param_type->had_template
&& $method_id
&& strpos($method_id->method_name, '__') !== 0
) {
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if ($declaring_method_id) {
$id_lc = strtolower((string) $declaring_method_id);
$codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset]
= Type::combineUnionTypes(
$codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset] ?? null,
clone $input_type,
$codebase
);
}
}
if ($cased_method_id) {
self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$method_id,
$argument_offset,
$arg_location,
$function_call_location,
$function_param,
$input_type,
$input_expr,
$context,
$specialize_taint
);
}
return null;
}
$method_identifier = $cased_method_id ? ' of ' . $cased_method_id : '';
if ($input_type->hasMixed()) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
$origin_locations = [];
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
foreach ($input_type->parent_nodes as $parent_node) {
$origin_locations = array_merge(
$origin_locations,
$statements_analyzer->data_flow_graph->getOriginLocations($parent_node)
);
}
}
$origin_location = count($origin_locations) === 1 ? reset($origin_locations) : null;
if ($origin_location && $origin_location->getHash() === $arg_location->getHash()) {
$origin_location = null;
}
IssueBuffer::maybeAdd(
new MixedArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier
. ' cannot be ' . $input_type->getId() . ', expecting ' .
$param_type,
$arg_location,
$cased_method_id,
$origin_location
),
$statements_analyzer->getSuppressedIssues()
);
if ($input_type->isMixed()) {
if (!$function_param->by_ref
&& !($function_param->is_variadic xor $unpack)
&& $cased_method_id !== 'echo'
&& $cased_method_id !== 'print'
&& (!$in_call_map || $context->strict_types)
) {
self::coerceValueAfterGatekeeperArgument(
$statements_analyzer,
$input_type,
false,
$input_expr,
$param_type,
$signature_param_type,
$context,
$unpack,
$unpacked_atomic_array
);
}
}
if ($cased_method_id) {
$input_type = self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$method_id,
$argument_offset,
$arg_location,
$function_call_location,
$function_param,
$input_type,
$input_expr,
$context,
$specialize_taint
);
}
if ($input_type->isMixed()) {
return null;
}
}
if ($input_type->isNever()) {
IssueBuffer::maybeAdd(
new NoValue(
'This function or method call never returns output',
$arg_location
),
$statements_analyzer->getSuppressedIssues()
);
return null;
}
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof TraitAnalyzer)
) {
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
}
if ($function_param->by_ref || $function_param->is_optional) {
//if the param is optional or a ref, we'll allow the input to be possibly_undefined
$param_type->possibly_undefined = true;
}
if ($param_type->hasCallableType() && $param_type->isSingle()) {
// we do this replacement early because later we don't have access to the
// $statements_analyzer, which is necessary to understand string function names
foreach ($input_type->getAtomicTypes() as $key => $atomic_type) {
if (!$atomic_type instanceof TLiteralString
|| InternalCallMapHandler::inCallMap($atomic_type->value)
) {
continue;
}
$candidate_callable = CallableTypeComparator::getCallableFromAtomic(
$codebase,
$atomic_type,
null,
$statements_analyzer,
true
);
if ($candidate_callable) {
$input_type->removeType($key);
$input_type->addType($candidate_callable);
}
}
}
$union_comparison_results = new TypeComparisonResult();
$type_match_found = UnionTypeComparator::isContainedBy(
$codebase,
$input_type,
$param_type,
true,
true,
$union_comparison_results
);
$replace_input_type = false;
if ($union_comparison_results->replacement_union_type) {
$replace_input_type = true;
$input_type = $union_comparison_results->replacement_union_type;
}
if ($cased_method_id) {
$old_input_type = $input_type;
$input_type = self::processTaintedness(
$statements_analyzer,
$cased_method_id,
$method_id,
$argument_offset,
$arg_location,
$function_call_location,
$function_param,
$input_type,
$input_expr,
$context,
$specialize_taint
);
if ($old_input_type !== $input_type) {
$replace_input_type = true;
}
}
if ($type_match_found
&& $param_type->hasCallableType()
) {
$potential_method_ids = [];
foreach ($input_type->getAtomicTypes() as $input_type_part) {
if ($input_type_part instanceof TKeyedArray) {
$potential_method_id = CallableTypeComparator::getCallableMethodIdFromTKeyedArray(
$input_type_part,
$codebase,
$context->calling_method_id,
$statements_analyzer->getFilePath()
);
if ($potential_method_id && $potential_method_id !== 'not-callable') {
$potential_method_ids[] = $potential_method_id;
}
} elseif ($input_type_part instanceof TLiteralString
&& strpos($input_type_part->value, '::')
) {
$parts = explode('::', $input_type_part->value);
$potential_method_ids[] = new MethodIdentifier(
$parts[0],
strtolower($parts[1])
);
}
}
foreach ($potential_method_ids as $potential_method_id) {
$codebase->methods->methodExists(
$potential_method_id,
$context->calling_method_id,
null,
$statements_analyzer,
$statements_analyzer->getFilePath(),
true,
$context->insideUse()
);
}
}
if ($context->strict_types
&& !$input_type->hasArray()
&& !$param_type->from_docblock
&& $cased_method_id !== 'echo'
&& $cased_method_id !== 'print'
&& $cased_method_id !== 'sprintf'
) {
$union_comparison_results->scalar_type_match_found = false;
if ($union_comparison_results->to_string_cast) {
$union_comparison_results->to_string_cast = false;
$type_match_found = false;
}
}
if ($union_comparison_results->type_coerced && !$input_type->hasMixed()) {
if ($union_comparison_results->type_coerced_from_mixed) {
$origin_locations = [];
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
foreach ($input_type->parent_nodes as $parent_node) {
$origin_locations = array_merge(
$origin_locations,
$statements_analyzer->data_flow_graph->getOriginLocations($parent_node)
);
}
}
$origin_location = count($origin_locations) === 1 ? reset($origin_locations) : null;
if ($origin_location && $origin_location->getHash() === $arg_location->getHash()) {
$origin_location = null;
}
IssueBuffer::maybeAdd(
new MixedArgumentTypeCoercion(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', parent type ' . $input_type->getId() . ' provided',
$arg_location,
$cased_method_id,
$origin_location
),
$statements_analyzer->getSuppressedIssues()
);
} elseif ($cased_method_id !== 'echo' && $cased_method_id !== 'print') {
IssueBuffer::maybeAdd(
new ArgumentTypeCoercion(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', parent type ' . $input_type->getId() . ' provided',
$arg_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
}
if ($union_comparison_results->to_string_cast && $cased_method_id !== 'echo' && $cased_method_id !== 'print') {
IssueBuffer::maybeAdd(
new ImplicitToStringCast(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' .
$param_type->getId() . ', ' . $input_type->getId() . ' provided with a __toString method',
$arg_location
),
$statements_analyzer->getSuppressedIssues()
);
}
if (!$type_match_found && !$union_comparison_results->type_coerced) {
$types_can_be_identical = UnionTypeComparator::canBeContainedBy(
$codebase,
$input_type,
$param_type,
true,
true
);
$type = ($input_type->possibly_undefined ? 'possibly undefined ' : '') . $input_type->getId();
if ($union_comparison_results->scalar_type_match_found) {
if ($cased_method_id !== 'echo' && $cased_method_id !== 'print') {
IssueBuffer::maybeAdd(
new InvalidScalarArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' .
$param_type->getId() . ', ' . $type . ' provided',
$arg_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
} elseif ($types_can_be_identical) {
IssueBuffer::maybeAdd(
new PossiblyInvalidArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', possibly different type ' . $type . ' provided',
$arg_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
} else {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() .
', ' . $type . ' provided',
$arg_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
return null;
}
if ($input_expr instanceof PhpParser\Node\Scalar\String_
|| $input_expr instanceof PhpParser\Node\Expr\Array_
|| $input_expr instanceof PhpParser\Node\Expr\BinaryOp\Concat
) {
self::verifyExplicitParam(
$statements_analyzer,
$param_type,
$arg_location,
$input_expr,
$context
);
return null;
}
if (!$param_type->isNullable() && $cased_method_id !== 'echo' && $cased_method_id !== 'print') {
if ($input_type->isNull()) {
IssueBuffer::maybeAdd(
new NullArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, ' .
'null value provided to parameter with type ' . $param_type->getId(),
$arg_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
return null;
}
if ($input_type->isNullable() && !$input_type->ignore_nullable_issues) {
IssueBuffer::maybeAdd(
new PossiblyNullArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, possibly ' .
'null value provided',
$arg_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
}
if (!$param_type->isFalsable() &&
!$param_type->hasBool() &&
!$param_type->hasScalar() &&
$cased_method_id !== 'echo' &&
$cased_method_id !== 'print'
) {
if ($input_type->isFalse()) {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be false, ' .
$param_type->getId() . ' value expected',
$arg_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
return null;
}
if ($input_type->isFalsable() && !$input_type->ignore_falsable_issues) {
IssueBuffer::maybeAdd(
new PossiblyFalseArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be false, possibly ' .
$param_type->getId() . ' value expected',
$arg_location,
$cased_method_id
),
$statements_analyzer->getSuppressedIssues()
);
}
}
if (($type_match_found || $input_type->hasMixed())
&& !$function_param->by_ref
&& !($function_param->is_variadic xor $unpack)
&& $cased_method_id !== 'echo'
&& $cased_method_id !== 'print'
&& (!$in_call_map || $context->strict_types)
) {
self::coerceValueAfterGatekeeperArgument(
$statements_analyzer,
$input_type,
$replace_input_type,
$input_expr,
$param_type,
$signature_param_type,
$context,
$unpack,
$unpacked_atomic_array
);
}
return null;
}
/**
* @param PhpParser\Node\Scalar\String_|PhpParser\Node\Expr\Array_|PhpParser\Node\Expr\BinaryOp\Concat $input_expr
*/
private static function verifyExplicitParam(
StatementsAnalyzer $statements_analyzer,
Union $param_type,
CodeLocation $arg_location,
PhpParser\Node\Expr $input_expr,
Context $context
): void {
$codebase = $statements_analyzer->getCodebase();
foreach ($param_type->getAtomicTypes() as $param_type_part) {
if ($param_type_part instanceof TClassString
&& $input_expr instanceof PhpParser\Node\Scalar\String_
&& $param_type->isSingle()
) {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$input_expr->value,
$arg_location,
$context->self,
$context->calling_method_id,
$statements_analyzer->getSuppressedIssues(),
new ClassLikeNameOptions(true)
) === false
) {
return;
}
} elseif ($param_type_part instanceof TArray
&& $input_expr instanceof PhpParser\Node\Expr\Array_
) {
foreach ($param_type_part->type_params[1]->getAtomicTypes() as $param_array_type_part) {
if ($param_array_type_part instanceof TClassString) {
foreach ($input_expr->items as $item) {
if ($item && $item->value instanceof PhpParser\Node\Scalar\String_) {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$item->value->value,
$arg_location,
$context->self,
$context->calling_method_id,
$statements_analyzer->getSuppressedIssues(),
new ClassLikeNameOptions(true)
) === false
) {
return;
}
}
}
}
}
} elseif ($param_type_part instanceof TCallable) {
$can_be_callable_like_array = false;
if ($param_type->hasArray()) {
/**
* @psalm-suppress PossiblyUndefinedStringArrayOffset
*/
$param_array_type = $param_type->getAtomicTypes()['array'];
$row_type = null;
if ($param_array_type instanceof TList) {
$row_type = $param_array_type->type_param;
} elseif ($param_array_type instanceof TArray) {
$row_type = $param_array_type->type_params[1];
} elseif ($param_array_type instanceof TKeyedArray) {
$row_type = $param_array_type->getGenericArrayType()->type_params[1];
}
if ($row_type &&
($row_type->hasMixed() || $row_type->hasString())
) {
$can_be_callable_like_array = true;
}
}
if (!$can_be_callable_like_array) {
$function_ids = CallAnalyzer::getFunctionIdsFromCallableArg(
$statements_analyzer,
$input_expr
);
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 = [];
$has_valid_method = false;
foreach ($function_id_parts as $function_id_part) {
[$callable_fq_class_name, $method_name] = explode('::', $function_id_part);
switch ($callable_fq_class_name) {
case 'self':
case 'static':
case 'parent':
$container_class = $statements_analyzer->getFQCLN();
if ($callable_fq_class_name === 'parent') {
$container_class = $statements_analyzer->getParentFQCLN();
}
if (!$container_class) {
continue 2;
}
$callable_fq_class_name = $container_class;
}
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$callable_fq_class_name,
$arg_location,
$context->self,
$context->calling_method_id,
$statements_analyzer->getSuppressedIssues(),
new ClassLikeNameOptions(true)
) === false
) {
return;
}
$function_id_part = new MethodIdentifier(
$callable_fq_class_name,
strtolower($method_name)
);
$call_method_id = new MethodIdentifier(
$callable_fq_class_name,
'__call'
);
if (!$codebase->classOrInterfaceOrEnumExists($callable_fq_class_name)) {
return;
}
if (!$codebase->methods->methodExists($function_id_part)
&& !$codebase->methods->methodExists($call_method_id)
) {
$non_existent_method_ids[] = $function_id_part;
} else {
$has_valid_method = true;
}
}
if (!$has_valid_method && !$param_type->hasString() && !$param_type->hasArray()) {
if (MethodAnalyzer::checkMethodExists(
$codebase,
$non_existent_method_ids[0],
$arg_location,
$statements_analyzer->getSuppressedIssues()
) === false
) {
return;
}
}
} else {
if (!$param_type->hasString()
&& !$param_type->hasArray()
&& CallAnalyzer::checkFunctionExists(
$statements_analyzer,
$function_id,
$arg_location,
false
) === false
) {
return;
}
}
}
}
}
}
}
/**
* @param TKeyedArray|TArray|TList|TClassStringMap $unpacked_atomic_array
*/
private static function coerceValueAfterGatekeeperArgument(
StatementsAnalyzer $statements_analyzer,
Union $input_type,
bool $input_type_changed,
PhpParser\Node\Expr $input_expr,
Union $param_type,
?Union $signature_param_type,
Context $context,
bool $unpack,
?Atomic $unpacked_atomic_array
): void {
if ($param_type->hasMixed()) {
return;
}
if (!$input_type_changed && $param_type->from_docblock && !$input_type->hasMixed()) {
$input_type = clone $input_type;
foreach ($param_type->getAtomicTypes() as $param_atomic_type) {
if ($param_atomic_type instanceof TGenericObject) {
foreach ($input_type->getAtomicTypes() as $input_atomic_type) {
if ($input_atomic_type instanceof TGenericObject
&& $input_atomic_type->value === $param_atomic_type->value
) {
foreach ($input_atomic_type->type_params as $i => $type_param) {
if ($type_param->isNever() && isset($param_atomic_type->type_params[$i])) {
$input_type_changed = true;
$input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i];
}
}
}
}
}
}
if (!$input_type_changed) {
return;
}
}
$var_id = ExpressionIdentifier::getVarId(
$input_expr,
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
if ($var_id) {
$was_cloned = false;
if ($input_type->isNullable() && !$param_type->isNullable()) {
$input_type = clone $input_type;
$was_cloned = true;
$input_type->removeType('null');
}
if ($input_type->getId() === $param_type->getId()) {
if ($input_type->from_docblock) {
if (!$was_cloned) {
$was_cloned = true;
$input_type = clone $input_type;
}
$input_type->from_docblock = false;
foreach ($input_type->getAtomicTypes() as $atomic_type) {
$atomic_type->from_docblock = false;
}
}
} elseif ($input_type->hasMixed() && $signature_param_type) {
$was_cloned = true;
$parent_nodes = $input_type->parent_nodes;
$by_ref = $input_type->by_ref;
$input_type = clone $signature_param_type;
if ($input_type->isNullable()) {
$input_type->ignore_nullable_issues = true;
}
$input_type->parent_nodes = $parent_nodes;
$input_type->by_ref = $by_ref;
}
if ($context->inside_conditional && !isset($context->assigned_var_ids[$var_id])) {
$context->assigned_var_ids[$var_id] = 0;
}
if ($was_cloned) {
$context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
}
if ($unpack) {
if ($unpacked_atomic_array instanceof TList) {
$unpacked_atomic_array = clone $unpacked_atomic_array;
$unpacked_atomic_array->type_param = $input_type;
$context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]);
} elseif ($unpacked_atomic_array instanceof TArray) {
$unpacked_atomic_array = clone $unpacked_atomic_array;
$unpacked_atomic_array->type_params[1] = $input_type;
$context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]);
} elseif ($unpacked_atomic_array instanceof TKeyedArray
&& $unpacked_atomic_array->is_list
) {
$unpacked_atomic_array = $unpacked_atomic_array->getList();
$unpacked_atomic_array->type_param = $input_type;
$context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]);
} else {
$context->vars_in_scope[$var_id] = new Union([
new TArray([
Type::getInt(),
$input_type
]),
]);
}
} else {
$context->vars_in_scope[$var_id] = $input_type;
}
}
}
private static function processTaintedness(
StatementsAnalyzer $statements_analyzer,
string $cased_method_id,
?MethodIdentifier $method_id,
int $argument_offset,
CodeLocation $arg_location,
CodeLocation $function_call_location,
FunctionLikeParameter $function_param,
Union $input_type,
PhpParser\Node\Expr $expr,
Context $context,
bool $specialize_taint
): Union {
$codebase = $statements_analyzer->getCodebase();
if (!$statements_analyzer->data_flow_graph
|| ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph
&& in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()))
) {
return $input_type;
}
// literal data cant be tainted
if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph
&& $input_type->isSingle()
&& $input_type->hasLiteralValue()
) {
return $input_type;
}
// numeric types can't be tainted, neither can bool
if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph
&& $input_type->isSingle()
&& ($input_type->isInt() || $input_type->isFloat() || $input_type->isBool())
) {
return $input_type;
}
$event = new AddRemoveTaintsEvent($expr, $context, $statements_analyzer, $codebase);
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
if ($function_param->type && $function_param->type->isString() && !$input_type->isString()) {
$cast_type = CastAnalyzer::castStringAttempt(
$statements_analyzer,
$context,
$input_type,
$expr,
false
);
$input_type = clone $input_type;
$input_type->parent_nodes += $cast_type->parent_nodes;
}
if ($specialize_taint) {
$method_node = DataFlowNode::getForMethodArgument(
$cased_method_id,
$cased_method_id,
$argument_offset,
$statements_analyzer->data_flow_graph instanceof TaintFlowGraph
? $function_param->location
: null,
$function_call_location
);
} else {
$method_node = DataFlowNode::getForMethodArgument(
$cased_method_id,
$cased_method_id,
$argument_offset,
$statements_analyzer->data_flow_graph instanceof TaintFlowGraph
? $function_param->location
: null
);
if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph
&& $method_id
&& $method_id->method_name !== '__construct'
) {
$fq_classlike_name = $method_id->fq_class_name;
$method_name = $method_id->method_name;
$cased_method_name = explode('::', $cased_method_id)[1];
$class_storage = $codebase->classlike_storage_provider->get($fq_classlike_name);
foreach ($class_storage->dependent_classlikes as $dependent_classlike_lc => $_) {
$dependent_classlike_storage = $codebase->classlike_storage_provider->get(
$dependent_classlike_lc
);
$new_sink = DataFlowNode::getForMethodArgument(
$dependent_classlike_lc . '::' . $method_name,
$dependent_classlike_storage->name . '::' . $cased_method_name,
$argument_offset,
$arg_location,
null
);
$statements_analyzer->data_flow_graph->addNode($new_sink);
$statements_analyzer->data_flow_graph->addPath(
$method_node,
$new_sink,
'arg',
$added_taints,
$removed_taints
);
}
}
}
if ($method_id && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if ($declaring_method_id && (string) $declaring_method_id !== (string) $method_id) {
$new_sink = DataFlowNode::getForMethodArgument(
(string) $declaring_method_id,
$codebase->methods->getCasedMethodId($declaring_method_id),
$argument_offset,
$arg_location,
null
);
$statements_analyzer->data_flow_graph->addNode($new_sink);
$statements_analyzer->data_flow_graph->addPath(
$method_node,
$new_sink,
'arg',
$added_taints,
$removed_taints
);
}
}
$statements_analyzer->data_flow_graph->addNode($method_node);
$argument_value_node = DataFlowNode::getForAssignment(
'call to ' . $cased_method_id,
$arg_location
);
$statements_analyzer->data_flow_graph->addNode($argument_value_node);
$statements_analyzer->data_flow_graph->addPath(
$argument_value_node,
$method_node,
'arg',
$added_taints,
$removed_taints
);
foreach ($input_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addNode($method_node);
$statements_analyzer->data_flow_graph->addPath(
$parent_node,
$argument_value_node,
'arg',
$added_taints,
$removed_taints
);
}
if ($function_param->assert_untainted) {
$input_type = clone $input_type;
$input_type->parent_nodes = [];
}
return $input_type;
}
}