From 90903faf3550c83203012d6dd0243b62dd00adb3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 5 Nov 2022 10:42:56 +0100 Subject: [PATCH 1/4] support 'HasMethodType' on StaticCall --- src/Type/Constant/ConstantStringType.php | 8 +++++++ .../MethodExistsTypeSpecifyingExtension.php | 22 ++++++++++++++----- .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/static-has-method.php | 21 ++++++++++++++++++ .../Methods/CallStaticMethodsRuleTest.php | 6 +++++ .../Rules/Methods/data/static-has-method.php | 18 +++++++++++++++ 6 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/static-has-method.php create mode 100644 tests/PHPStan/Rules/Methods/data/static-has-method.php diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 08ea58759e..d3a4471b75 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -64,6 +64,14 @@ public function getValue(): string return $this->value; } + public function hasMethod(string $methodName): TrinaryLogic + { + if ($this->isClassString()) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createNo(); + } + public function isClassString(): bool { if ($this->isClassString) { diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 1498960929..83cf04943c 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -47,18 +47,30 @@ public function specifyTypes( TypeSpecifierContext $context, ): SpecifiedTypes { + $methodNameType = $scope->getType($node->getArgs()[1]->value); + if (!$methodNameType instanceof ConstantStringType) { + return new SpecifiedTypes([], []); + } + $objectType = $scope->getType($node->getArgs()[0]->value); if (!$objectType instanceof ObjectType) { + if ($objectType instanceof ConstantStringType && $objectType->isClassString()) { + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + new IntersectionType([ + $objectType, + new HasMethodType($methodNameType->getValue()), + ]), + $context, + false, + $scope, + ); + } if ($objectType->isString()->yes()) { return new SpecifiedTypes([], []); } } - $methodNameType = $scope->getType($node->getArgs()[1]->value); - if (!$methodNameType instanceof ConstantStringType) { - return new SpecifiedTypes([], []); - } - return $this->typeSpecifier->create( $node->getArgs()[0]->value, new UnionType([ diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index a827d06f3b..e300c57340 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1124,6 +1124,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8272.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8277.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/strtr.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/static-has-method.php'); } /** diff --git a/tests/PHPStan/Analyser/data/static-has-method.php b/tests/PHPStan/Analyser/data/static-has-method.php new file mode 100644 index 0000000000..b00640bc91 --- /dev/null +++ b/tests/PHPStan/Analyser/data/static-has-method.php @@ -0,0 +1,21 @@ +analyse([__DIR__ . '/data/bug-7489.php'], []); } + public function testHasMethodStaticCall(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/static-has-method.php'], []); + } } diff --git a/tests/PHPStan/Rules/Methods/data/static-has-method.php b/tests/PHPStan/Rules/Methods/data/static-has-method.php new file mode 100644 index 0000000000..df5ac26e11 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-has-method.php @@ -0,0 +1,18 @@ + Date: Sat, 5 Nov 2022 11:03:07 +0100 Subject: [PATCH 2/4] fix StaticMethodCallCheck --- src/Rules/Methods/StaticMethodCallCheck.php | 5 +++++ tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php | 1 + 2 files changed, 6 insertions(+) diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index 64fb1adf87..48f50f9dff 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -109,6 +109,11 @@ public function check( $classType = $scope->resolveTypeByName($class); } else { + $classStringType = $scope->getType(new Expr\ClassConstFetch($class, 'class')); + if ($classStringType->hasMethod($methodName)->yes()) { + return [[], null]; + } + if (!$this->reflectionProvider->hasClass($className)) { if ($scope->isInClassExists($className)) { return [[], null]; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index f7780a6ce0..5342e72a18 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -537,4 +537,5 @@ public function testHasMethodStaticCall(): void $this->checkExplicitMixed = false; $this->analyse([__DIR__ . '/data/static-has-method.php'], []); } + } From 5682ccab5205433dc1d434e875193342e3913f8a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 5 Nov 2022 12:08:03 +0100 Subject: [PATCH 3/4] simplify --- src/Type/Php/MethodExistsTypeSpecifyingExtension.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 83cf04943c..21485d6a75 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -14,7 +14,6 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; -use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\UnionType; use function count; @@ -53,7 +52,7 @@ public function specifyTypes( } $objectType = $scope->getType($node->getArgs()[0]->value); - if (!$objectType instanceof ObjectType) { + if ($objectType->isString()->yes()) { if ($objectType instanceof ConstantStringType && $objectType->isClassString()) { return $this->typeSpecifier->create( $node->getArgs()[0]->value, @@ -66,9 +65,8 @@ public function specifyTypes( $scope, ); } - if ($objectType->isString()->yes()) { - return new SpecifiedTypes([], []); - } + + return new SpecifiedTypes([], []); } return $this->typeSpecifier->create( From 9d1bca97c585a28ad81fb9d9c3f369bc2e6715cd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 5 Nov 2022 14:27:42 +0100 Subject: [PATCH 4/4] more testcases --- .../Methods/CallStaticMethodsRuleTest.php | 11 +++++- .../Rules/Methods/data/static-has-method.php | 39 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 5342e72a18..d85fd40584 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -535,7 +535,16 @@ public function testHasMethodStaticCall(): void { $this->checkThisOnly = false; $this->checkExplicitMixed = false; - $this->analyse([__DIR__ . '/data/static-has-method.php'], []); + $this->analyse([__DIR__ . '/data/static-has-method.php'], [ + [ + 'Call to an undefined static method StaticHasMethodCall\rex_var::doesNotExist().', + 38, + ], + [ + 'Call to an undefined static method StaticHasMethodCall\rex_var::doesNotExist().', + 48, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Methods/data/static-has-method.php b/tests/PHPStan/Rules/Methods/data/static-has-method.php index df5ac26e11..177bde0ab4 100644 --- a/tests/PHPStan/Rules/Methods/data/static-has-method.php +++ b/tests/PHPStan/Rules/Methods/data/static-has-method.php @@ -2,17 +2,50 @@ namespace StaticHasMethodCall; -class rex_var {} +class rex_var { + public static function aMethod() {} +} class HelloWorld { public function sayHello(): void { - if (!method_exists(rex_var::class, 'varsIterator')) { + if (!method_exists(rex_var::class, 'doesNotExist')) { + return; + } + + // should not error + $it = rex_var::doesNotExist(); + } + + public function sayHello2(): void + { + if (!method_exists(rex_var::class, 'doesNotExist')) { + return; + } + + // should not error + $it = rex_var::aMethod(); + } + + public function sayHello3(): void + { + if (!method_exists(rex_var::class, 'anotherNotExistingMethod')) { + return; + } + + // should error + $it = rex_var::doesNotExist(); + } + + public function sayHello4(): void + { + if (!method_exists(notExistentClass::class, 'doesNotExist')) { return; } - $it = rex_var::varsIterator(); + // should error + $it = rex_var::doesNotExist(); } }