Skip to content

Collect all remaining callable parameter types for variadic closure parameters instead of using only the matching index#5634

Merged
VincentLanglet merged 4 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-e1mgzvb
May 11, 2026
Merged

Collect all remaining callable parameter types for variadic closure parameters instead of using only the matching index#5634
VincentLanglet merged 4 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-e1mgzvb

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a closure with a variadic parameter (...$postFiles) is passed to a function expecting a callable with multiple non-variadic parameters (e.g. Closure(PhpFileArray, PhpFileArray, PhpFileArray): bool), PHPStan incorrectly inferred the variadic parameter's type as the single element type from the matching index (array{error: int, name: string}) instead of the proper variadic array type (array<int|string, array{error: int, name: string}>). This caused false positives like "Cannot access offset 'error' on int|string".

Changes

  • src/Analyser/MutatingScope.php: Added buildVariadicArrayTypeFromCallableParameters() helper method that collects element types from all callable parameters at and beyond the variadic parameter's index, unions them, and constructs the correct variadic array type (respecting PHP version for named argument support)
  • src/Analyser/MutatingScope.php: In enterAnonymousFunctionWithoutReflection() and enterArrowFunctionWithoutReflection(), when the closure parameter is variadic, use the new helper instead of directly intersecting with $callableParameters[$i]->getType()
  • src/Analyser/NodeScopeResolver.php: In createCallableParameters(), when processing immediately-invoked closures/arrow functions and the callable parameter is variadic, union all argument types from that index onward instead of only using the first argument's type

Root cause

The bug had two manifestations sharing the same root cause pattern — treating a variadic parameter's index as a 1:1 mapping to callable parameters:

  1. Passed-to-type path (enterAnonymousFunctionWithoutReflection/enterArrowFunctionWithoutReflection): When callable parameters [PhpFileArray, PhpFileArray, PhpFileArray] existed and the closure had variadic ...$postFiles at index 0, only $callableParameters[0]->getType() was used. Since array{error: int, name: string} (a constant array) is a subtype of array<int|string, mixed>, the intersection collapsed to just array{error: int, name: string} — a single file array instead of an array of file arrays.

  2. Immediately-invoked path (createCallableParameters): When (function(...$args){})(1, 'hello', 3.14) was analyzed, the variadic parameter's type was only updated with args[0]'s type (1) instead of the union of all argument types (1|'hello'|3.14).

Test

  • tests/PHPStan/Analyser/nsrt/bug-9240.php: Regression test covering:
    • Main reported case: closure with variadic param passed to typed callable expectation
    • Mixed element types in the expected callable signature
    • Non-first-position variadic parameter with typed prefix
    • Arrow function variant
    • Immediately-invoked closure with variadic parameter
    • Immediately-invoked arrow function with variadic parameter
    • Expected callable type that itself has a variadic parameter

Fixes phpstan/phpstan#9240

@VincentLanglet VincentLanglet requested a review from staabm May 10, 2026 14:08
@VincentLanglet VincentLanglet self-assigned this May 11, 2026
…arameters instead of using only the matching index

- When a closure with a variadic parameter (e.g. `...$postFiles`) is passed to
  a function expecting a callable with multiple parameters, collect ALL remaining
  callable parameter types from the variadic param's index onward and build the
  proper variadic array type, instead of using only `$callableParameters[$i]`
- Fix applies to both `enterAnonymousFunctionWithoutReflection` and
  `enterArrowFunctionWithoutReflection` in MutatingScope
- Also fix `createCallableParameters` in NodeScopeResolver: when the closure's
  declared parameter is variadic and it is immediately invoked, union all
  argument types from that index onward instead of only the first argument
- Add `buildVariadicArrayTypeFromCallableParameters` helper that correctly
  handles PHP version (named arguments support) when constructing the array type
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-e1mgzvb branch from 5874fc6 to fa1b686 Compare May 11, 2026 10:28
Copy link
Copy Markdown
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

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

the origin bug report is about unexpected "Cannot access offset..." errors.

this PR should contain a *RuleTest which shows the "Cannot access offset" no longer is reported

Comment thread tests/PHPStan/Analyser/nsrt/bug-9240.php
Adds a NonexistentOffsetInArrayDimFetchRule test with the original
reproducer to verify that "Cannot access offset 'error' on int|string"
is no longer reported when accessing offsets on variadic closure
parameters passed to typed callable expectations.

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

Done. Added a NonexistentOffsetInArrayDimFetchRule test (testBug9240) with a test data file that reproduces the original bug report — a closure with variadic ...$postFiles passed to a typed callable, then accessing $postFile['error'] inside a foreach. The test expects zero errors, confirming the "Cannot access offset 'error' on int|string" false positive is gone.

- Add bug-9240-php7.php with list<T> assertions for PHP < 8.0 where
  named arguments are not supported (variadic params produce list types
  instead of array<int|string, T>)
- Add NonexistentOffsetInArrayDimFetchRule test to verify the original
  "Cannot access offset" error is no longer reported

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet merged commit 1300ea4 into phpstan:2.1.x May 11, 2026
657 of 661 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-e1mgzvb branch May 11, 2026 12:38
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.

3 participants