-
Notifications
You must be signed in to change notification settings - Fork 545
Detect static calls to non-static methods #727
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
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
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
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!
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
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
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.
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.
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.
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();
}
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.
97c2f21 to
a03a78f
Compare
I forgot about this and will work on follow ups – for that InAnyClass object.
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 InAnyClassScope locally and running the tests.
38a7c84 to
487c7ca
Compare
Blah. InAnyClassScope fixed the failures I caused, but doesn't report the failures in the test I created:
1) PHPStan\Rules\Methods\CallStaticMethodsRuleTest::testBug5782
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'23: Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{'Bug5782\\HelloWorld', 'sayGoodbye'} given.
-30: Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{'Bug5782\\HelloWorld', 'sayGoodbye'} given.
-31: Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{'Bug5782\\HelloWorld', 'sayGoodbye'} given.
+'
'
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:
$accepts = $acceptingType->accepts($acceptedType, $strictTypes);
if (!$accepts->yes() && $acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) {
The $accepts trinary logic is always YES because of CallableType:
private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): TrinaryLogic
{
$isCallable = $type->isCallable();
if ($isCallable->no() || $this->isCommonCallable) {
return $isCallable;
}
The isCommonCallable is always true since there are no parameters.
So even if getCallableParametersAcceptors returns TrivialParametersAcceptor it isn't reported correctly.
Uh oh!
There was an error while loading. Please reload this page.
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.