Skip to content

Commit

Permalink
Bleeding edge - RuntimeReflectionFunctionRule
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jun 15, 2022
1 parent 1d58f26 commit c4a662a
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ parameters:
checkUnresolvableParameterTypes: true
readOnlyByPhpDoc: true
phpDocParserRequireWhitespaceBeforeDescription: true
runtimeReflectionRules: true
4 changes: 4 additions & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ conditionalTags:
phpstan.rules.rule: %checkUninitializedProperties%
PHPStan\Rules\Methods\ConsistentConstructorRule:
phpstan.rules.rule: %featureToggles.consistentConstructor%
PHPStan\Rules\Api\RuntimeReflectionFunctionRule:
phpstan.rules.rule: %featureToggles.runtimeReflectionRules%

rules:
- PHPStan\Rules\Api\ApiInstantiationRule
Expand Down Expand Up @@ -78,6 +80,8 @@ rules:
services:
-
class: PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule
-
class: PHPStan\Rules\Api\RuntimeReflectionFunctionRule
-
class: PHPStan\Rules\Classes\ExistingClassInClassExtendsRule
tags:
Expand Down
2 changes: 2 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ parameters:
checkUnresolvableParameterTypes: false
readOnlyByPhpDoc: false
phpDocParserRequireWhitespaceBeforeDescription: false
runtimeReflectionRules: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -243,6 +244,7 @@ parametersSchema:
checkUnresolvableParameterTypes: bool()
readOnlyByPhpDoc: bool()
phpDocParserRequireWhitespaceBeforeDescription: bool()
runtimeReflectionRules: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
10 changes: 10 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
parameters:
ignoreErrors:
-
message: "#^Function is_a\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#"
count: 1
path: src/Analyser/DirectScopeFactory.php

-
message: "#^Function is_a\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#"
count: 1
path: src/Analyser/LazyScopeFactory.php

-
message: """
#^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\:
Expand Down
76 changes: 76 additions & 0 deletions src/Rules/Api/RuntimeReflectionFunctionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Api;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function array_keys;
use function in_array;
use function sprintf;
use function strpos;

/**
* @implements Rule<Node\Expr\FuncCall>
*/
class RuntimeReflectionFunctionRule implements Rule
{

public function __construct(private ReflectionProvider $reflectionProvider)
{
}

public function getNodeType(): string
{
return Node\Expr\FuncCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Name) {
return [];
}

if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
return [];
}

$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
if (!in_array($functionReflection->getName(), [
'is_a',
'is_subclass_of',
'class_parents',
'class_implements',
'class_uses',
], true)) {
return [];
}

if (!$scope->isInClass()) {
return [];
}

$classReflection = $scope->getClassReflection();
$hasPhpStanInterface = false;
foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) {
if (strpos($interfaceName, 'PHPStan\\') !== 0) {
continue;
}

$hasPhpStanInterface = true;
}

if (!$hasPhpStanInterface) {
return [];
}

return [
RuleErrorBuilder::message(
sprintf('Function %s() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', $functionReflection->getName()),
)->build(),
];
}

}
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Api;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<RuntimeReflectionFunctionRule>
*/
class RuntimeReflectionFunctionRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new RuntimeReflectionFunctionRule($this->createReflectionProvider());
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/runtime-reflection-function.php'], [
[
'Function is_a() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.',
43,
],
[
'Function is_subclass_of() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.',
46,
],
[
'Function class_parents() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.',
49,
],
[
'Function class_implements() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.',
50,
],
[
'Function class_uses() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.',
51,
],
]);
}

}
54 changes: 54 additions & 0 deletions tests/PHPStan/Rules/Api/data/runtime-reflection-function.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace RuntimeReflectionFunction;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;

class Foo
{

public function doFoo(object $o): void
{
if (is_a($o, \stdClass::class)) {

}
}

}

class Bar implements DynamicMethodReturnTypeExtension
{

public function getClass(): string
{
// TODO: Implement getClass() method.
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
// TODO: Implement isMethodSupported() method.
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
// TODO: Implement getTypeFromMethodCall() method.
}

public function doFoo(object $o): void
{
if (is_a($o, \stdClass::class)) {

}
if (is_subclass_of($o, \stdClass::class)) {

}
$p = class_parents($o);
$i = class_implements($o);
$t = class_uses($o);
}

}

2 comments on commit c4a662a

@staabm
Copy link
Contributor

@staabm staabm commented on c4a662a Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you are aware of it, but if not: we recently fixed a static vs runtime reflection issue which was triggered by extension code which instantiated a ReflectionMethod object

see deprecated-packages/symplify#4166

these should also be reported as an error I guess…?

@ondrejmirtes
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s literally the next item on my todolist 😊

Please sign in to comment.