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 vimeo#10823
Fix vimeo#8509
  • Loading branch information
kkmuffme committed Mar 19, 2024
1 parent ed32926 commit 4133be0
Show file tree
Hide file tree
Showing 2 changed files with 1,064 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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;
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 @@ -971,6 +987,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 +1028,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 +1279,89 @@ 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
// 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
if ($potential_method_id->fq_class_name !== $context->self
|| ($cased_method_id !== null && !InternalCallMapHandler::inCallMap($cased_method_id))
|| ($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 4133be0

Please sign in to comment.