Skip to content

Conversation

@mglaman
Copy link
Contributor

@mglaman mglaman commented Oct 21, 2021

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.

Copy link
Contributor Author

@mglaman mglaman left a 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.

@mglaman
Copy link
Contributor Author

mglaman commented Oct 21, 2021

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.

@mglaman mglaman force-pushed the non-static-methods-call-directly branch from 91ef10d to 67c6ac7 Compare October 28, 2021 19:05
@mglaman mglaman requested a review from ondrejmirtes October 28, 2021 19:25
Comment on lines 435 to 442
Copy link
Contributor Author

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.

Copy link
Member

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:

$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).

Copy link
Contributor Author

@mglaman mglaman Nov 11, 2021

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.

Copy link
Contributor Author

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!

@mglaman mglaman requested a review from ondrejmirtes November 4, 2021 19:42
Comment on lines 435 to 442
Copy link
Member

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:

$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).

@mglaman mglaman force-pushed the non-static-methods-call-directly branch from a9cfcd5 to 6b932d9 Compare November 11, 2021 22:14
Comment on lines 317 to 325
Copy link
Contributor Author

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

Copy link
Contributor Author

@mglaman mglaman left a 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.

Comment on lines 316 to 325
Copy link
Contributor Author

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.

Copy link
Contributor Author

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.
 '

Copy link
Member

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.

Comment on lines 399 to 401
Copy link
Contributor Author

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.

Comment on lines +460 to +465
Copy link
Contributor Author

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();
			}

@mglaman mglaman requested a review from ondrejmirtes November 18, 2021 20:17
Comment on lines 316 to 325
Copy link
Member

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.

@mglaman
Copy link
Contributor Author

mglaman commented Jan 5, 2022

I forgot about this and will work on follow ups – for that InAnyClass object.

@ondrejmirtes
Copy link
Member

Do you plan to finish this or can I close the PR? :)

@mglaman
Copy link
Contributor Author

mglaman commented Feb 8, 2022

I will finish, I forgot (😬). Started a new job and things fell apart in that shuffle. Added back to my Todoist

@mglaman mglaman force-pushed the non-static-methods-call-directly branch from c815c71 to 6db6c4b Compare February 11, 2022 15:37
@mglaman
Copy link
Contributor Author

mglaman commented Feb 11, 2022

Rebased and re-uploading this to my head. I made the InAnyClassScope locally and running the tests.

@mglaman mglaman force-pushed the non-static-methods-call-directly branch from 38a7c84 to 487c7ca Compare February 11, 2022 17:42
@mglaman
Copy link
Contributor Author

mglaman commented Feb 11, 2022

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.
+'
 '

@mglaman
Copy link
Contributor Author

mglaman commented Feb 11, 2022

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.

@mglaman mglaman closed this Feb 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The ability to call non-static methods statically has been removed.

2 participants