Skip to content

Commit

Permalink
Merge pull request #10839 from kkmuffme/misc-class-callable-errors-no…
Browse files Browse the repository at this point in the history
…t-reported
  • Loading branch information
weirdan authored Mar 20, 2024
2 parents b47449f + 375fe32 commit 4266a8e
Show file tree
Hide file tree
Showing 2 changed files with 1,304 additions and 32 deletions.
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 @@ -60,6 +61,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 +888,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 @@ -952,18 +968,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 @@ -1200,6 +1279,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 4266a8e

Please sign in to comment.