New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Detect static calls to non-static methods #727
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried writing a test, but to be honest I had no idea what rule or test this would affect. So I finally just made some changes and tossed it up.
I made the quick changes for $has->yes(), I'll inspect test failures when I have my PHPStan dev time. Looks like 1 test added more failures, the other is an error. I didn't have time to inspect the test cases. |
91ef10d
to
67c6ac7
Compare
[ | ||
'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{\'Bug1971\\\HelloWorld\', \'sayHello\'} given.', | ||
14, | ||
], | ||
[ | ||
'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string<static(Bug1971\\HelloWorld)>, \'sayHello\'} given.', | ||
15, | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did not update the test data, but cover the fact these instance methods called statically do not exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem here is that this actually works: https://3v4l.org/mTa9S
It only doesn't work from the outside: https://3v4l.org/dUmHk
The same problem is covered in CallStaticMethodRule here:
phpstan-src/src/Rules/Methods/CallStaticMethodsRule.php
Lines 215 to 236 in 625cb84
$method = $classType->getMethod($methodName, $scope); | |
if (!$method->isStatic()) { | |
$function = $scope->getFunction(); | |
if ( | |
!$function instanceof MethodReflection | |
|| $function->isStatic() | |
|| !$scope->isInClass() | |
|| ( | |
$classType instanceof TypeWithClassName | |
&& $scope->getClassReflection()->getName() !== $classType->getClassName() | |
&& !$scope->getClassReflection()->isSubclassOf($classType->getClassName()) | |
) | |
) { | |
return array_merge($errors, [ | |
RuleErrorBuilder::message(sprintf( | |
'Static call to instance method %s::%s().', | |
$method->getDeclaringClass()->getDisplayName(), | |
$method->getName() | |
))->build(), | |
]); | |
} | |
} |
So we should replicate the same logic here. We can take advantage of Scope being passed to getCallableParametersAcceptors
and also pass it along to findTypeAndMethodName
but as an optional parameter because of backward compatibility (but go through all the current call sites and pass Scope in it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One problem I'm hitting: isCallable
does not have a scope, and that's where findTypeAndMethodName
seems to be invoked from. So I added OutOfClassScope
, to check if we're in a class. If not, return null
, but that always returns false.
EDIT: The fix appears to update $has
to maybe if we are not in a class context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having ConstantArrayType return maybe
on a static call outside of the class or in class with mismatching class names seems to have resolved this. Thanks for the tip!
[ | ||
'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{\'Bug1971\\\HelloWorld\', \'sayHello\'} given.', | ||
14, | ||
], | ||
[ | ||
'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string<static(Bug1971\\HelloWorld)>, \'sayHello\'} given.', | ||
15, | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem here is that this actually works: https://3v4l.org/mTa9S
It only doesn't work from the outside: https://3v4l.org/dUmHk
The same problem is covered in CallStaticMethodRule here:
phpstan-src/src/Rules/Methods/CallStaticMethodsRule.php
Lines 215 to 236 in 625cb84
$method = $classType->getMethod($methodName, $scope); | |
if (!$method->isStatic()) { | |
$function = $scope->getFunction(); | |
if ( | |
!$function instanceof MethodReflection | |
|| $function->isStatic() | |
|| !$scope->isInClass() | |
|| ( | |
$classType instanceof TypeWithClassName | |
&& $scope->getClassReflection()->getName() !== $classType->getClassName() | |
&& !$scope->getClassReflection()->isSubclassOf($classType->getClassName()) | |
) | |
) { | |
return array_merge($errors, [ | |
RuleErrorBuilder::message(sprintf( | |
'Static call to instance method %s::%s().', | |
$method->getDeclaringClass()->getDisplayName(), | |
$method->getName() | |
))->build(), | |
]); | |
} | |
} |
So we should replicate the same logic here. We can take advantage of Scope being passed to getCallableParametersAcceptors
and also pass it along to findTypeAndMethodName
but as an optional parameter because of backward compatibility (but go through all the current call sites and pass Scope in it).
a9cfcd5
to
6b932d9
Compare
$typeAndMethod = $this->findTypeAndMethodName(); | ||
$typeAndMethod = $this->findTypeAndMethodName(new OutOfClassScope()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing out of class scope here is causing grief for CallStaticMethodsRuleTest::testBug1971
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ondrejmirtes I'd like your feedback on a proposal. I believe we need to add the scope to \PHPStan\Type\Type::isCallable
like \PHPStan\Type\Type::getCallableParametersAcceptors
.
This way we can determine the class context if a static method is truly callable or not.
public function isCallable(): TrinaryLogic | ||
{ | ||
$typeAndMethod = $this->findTypeAndMethodName(); | ||
// @todo is there a way to fetch the full scope here. | ||
// An OutOfClassScope causes frivolous errors when checking if a | ||
// non-static method was invoked statically in its own class, which | ||
// is allowed per: https://3v4l.org/mTa9S. | ||
// Should add an optional scope parameter to isCallable and if null, | ||
// create a new OutOfClassScope instance. | ||
$typeAndMethod = $this->findTypeAndMethodName(new OutOfClassScope()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After circling around this issue for the past few weeks, I think we need to pass the scope to isCallable
. Knowing if something is callable depends on the scope so we can check the current class. A non-static method on a class can be invoked statically if it is within the same class. This is will always be false given OutOfClassScope
.
This would also improve detection in ConstantStringType
. I didn't want to make the big change without first discussing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OutOfClassScope
scope here is what is causing the test failures for CallStaticMethodsRuleTest::testBug1971
.
1) PHPStan\Rules\Methods\CallStaticMethodsRuleTest::testBug1971
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'16: Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string<static(Bug1971\HelloWorld)>, 'sayHello2'} given.
+'14: Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{'Bug1971\\HelloWorld', 'sayHello'} given.
+15: Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string<static(Bug1971\HelloWorld)>, 'sayHello'} given.
+16: Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string<static(Bug1971\HelloWorld)>, 'sayHello2'} given.
'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about a new class called InAnyClassScope
that returns true
from all the can*
methods?
We'd still get the actual validation because getCallableParametersAcceptors
accepts the real scope.
if ($scope === null) { | ||
$scope = new OutOfClassScope(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure if this should be pushed to the top of the method – to provide a value for a default nullable meant for backward compatibility. Or instantiated when it's finally needed.
// @todo this should also error on mixed type, but doesn't? | ||
// ConstantStringType::getCallableParametersAcceptors returns a | ||
// MixedType but that doesn't cause an error like ConstantArrayType. | ||
[ | ||
'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{\'Bug5782\\\HelloWorld\', \'sayGoodbye\'} given.', | ||
31, | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe adding the scope to ConstantStringType::isCallable
will also cause this test failure to be fixed. Without the scope to know if we're in a class or not, this always returns as yes
for the method being callable when it actually may not be.
$classRef = $reflectionProvider->getClass($matches[1]);
if ($classRef->hasMethod($matches[2])) {
return TrinaryLogic::createYes();
}
public function isCallable(): TrinaryLogic | ||
{ | ||
$typeAndMethod = $this->findTypeAndMethodName(); | ||
// @todo is there a way to fetch the full scope here. | ||
// An OutOfClassScope causes frivolous errors when checking if a | ||
// non-static method was invoked statically in its own class, which | ||
// is allowed per: https://3v4l.org/mTa9S. | ||
// Should add an optional scope parameter to isCallable and if null, | ||
// create a new OutOfClassScope instance. | ||
$typeAndMethod = $this->findTypeAndMethodName(new OutOfClassScope()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about a new class called InAnyClassScope
that returns true
from all the can*
methods?
We'd still get the actual validation because getCallableParametersAcceptors
accepts the real scope.
I forgot about this and will work on follow ups – for that |
Do you plan to finish this or can I close the PR? :) |
I will finish, I forgot (😬). Started a new job and things fell apart in that shuffle. Added back to my Todoist |
Adds a new test case with static methods for PR727 matching test data for Issue 1971.
c815c71
to
6db6c4b
Compare
Rebased and re-uploading this to my head. I made the |
38a7c84
to
487c7ca
Compare
Blah.
|
I am just going to close this. It's over my head, and I keep bumping into other areas. Now I've entered this realm: RuleLevelHelper::accepts:
The
The So even if |
Fixes phpstan/phpstan#5782
This initial commit is definitely not the right way, but I'm going in circles and want to at least get something up for review and direction.