Skip to content

Commit ff4d02d

Browse files
committed
Bleeding edge - ApiInstanceofRule
1 parent ffc355d commit ff4d02d

File tree

5 files changed

+160
-0
lines changed

5 files changed

+160
-0
lines changed

conf/config.level0.neon

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ conditionalTags:
1212
phpstan.rules.rule: %checkUninitializedProperties%
1313
PHPStan\Rules\Methods\ConsistentConstructorRule:
1414
phpstan.rules.rule: %featureToggles.consistentConstructor%
15+
PHPStan\Rules\Api\ApiInstanceofRule:
16+
phpstan.rules.rule: %featureToggles.runtimeReflectionRules%
1517
PHPStan\Rules\Api\RuntimeReflectionFunctionRule:
1618
phpstan.rules.rule: %featureToggles.runtimeReflectionRules%
1719
PHPStan\Rules\Api\RuntimeReflectionInstantiationRule:
@@ -80,6 +82,8 @@ rules:
8082
- PHPStan\Rules\Whitespace\FileWhitespaceRule
8183

8284
services:
85+
-
86+
class: PHPStan\Rules\Api\ApiInstanceofRule
8387
-
8488
class: PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule
8589
-

src/Rules/Api/ApiInstanceofRule.php

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Api;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\ReflectionProvider;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use function count;
11+
use function sprintf;
12+
13+
/**
14+
* @implements Rule<Node\Expr\Instanceof_>
15+
*/
16+
class ApiInstanceofRule implements Rule
17+
{
18+
19+
public function __construct(
20+
private ApiRuleHelper $apiRuleHelper,
21+
private ReflectionProvider $reflectionProvider,
22+
)
23+
{
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return Node\Expr\Instanceof_::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
if (!$node->class instanceof Node\Name) {
34+
return [];
35+
}
36+
37+
$className = $scope->resolveName($node->class);
38+
if (!$this->reflectionProvider->hasClass($className)) {
39+
return [];
40+
}
41+
42+
$classReflection = $this->reflectionProvider->getClass($className);
43+
if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName())) {
44+
return [];
45+
}
46+
47+
$ruleError = RuleErrorBuilder::message(sprintf(
48+
'Asking about instanceof %s is not covered by backward compatibility promise. The %s might change in a minor PHPStan version.',
49+
$classReflection->getDisplayName(),
50+
$classReflection->isInterface() ? 'interface' : 'class',
51+
))->tip(sprintf(
52+
"If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise",
53+
'https://github.com/phpstan/phpstan/discussions',
54+
))->build();
55+
56+
$docBlock = $classReflection->getResolvedPhpDoc();
57+
if ($docBlock === null) {
58+
return [$ruleError];
59+
}
60+
61+
foreach ($docBlock->getPhpDocNodes() as $phpDocNode) {
62+
$apiTags = $phpDocNode->getTagsByName('@api');
63+
if (count($apiTags) > 0) {
64+
return [];
65+
}
66+
}
67+
68+
return [$ruleError];
69+
}
70+
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Api;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use function sprintf;
8+
9+
/**
10+
* @extends RuleTestCase<ApiInstanceofRule>
11+
*/
12+
class ApiInstanceofRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new ApiInstanceofRule(new ApiRuleHelper(), $this->createReflectionProvider());
18+
}
19+
20+
public function testRuleInPhpStan(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/instanceof-in-phpstan.php'], []);
23+
}
24+
25+
public function testRuleOutOfPhpStan(): void
26+
{
27+
$tip = sprintf(
28+
"If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise",
29+
'https://github.com/phpstan/phpstan/discussions',
30+
);
31+
32+
$this->analyse([__DIR__ . '/data/instanceof-out-of-phpstan.php'], [
33+
[
34+
'Asking about instanceof PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator is not covered by backward compatibility promise. The class might change in a minor PHPStan version.',
35+
17,
36+
$tip,
37+
],
38+
]);
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace PHPStan\TestInstanceof;
4+
5+
use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator;
6+
use PHPStan\Reflection\ClassReflection;
7+
8+
class Foo
9+
{
10+
11+
public function doFoo(object $o): void
12+
{
13+
if ($o instanceof ClassReflection) {
14+
15+
}
16+
17+
if ($o instanceof AutoloadSourceLocator) {
18+
19+
}
20+
}
21+
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace App\TestInstanceof;
4+
5+
use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator;
6+
use PHPStan\Reflection\ClassReflection;
7+
8+
class Foo
9+
{
10+
11+
public function doFoo(object $o): void
12+
{
13+
if ($o instanceof ClassReflection) {
14+
15+
}
16+
17+
if ($o instanceof AutoloadSourceLocator) {
18+
19+
}
20+
}
21+
22+
}

0 commit comments

Comments
 (0)