Extract polymorphic Type methods for class-name narrowing#5612
Merged
ondrejmirtes merged 5 commits into2.1.xfrom May 7, 2026
Merged
Extract polymorphic Type methods for class-name narrowing#5612ondrejmirtes merged 5 commits into2.1.xfrom
ondrejmirtes merged 5 commits into2.1.xfrom
Conversation
Replaces `InitializerExprTypeResolver::getBitwiseNotTypeFromType()`'s hand-rolled `TypeTraverser::map` with a polymorphic `Type` method. Each leaf type knows what `~` produces: - `ConstantStringType` / `ConstantIntegerType` / `ConstantFloatType`: the actual computed value as a constant. - `StringType`: `string`. - `IntegerType` / `FloatType` (and `IntegerRangeType` via inheritance): `int`. - Accessory string types: those whose `isNonEmptyString()` is `Yes` (`NonEmpty`, `NonFalsy`, `Numeric`) survive as `string&non-empty-string`; `Literal`/`Lowercase`/`Uppercase` collapse to plain `string` (`~$s` doesn't preserve those refinements). - `UnionType` / `IntersectionType`: distribute via `unionTypes` / `intersectTypes`. - `NeverType`: pass through (was `ErrorType` before — incidental improvement of polymorphic dispatch). - Everything else (objects, arrays, bools, null, void, resource, mixed, etc.): `ErrorType`. Pure refactor: full test suite + phpstan + cs pass.
Replaces `GetClassDynamicReturnTypeExtension`'s hand-rolled `TypeTraverser::map` with a polymorphic `Type` method. Each leaf projects to: - definite object: `class-string<X>` (via `$this->getClassStringType()`) - definite non-object: `false` - possibly-object: `class-string<X>|false` The three default-impl traits cover most concrete types — one body in each: - `NonObjectTypeTrait` returns `ConstantBooleanType(false)`. - `MaybeObjectTypeTrait` returns the union with `false`. - `ObjectTypeTrait` delegates to `$this->getClassStringType()`. Special cases: - `StaticType` returns `$this->getClassStringType()` directly so the static binding is preserved as `class-string<static>` / `class-string<$this(X)>` rather than collapsing to the underlying object type. - `ObjectType`, `ClosureType`, `NonexistentParentClassType` use `getClassStringType()` directly. - `MixedType` branches on `isObject()` (its result depends on `$subtractedType`, so the trait isn't enough). - `StrictMixedType` returns `false` (its `isObject()` is `No`). - `NeverType` propagates. - `UnionType`/`IntersectionType` distribute via `unionTypes` / `intersectTypes`. - `LateResolvableTypeTrait` delegates to `resolve()`. Pure refactor: full test suite + phpstan + cs pass.
Replaces `InitializerExprTypeResolver::getClassConstFetchTypeByReflection()`'s hand-rolled `TypeTraverser::map` for the `::class` constant projection with a polymorphic `Type` method. Per leaf: - `NullType` (incl. `TemplateNullType` via `isNull()` check): pass through (`null::class` reads as `null` in PHPStan's modeling). - Definite-non-object types (`NonObjectTypeTrait`, `MaybeObjectTypeTrait`, `MixedType`, `StrictMixedType`): `ErrorType`. - `ObjectType` and `ObjectTypeTrait` users (and `ClosureType` via its inner `ObjectType`): if the class is known and `isFinalByKeyword()`, return the literal class name (`ConstantStringType($name, true)`); otherwise `IntersectionType[ClassString<X>, AccessoryLiteralStringType]`. - `EnumCaseObjectType`: explicitly skips the finality collapse and always returns `class-string<EnumName>&literal-string` — even though PHP enums report as `final`, the case-binding shape is what call sites expect. - `StaticType`: uses its own `getClassStringType()` so static binding is preserved (`class-string<static>` / `class-string<$this>`). - `NonexistentParentClassType`: `class-string&literal-string` (matches the original "isObject->yes, no class names" branch). - All template variants (via `TemplateTypeTrait`): `class-string<T>& literal-string`, with the same final-class collapse when the bound class is final. Required because templates with non-object bounds (e.g. `T of mixed`) fall through `MaybeObjectTypeTrait`'s `ErrorType` default and would otherwise lose the `class-string<T>` shape. - `NeverType`: pass through. - `UnionType`/`IntersectionType`: distribute, threading the `ReflectionProvider` through. - `LateResolvableTypeTrait`: delegates to `resolve()`. The `ReflectionProvider` is passed as a method argument because the final-class collapse can only be decided at the `ReflectionProvider` level (matches the `Type::getCallableParametersAcceptors($scope)` precedent for dependency-carrying Type methods). Pure refactor: full test suite + phpstan + cs pass.
Replaces the duplicate `TypeTraverser::map` callback in `TypeSpecifier::specifyTypesInCondition()` (the `instanceof <expr>` branch) and the `InstanceOfClassTypeTraverser` helper used by `InstanceofHandler` with one polymorphic `Type` method. Both sites needed an out-of-band by-ref `$uncertainty` flag to carry "we kept this symbolically, don't decide yes/no definitively"; the new method returns a small `ClassNameToObjectTypeResult` value object (`Type $type, bool $uncertainty`) so each leaf can carry its own answer through composite-type distribution. Per leaf: - `ObjectType` and `ObjectTypeTrait` users (and `ClosureType`, `NonexistentParentClassType`, `StaticType`): keep `$this` as the comparison target and mark uncertain — the runtime class is only known when `instanceof` actually executes. - `GenericClassStringType`: project to `getGenericType()` and mark uncertain (the class name is symbolic). - `ConstantStringType`: collapse to `new ObjectType($value)` with no uncertainty (the class name is concrete). - All other non-objects (via `NonObjectTypeTrait`, `MaybeObjectTypeTrait`): `MixedType`, no uncertainty (matches the original `return new MixedType()` fallback). - `MixedType` / `StrictMixedType`: same `MixedType` fallback. - `NeverType`: pass through. - `UnionType`/`IntersectionType`: distribute, OR-folding the uncertainty across members (matches the original closure-captured behavior). - `LateResolvableTypeTrait`: delegate to `resolve()`. `InstanceOfClassTypeTraverser` is removed (no longer used). Pure refactor: full test suite + phpstan + cs pass.
…wString)` Replaces `IsAFunctionTypeSpecifyingHelper::determineType()`'s hand-rolled `TypeTraverser::map` callback with a polymorphic `Type` method. The helper shrinks to a few lines that: - call `$classType->toObjectTypeForIsACheck($objectOrClassType, ...)`, - OR-fold the polymorphic uncertainty with the original "no constant strings in input" initial state, - run the same false-positive suppression check. Per leaf: - `ConstantStringType`: the only branch with real logic — collapses to `NeverType` when the input is the same final class (`!$allowSameClass`), sets uncertainty when the same class name appears in the input's class names (or when `$allowString` matches the input's superclass), then projects to `ObjectType($value)` (or `ObjectType|class-string<X>` if `$allowString`). - `GenericClassStringType`: projects to its `getGenericType()` (or a union with the class-string itself if `$allowString`), no uncertainty. - All other leaves (default in `NonObjectTypeTrait`, `MaybeObjectTypeTrait`, `ObjectTypeTrait`, plus direct overrides on `ObjectType`, `StaticType`, `ClosureType`, `NonexistentParentClassType`, `MixedType`, `StrictMixedType`): `ObjectWithoutClassType` (or `ObjectWithoutClassType|class-string` if `$allowString`), no uncertainty. - `NeverType`: pass through. - `UnionType`/`IntersectionType`: distribute, OR-folding uncertainty. - `LateResolvableTypeTrait`: delegate to `resolve()`. Reuses the `ClassNameToObjectTypeResult` value object introduced for `toObjectTypeForInstanceofCheck()`. Pure refactor: full test suite + phpstan + cs pass.
03c5f6b to
9545e40
Compare
4 tasks
ondrejmirtes
added a commit
that referenced
this pull request
May 7, 2026
Follow-up cleanup across #5611, #5612, #5613: - `FuncCallHandler::getArrayWalkResultType()` and `getArraySortDoNotPreserveListFunctionType()` were single-statement wrappers around `Type::mapValueType()` / `Type::makeListMaybe()`. Both private, both fully replaced by their polymorphic call; inline at the two/one call sites and delete the helpers. - `InstanceofHandler` and `TypeSpecifier` (the `instanceof <expr>` branch) used a throwaway `$classType = $scope->getType(...)` only to immediately overwrite it with `$result->type`. Drop the intermediate; chain the call. Pure simplification: full test suite + phpstan + cs pass.
ondrejmirtes
added a commit
that referenced
this pull request
May 7, 2026
Follow-up cleanup across #5611, #5612, #5613: - `FuncCallHandler::getArrayWalkResultType()` and `getArraySortDoNotPreserveListFunctionType()` were single-statement wrappers around `Type::mapValueType()` / `Type::makeListMaybe()`. Both private, both fully replaced by their polymorphic call; inline at the two/one call sites and delete the helpers. - `InstanceofHandler` and `TypeSpecifier` (the `instanceof <expr>` branch) used a throwaway `$classType = $scope->getType(...)` only to immediately overwrite it with `$result->type`. Drop the intermediate; chain the call. Pure simplification: full test suite + phpstan + cs pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pure refactor — no behaviour changes. Extracts five hand-rolled
TypeTraverser::mapcallbacks into polymorphicTypemethods.Same motivation as #5611: each call site opened with composite-recursion boilerplate (
if (\$type instanceof UnionType || \$type instanceof IntersectionType) { return \$traverse(\$type); }) and dispatched per-leaf via furtherinstanceofchecks. Polymorphic dispatch shrinks the call site to a one-liner and lets eachTypeimpl give the right answer via its own override. Two of the five sites also relied on a by-ref&\$uncertaintyclosure capture; those return a smallClassNameToObjectTypeResultvalue object instead, so composite types can OR-fold the flag during distribution.Typeis@api-do-not-implement, so adding methods is BC-safe per the project's API promise.Five new methods, one per commit:
Type::toBitwiseNotType(): TypeInitializerExprTypeResolver::getBitwiseNotTypeFromType()(~\$x)Type::toGetClassResultType(): TypeGetClassDynamicReturnTypeExtension(get_class(\$x))Type::toClassConstantType(ReflectionProvider): TypeInitializerExprTypeResolver::getClassConstFetchTypeByReflection()(\$x::class)Type::toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResultTypeSpecifier:169+InstanceOfClassTypeTraverser(deleted, also used byInstanceofHandler)Type::toObjectTypeForIsACheck(Type, bool, bool): ClassNameToObjectTypeResultIsAFunctionTypeSpecifyingHelper::determineType()The two value-object methods reuse the same
ClassNameToObjectTypeResult(`Type $type, bool $uncertainty`); commit 4 introduces it.Each method follows the standard plumbing pattern: declared on the
Typeinterface, with default bodies in the relevantObject*Trait(Non/Maybe/ObjectTypeTrait), explicit overrides on the direct-implementing classes (ObjectType,StaticType,ClosureType,NonexistentParentClassType,MixedType,StrictMixedType,NeverType), andLateResolvableTypeTraitdelegating viaresolve(). Composite types (UnionType,IntersectionType) distribute. Where a leaf type needs special handling (ConstantStringType,GenericClassStringType,EnumCaseObjectType,NullType, allTemplate*Typevariants viaTemplateTypeTrait), it gets its own override.InstanceOfClassTypeTraverserbecomes dead code and is removed.Net diff: +844/-257 across 49 files.
Test plan