Skip to content

Fix phpstan/phpstan#11619: Parameter #2 $callback of function uasort expects callable(Foo, Foo): int, 'strnatcasecmp' given.#5171

Merged
VincentLanglet merged 12 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-mzyn37q
Mar 29, 2026
Merged

Fix phpstan/phpstan#11619: Parameter #2 $callback of function uasort expects callable(Foo, Foo): int, 'strnatcasecmp' given.#5171
VincentLanglet merged 12 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-mzyn37q

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHPStan incorrectly reported an error when passing a string-comparison function like strnatcasecmp to uasort() for arrays of Stringable objects. For example, uasort($options, 'strnatcasecmp') where $options is Foo[] and Foo implements Stringable would produce: "Parameter #2 $callback of function uasort expects callable(Foo, Foo): int, 'strnatcasecmp' given."

This is a false positive because PHP's internal functions like strnatcasecmp perform implicit type coercion from Stringable to string.

Changes

  • src/Type/CallableType.php: Propagated the $strictTypes parameter from accepts() through isSuperTypeOfInternal() to CallableTypeHelper::isParametersAcceptorSuperTypeOf()
  • src/Type/CallableTypeHelper.php: Added optional $strictTypes parameter (default true) and used it in the callable parameter acceptance check instead of the hardcoded true
  • tests/PHPStan/Rules/Functions/data/bug-11619.php: New regression test with Stringable class and uasort/usort with strnatcasecmp
  • tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php: Added testBug11619 test method

Root cause

CallableType::accepts() received a $strictTypes parameter but ignored it, always passing treatMixedAsAny=true to its internal method without forwarding $strictTypes. The CallableTypeHelper::isParametersAcceptorSuperTypeOf() hardcoded strictTypes=true when calling $theirParameter->getType()->accepts(), which caused StringType::accepts() to reject Stringable objects without checking for __toString().

The fix propagates the scope's $strictTypes value through the chain. In non-strict mode (no declare(strict_types=1)), StringType::accepts() now checks for __toString() and accepts Stringable objects. In strict mode, the existing behavior is preserved, which correctly handles cases like bug-12317 where a user-defined closure with explicit string parameter types should still be flagged.

Test

Added tests/PHPStan/Rules/Functions/data/bug-11619.php with a Stringable class Foo and calls to uasort($options, 'strnatcasecmp') and usort($options, 'strnatcasecmp'). The test expects no errors.

Fixes phpstan/phpstan#11619

@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-mzyn37q branch from befcc3e to b5e3d6e Compare March 26, 2026 07:17
@VincentLanglet VincentLanglet self-assigned this Mar 26, 2026
staabm
staabm previously approved these changes Mar 26, 2026
@staabm staabm dismissed their stale review March 26, 2026 12:58

needs more work

return $this->function->getAsserts();
}

public function isBuiltin(): bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method should return TrinaryLogic instead of bool

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Changed FunctionCallableVariant::isBuiltin() to return TrinaryLogic instead of bool, as requested by staabm. The method now returns the TrinaryLogic directly when the underlying function provides one, or wraps the bool via TrinaryLogic::createFromBoolean(). The call site in CallableType.php was updated to use ->yes() on the result. Both PHPStan and tests pass.

ondrejmirtes and others added 4 commits March 26, 2026 15:13
…mode

- Propagated $strictTypes from CallableType::accepts() through to CallableTypeHelper
- In non-strict mode, Stringable objects are now accepted where string is expected in callable parameters
- This fixes false positives for patterns like uasort($stringableArray, 'strnatcasecmp')
- In strict_types mode, the stricter behavior is preserved (e.g. closures with explicit string params)
- New regression test in tests/PHPStan/Rules/Functions/data/bug-11619.php

Closes phpstan/phpstan#11619
Adds a second test file with declare(strict_types=1) to verify that
in strict mode, passing strnatcasecmp to uasort/usort with Stringable
objects is correctly reported as an error.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Built-in PHP functions always perform implicit type coercion regardless
of declare(strict_types=1), so Stringable objects should be accepted as
string parameters even in strict mode. Instead of propagating the calling
scope's $strictTypes, detect whether the callable is a built-in function
via FunctionCallableVariant::isBuiltin() and use non-strict checking for
its parameters.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@staabm staabm force-pushed the create-pull-request/patch-mzyn37q branch from b7187f1 to e1817a5 Compare March 26, 2026 14:13
return $this->function->getAsserts();
}

public function isBuiltin(): TrinaryLogic
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implement this method on CallableParametersAcceptor instead

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The changes:

  • Added isBuiltin(): TrinaryLogic to the CallableParametersAcceptor interface
  • Implemented it in all 7 implementors: ClosureType, CallableType, TrivialParametersAcceptor, InaccessibleMethod (all return TrinaryLogic::createNo()), ExtendedCallableFunctionVariant, ResolvedFunctionVariantWithCallable (propagate via constructor parameter), and FunctionCallableVariant (existing implementation)
  • Updated creation sites in GenericParametersAcceptorResolver and ParametersAcceptorSelector to propagate isBuiltin through
  • Simplified CallableType::isSuperTypeOfInternal() to use $variant->isBuiltin()->yes() instead of $variant instanceof FunctionCallableVariant && $variant->isBuiltin()->yes()

phpstan-bot and others added 3 commits March 26, 2026 14:26
Instead of checking `instanceof FunctionCallableVariant` in CallableType,
the isBuiltin() method is now part of the CallableParametersAcceptor
interface and implemented by all classes. The value is propagated through
ExtendedCallableFunctionVariant and ResolvedFunctionVariantWithCallable.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 26, 2026

I have added more tests, but I am getting to a point in which I don't know when/why php-src accepts a Stringable object and when it only accepts a string but not Stringable.

it clearly depends on strict-types, but there is some other factor playing in.
I was not able to find in the php-src documentation how this is supposed to work

@VincentLanglet
Copy link
Copy Markdown
Contributor

I have added more tests, but I am getting to a point in which I don't know when/why php-src accepts a Stringable object and when it only accepts a string but not Stringable.

it clearly depends on strict-types, but there is some other factor playing in. I was not able to find in the php-src documentation how this is supposed to work

Looking at

I wonder if the initial commit is not good enough for the fix
6938ec5

  • Someone who doesn't care with strict types like the author of the issue will see the issue solved
  • We won't rely on weird behavior of php on build-in functions

And then we could open an issue on php-src to know

  • why uasort is not consistent with the function called strnatcasecmp with strict types

NB: it's more related to uasort than to the fact strnatcasecmp is buildin because

@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 28, 2026

And then we could open an issue on php-src to know

yeah lets get in contact with php-src to get an idea how it is supposed to work.
would you open a issue?

I wonder if the initial commit is not good enough for the fix
6938ec5

I think we can decide better about that after we got some more information from php-src.
I would not merge half baked fixes into the type-system which seem to only affect edge-cases of some functions.

@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 28, 2026

upstream php-src report

@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 29, 2026

ok - so to sum up the result of the upstream report: when a built-in function is directly invoked from a built-in function it is always treated as strict-types=0.

php/php-src#21568 (comment)

@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-mzyn37q branch from 218d339 to ef20977 Compare March 29, 2026 10:17
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simplified the test, but didn't udpate the error expected.

In the call uasort($options, 'strnatcasecmp'); we don't have error. It's not because strnatcasecmp is build-in but because uasort is buildin. Please ensure the implementation is checking the right function, then update the tests.

The non-error on `uasort($options, 'strnatcasecmp')` in strict mode is
because uasort (the caller) is builtin, not because strnatcasecmp (the
callback) is builtin. Per PHP internals, when a builtin function calls
a callback, it always uses strict_types=0.

- Remove isBuiltin() from CallableParametersAcceptor interface and all
  implementations (was checking the wrong function)
- Forward $strictTypes through CallableType/ClosureType accepts() to
  CallableTypeHelper
- In FunctionCallParametersCheck, set strictTypes=false for callable
  parameters of builtin functions
- Update tests: customUsort (user-defined) in strict mode now correctly
  reports an error, while uasort/usort (builtin) do not

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Both background tasks completed successfully as well. All done — the commit has been pushed to the PR.

Comment on lines +366 to +369
$callableStrictTypes = $scope->isDeclareStrictTypes();
if ($isBuiltin && $parameterType->isCallable()->yes()) {
$callableStrictTypes = false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a one liner

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background PHPStan task also completed successfully (exit code 0). Everything is green and pushed.

Foo::fromString('a'),
];

uasort($options, 'strnatcasecmp');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs another test like

function userlandComparator(string $a, string $b): int {
	return strnatcasecmp($a, $b);
}

$options = [
	Foo::fromString('c'),
	Foo::fromString('b'),
	Foo::fromString('a'),
];


uasort($options, 'userlandComparator');
var_dump($options);

for both, strict-types 0 or 1. there should be no error for userland comparators, see https://3v4l.org/gEURZ#veol

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background make tests task also completed successfully. All 11666 tests passed. The changes have already been committed and pushed.

VincentLanglet and others added 2 commits March 29, 2026 12:57
When a builtin function like uasort calls a userland callback,
it's always treated as strict_types=0, so Stringable objects
are accepted for string parameters. Added test cases for both
strict and non-strict mode to verify no false positives.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@VincentLanglet VincentLanglet merged commit 4c6ef6e into phpstan:2.1.x Mar 29, 2026
656 of 657 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-mzyn37q branch March 29, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants