Skip to content

Commit

Permalink
report invalid callable if callable cannot be called like this from c…
Browse files Browse the repository at this point in the history
…urrent context

Fix #10823
Fix #8509
  • Loading branch information
kkmuffme committed Mar 19, 2024
1 parent b341a2b commit 8e98e5c
Show file tree
Hide file tree
Showing 2 changed files with 1,118 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
use UnexpectedValueException;

use function count;
use function explode;
Expand Down Expand Up @@ -886,6 +887,20 @@ public static function verifyType(
true,
$context->insideUse(),
);

if (self::verifyCallableInContext(
$potential_method_id,
$cased_method_id,
$method_id,
$atomic_type,
$argument_offset,
$arg_location,
$context,
$codebase,
$statements_analyzer,
) === false) {
continue;
}
}

$input_type->removeType($key);
Expand Down Expand Up @@ -971,6 +986,20 @@ public static function verifyType(
}

if ($potential_method_id && $potential_method_id !== 'not-callable') {
if (self::verifyCallableInContext(
$potential_method_id,
$cased_method_id,
$method_id,
$input_type_part,
$argument_offset,
$arg_location,
$context,
$codebase,
$statements_analyzer,
) === false) {
continue;
}

$potential_method_ids[] = $potential_method_id;
}
} elseif ($input_type_part instanceof TLiteralString
Expand Down Expand Up @@ -998,6 +1027,20 @@ public static function verifyType(
);
}

if (self::verifyCallableInContext(
$potential_method_id,
$cased_method_id,
$method_id,
$input_type_part,
$argument_offset,
$arg_location,
$context,
$codebase,
$statements_analyzer,
) === false) {
continue;
}

$potential_method_ids[] = $potential_method_id;
}
}
Expand Down Expand Up @@ -1235,6 +1278,131 @@ public static function verifyType(
return null;
}

private static function verifyCallableInContext(
MethodIdentifier $potential_method_id,
?string $cased_method_id,
?MethodIdentifier $method_id,
Atomic $input_type_part,
int $argument_offset,
CodeLocation $arg_location,
Context $context,
Codebase $codebase,
StatementsAnalyzer $statements_analyzer
): ?bool {
$method_identifier = $cased_method_id !== null ? ' of ' . $cased_method_id : '';

if (!$method_id
|| $potential_method_id->fq_class_name !== $context->self
|| $method_id->fq_class_name !== $context->self) {
if ($input_type_part instanceof TKeyedArray) {
[$lhs,] = $input_type_part->properties;
} else {
$lhs = Type::getString($potential_method_id->fq_class_name);
}

try {
$method_storage = $codebase->methods->getStorage($potential_method_id);

$lhs_atomic = $lhs->getSingleAtomic();
if ($lhs->isSingle()
&& $lhs->hasNamedObjectType()
&& ($lhs->isStaticObject()
|| ($lhs_atomic instanceof TNamedObject
&& !$lhs_atomic->definite_class
&& $lhs_atomic->value === $context->self))) {
// callable $this
// some PHP-internal functions (e.g. array_filter) will call the callback within the current context
// unlike user-defined functions which call the callback in their context
// however this doesn't apply to all
// e.g. header_register_callback will not throw an error immediately like user-land functions
// however error log "Could not call the sapi_header_callback" if it's not public
// this is NOT a complete list, but just what was easily available and to be extended
$php_native_non_public_cb = [
'array_diff_uassoc',
'array_diff_ukey',
'array_filter',
'array_intersect_uassoc',
'array_intersect_ukey',
'array_map',
'array_reduce',
'array_udiff',
'array_udiff_assoc',
'array_udiff_uassoc',
'array_uintersect',
'array_uintersect_assoc',
'array_uintersect_uassoc',
'array_walk',
'array_walk_recursive',
'preg_replace_callback',
'preg_replace_callback_array',
'call_user_func',
'call_user_func_array',
'forward_static_call',
'forward_static_call_array',
'is_callable',
'ob_start',
'register_shutdown_function',
'register_tick_function',
'session_set_save_handler',
'set_error_handler',
'set_exception_handler',
'spl_autoload_register',
'spl_autoload_unregister',
'uasort',
'uksort',
'usort',
];

if ($potential_method_id->fq_class_name !== $context->self
|| ($cased_method_id !== null
&& !$method_id
&& !in_array($cased_method_id, $php_native_non_public_cb, true))
|| ($method_id
&& $method_id->fq_class_name !== $context->self
&& $method_id->fq_class_name !== 'Closure')
) {
if ($method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC) {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier
. ' expects a public callable, but a non-public callable provided',
$arg_location,
$cased_method_id,
),
$statements_analyzer->getSuppressedIssues(),
);
return false;
}
}
} elseif ($lhs->isSingle()) {
// instance from e.g. new Foo() or static string like Foo::bar
if ((!$method_storage->is_static && !$lhs->hasNamedObjectType())
|| $method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC) {
IssueBuffer::maybeAdd(
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier
. ' expects a public static callable, but a '
. ($method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC ?
'non-public ' : '')
. (!$method_storage->is_static ? 'non-static ' : '')
. 'callable provided',
$arg_location,
$cased_method_id,
),
$statements_analyzer->getSuppressedIssues(),
);

return false;
}
}
} catch (UnexpectedValueException $e) {
// do nothing
}
}

return null;
}

/**
* @param PhpParser\Node\Scalar\String_|PhpParser\Node\Expr\Array_|PhpParser\Node\Expr\BinaryOp\Concat $input_expr
*/
Expand Down
Loading

0 comments on commit 8e98e5c

Please sign in to comment.