Skip to content

Do not use callable parameter types as native types for closure and arrow function parameters#5632

Open
phpstan-bot wants to merge 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-g6kjenw
Open

Do not use callable parameter types as native types for closure and arrow function parameters#5632
phpstan-bot wants to merge 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-g6kjenw

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When treatPhpDocTypesAsCertain is false, PHPStan incorrectly reports function.alreadyNarrowedType for type checks inside closures/arrow functions passed to array_map, array_filter, and similar functions. The issue is that PHPDoc-derived callable parameter types were being treated as native types.

Changes

  • Modified MutatingScope::enterAnonymousFunctionWithoutReflection() to track the declared PHP type (before intersection with callable parameters) separately and use it for native expression types
  • Modified MutatingScope::enterArrowFunctionWithoutReflection() with the same fix — pass the declared type as the native type argument to assignVariable()
  • Both fixes ensure that callable parameter types derived from PHPDoc/generics in the calling context are treated as PHPDoc-level information only, not as native types

Root cause

In enterAnonymousFunctionWithoutReflection, lines 2037-2039 (before fix):

$holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
$expressionTypes[$paramExprString] = $holder;
$nativeTypes[$paramExprString] = $holder;  // BUG: same holder for both

The $parameterType included the callable parameter type from the calling context (e.g., string from array_map(fn($item) => ..., $stringArray)). This type was derived from PHPDoc annotations (@param string[] $values), but was incorrectly used as the native type. When treatPhpDocTypesAsCertain is false, the rule uses $scope->getNativeType() which returned string instead of mixed, making it report is_string() as always true.

The same pattern existed in enterArrowFunctionWithoutReflection where assignVariable($name, $parameterType, $parameterType, ...) used the same enriched type for both the expression type and native type.

This mirrors how foreach correctly handles the distinction: it calls getIterableValueType() on both the PHPDoc type AND the native type separately, using assignVariable($name, $valueType, $nativeValueType, ...).

Analogous cases probed

  • Closure in array_map — was broken, now fixed ✓
  • Arrow function in array_map — was broken, now fixed ✓
  • Closure in array_filter — was broken, now fixed ✓
  • Arrow function in array_filter — was broken, now fixed ✓
  • usort, uasort, uksort and all other callback-accepting functions — go through the same code path, now fixed ✓
  • Method and static method call rules (ImpossibleCheckTypeMethodCallRule, ImpossibleCheckTypeStaticMethodCallRule) — use the same ImpossibleCheckTypeHelper and same $scope->getNativeType(), so they benefit from the scope fix ✓
  • foreach loop — was already correct (uses separate native/PHPDoc iterable types) ✓
  • Closure with explicit PHP type hint — still works correctly (native type = declared type) ✓

Test

  • Added rule test testBug11014 in ImpossibleCheckTypeFunctionCallRuleTest with test data covering all four combinations (closure/arrow × array_map/array_filter)
  • Added NSRT test tests/PHPStan/Analyser/nsrt/bug-11014.php verifying native types via assertNativeType('mixed', $item) for untyped callback parameters and assertNativeType('string', $item) for typed ones

Fixes phpstan/phpstan#11014

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.

Add non regression test for phpstan/phpstan#5271

Add a nsrt test for the constant array

$undefined_curl_functions = array_filter(
	[
		'curl_multi_add_handle',
		'curl_multi_exec',
		'curl_multi_init',
	],
	static function( $function_name ) {
        assertType(..., $function_name);
        assertNativeType(..., $function_name);

		return true;
	}
);

@VincentLanglet VincentLanglet self-assigned this May 10, 2026
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

make tests also completed successfully. Both verification steps passed, and the commit is already pushed. All done.

Comment thread tests/PHPStan/Analyser/nsrt/bug-11014.php Outdated
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.

Do not default to null for the param nativeCallableParameters this will simplify the code

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Removed the = null default from all four $nativeCallableParameters parameters in MutatingScope.php. All callers already pass the argument explicitly, so no other changes were needed. Tests and make phpstan both pass.

Comment thread src/Analyser/MutatingScope.php
Comment thread src/Analyser/MutatingScope.php
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.

Merge both bug11014 into a single file use for both nsrt and rule test.

And use // lint >= 8.0 on it

Comment thread src/Analyser/MutatingScope.php
Comment thread src/Analyser/MutatingScope.php
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Merged the two bug-11014 test files into a single tests/PHPStan/Analyser/nsrt/bug-11014.php with // lint >= 8.0, deleted the separate rule test data file, and updated the rule test to reference the nsrt file. All tests and make phpstan pass.

@VincentLanglet
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet requested review from VincentLanglet and staabm and removed request for staabm May 10, 2026 19:03
Comment thread src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php Outdated
Comment thread src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php Outdated
Comment thread src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php Outdated
Comment thread src/Analyser/NodeScopeResolver.php
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-g6kjenw branch 5 times, most recently from 29216a3 to 8e47fa3 Compare May 11, 2026 14:19
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.

Tests are failing

1) PHPStan\Analyser\NodeScopeResolverTest::testFile@tests\PHPStan\Analyser\nsrt\bug-11014.php with data ('D:\a\phpstan-src\phpstan-src\...14.php')
Failed assertions in D:\a\phpstan-src\phpstan-src\tests\PHPStan\Analyser\nsrt\bug-11014.php:

Line 84:
Expected: 'curl_multi_add_handle'|'curl_multi_exec'|'curl_multi_init'
Actual:   mixed

Line 96:
Expected: 'curl_multi_add_handle'|'curl_multi_exec'|'curl_multi_init'
Actual:   mixed

Why the native type is not correct for constantArray which are nativeType

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's a summary of what was done:

Problem: For constant arrays passed to array_map/array_filter, the native type of callback parameters was mixed instead of the literal string union. The callback parameter's native PHP type is just callable, which doesn't carry the resolved template types.

Fix (3 files):

  1. ArrayFilterArgVisitor — Added CALLBACK_ATTRIBUTE_NAME that tags the callback's value with the array expression (matching how ArrayMapArgVisitor tags callbacks for array_map). Only applies when there's no explicit $mode argument (default filter-by-value mode).

  2. NodeScopeResolver — Added createNativeCallableParametersFromArrayArgs() helper that checks for ArrayMapArgVisitor and ArrayFilterArgVisitor::CALLBACK_ATTRIBUTE_NAME attributes on the closure expression. When present, it computes native callable parameters from $scope->getNativeType(arrayArg)->getIterableValueType() instead of from the bare callable type. Used in both processClosureNode and processArrowFunctionNode.

  3. ClosureTypeResolver — Combined ArrayMapArgVisitor and ArrayFilterArgVisitor::CALLBACK_ATTRIBUTE_NAME into a single check, so array_filter callbacks use the same code path as array_map for both PHPDoc and native callable parameter computation.

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.

Don't look for a specific fix for array_map/array_filter but more for a general fix.

You say that the callback parameter's native PHP type is just callable which loses this information but the type is correctly understood. So we maybe need to update the native PHP type too in these case.

VincentLanglet and others added 2 commits May 11, 2026 18:32
…rrow function parameters

- In `enterAnonymousFunctionWithoutReflection` and `enterArrowFunctionWithoutReflection`,
  the callable parameter types (derived from calling context, often via PHPDoc/generics)
  were used as both expression types AND native types for closure parameters
- Now only the declared PHP type hint is used for native types, while the intersected
  callable parameter type is used for expression types (PHPDoc-aware)
- This matches the behavior of `foreach` which correctly distinguishes native vs PHPDoc
  iterable value types
- Fixes false positives from `function.alreadyNarrowedType` when `treatPhpDocTypesAsCertain`
  is false and callbacks are passed to array_map, array_filter, usort, etc.
…array_filter

For constant arrays passed to array_map/array_filter, the native types
are known (e.g. literal string unions), but the callback parameter's
native PHP type is just `callable` which loses this information.

Tag array_filter callbacks with the array expression (like ArrayMapArgVisitor
does for array_map), then compute native callable parameters from the actual
array arguments' native types in processClosureNode, processArrowFunctionNode,
and ClosureTypeResolver.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-g6kjenw branch from bdec861 to a37a1f5 Compare May 11, 2026 16:32
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.

Don't look for a specific fix for array_map/array_filter but more for a general fix.

You say that "the callback parameter's
native PHP type is just callable which loses this information" but the type is correctly understood. So we maybe need to update the native PHP type too in these case.

…neral native type resolution

Instead of special-casing array_map/array_filter in NodeScopeResolver
and ClosureTypeResolver, compute native callable parameter types
alongside PHPDoc types in ParametersAcceptorSelector::selectFromArgs().

This uses ExtendedDummyParameter + ExtendedFunctionVariant to preserve
native types through the template resolution pipeline, so that
$matchedParameter->getNativeType() returns the correctly resolved
native callable type for all callback-accepting functions.

Changes:
- ParametersAcceptorSelector: array_map, array_filter, array_walk, and
  array_find special handling now computes native callback parameter
  types using getNativeType() and stores them via ExtendedDummyParameter
- NodeScopeResolver: removed createNativeCallableParametersFromArrayArgs()
  since createNativeCallableParameters() now works via the general path
- ClosureTypeResolver: removed ArrayFilterArgVisitor::CALLBACK_ATTRIBUTE_NAME
  fallback; array_filter closures use the inFunctionCallsStack general path
- ArrayFilterArgVisitor: removed CALLBACK_ATTRIBUTE_NAME constant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's a summary of what was changed to address the review:

Problem: The previous approach special-cased array_map and array_filter in NodeScopeResolver (via createNativeCallableParametersFromArrayArgs()) and ClosureTypeResolver (via ArrayFilterArgVisitor::CALLBACK_ATTRIBUTE_NAME). The reviewer wanted a general fix instead.

Root cause: ParametersAcceptorSelector::selectFromArgs() already computes concrete callable types for callback parameters of array_map, array_filter, array_walk, and array_find. But it used NativeParameterReflection + FunctionVariant, which lose native type information when GenericParametersAcceptorResolver::resolve() wraps them (native types default to MixedType).

Fix: In selectFromArgs(), use ExtendedDummyParameter (which carries separate native types) + ExtendedFunctionVariant (which preserves them through resolution). Native callable types are computed using $scope->getNativeType() alongside the existing $scope->getType() calls. This makes $matchedParameter->getNativeType() return the correctly resolved native callable type, so the general createNativeCallableParameters() path works for all callback-accepting functions.

Removed:

  • NodeScopeResolver::createNativeCallableParametersFromArrayArgs() (special-case method)
  • ArrayFilterArgVisitor::CALLBACK_ATTRIBUTE_NAME (no longer needed)
  • Array_map/filter-specific imports from NodeScopeResolver and ClosureTypeResolver

All tests pass (make tests: 12056 tests, 0 failures) and make phpstan reports no errors.

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.

2 participants