Skip to content

[cleanup] Remove three unreachable PHPStan type mappers - #8230

Merged
TomasVotruba merged 1 commit into
mainfrom
drop-unreachable-type-mappers
Jul 30, 2026
Merged

[cleanup] Remove three unreachable PHPStan type mappers #8230
TomasVotruba merged 1 commit into
mainfrom
drop-unreachable-type-mappers

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

PHPStanStaticTypeMapper picks a mapper with a linear is_a() scan in registration order:

foreach ($this->typeMappers as $typeMapper) {
    if (! is_a($type, $typeMapper->getNodeClass(), true)) {
        continue;
    }

    return $typeMapper->mapToPHPStanPhpDocTypeNode($type);
}

So a mapper for a child type has to be registered before the mapper for its parent type. Three were registered after theirs, and can never be reached:

Mapper Handles Shadowed by Position
GenericClassStringTypeMapper GenericClassStringType ClassStringTypeMapper #13 vs #8
SelfObjectTypeMapper SelfObjectType ObjectTypeMapper #30 vs #25
ThisTypeMapper ThisType StaticTypeMapper #34 vs #31

None of the three is injected anywhere — they appeared only in the LazyContainerFactory list — so no call path reaches them and removing them changes no behavior.

They were also lying about what happens

Two of them describe an outcome that differs from what actually runs today. Feeding each type to the dead mapper and to the mapper that really wins:

GenericClassStringType, phpdoc:  dead mapper => class-string<Foo>    actual => class-string<\Foo>
ThisType, return position:       dead mapper => self                 actual => static

The live behavior looks like the correct one in both cases (fully qualified name in a docblock, static for a @return $this), so this is stale code rather than a bug — but it is the kind of stale code that reads as authoritative.

Guard

Added TypeMapperOrderTest, which walks the registered mappers and fails when one is shadowed by an earlier one. Reverting any single deletion makes it fail with:

The "Rector\PHPStanStaticTypeMapper\TypeMapper\ThisTypeMapper" is registered after
"Rector\PHPStanStaticTypeMapper\TypeMapper\StaticTypeMapper", but "PHPStan\Type\ThisType"
is a "PHPStan\Type\StaticType". It can never be reached, register it earlier.

That ordering constraint was previously enforced by nothing, which is how these three went unnoticed.

PHPStanStaticTypeMapper picks a mapper with a linear is_a() scan in registration
order, so a mapper for a child type must be registered before the mapper for its
parent type. Three were registered after theirs and could never be reached:

  GenericClassStringTypeMapper  shadowed by ClassStringTypeMapper
  SelfObjectTypeMapper          shadowed by ObjectTypeMapper
  ThisTypeMapper                shadowed by StaticTypeMapper

None of them is injected anywhere; they only appeared in the container list, so
removing them does not change behavior.

Add a test that walks the registered mappers and fails when one is shadowed by an
earlier one, which is what let these three go unnoticed.
@TomasVotruba TomasVotruba changed the title Remove three unreachable PHPStan type mappers [cleanup] Remove three unreachable PHPStan type mappers Jul 30, 2026
@TomasVotruba
TomasVotruba merged commit 91f0ec8 into main Jul 30, 2026
65 checks passed
@TomasVotruba
TomasVotruba deleted the drop-unreachable-type-mappers branch July 30, 2026 20:20
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