From ff42ae65c01d2753b8fb9bb85de841522ef3a1b3 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Wed, 6 May 2026 19:17:14 +0000 Subject: [PATCH 01/13] Do not let `method_exists()` check fall through to general logic when object type contains template types - When the specific `method_exists` handling in `ImpossibleCheckTypeHelper` cannot determine the result (no concrete class names, no GenericClassStringType), it falls through to the general type specifier logic - The general logic uses `HasMethodType::isSuperTypeOf()` which calls `hasMethod()` on the argument type; for `object&T` where T is a TemplateMixedType, this returns Yes (inherited from MixedType), causing a false positive "will always evaluate to true" - Add early `return null` when `$objectType->hasTemplateOrLateResolvableType()` is true, preventing the general logic from running on types where method existence is uncertain - Verified analogous cases: `property_exists` is not affected (its type specifying extension returns empty SpecifiedTypes for non-native properties); `is_callable` is not affected; `class-string` template bounds are correctly handled by the existing GenericClassStringType block --- .../Comparison/ImpossibleCheckTypeHelper.php | 4 ++++ ...ImpossibleCheckTypeFunctionCallRuleTest.php | 6 ++++++ .../PHPStan/Rules/Comparison/data/bug-8217.php | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-8217.php diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 25c7070579..710b614460 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -242,6 +242,10 @@ public function findSpecifiedType( return false; } } + + if ($objectType->hasTemplateOrLateResolvableType()) { + return null; + } } } } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 2e50b4c511..17b6d29c7b 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1224,4 +1224,10 @@ public function testBug12063(): void $this->analyse([__DIR__ . '/data/bug-12063.php'], []); } + public function testBug8217(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8217.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8217.php b/tests/PHPStan/Rules/Comparison/data/bug-8217.php new file mode 100644 index 0000000000..92439daf1d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8217.php @@ -0,0 +1,18 @@ + Date: Wed, 6 May 2026 19:41:11 +0000 Subject: [PATCH 02/13] Add test cases for method_exists with mixed and object types Demonstrates that the false positive only occurs with template types (object&T), not with plain mixed or object. For mixed, ObjectWithoutClassType::isSuperTypeOf(MixedType) returns Maybe (mixed is not necessarily an object), preventing the false positive. For plain object, ObjectWithoutClassType::hasMethod() returns Maybe (via ObjectTypeTrait), also preventing it. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Rules/Comparison/data/bug-8217.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8217.php b/tests/PHPStan/Rules/Comparison/data/bug-8217.php index 92439daf1d..d6fe3243d6 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-8217.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-8217.php @@ -9,10 +9,24 @@ class HelloWorld * * @param T $object */ - public static function check($object): void + public static function checkTemplate($object): void { if (is_object($object) && method_exists($object, 'method')) { echo 1; } } + + public static function checkMixed(mixed $value): void + { + if (method_exists($value, 'method')) { + echo 1; + } + } + + public static function checkObject(object $object): void + { + if (method_exists($object, 'method')) { + echo 1; + } + } } From 6d56134f8f473bf989ac6faed6d19b7ee0cb5d7c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 20:39:38 +0000 Subject: [PATCH 03/13] Fix at type system level: override hasMethod() on TemplateMixedType to return Maybe A template type T represents a specific-but-unknown type. MixedType::hasMethod() returns Yes (mixed encompasses all types including those with any method), but for a template T we don't know whether the specific type has a given method. This fixes the root cause instead of working around it in ImpossibleCheckTypeHelper. The previous workaround checked hasTemplateOrLateResolvableType() in the method_exists-specific handling to bail out early. With this fix, the general type specifier logic works correctly because HasMethodType::isSuperTypeOf() now gets Maybe (not Yes) from TemplateMixedType, so it no longer concludes that method_exists() on object&T "will always evaluate to true." Co-Authored-By: Claude Opus 4.6 --- src/Rules/Comparison/ImpossibleCheckTypeHelper.php | 3 --- src/Type/Generic/TemplateMixedType.php | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 710b614460..7a1af2d131 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -243,9 +243,6 @@ public function findSpecifiedType( } } - if ($objectType->hasTemplateOrLateResolvableType()) { - return null; - } } } } diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index d7729cc353..d8d3a04698 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; @@ -63,6 +64,11 @@ public function toStrictMixedType(): TemplateStrictMixedType ); } + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getClassStringType(): Type { return new GenericClassStringType($this); From 03a02e256e54b995a6e12f95108efc70a1e25e55 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 21:02:53 +0000 Subject: [PATCH 04/13] Move fix from TemplateMixedType::hasMethod() to ImpossibleCheckTypeHelper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the hasMethod() override on TemplateMixedType so that it inherits MixedType::hasMethod() (returning Yes) as before — the two should not diverge. Instead, prevent the method_exists specific handling in ImpossibleCheckTypeHelper from falling through to the general type specifier logic when the object type contains template or late-resolvable types. The general logic incorrectly concludes "always true" because MixedType::hasMethod() returns Yes, which dominates the IntersectionType result via lazyMaxMin. Alternatives considered: - HasMethodType::isSuperTypeOf(): could check for template types, but would incorrectly return Maybe for bounded templates like T of SomeClass where the method genuinely exists on the bound. - IntersectionType::hasMethod(): changing from lazyMaxMin to lazyMin would be a large behavioral change affecting all intersections. - General result processing: capping results at Maybe for template types is equivalent to this fix but less targeted. Co-Authored-By: Claude Opus 4.6 --- src/Rules/Comparison/ImpossibleCheckTypeHelper.php | 3 +++ src/Type/Generic/TemplateMixedType.php | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 7a1af2d131..710b614460 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -243,6 +243,9 @@ public function findSpecifiedType( } } + if ($objectType->hasTemplateOrLateResolvableType()) { + return null; + } } } } diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index d8d3a04698..d7729cc353 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -2,7 +2,6 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; @@ -64,11 +63,6 @@ public function toStrictMixedType(): TemplateStrictMixedType ); } - public function hasMethod(string $methodName): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function getClassStringType(): Type { return new GenericClassStringType($this); From 559769ec0320bec0bd40edb712cdfbd60ba24ad8 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 21:39:16 +0000 Subject: [PATCH 05/13] Fix HasMethodType::isSuperTypeOf() to delegate to isSubTypeOf() for CompoundTypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HasMethodType::isSuperTypeOf(MixedType) was returning Yes (via MixedType::hasMethod() which always returns Yes), inconsistent with MixedType::isSubTypeOf(HasMethodType) which returns Maybe. This caused false positives for method_exists() on template types like object&T, where TemplateMixedType (inheriting MixedType::hasMethod()) dominated IntersectionType::hasMethod() via lazyMaxMin. The fix delegates to $type->isSubTypeOf($this) for CompoundType instances (the standard pattern used by other accessory types like AccessoryNonEmptyStringType). When isSubTypeOf returns No, we fall back to hasMethod() — this prevents AccessoryTypes in intersections (e.g. AccessoryLiteralStringType in class-string&literal-string) from being overly strict, since their isSubTypeOf checks orthogonal properties that can incorrectly poison the lazyMaxMin result. Co-Authored-By: Claude Opus 4.6 --- src/Rules/Comparison/ImpossibleCheckTypeHelper.php | 3 --- src/Type/Accessory/HasMethodType.php | 9 +++++++++ tests/PHPStan/Type/Accessory/HasMethodTypeTest.php | 5 +++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 710b614460..7a1af2d131 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -243,9 +243,6 @@ public function findSpecifiedType( } } - if ($objectType->hasTemplateOrLateResolvableType()) { - return null; - } } } } diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 2201e0e5be..43ba231eb1 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -82,6 +82,15 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + if ($type instanceof CompoundType) { + $result = $type->isSubTypeOf($this); + if (!$result->no()) { + return $result; + } + + return new IsSuperTypeOfResult($type->hasMethod($this->methodName), []); + } + return new IsSuperTypeOfResult($type->hasMethod($this->methodName), []); } diff --git a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php index 057ae63a2f..219ce88097 100644 --- a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php @@ -134,6 +134,11 @@ public static function dataIsSuperTypeOf(): array ]), TrinaryLogic::createYes(), ], + [ + new HasMethodType('foo'), + new MixedType(), + TrinaryLogic::createMaybe(), + ], ]; } From 0d4198294b6e302e4441ae97b64d541421f73fdc Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 07:22:33 +0000 Subject: [PATCH 06/13] Fix HasPropertyType::isSuperTypeOf() to delegate to isSubTypeOf() for CompoundTypes Same pattern as the HasMethodType fix: delegate to $type->isSubTypeOf($this) for CompoundTypes so that MixedType correctly returns Maybe instead of Yes. property_exists() doesn't currently trigger the same false positive as method_exists() because PropertyExistsTypeSpecifyingExtension bails out early for non-native properties (which MixedType provides). But the underlying type inconsistency is the same: HasPropertyType::isSuperTypeOf(MixedType) was returning Yes when it should return Maybe. Added property_exists test cases to bug-8217 regression test to cover the template, mixed, and object scenarios. Co-Authored-By: Claude Opus 4.6 --- src/Type/Accessory/HasPropertyType.php | 12 +++++++++ .../Rules/Comparison/data/bug-8217.php | 26 +++++++++++++++++++ .../Type/Accessory/HasPropertyTypeTest.php | 5 ++++ 3 files changed, 43 insertions(+) diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 9c471e44cd..73cb2fbb49 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -79,6 +79,18 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { + if ($type instanceof CompoundType) { + $result = $type->isSubTypeOf($this); + if (!$result->no()) { + return $result; + } + + return new IsSuperTypeOfResult( + $type->hasInstanceProperty($this->propertyName)->or($type->hasStaticProperty($this->propertyName)), + [], + ); + } + return new IsSuperTypeOfResult( $type->hasInstanceProperty($this->propertyName)->or($type->hasStaticProperty($this->propertyName)), [], diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8217.php b/tests/PHPStan/Rules/Comparison/data/bug-8217.php index d6fe3243d6..7da3fe9946 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-8217.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-8217.php @@ -29,4 +29,30 @@ public static function checkObject(object $object): void echo 1; } } + + /** + * @template T + * + * @param T $object + */ + public static function checkPropertyExistsTemplate($object): void + { + if (is_object($object) && property_exists($object, 'prop')) { + echo 1; + } + } + + public static function checkPropertyExistsMixed(mixed $value): void + { + if (property_exists($value, 'prop')) { + echo 1; + } + } + + public static function checkPropertyExistsObject(object $object): void + { + if (property_exists($object, 'prop')) { + echo 1; + } + } } diff --git a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php index 087bd96910..d25f436cd6 100644 --- a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php @@ -100,6 +100,11 @@ public static function dataIsSuperTypeOf(): array ]), TrinaryLogic::createYes(), ], + [ + new HasPropertyType('foo'), + new MixedType(), + TrinaryLogic::createMaybe(), + ], ]; } From c34ef49a57c01e6279060ba3fc7735ed25749568 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 07:42:13 +0000 Subject: [PATCH 07/13] Simplify HasMethodType/HasPropertyType::isSuperTypeOf() CompoundType delegation Use the standard pattern `return $type->isSubTypeOf($this)` instead of falling back to hasMethod()/hasProperty() when isSubTypeOf() returns No. This is consistent with how other AccessoryType classes handle CompoundTypes. The simplified delegation changes behavior for `class-string&literal-string`: since IntersectionType::isSubTypeOf(HasMethodType) uses lazyMaxMin over individual members, and neither ClassStringType nor AccessoryLiteralStringType has methods, the result becomes No instead of the previous Maybe. To prevent this from causing false "always false" reports for method_exists() on class-string types (where the actual class is unknown), add explicit return null in ImpossibleCheckTypeHelper for both generic class-string types (non-final TypeWithClassName) and plain class-string types. Co-Authored-By: Claude Opus 4.6 --- src/Rules/Comparison/ImpossibleCheckTypeHelper.php | 6 ++++++ src/Type/Accessory/HasMethodType.php | 7 +------ src/Type/Accessory/HasPropertyType.php | 10 +--------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 7a1af2d131..20c6257cd6 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -241,6 +241,12 @@ public function findSpecifiedType( && $genericType->hasMethod($methodType->getValue())->no()) { return false; } + + return null; + } + + if ($objectType->isClassString()->yes()) { + return null; } } diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 43ba231eb1..2dcda784c2 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -83,12 +83,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { - $result = $type->isSubTypeOf($this); - if (!$result->no()) { - return $result; - } - - return new IsSuperTypeOfResult($type->hasMethod($this->methodName), []); + return $type->isSubTypeOf($this); } return new IsSuperTypeOfResult($type->hasMethod($this->methodName), []); diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 73cb2fbb49..3725d9c93f 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -80,15 +80,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { - $result = $type->isSubTypeOf($this); - if (!$result->no()) { - return $result; - } - - return new IsSuperTypeOfResult( - $type->hasInstanceProperty($this->propertyName)->or($type->hasStaticProperty($this->propertyName)), - [], - ); + return $type->isSubTypeOf($this); } return new IsSuperTypeOfResult( From 5732696edfed7df0a673e3102e78be10b5e73805 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 08:19:50 +0000 Subject: [PATCH 08/13] Fall back to hasMethod()/hasProperty() when CompoundType::isSubTypeOf() returns No AccessoryLiteralStringType implements CompoundType, so HasMethodType::isSuperTypeOf(AccessoryLiteralStringType) was delegating to AccessoryLiteralStringType::isSubTypeOf(HasMethodType), which returns No (because HasMethodType::isLiteralString() is No). This made HasMethodType::isSuperTypeOf(class-string&literal-string) return No instead of Maybe, since lazyMaxMin propagated the No from the accessory type even though ClassStringType::hasMethod() correctly returned Maybe. The fix: when the CompoundType delegation returns No, fall back to the hasMethod()/hasProperty() check which gives the correct result. This removes the need for the class-string guard in ImpossibleCheckTypeHelper. Co-Authored-By: Claude Opus 4.6 --- src/Rules/Comparison/ImpossibleCheckTypeHelper.php | 4 ---- src/Type/Accessory/HasMethodType.php | 5 ++++- src/Type/Accessory/HasPropertyType.php | 5 ++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 20c6257cd6..859a51dceb 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -245,10 +245,6 @@ public function findSpecifiedType( return null; } - if ($objectType->isClassString()->yes()) { - return null; - } - } } } diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 2dcda784c2..e6c4b0ced3 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -83,7 +83,10 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); + $result = $type->isSubTypeOf($this); + if (!$result->no()) { + return $result; + } } return new IsSuperTypeOfResult($type->hasMethod($this->methodName), []); diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 3725d9c93f..78bea8b1c3 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -80,7 +80,10 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); + $result = $type->isSubTypeOf($this); + if (!$result->no()) { + return $result; + } } return new IsSuperTypeOfResult( From 56412ddaadf93b630fdbb88ef2089bd51347ef99 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 09:12:27 +0000 Subject: [PATCH 09/13] Override string/scalar type queries on HasMethodType/HasPropertyType to return Maybe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HasMethodType and HasPropertyType use ObjectTypeTrait which returns No for isString(), isClassString(), isLiteralString(), etc. But these accessory types are agnostic about whether the underlying type is an object or a class-string — method_exists() and property_exists() work on both. Returning No breaks CompoundType delegation: when AccessoryLiteralStringType::isSubTypeOf(HasMethodType) checks HasMethodType::isLiteralString(), it gets No and incorrectly concludes the types are incompatible. Override the 9 string/scalar methods to return Maybe, keeping ObjectTypeTrait for object-related behavior (isObject, canCallMethods, etc.). This fixes the CompoundType delegation and removes the need for the ImpossibleCheckTypeHelper workaround. Co-Authored-By: Claude Opus 4.6 --- .../Comparison/ImpossibleCheckTypeHelper.php | 3 -- src/Type/Accessory/HasMethodType.php | 45 +++++++++++++++++++ src/Type/Accessory/HasPropertyType.php | 45 +++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 859a51dceb..25c7070579 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -241,10 +241,7 @@ public function findSpecifiedType( && $genericType->hasMethod($methodType->getValue())->no()) { return false; } - - return null; } - } } } diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index e6c4b0ced3..f597c1149a 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -181,6 +181,51 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) ]; } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getEnumCases(): array { return []; diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 78bea8b1c3..ec5c573dcb 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -158,6 +158,51 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return [new TrivialParametersAcceptor()]; } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getEnumCases(): array { return []; From f54febd9a2164c4e9874217e6fe2a1fa502fd0b2 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 09:20:48 +0000 Subject: [PATCH 10/13] Simplify HasMethodType/HasPropertyType::isSuperTypeOf() CompoundType delegation Now that string/scalar type queries return Maybe on HasMethodType and HasPropertyType, the fallback-on-No guard is no longer needed. The CompoundType delegation can unconditionally return the isSubTypeOf() result. Co-Authored-By: Claude Opus 4.6 --- src/Type/Accessory/HasMethodType.php | 5 +---- src/Type/Accessory/HasPropertyType.php | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index f597c1149a..17bae91313 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -83,10 +83,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { - $result = $type->isSubTypeOf($this); - if (!$result->no()) { - return $result; - } + return $type->isSubTypeOf($this); } return new IsSuperTypeOfResult($type->hasMethod($this->methodName), []); diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index ec5c573dcb..da9e34c84b 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -80,10 +80,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof CompoundType) { - $result = $type->isSubTypeOf($this); - if (!$result->no()) { - return $result; - } + return $type->isSubTypeOf($this); } return new IsSuperTypeOfResult( From d84bea2743eb6aba22796279e0b0799013275e19 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 09:41:51 +0000 Subject: [PATCH 11/13] Extract MaybeStringTypeTrait from HasOffsetType/HasOffsetValueType Deduplicate the 9 string/scalar type query methods (isString, isNumericString, isNonEmptyString, isNonFalsyString, isLiteralString, isLowercaseString, isUppercaseString, isClassString, isScalar) that return Maybe into a reusable MaybeStringTypeTrait. Co-Authored-By: Claude Opus 4.6 --- src/Type/Accessory/HasOffsetType.php | 47 +------------------ src/Type/Accessory/HasOffsetValueType.php | 47 +------------------ src/Type/Traits/MaybeStringTypeTrait.php | 55 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 90 deletions(-) create mode 100644 src/Type/Traits/MaybeStringTypeTrait.php diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index b6757fefb6..8cfd24c136 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -22,6 +22,7 @@ use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; +use PHPStan\Type\Traits\MaybeStringTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; @@ -40,6 +41,7 @@ class HasOffsetType implements CompoundType, AccessoryType use MaybeCallableTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; + use MaybeStringTypeTrait; use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; @@ -285,46 +287,6 @@ public function isInteger(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isNumericString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isNonEmptyString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isNonFalsyString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isLiteralString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isLowercaseString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isClassString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isUppercaseString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function getClassStringObjectType(): Type { return new ObjectWithoutClassType(); @@ -340,11 +302,6 @@ public function isVoid(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isScalar(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { return new BooleanType(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 66872dc2f3..f934183fb2 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -26,6 +26,7 @@ use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; +use PHPStan\Type\Traits\MaybeStringTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; @@ -44,6 +45,7 @@ class HasOffsetValueType implements CompoundType, AccessoryType use MaybeCallableTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; + use MaybeStringTypeTrait; use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; @@ -373,46 +375,6 @@ public function isInteger(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isNumericString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isNonEmptyString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isNonFalsyString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isLiteralString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isLowercaseString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isUppercaseString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - - public function isClassString(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function getClassStringObjectType(): Type { return new ObjectWithoutClassType(); @@ -428,11 +390,6 @@ public function isVoid(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isScalar(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { return new BooleanType(); diff --git a/src/Type/Traits/MaybeStringTypeTrait.php b/src/Type/Traits/MaybeStringTypeTrait.php new file mode 100644 index 0000000000..4729b3ae99 --- /dev/null +++ b/src/Type/Traits/MaybeStringTypeTrait.php @@ -0,0 +1,55 @@ + Date: Thu, 7 May 2026 09:41:59 +0000 Subject: [PATCH 12/13] Switch HasMethodType/HasPropertyType from ObjectTypeTrait to MaybeObjectTypeTrait These accessory types can be intersected with both objects and class-strings, so they should not claim to definitely be objects. Replace ObjectTypeTrait with MaybeObjectTypeTrait plus explicit sub-traits (MaybeCallableTypeTrait, MaybeIterableTypeTrait, MaybeOffsetAccessibleTypeTrait, NonArrayTypeTrait, TruthyBooleanTypeTrait) and MaybeStringTypeTrait for the string/scalar queries. Methods that were provided by ObjectTypeTrait but not by the new traits are now defined explicitly on each class. Co-Authored-By: Claude Opus 4.6 --- src/Type/Accessory/HasMethodType.php | 141 ++++++++++++++++++++----- src/Type/Accessory/HasPropertyType.php | 132 ++++++++++++++++++----- 2 files changed, 219 insertions(+), 54 deletions(-) diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 17bae91313..60cede22a7 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Accessory; use Closure; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; @@ -13,20 +14,29 @@ use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; +use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\MaybeIterableTypeTrait; +use PHPStan\Type\Traits\MaybeObjectTypeTrait; +use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\MaybeStringTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; -use PHPStan\Type\Traits\ObjectTypeTrait; +use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -35,7 +45,13 @@ class HasMethodType implements AccessoryType, CompoundType { - use ObjectTypeTrait; + use MaybeCallableTypeTrait; + use MaybeIterableTypeTrait; + use MaybeObjectTypeTrait; + use MaybeOffsetAccessibleTypeTrait; + use MaybeStringTypeTrait; + use NonArrayTypeTrait; + use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonRemoveableTypeTrait; @@ -61,6 +77,11 @@ public function getObjectClassReflections(): array return []; } + public function getConstantStrings(): array + { + return []; + } + public function getClassStringType(): Type { return new GenericClassStringType($this); @@ -162,65 +183,129 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function toString(): Type + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - if ($this->getCanonicalMethodName() === '__tostring') { - return new StringType(); - } + return [ + new TrivialParametersAcceptor(), + ]; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { return new ErrorType(); } - public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + public function getObjectTypeOrClassStringObjectType(): Type { - return [ - new TrivialParametersAcceptor(), - ]; + return $this; } - public function isString(): TrinaryLogic + public function isVoid(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); } - public function isNumericString(): TrinaryLogic + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - return TrinaryLogic::createMaybe(); + return new BooleanType(); } - public function isNonEmptyString(): TrinaryLogic + public function toNumber(): Type { - return TrinaryLogic::createMaybe(); + return new ErrorType(); } - public function isNonFalsyString(): TrinaryLogic + public function toAbsoluteNumber(): Type { - return TrinaryLogic::createMaybe(); + return new ErrorType(); + } + + public function toString(): Type + { + if ($this->getCanonicalMethodName() === '__tostring') { + return new StringType(); + } + + return new ErrorType(); } - public function isLiteralString(): TrinaryLogic + public function toInteger(): Type { - return TrinaryLogic::createMaybe(); + return new ErrorType(); } - public function isLowercaseString(): TrinaryLogic + public function toFloat(): Type { - return TrinaryLogic::createMaybe(); + return new ErrorType(); } - public function isUppercaseString(): TrinaryLogic + public function toArray(): Type { - return TrinaryLogic::createMaybe(); + return new MixedType(); } - public function isClassString(): TrinaryLogic + public function toArrayKey(): Type { - return TrinaryLogic::createMaybe(); + return new ErrorType(); } - public function isScalar(): TrinaryLogic + public function toCoercedArgumentType(bool $strictTypes): Type { - return TrinaryLogic::createMaybe(); + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toString()); + } + + return $this; } public function getEnumCases(): array diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index da9e34c84b..eb8cbc5e29 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -2,24 +2,34 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\MixedType; +use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\MaybeIterableTypeTrait; +use PHPStan\Type\Traits\MaybeObjectTypeTrait; +use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\MaybeStringTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; -use PHPStan\Type\Traits\ObjectTypeTrait; +use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -27,7 +37,13 @@ class HasPropertyType implements AccessoryType, CompoundType { - use ObjectTypeTrait; + use MaybeCallableTypeTrait; + use MaybeIterableTypeTrait; + use MaybeObjectTypeTrait; + use MaybeOffsetAccessibleTypeTrait; + use MaybeStringTypeTrait; + use NonArrayTypeTrait; + use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonRemoveableTypeTrait; @@ -53,14 +69,14 @@ public function getObjectClassReflections(): array return []; } - public function getClassStringType(): Type + public function getConstantStrings(): array { - return new GenericClassStringType($this); + return []; } - public function getConstantStrings(): array + public function getClassStringType(): Type { - return []; + return new GenericClassStringType($this); } public function getPropertyName(): string @@ -150,54 +166,118 @@ public function hasStaticProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + public function isNull(): TrinaryLogic { - return [new TrivialParametersAcceptor()]; + return TrinaryLogic::createNo(); } - public function isString(): TrinaryLogic + public function isConstantValue(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); } - public function isNumericString(): TrinaryLogic + public function isConstantScalarValue(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); } - public function isNonEmptyString(): TrinaryLogic + public function getConstantScalarTypes(): array { - return TrinaryLogic::createMaybe(); + return []; } - public function isNonFalsyString(): TrinaryLogic + public function getConstantScalarValues(): array { - return TrinaryLogic::createMaybe(); + return []; } - public function isLiteralString(): TrinaryLogic + public function isTrue(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); } - public function isLowercaseString(): TrinaryLogic + public function isFalse(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); } - public function isUppercaseString(): TrinaryLogic + public function isBoolean(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); } - public function isClassString(): TrinaryLogic + public function isFloat(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); } - public function isScalar(): TrinaryLogic + public function isInteger(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + + public function toString(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new ErrorType(); + } + + public function toFloat(): Type + { + return new ErrorType(); + } + + public function toArray(): Type + { + return new MixedType(); + } + + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toString()); + } + + return $this; } public function getEnumCases(): array From e729d8627eba49bcbed7b727de94c870b8681c40 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 13:38:02 +0000 Subject: [PATCH 13/13] Address review: deduplicate via MaybeStringTypeTrait, remove redundant overrides, fix getClassStringObjectType - Move getConstantStrings() into MaybeStringTypeTrait, removing duplicates from HasMethodType, HasPropertyType, HasOffsetType, HasOffsetValueType - Remove redundant getCallableParametersAcceptors() from HasMethodType (already provided by MaybeCallableTypeTrait) - Remove unused TrivialParametersAcceptor imports - Change getClassStringObjectType() to return $this on HasMethodType and HasPropertyType so that class-string intersections preserve method/property info (needed for e.g. new $classString() after method_exists check) - Keep getObjectTypeOrClassStringObjectType() returning $this (same reason) Co-Authored-By: Claude Opus 4.6 --- src/Type/Accessory/HasMethodType.php | 15 +-------------- src/Type/Accessory/HasOffsetType.php | 5 ----- src/Type/Accessory/HasOffsetValueType.php | 5 ----- src/Type/Accessory/HasPropertyType.php | 8 +------- src/Type/Traits/MaybeStringTypeTrait.php | 5 +++++ 5 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 60cede22a7..f25e8c844e 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -9,7 +9,6 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\Dummy\DummyMethodReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\TrinaryLogic; @@ -77,11 +76,6 @@ public function getObjectClassReflections(): array return []; } - public function getConstantStrings(): array - { - return []; - } - public function getClassStringType(): Type { return new GenericClassStringType($this); @@ -183,13 +177,6 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array - { - return [ - new TrivialParametersAcceptor(), - ]; - } - public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -242,7 +229,7 @@ public function isInteger(): TrinaryLogic public function getClassStringObjectType(): Type { - return new ErrorType(); + return $this; } public function getObjectTypeOrClassStringObjectType(): Type diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 8cfd24c136..faf598ac8c 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -75,11 +75,6 @@ public function getObjectClassReflections(): array return []; } - public function getConstantStrings(): array - { - return []; - } - public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index f934183fb2..eadf8f2a17 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -81,11 +81,6 @@ public function getObjectClassReflections(): array return []; } - public function getConstantStrings(): array - { - return []; - } - public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index eb8cbc5e29..86ed660242 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -6,7 +6,6 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; use PHPStan\Type\BooleanType; @@ -69,11 +68,6 @@ public function getObjectClassReflections(): array return []; } - public function getConstantStrings(): array - { - return []; - } - public function getClassStringType(): Type { return new GenericClassStringType($this); @@ -218,7 +212,7 @@ public function isInteger(): TrinaryLogic public function getClassStringObjectType(): Type { - return new ErrorType(); + return $this; } public function getObjectTypeOrClassStringObjectType(): Type diff --git a/src/Type/Traits/MaybeStringTypeTrait.php b/src/Type/Traits/MaybeStringTypeTrait.php index 4729b3ae99..caa9abacb4 100644 --- a/src/Type/Traits/MaybeStringTypeTrait.php +++ b/src/Type/Traits/MaybeStringTypeTrait.php @@ -7,6 +7,11 @@ trait MaybeStringTypeTrait { + public function getConstantStrings(): array + { + return []; + } + public function isString(): TrinaryLogic { return TrinaryLogic::createMaybe();