Do not treat method_exists() as always true for @method-annotated methods#5664
Do not treat method_exists() as always true for @method-annotated methods#5664phpstan-bot wants to merge 3 commits into
method_exists() as always true for @method-annotated methods#5664Conversation
… methods - In `ImpossibleCheckTypeHelper::findSpecifiedType()`, the `method_exists()` check used `hasMethod()` which includes `@method` PHPDoc annotations. Since `@method` methods are virtual (handled by `__call()`), they don't guarantee `method_exists()` returns true at runtime. - Added `hasNativeMethod()` check for object types to only report always-true when the method is actually declared in PHP code, not just via `@method`. - Applied the same fix for the generic class-string code path. - Verified that `property_exists()` already handles `@property` correctly via `PropertyExistsTypeSpecifyingExtension::isNative()` check.
VincentLanglet
left a comment
There was a problem hiding this comment.
The rule should still report a method_exists already specified to true.
// @method should not make method_exists always true
if (\method_exists($hell, 'isTrue')) {
if (\method_exists($hell, 'isTrue')) { // This one should be reported
}
}
Also if we finally check native type ; is it still needed to check hasMethod($methodType->getValue())->yes() before ?
When inside an `if (method_exists(...))` block, a repeated method_exists() check for the same object and method is redundant and should be reported as always-true, even for @method-annotated methods. To detect this, the MethodExistsTypeSpecifyingExtension now also tracks the method_exists() FuncCall expression as ConstantBooleanType(true) in the scope (in addition to narrowing the argument type). This allows ImpossibleCheckTypeHelper to detect the nested case via Scope::hasExpressionType() before any type-specific logic runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's a summary of the changes made to address the review: Review point 1: Nested Two coordinated changes:
Review point 2: "Is Yes, it's still needed. The |
VincentLanglet
left a comment
There was a problem hiding this comment.
Does the issue exists for property_exists ; if not, why ? Can't the method_exist behave the same way ?
|
|
||
| if ($methodType instanceof ConstantStringType) { | ||
| if ($scope->hasExpressionType($node)->yes()) { | ||
| return true; |
There was a problem hiding this comment.
This feel wrong in a way that if an expressionType exists with the value "false" we still return...
There was a problem hiding this comment.
Pushed. Here's a summary of the changes:
Addressing the review comment: "Does the issue exist for property_exists; if not, why? Can't the method_exist behave the same way?"
The answer is: property_exists already handles this correctly because PropertyExistsTypeSpecifyingExtension checks $propertyReflection->isNative() and returns empty SpecifiedTypes for @property annotations (line 87). This prevents the type from being narrowed, so the check is never reported as always-true.
I've now made method_exists follow the same pattern:
-
MethodExistsTypeSpecifyingExtension: AddedReflectionProviderdependency andhasNativeMethod()checks, mirroringPropertyExistsTypeSpecifyingExtension. When the method exists only via@methodannotation, returns emptySpecifiedTypes([], [])— no type narrowing occurs. -
ImpossibleCheckTypeHelper: Simplified thehasNativeMethod()check (since the type specifier now handles the primary case). Removed the$scope->hasExpressionType()early return that the reviewer flagged as wrong (it would returntrueeven if the expression wasfalse). Removed thefuncCallSpecunion approach. -
Test data: Removed nested
method_exists()test cases (no longer relevant since the type isn't narrowed for@methodmethods, matchingproperty_existsbehavior).
…ropertyExistsTypeSpecifyingExtension Move the @method annotation check into MethodExistsTypeSpecifyingExtension to match how PropertyExistsTypeSpecifyingExtension handles @Property annotations. When the method only exists via @method PHPDoc, return empty SpecifiedTypes so the type is not narrowed. Simplify ImpossibleCheckTypeHelper accordingly and remove the nested funcCallSpec approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
property_exists is not statisfying cause it reports
if (property_exists($task, $a)) {
if (property_exists($task, $a)) {
}
}
but not
if (property_exists($task, '$a')) {
if (property_exists($task, '$a')) {
}
}
VincentLanglet
left a comment
There was a problem hiding this comment.
property_exists is not statisfying cause it reports
if (property_exists($task, $a)) {
if (property_exists($task, $a)) {
}
}
but not
if (property_exists($task, '$a')) {
if (property_exists($task, '$a')) {
}
}
Summary
When an interface or class has a
@methodPHPDoc annotation, PHPStan incorrectly reportsmethod_exists()checks as always evaluating to true. Since@method-annotated methods are virtual (typically handled by__call()) and don't exist as actual PHP methods,method_exists()may return false at runtime.Changes
src/Rules/Comparison/ImpossibleCheckTypeHelper.php:hasMethod()returns yes, additionally verify viaClassReflection::hasNativeMethod()that the method is actually declared in PHP code, not just via@methodannotation. Returnnull(indeterminate) instead oftruewhen only a@methodannotation exists.hasNativeMethod()guard before reporting always-true.tests/PHPStan/Rules/Comparison/data/bug-6211.phpcovering:@methodon an interface, checked via object instance and class string@methodon a class directly@propertywith__get()correctly handled (already working viaPropertyExistsTypeSpecifyingExtension)Root cause
ImpossibleCheckTypeHelper::findSpecifiedType()used$objectType->hasMethod()to determine ifmethod_exists()would always return true.hasMethod()delegates toClassReflection::hasMethod(), which includes methods fromAnnotationsMethodsClassReflectionExtension(i.e.,@methodPHPDoc annotations). Since these annotations describe methods that may be handled by__call()but don't create real PHP methods,method_exists()should not be considered always-true for them.The fix uses
ClassReflection::hasNativeMethod()to verify the method is an actual PHP method before reporting the check as always-true.Analogous cases probed
property_exists()with@property: Already correctly handled —PropertyExistsTypeSpecifyingExtensionchecks$propertyReflection->isNative()at line 87 and returns empty specifiers for non-native properties.method_exists()with class-string: Fixed in the same PR — the generic class-string code path inImpossibleCheckTypeHelperhad the same issue.Test
Regression test
testBug6211inImpossibleCheckTypeFunctionCallRuleTestwith test data covering@methodon interfaces and classes, both via object instances and class strings, alongside native methods that should still be reported as always-true.Fixes phpstan/phpstan#6211