From b597ab59baadd81d3ed2ff8655534dd7a612fff0 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/15] 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/15] 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 e22ac8b5c9ef9d1932bc4d935d568b507d57e981 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 20:39:38 +0000 Subject: [PATCH 03/15] 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 778438a9d149117a2215473ad810cf95f466d4ec Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 21:02:53 +0000 Subject: [PATCH 04/15] 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 1d122f059b47ffd11144d30939dd664bdc75881c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 21:39:16 +0000 Subject: [PATCH 05/15] 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 e6c6ae12486c3d3af47cf1e5eb03e543d62a7626 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 07:22:33 +0000 Subject: [PATCH 06/15] 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 c2eb4fdbf0b505ce4503e4a16466c157f466ab80 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 07:42:13 +0000 Subject: [PATCH 07/15] 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 fe9f070b9d61a82d8096c0ae99429ba33c09ebdb Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 08:19:50 +0000 Subject: [PATCH 08/15] 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 027fc88f1d88d55db35bc7f184ad69151d9bcb2c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 09:12:27 +0000 Subject: [PATCH 09/15] 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 8504887894acf6671c19f5b5b1429931f01d08a4 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 09:20:48 +0000 Subject: [PATCH 10/15] 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 a4e55aa3a4d5ec5235742d8efc002ed74debe3df Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 09:41:51 +0000 Subject: [PATCH 11/15] 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 d0c5740db2..3c056dbb6b 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; @@ -44,6 +45,7 @@ class HasOffsetType implements CompoundType, AccessoryType use MaybeCallableTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; + use MaybeStringTypeTrait; use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; @@ -347,46 +349,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(); @@ -402,11 +364,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 1ae12e2ced..2b19215b10 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -27,6 +27,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; @@ -49,6 +50,7 @@ class HasOffsetValueType implements CompoundType, AccessoryType use MaybeCallableTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; + use MaybeStringTypeTrait; use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; @@ -441,46 +443,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(); @@ -496,11 +458,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/15] 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 efa69dcde769ff9ca74edb4c4366c6150f5819bc Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 13:38:02 +0000 Subject: [PATCH 13/15] 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 3c056dbb6b..e65fe55b72 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -79,11 +79,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 2b19215b10..54bcea9872 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -86,11 +86,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(); From 890a77ddff6910852698dbcdcf525b5841fdbedb Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 10 May 2026 11:34:19 +0200 Subject: [PATCH 14/15] Fix --- src/Type/Accessory/HasMethodType.php | 5 +++++ src/Type/Accessory/HasPropertyType.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index f25e8c844e..07c6b3c20f 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -252,6 +252,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 86ed660242..c56a6a17d8 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -235,6 +235,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); From 6785bbcf50d854ccca7bceefb97dc1eba92835c1 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 10 May 2026 10:51:12 +0000 Subject: [PATCH 15/15] Remove unused ClassMemberAccessAnswerer import from HasPropertyType Co-Authored-By: Claude Opus 4.6 --- src/Type/Accessory/HasPropertyType.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index c56a6a17d8..2a597dae0d 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -5,7 +5,6 @@ use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; use PHPStan\Type\BooleanType;