Skip to content

Commit 1f0c70e

Browse files
Support libraries with @api phpdoc (#200)
Co-authored-by: Anderson Müller <anderson.a.muller@gmail.com>
1 parent 3c273e0 commit 1f0c70e

File tree

8 files changed

+332
-42
lines changed

8 files changed

+332
-42
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,11 @@ If you set up `editorUrl` [parameter](https://phpstan.org/user-guide/output-form
420420
> [!TIP]
421421
> You can change the list of debug references without affecting result cache, so rerun is instant!
422422
423+
## Usage in libraries:
424+
- Libraries typically contain public api, that is unused
425+
- If you mark such methods with `@api` phpdoc, those will be considered entrypoints
426+
- You can also mark whole class or interface with `@api` to mark all its methods as entrypoints
427+
423428
## Future scope:
424429
- Dead class property detection
425430
- Dead class detection

rules.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ services:
1414
-
1515
class: ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter
1616

17+
-
18+
class: ShipMonk\PHPStan\DeadCode\Provider\ApiPhpDocUsageProvider
19+
tags:
20+
- shipmonk.deadCode.memberUsageProvider
21+
arguments:
22+
enabled: %shipmonkDeadCode.usageProviders.apiPhpDoc.enabled%
23+
1724
-
1825
class: ShipMonk\PHPStan\DeadCode\Provider\VendorUsageProvider
1926
tags:
@@ -137,6 +144,8 @@ parameters:
137144
trackMixedAccess: null
138145
reportTransitivelyDeadMethodAsSeparateError: false
139146
usageProviders:
147+
apiPhpDoc:
148+
enabled: true
140149
vendor:
141150
enabled: true
142151
reflection:
@@ -168,6 +177,9 @@ parametersSchema:
168177
trackMixedAccess: schema(bool(), nullable()) # deprecated, use usageExcluders.usageOverMixed.enabled
169178
reportTransitivelyDeadMethodAsSeparateError: bool()
170179
usageProviders: structure([
180+
apiPhpDoc: structure([
181+
enabled: bool()
182+
])
171183
vendor: structure([
172184
enabled: bool()
173185
])

src/Debug/DebugUsagePrinter.php

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@
55
use LogicException;
66
use PHPStan\Command\Output;
77
use PHPStan\DependencyInjection\Container;
8-
use PHPStan\Reflection\ClassReflection;
98
use PHPStan\Reflection\ReflectionProvider;
10-
use ReflectionException;
119
use ShipMonk\PHPStan\DeadCode\Enum\MemberType;
1210
use ShipMonk\PHPStan\DeadCode\Error\BlackMember;
1311
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
1412
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
1513
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
1614
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
1715
use ShipMonk\PHPStan\DeadCode\Output\OutputEnhancer;
16+
use ShipMonk\PHPStan\DeadCode\Reflection\ReflectionHelper;
1817
use function array_map;
1918
use function array_sum;
2019
use function array_unique;
@@ -345,13 +344,13 @@ private function buildDebugMemberKeys(array $debugMembers): array
345344

346345
$classReflection = $this->reflectionProvider->getClass($normalizedClass);
347346

348-
if ($this->hasOwnMethod($classReflection, $memberName)) {
347+
if (ReflectionHelper::hasOwnMethod($classReflection, $memberName)) {
349348
$key = ClassMethodRef::buildKey($normalizedClass, $memberName);
350349

351-
} elseif ($this->hasOwnConstant($classReflection, $memberName)) {
350+
} elseif (ReflectionHelper::hasOwnConstant($classReflection, $memberName)) {
352351
$key = ClassConstantRef::buildKey($normalizedClass, $memberName);
353352

354-
} elseif ($this->hasOwnProperty($classReflection, $memberName)) {
353+
} elseif (ReflectionHelper::hasOwnProperty($classReflection, $memberName)) {
355354
throw new LogicException("Cannot debug '$debugMember', properties are not supported yet");
356355

357356
} else {
@@ -366,43 +365,6 @@ private function buildDebugMemberKeys(array $debugMembers): array
366365
return $result;
367366
}
368367

369-
private function hasOwnMethod(ClassReflection $classReflection, string $methodName): bool
370-
{
371-
if (!$classReflection->hasMethod($methodName)) {
372-
return false;
373-
}
374-
375-
try {
376-
return $classReflection->getNativeReflection()->getMethod($methodName)->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
377-
} catch (ReflectionException $e) {
378-
return false;
379-
}
380-
}
381-
382-
private function hasOwnConstant(ClassReflection $classReflection, string $constantName): bool
383-
{
384-
$constantReflection = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
385-
386-
if ($constantReflection === false) {
387-
return false;
388-
}
389-
390-
return $constantReflection->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
391-
}
392-
393-
private function hasOwnProperty(ClassReflection $classReflection, string $propertyName): bool
394-
{
395-
if (!$classReflection->hasProperty($propertyName)) {
396-
return false;
397-
}
398-
399-
try {
400-
return $classReflection->getNativeReflection()->getProperty($propertyName)->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
401-
} catch (ReflectionException $e) {
402-
return false;
403-
}
404-
}
405-
406368
/**
407369
* @param list<CollectedUsage> $collectedUsages
408370
*/
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Provider;
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
use PHPStan\Reflection\ReflectionProvider;
7+
use ReflectionClassConstant;
8+
use ReflectionMethod;
9+
use ShipMonk\PHPStan\DeadCode\Reflection\ReflectionHelper;
10+
use function strpos;
11+
12+
class ApiPhpDocUsageProvider extends ReflectionBasedMemberUsageProvider
13+
{
14+
15+
private ReflectionProvider $reflectionProvider;
16+
17+
private bool $enabled;
18+
19+
public function __construct(
20+
ReflectionProvider $reflectionProvider,
21+
bool $enabled
22+
)
23+
{
24+
$this->reflectionProvider = $reflectionProvider;
25+
$this->enabled = $enabled;
26+
}
27+
28+
public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData
29+
{
30+
return $this->enabled ? $this->shouldMarkMemberAsUsed($method) : null;
31+
}
32+
33+
public function shouldMarkConstantAsUsed(ReflectionClassConstant $constant): ?VirtualUsageData
34+
{
35+
return $this->enabled ? $this->shouldMarkMemberAsUsed($constant) : null;
36+
}
37+
38+
/**
39+
* @param ReflectionClassConstant|ReflectionMethod $member
40+
*/
41+
public function shouldMarkMemberAsUsed(object $member): ?VirtualUsageData
42+
{
43+
$reflectionClass = $this->reflectionProvider->getClass($member->getDeclaringClass()->getName());
44+
$memberType = $member instanceof ReflectionClassConstant ? 'constant' : 'method';
45+
$memberName = $member->getName();
46+
47+
if ($this->isApiMember($reflectionClass, $member)) {
48+
return VirtualUsageData::withNote("Class {$reflectionClass->getName()} is public @api");
49+
}
50+
51+
do {
52+
foreach ($reflectionClass->getInterfaces() as $interface) {
53+
if ($this->isApiMember($interface, $member)) {
54+
return VirtualUsageData::withNote("Interface $memberType {$interface->getName()}::{$memberName} is public @api");
55+
}
56+
}
57+
58+
foreach ($reflectionClass->getParents() as $parent) {
59+
if ($this->isApiMember($parent, $member)) {
60+
return VirtualUsageData::withNote("Class $memberType {$parent->getName()}::{$memberName} is public @api");
61+
}
62+
}
63+
64+
$reflectionClass = $reflectionClass->getParentClass();
65+
} while ($reflectionClass !== null);
66+
67+
return null;
68+
}
69+
70+
/**
71+
* @param ReflectionClassConstant|ReflectionMethod $member
72+
*/
73+
private function isApiMember(ClassReflection $reflection, object $member): bool
74+
{
75+
if (!$this->hasOwnMember($reflection, $member)) {
76+
return false;
77+
}
78+
79+
if ($this->isApiClass($reflection)) {
80+
return true;
81+
}
82+
83+
if ($member instanceof ReflectionClassConstant) {
84+
$constant = $reflection->getConstant($member->getName());
85+
$phpDoc = $constant->getDocComment();
86+
87+
if ($this->isApiPhpDoc($phpDoc)) {
88+
return true;
89+
}
90+
91+
return false;
92+
}
93+
94+
$phpDoc = $reflection->getNativeMethod($member->getName())->getDocComment();
95+
96+
if ($this->isApiPhpDoc($phpDoc)) {
97+
return true;
98+
}
99+
100+
return false;
101+
}
102+
103+
/**
104+
* @param ReflectionClassConstant|ReflectionMethod $member
105+
*/
106+
private function hasOwnMember(ClassReflection $reflection, object $member): bool
107+
{
108+
if ($member instanceof ReflectionClassConstant) {
109+
return ReflectionHelper::hasOwnConstant($reflection, $member->getName());
110+
}
111+
112+
return ReflectionHelper::hasOwnMethod($reflection, $member->getName());
113+
}
114+
115+
private function isApiClass(ClassReflection $reflection): bool
116+
{
117+
$phpDoc = $reflection->getResolvedPhpDoc();
118+
119+
if ($phpDoc === null) {
120+
return false;
121+
}
122+
123+
if ($this->isApiPhpDoc($phpDoc->getPhpDocString())) {
124+
return true;
125+
}
126+
127+
return false;
128+
}
129+
130+
private function isApiPhpDoc(?string $phpDoc): bool
131+
{
132+
return $phpDoc !== null && strpos($phpDoc, '@api') !== false;
133+
}
134+
135+
}

src/Reflection/ReflectionHelper.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Reflection;
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
use ReflectionException;
7+
8+
final class ReflectionHelper
9+
{
10+
11+
public static function hasOwnMethod(ClassReflection $classReflection, string $methodName): bool
12+
{
13+
if (!$classReflection->hasMethod($methodName)) {
14+
return false;
15+
}
16+
17+
try {
18+
return $classReflection->getNativeReflection()->getMethod($methodName)->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
19+
} catch (ReflectionException $e) {
20+
return false;
21+
}
22+
}
23+
24+
public static function hasOwnConstant(ClassReflection $classReflection, string $constantName): bool
25+
{
26+
$constantReflection = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
27+
28+
if ($constantReflection === false) {
29+
return false;
30+
}
31+
32+
return $constantReflection->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
33+
}
34+
35+
public static function hasOwnProperty(ClassReflection $classReflection, string $propertyName): bool
36+
{
37+
if (!$classReflection->hasProperty($propertyName)) {
38+
return false;
39+
}
40+
41+
try {
42+
return $classReflection->getNativeReflection()->getProperty($propertyName)->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
43+
} catch (ReflectionException $e) {
44+
return false;
45+
}
46+
}
47+
48+
}

tests/AllServicesInConfigTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ShipMonk\PHPStan\DeadCode\Transformer\RemoveDeadCodeTransformer;
2121
use function array_merge;
2222
use function class_exists;
23+
use function count;
2324
use function in_array;
2425
use function interface_exists;
2526
use function str_replace;
@@ -71,6 +72,10 @@ public function test(): void
7172
continue;
7273
}
7374

75+
if ($this->hasAllMethodsStatic($reflectionClass)) {
76+
continue;
77+
}
78+
7479
if (in_array($className, $excluded, true)) {
7580
continue;
7681
}
@@ -106,4 +111,24 @@ private function mapPathToClassName(string $pathname): string
106111
return $classString;
107112
}
108113

114+
/**
115+
* @param ReflectionClass<object> $reflectionClass
116+
*/
117+
private function hasAllMethodsStatic(ReflectionClass $reflectionClass): bool
118+
{
119+
$methods = $reflectionClass->getMethods();
120+
121+
if (count($methods) === 0) {
122+
return false;
123+
}
124+
125+
foreach ($methods as $method) {
126+
if (!$method->isStatic()) {
127+
return false;
128+
}
129+
}
130+
131+
return true;
132+
}
133+
109134
}

tests/Rule/DeadCodeRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
3535
use ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy;
3636
use ShipMonk\PHPStan\DeadCode\Output\OutputEnhancer;
37+
use ShipMonk\PHPStan\DeadCode\Provider\ApiPhpDocUsageProvider;
3738
use ShipMonk\PHPStan\DeadCode\Provider\DoctrineUsageProvider;
3839
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
3940
use ShipMonk\PHPStan\DeadCode\Provider\NetteUsageProvider;
@@ -737,6 +738,7 @@ public static function provideFiles(): Traversable
737738
yield 'provider-doctrine' => [__DIR__ . '/data/providers/doctrine.php', self::requiresPhp(8_00_00)];
738739
yield 'provider-phpstan' => [__DIR__ . '/data/providers/phpstan.php'];
739740
yield 'provider-nette' => [__DIR__ . '/data/providers/nette.php'];
741+
yield 'provider-apiphpdoc' => [__DIR__ . '/data/providers/api-phpdoc.php'];
740742

741743
// excluders
742744
yield 'excluder-tests' => [[__DIR__ . '/data/excluders/tests/src/code.php', __DIR__ . '/data/excluders/tests/tests/code.php']];
@@ -874,6 +876,10 @@ private function getMemberUsageProviders(): array
874876
new TwigUsageProvider(
875877
$this->providersEnabled,
876878
),
879+
new ApiPhpDocUsageProvider(
880+
self::createReflectionProvider(),
881+
$this->providersEnabled,
882+
),
877883
];
878884

879885
if ($this->providersEnabled) {

0 commit comments

Comments
 (0)