Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add misc missing errors for invalid callable methods #10839

Merged
merged 3 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
use Psalm\Internal\Type\TypeExpander;
use Psalm\Issue\ArgumentTypeCoercion;
use Psalm\Issue\DeprecatedConstant;
use Psalm\Issue\ImplicitToStringCast;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\InvalidLiteralArgument;
Expand Down Expand Up @@ -59,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 @@ -885,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 @@ -951,18 +967,81 @@ public static function verifyType(
$statements_analyzer->getFilePath(),
);

if ($potential_method_id === null && $codebase->analysis_php_version_id >= 8_02_00) {
[$lhs,] = $input_type_part->properties;
if ($lhs->isSingleStringLiteral()
&& in_array(
strtolower($lhs->getSingleStringLiteral()->value),
['self', 'parent', 'static'],
true,
)) {
IssueBuffer::maybeAdd(
new DeprecatedConstant(
'Use of "' . $lhs->getSingleStringLiteral()->value . '" in callables is deprecated',
$arg_location,
),
$statements_analyzer->getSuppressedIssues(),
);
}
}

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
&& strpos($input_type_part->value, '::')
) {
$parts = explode('::', $input_type_part->value);
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
$potential_method_ids[] = new MethodIdentifier(
$potential_method_id = new MethodIdentifier(
$parts[0],
strtolower($parts[1]),
);

if ($codebase->analysis_php_version_id >= 8_02_00
&& in_array(
strtolower($potential_method_id->fq_class_name),
['self', 'parent', 'static'],
true,
)) {
IssueBuffer::maybeAdd(
new DeprecatedConstant(
'Use of "' . $potential_method_id->fq_class_name . '" in callables is deprecated',
$arg_location,
),
$statements_analyzer->getSuppressedIssues(),
);
}

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 @@ -1199,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