Skip to content

Let a type mapper cover several types at once - #8231

Merged
TomasVotruba merged 1 commit into
mainfrom
collapse-identical-type-mappers
Jul 30, 2026
Merged

Let a type mapper cover several types at once#8231
TomasVotruba merged 1 commit into
mainfrom
collapse-identical-type-mappers

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

Follow-up to #8230.

Why the duplication could not be removed before

Eight mappers were byte-identical to a sibling except for the PHPStan type they named:

AccessoryLiteralStringTypeMapper   AccessoryNonEmptyStringTypeMapper
AccessoryNonFalsyStringTypeMapper  AccessoryNumericStringTypeMapper    -> all map to "string"

HasOffsetTypeMapper                HasOffsetValueTypeTypeMapper
NonEmptyArrayTypeMapper            OversizedArrayTypeMapper            -> all map to "array"

The obvious fix — one mapper registered against a shared parent type — does not work here. All eight share exactly the same ancestors:

AccessoryLiteralStringType   ancestors: AccessoryType, CompoundType, Type
HasOffsetType                ancestors: AccessoryType, CompoundType, Type

AccessoryType covers both groups, but one group maps to string and the other to array, so no single class-string separates them. getNodeClass(): string was the blocker.

The change

Widen the contract to return several classes:

-    /**
-     * @return class-string<TType>
-     */
-    public function getNodeClass(): string;
+    /**
+     * A mapper can cover several types at once, when they all map to the same output,
+     * e.g. every accessory string type maps to "string"
+     *
+     * @return array<class-string<TType>>
+     */
+    public function getNodeClasses(): array;

Not a new shape for the codebase — NodeTypeResolverInterface next door already declares getNodeClasses(): array and is dispatched the same way.

The two groups then collapse to AccessoryStringTypeMapper and AccessoryArrayTypeMapper: 8 classes to 2.

NeverTypeMapper and ResourceTypeMapper are identical too, but never and resource are unrelated concepts — merging them would shorten the file count and nothing else, so they stay apart.

Cost

Every remaining mapper gains one bracket pair:

-    public function getNodeClass(): string
+    public function getNodeClasses(): array
     {
-        return VoidType::class;
+        return [VoidType::class];
     }

29 files, mechanical. Net -170 lines.

Behavior

Checked, not assumed. I ran all eight collapsed types through the container's PHPStanStaticTypeMapper on this branch and on main, comparing the phpdoc node plus the PhpParser node for PARAM, RETURN and PROPERTY kinds — 32 outputs, all identical.

To make sure the two runs really executed different code (a shared vendor/ autoloader would otherwise resolve both to the same sources), the baseline ran in a separate worktree with a checkout-first autoloader prepended. It demonstrably loaded main's AccessoryLiteralStringTypeMapper, and rejected this branch's AccessoryStringTypeMapper against main's narrower interface.

TypeMapperOrderTest from #8230 is updated for the widened contract and still guards registration order across every declared class.

Eight mappers were byte-identical to a sibling except for the PHPStan type they
named. They could not be merged, because getNodeClass() returned a single class
and the two groups share the exact same ancestors (AccessoryType, CompoundType,
Type) while mapping to different output - one to "string", the other to "array".

Widen the contract to getNodeClasses(): array, matching NodeTypeResolverInterface
which already uses that shape, and merge the two groups:

  Accessory{LiteralString,NonEmptyString,NonFalsyString,NumericString}TypeMapper
    -> AccessoryStringTypeMapper
  {HasOffset,HasOffsetValue,NonEmptyArray,OversizedArray}TypeMapper
    -> AccessoryArrayTypeMapper

NeverTypeMapper and ResourceTypeMapper are also identical, but "never" and
"resource" are unrelated concepts, so they stay apart on purpose.
@TomasVotruba

Copy link
Copy Markdown
Member Author

LGTM 👍

@TomasVotruba
TomasVotruba merged commit a4153d9 into main Jul 30, 2026
65 checks passed
@TomasVotruba
TomasVotruba deleted the collapse-identical-type-mappers branch July 30, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant