Skip to content

Commit 9e00725

Browse files
committed
Bleeding edge - API class const fetch rule
1 parent 7aa19e0 commit 9e00725

5 files changed

+154
-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\ApiClassConstFetchRule:
16+
phpstan.rules.rule: %featureToggles.runtimeReflectionRules%
1517
PHPStan\Rules\Api\ApiInstanceofRule:
1618
phpstan.rules.rule: %featureToggles.runtimeReflectionRules%
1719
PHPStan\Rules\Api\RuntimeReflectionFunctionRule:
@@ -82,6 +84,8 @@ rules:
8284
- PHPStan\Rules\Whitespace\FileWhitespaceRule
8385

8486
services:
87+
-
88+
class: PHPStan\Rules\Api\ApiClassConstFetchRule
8589
-
8690
class: PHPStan\Rules\Api\ApiInstanceofRule
8791
-
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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\ClassConstFetch>
15+
*/
16+
class ApiClassConstFetchRule 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\ClassConstFetch::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
if (!$node->name instanceof Node\Identifier) {
34+
return [];
35+
}
36+
37+
if (!$node->class instanceof Node\Name) {
38+
return [];
39+
}
40+
41+
$className = $scope->resolveName($node->class);
42+
if (!$this->reflectionProvider->hasClass($className)) {
43+
return [];
44+
}
45+
46+
$classReflection = $this->reflectionProvider->getClass($className);
47+
if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName())) {
48+
return [];
49+
}
50+
51+
$ruleError = RuleErrorBuilder::message(sprintf(
52+
'Accessing %s::%s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.',
53+
$classReflection->getDisplayName(),
54+
$node->name->toString(),
55+
))->tip(sprintf(
56+
"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",
57+
'https://github.com/phpstan/phpstan/discussions',
58+
))->build();
59+
60+
$docBlock = $classReflection->getResolvedPhpDoc();
61+
if ($docBlock === null) {
62+
return [$ruleError];
63+
}
64+
65+
foreach ($docBlock->getPhpDocNodes() as $phpDocNode) {
66+
$apiTags = $phpDocNode->getTagsByName('@api');
67+
if (count($apiTags) > 0) {
68+
return [];
69+
}
70+
}
71+
72+
return [$ruleError];
73+
}
74+
75+
}
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<ApiClassConstFetchRule>
11+
*/
12+
class ApiClassConstFetchRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new ApiClassConstFetchRule(new ApiRuleHelper(), $this->createReflectionProvider());
18+
}
19+
20+
public function testRuleInPhpStan(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/class-const-fetch-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/class-const-fetch-out-of-phpstan.php'], [
33+
[
34+
'Accessing PHPStan\Command\AnalyseCommand::class is not covered by backward compatibility promise. The class might change in a minor PHPStan version.',
35+
14,
36+
$tip,
37+
],
38+
]);
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PHPStan\ClassConstFetch;
4+
5+
use PHPStan\Command\AnalyseCommand;
6+
use PHPStan\Reflection\ClassReflection;
7+
8+
class Foo
9+
{
10+
11+
public function doFoo()
12+
{
13+
echo ClassReflection::class;
14+
echo AnalyseCommand::class;
15+
}
16+
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\ClassConstFetch;
4+
5+
use PHPStan\Command\AnalyseCommand;
6+
use PHPStan\Reflection\ClassReflection;
7+
8+
class Foo
9+
{
10+
11+
public function doFoo()
12+
{
13+
echo ClassReflection::class;
14+
echo AnalyseCommand::class;
15+
}
16+
17+
}

0 commit comments

Comments
 (0)