Skip to content

Skip non-template keys when a constant offset misses ConstantArrayType's key index - #6674

Open
Kocal wants to merge 2 commits into
phpstan:2.3.xfrom
Kocal:constant-array-skip-keys-for-missing-constant-offset
Open

Kocal wants to merge 2 commits into
phpstan:2.3.xfrom
Kocal:constant-array-skip-keys-for-missing-constant-offset

Conversation

@Kocal

@Kocal Kocal commented Oct 4, 2026 •

Copy link
Copy Markdown

In ConstantArrayType::recursiveHasOffsetValueType(), a constant offset (ConstantStringType / ConstantIntegerType) whose value isn't in the key index used to fall through to a scan calling isSuperTypeOf() on every key of the array. A non-template constant key can only answer "no" there, since its isSuperTypeOf() compares values of the same class, so those keys are now skipped. Template keys are still compared, and the unsealed-extras handling after the scan is unchanged. The turbo-ext mirror (ConstantArrayType.cpp) gets the same change.

This matters when a literal array is passed to a parameter typed with a large shape, like the symfony/ux-css CssStyles shape (847 keys): checkOurKeys() asks the literal array's hasOffsetValueType() for every key of the shape, and each miss used to compare against every key of the literal array.

As a safety check, I temporarily logged the result of every skipped comparison over the full test suite and PHPStan's self-analysis: 5.6M skipped comparisons, all of them "no".

The baseline grows by one more instanceof ConstantStringType in ConstantArrayType.php (5 -> 6), on purpose: the skip relies on the exact class, since only then does isSuperTypeOf() compare values.

Benchmarked with 50 calls to CssRuntime::css() using literal arrays against the nested shape from the reproducer, --debug, interleaved runs:

Build Before After
On top of #6652, #6671, #6672, #6673 + #6675 (no turbo) 1.56-1.59 s 1.43-1.49 s
Plain 2.3.x (no turbo) 1 min 12 s 1 min 12 s
Plain 2.3.x (turbo) 22.11-22.68 s 22.07-22.55 s
PHPStan analysing its own src/Type 28.13-28.29 s 28.08-28.71 s

The gain only shows up on top of the other PRs: elsewhere the walks they remove dominate.

make tests passes with and without the extension, make phpstan and make cs pass, turbo's smoke.php, side-by-side.php and signature-parity.php pass, and --error-format=raw output is byte-identical with the extension on and off.

Refs phpstan/phpstan#15348

Kocal added 2 commits October 4, 2026 14:48
…e's key index

In `ConstantArrayType::recursiveHasOffsetValueType()`, a constant offset (`ConstantStringType` / `ConstantIntegerType`) whose value isn't in the key index used to fall through to a scan calling `isSuperTypeOf()` on every key of the array. A non-template constant key can only answer "no" there, since its `isSuperTypeOf()` compares values of the same class, so those keys are now skipped. Template keys are still compared, and the unsealed-extras handling after the scan is unchanged. The turbo-ext mirror (`ConstantArrayType.cpp`) gets the same change.

This matters when a literal array is passed to a parameter typed with a large shape, like the symfony/ux-css `CssStyles` shape (847 keys): `checkOurKeys()` asks the literal array's `hasOffsetValueType()` for every key of the shape, and each miss used to compare against every key of the literal array.

As a safety check, I temporarily logged the result of every skipped comparison over the full test suite and PHPStan's self-analysis: 5.6M skipped comparisons, all of them "no".

The baseline grows by one more `instanceof ConstantStringType` in `ConstantArrayType.php` (5 -> 6), on purpose: the skip relies on the exact class, since only then does `isSuperTypeOf()` compare values.

Benchmarked with 50 calls to `CssRuntime::css()` using literal arrays against the nested shape from the [reproducer](https://github.com/Kocal/sf-ux-css-phpstan-reproducer), `--debug`, interleaved runs:

| Build | Before | After |
| --- | --- | --- |
| On top of phpstan#6652, phpstan#6671, phpstan#6672, phpstan#6673 + the `FunctionCallParametersCheck` reordering from the sibling PR (no turbo) | 1.56-1.59 s | 1.43-1.49 s |
| Plain 2.3.x (no turbo) | 1 min 12 s | 1 min 12 s |
| Plain 2.3.x (turbo) | 22.11-22.68 s | 22.07-22.55 s |
| PHPStan analysing its own `src/Type` | 28.13-28.29 s | 28.08-28.71 s |

The gain only shows up on top of the other PRs: elsewhere the walks they remove dominate.

`make tests` passes with and without the extension, `make phpstan` and `make cs` pass, turbo's `smoke.php`, `side-by-side.php` and `signature-parity.php` pass, and `--error-format=raw` output is byte-identical with the extension on and off.

Refs phpstan/phpstan#15348
@staabm staabm self-assigned this Oct 4, 2026
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