diff --git a/.ecrc b/.ecrc
new file mode 100644
index 00000000..430e6751
--- /dev/null
+++ b/.ecrc
@@ -0,0 +1,5 @@
+{
+ "Exclude": [
+ "^tests/Rule/data/debug/expected_output.txt"
+ ]
+}
diff --git a/README.md b/README.md
index ac6605ab..69287ee4 100644
--- a/README.md
+++ b/README.md
@@ -157,7 +157,7 @@ use PHPStan\Analyser\Scope;
use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
-use ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
use Symfony\Component\Serializer\SerializerInterface;
@@ -185,13 +185,13 @@ class DeserializationUsageProvider implements MemberUsageProvider
$secondArgument = $node->getArgs()[1]->value;
$serializedClass = $scope->getType($secondArgument)->getConstantStrings()[0];
- // record the method it was called from (needed for proper transitive dead code elimination)
- $originRef = $this->originDetector->detectOrigin($scope);
+ // record the place it was called from (needed for proper transitive dead code elimination)
+ $usageOrigin = UsageOrigin::createRegular($node, $scope);
// record the hidden constructor call
$constructorRef = new ClassMethodRef($serializedClass->getValue(), '__construct', false);
- return [new ClassMethodUsage($originRef, $constructorRef)];
+ return [new ClassMethodUsage($usageOrigin, $constructorRef)];
}
return [];
@@ -350,6 +350,34 @@ class IgnoreDeadInterfaceUsageProvider extends ReflectionBasedMemberUsageProvide
}
```
+## Debugging:
+- If you want to see how dead code detector evaluated usages of certain method, you do the following:
+
+```neon
+parameters:
+ shipmonkDeadCode:
+ debug:
+ usagesOf:
+ - App\User\Entity\Address::__construct
+```
+
+Then, run PHPStan with `-vvv` CLI option and you will see the output like this:
+
+```txt
+App\User\Entity\Address::__construct
+|
+| Marked as alive by:
+| entry virtual usage from ShipMonk\PHPStan\DeadCode\Provider\SymfonyUsageProvider
+| calls App\User\RegisterUserController::__invoke:36
+| calls App\User\UserFacade::registerUser:142
+| calls App\User\Entity\Address::__construct
+|
+| Found 2 usages:
+| • src/User/UserFacade.php:142
+| • tests/User/Entity/AddressTest.php:64 - excluded by tests excluder
+```
+
+If you set up `editorUrl` [parameter](https://phpstan.org/user-guide/output-format#opening-file-in-an-editor), you can click on the usages to open it in your IDE.
## Future scope:
- Dead class property detection
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 124aafd5..6cd93ebc 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -196,7 +196,6 @@
-
diff --git a/rules.neon b/rules.neon
index e85ef4a2..1af62305 100644
--- a/rules.neon
+++ b/rules.neon
@@ -8,7 +8,10 @@ services:
class: ShipMonk\PHPStan\DeadCode\Transformer\FileSystem
-
- class: ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector
+ class: ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter
+ arguments:
+ trackMixedAccess: %shipmonkDeadCode.trackMixedAccess%
+ editorUrl: %editorUrl%
-
class: ShipMonk\PHPStan\DeadCode\Provider\VendorUsageProvider
@@ -105,7 +108,6 @@ services:
- phpstan.diagnoseExtension
arguments:
reportTransitivelyDeadMethodAsSeparateError: %shipmonkDeadCode.reportTransitivelyDeadMethodAsSeparateError%
- trackMixedAccess: %shipmonkDeadCode.trackMixedAccess%
-
class: ShipMonk\PHPStan\DeadCode\Compatibility\BackwardCompatibilityChecker
@@ -137,6 +139,8 @@ parameters:
tests:
enabled: false
devPaths: null
+ debug:
+ usagesOf: []
parametersSchema:
shipmonkDeadCode: structure([
@@ -172,4 +176,7 @@ parametersSchema:
devPaths: schema(listOf(string()), nullable())
])
])
+ debug: structure([
+ usagesOf: listOf(string())
+ ])
])
diff --git a/src/Collector/ConstantFetchCollector.php b/src/Collector/ConstantFetchCollector.php
index 07d9c11e..98a21061 100644
--- a/src/Collector/ConstantFetchCollector.php
+++ b/src/Collector/ConstantFetchCollector.php
@@ -18,7 +18,7 @@
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage;
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
-use ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use function array_map;
use function count;
use function current;
@@ -33,8 +33,6 @@ class ConstantFetchCollector implements Collector
use BufferedUsageCollector;
- private UsageOriginDetector $usageOriginDetector;
-
private ReflectionProvider $reflectionProvider;
private bool $trackMixedAccess;
@@ -48,7 +46,6 @@ class ConstantFetchCollector implements Collector
* @param list $memberUsageExcluders
*/
public function __construct(
- UsageOriginDetector $usageOriginDetector,
ReflectionProvider $reflectionProvider,
bool $trackMixedAccess,
array $memberUsageExcluders
@@ -56,7 +53,6 @@ public function __construct(
{
$this->reflectionProvider = $reflectionProvider;
$this->trackMixedAccess = $trackMixedAccess;
- $this->usageOriginDetector = $usageOriginDetector;
$this->memberUsageExcluders = $memberUsageExcluders;
}
@@ -125,7 +121,7 @@ private function registerFunctionCall(FuncCall $node, Scope $scope): void
$this->registerUsage(
new ClassConstantUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($node, $scope),
new ClassConstantRef($className, $constantName, true),
),
$node,
@@ -157,7 +153,7 @@ private function registerFetch(ClassConstFetch $node, Scope $scope): void
foreach ($this->getDeclaringTypesWithConstant($ownerType, $constantName) as $className) {
$this->registerUsage(
new ClassConstantUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($node, $scope),
new ClassConstantRef($className, $constantName, $possibleDescendantFetch),
),
$node,
diff --git a/src/Collector/MethodCallCollector.php b/src/Collector/MethodCallCollector.php
index a6b8e6b4..bc1f4c20 100644
--- a/src/Collector/MethodCallCollector.php
+++ b/src/Collector/MethodCallCollector.php
@@ -25,7 +25,7 @@
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
-use ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
/**
* @implements Collector>
@@ -35,8 +35,6 @@ class MethodCallCollector implements Collector
use BufferedUsageCollector;
- private UsageOriginDetector $usageOriginDetector;
-
private bool $trackMixedAccess;
/**
@@ -48,12 +46,10 @@ class MethodCallCollector implements Collector
* @param list $memberUsageExcluders
*/
public function __construct(
- UsageOriginDetector $usageOriginDetector,
bool $trackMixedAccess,
array $memberUsageExcluders
)
{
- $this->usageOriginDetector = $usageOriginDetector;
$this->trackMixedAccess = $trackMixedAccess;
$this->memberUsageExcluders = $memberUsageExcluders;
}
@@ -133,7 +129,7 @@ private function registerMethodCall(
foreach ($this->getDeclaringTypesWithMethod($methodName, $callerType, TrinaryLogic::createNo(), $possibleDescendantCall) as $methodRef) {
$this->registerUsage(
new ClassMethodUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($methodCall, $scope),
$methodRef,
),
$methodCall,
@@ -163,7 +159,7 @@ private function registerStaticCall(
foreach ($this->getDeclaringTypesWithMethod($methodName, $callerType, TrinaryLogic::createYes(), $possibleDescendantCall) as $methodRef) {
$this->registerUsage(
new ClassMethodUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($staticCall, $scope),
$methodRef,
),
$staticCall,
@@ -189,7 +185,7 @@ private function registerArrayCallable(
foreach ($this->getDeclaringTypesWithMethod($methodName, $caller, TrinaryLogic::createMaybe()) as $methodRef) {
$this->registerUsage(
new ClassMethodUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($array, $scope),
$methodRef,
),
$array,
@@ -205,7 +201,7 @@ private function registerAttribute(Attribute $node, Scope $scope): void
{
$this->registerUsage(
new ClassMethodUsage(
- null,
+ UsageOrigin::createRegular($node, $scope),
new ClassMethodRef($scope->resolveName($node->name), '__construct', false),
),
$node,
@@ -221,7 +217,7 @@ private function registerClone(Clone_ $node, Scope $scope): void
foreach ($this->getDeclaringTypesWithMethod($methodName, $callerType, TrinaryLogic::createNo()) as $methodRef) {
$this->registerUsage(
new ClassMethodUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($node, $scope),
$methodRef,
),
$node,
diff --git a/src/Collector/ProvidedUsagesCollector.php b/src/Collector/ProvidedUsagesCollector.php
index 6df74e3b..fb26a570 100644
--- a/src/Collector/ProvidedUsagesCollector.php
+++ b/src/Collector/ProvidedUsagesCollector.php
@@ -86,12 +86,13 @@ private function validateUsage(
$memberRef = $usage->getMemberRef();
$memberRefClass = $memberRef->getClassName();
- $originRef = $usage->getOrigin();
- $originRefClass = $originRef === null ? null : $originRef->getClassName();
+ $origin = $usage->getOrigin();
+ $originClass = $origin->getClassName();
+ $originMethod = $origin->getMethodName();
$context = sprintf(
- "It was emitted as %s by %s for node '%s' in '%s' on line %s",
- $usage->toHumanString(),
+ "It emitted usage of %s by %s for node '%s' in '%s' on line %s",
+ $usage->getMemberRef()->toHumanString(),
get_class($provider),
get_class($node),
$scope->getFile(),
@@ -102,16 +103,13 @@ private function validateUsage(
throw new LogicException("Class '$memberRefClass' does not exist. $context");
}
- if (
- $originRef !== null
- && $originRefClass !== null
- ) {
- if (!$this->reflectionProvider->hasClass($originRefClass)) {
- throw new LogicException("Class '{$originRefClass}' does not exist. $context");
+ if ($originClass !== null) {
+ if (!$this->reflectionProvider->hasClass($originClass)) {
+ throw new LogicException("Class '{$originClass}' does not exist. $context");
}
- if (!$this->reflectionProvider->getClass($originRefClass)->hasMethod($originRef->getMemberName())) {
- throw new LogicException("Method '{$originRef->getMemberName()}' does not exist in class '$originRefClass'. $context");
+ if ($originMethod !== null && !$this->reflectionProvider->getClass($originClass)->hasMethod($originMethod)) {
+ throw new LogicException("Method '{$originMethod}' does not exist in class '$originClass'. $context");
}
}
}
diff --git a/src/Debug/DebugUsagePrinter.php b/src/Debug/DebugUsagePrinter.php
new file mode 100644
index 00000000..65699051
--- /dev/null
+++ b/src/Debug/DebugUsagePrinter.php
@@ -0,0 +1,389 @@
+ usage info
+ *
+ * @var array, eliminationPath?: array>, neverReported?: string}>
+ */
+ private array $debugMembers;
+
+ public function __construct(
+ Container $container,
+ RelativePathHelper $relativePathHelper,
+ ReflectionProvider $reflectionProvider,
+ ?string $editorUrl,
+ bool $trackMixedAccess
+ )
+ {
+ $this->relativePathHelper = $relativePathHelper;
+ $this->reflectionProvider = $reflectionProvider;
+ $this->editorUrl = $editorUrl;
+ $this->trackMixedAccess = $trackMixedAccess;
+ $this->debugMembers = $this->buildDebugMemberKeys(
+ // @phpstan-ignore offsetAccess.nonOffsetAccessible, offsetAccess.nonOffsetAccessible, missingType.checkedException, argument.type
+ $container->getParameter('shipmonkDeadCode')['debug']['usagesOf'], // prevents https://github.com/phpstan/phpstan/issues/12740
+ );
+ }
+
+ /**
+ * @param array>> $mixedMemberUsages
+ */
+ public function printMixedMemberUsages(Output $output, array $mixedMemberUsages): void
+ {
+ if ($mixedMemberUsages === [] || !$output->isDebug() || !$this->trackMixedAccess) {
+ return;
+ }
+
+ $totalCount = array_sum(array_map('count', $mixedMemberUsages));
+ $maxExamplesToShow = 20;
+ $examplesShown = 0;
+ $plural = $totalCount > 1 ? 's' : '';
+ $output->writeLineFormatted(sprintf('Found %d usage%s over unknown type>:', $totalCount, $plural));
+
+ foreach ($mixedMemberUsages as $memberType => $collectedUsages) {
+ foreach ($collectedUsages as $memberName => $usages) {
+ $examplesShown++;
+ $memberTypeString = $memberType === MemberType::METHOD ? 'method' : 'constant';
+ $output->writeFormatted(sprintf(' • %s> %s', $memberName, $memberTypeString));
+
+ $exampleCaller = $this->getExampleCaller($usages);
+
+ if ($exampleCaller !== null) {
+ $output->writeFormatted(sprintf(', for example in %s>', $exampleCaller));
+ }
+
+ $output->writeLineFormatted('');
+
+ if ($examplesShown >= $maxExamplesToShow) {
+ break 2;
+ }
+ }
+ }
+
+ if ($totalCount > $maxExamplesToShow) {
+ $output->writeLineFormatted(sprintf('... and %d more', $totalCount - $maxExamplesToShow));
+ }
+
+ $output->writeLineFormatted('');
+ $output->writeLineFormatted('Thus, any member named the same is considered used, no matter its declaring class!');
+ $output->writeLineFormatted('');
+ }
+
+ /**
+ * @param list $usages
+ */
+ private function getExampleCaller(array $usages): ?string
+ {
+ foreach ($usages as $usage) {
+ $origin = $usage->getUsage()->getOrigin();
+
+ if ($origin->getFile() !== null) {
+ return $this->getOriginReference($origin);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param array $analysedClasses
+ */
+ public function printDebugMemberUsages(Output $output, array $analysedClasses): void
+ {
+ if ($this->debugMembers === [] || !$output->isDebug()) {
+ return;
+ }
+
+ $output->writeLineFormatted("\nUsage debugging information:>");
+
+ foreach ($this->debugMembers as $memberKey => $debugMember) {
+ $typeName = $debugMember['typename'];
+
+ $output->writeLineFormatted(sprintf("\n%s>", $this->prettyMemberKey($memberKey)));
+
+ if (isset($debugMember['eliminationPath'])) {
+ $output->writeLineFormatted("|\n| Marked as alive at:>");
+ $depth = 1;
+
+ foreach ($debugMember['eliminationPath'] as $fragmentKey => $fragmentUsages) {
+ if ($depth === 1) {
+ $entrypoint = $this->getOriginReference($fragmentUsages[0]->getOrigin(), false);
+ $output->writeLineFormatted(sprintf('| entry> %s>', $entrypoint));
+ }
+
+ $indent = str_repeat(' ', $depth) . 'calls> ';
+
+ $nextFragmentUsages = next($debugMember['eliminationPath']);
+ $nextFragmentFirstUsage = $nextFragmentUsages !== false ? reset($nextFragmentUsages) : null;
+ $nextFragmentFirstUsageOrigin = $nextFragmentFirstUsage instanceof ClassMemberUsage ? $nextFragmentFirstUsage->getOrigin() : null;
+
+ $pathFragment = $nextFragmentFirstUsageOrigin === null
+ ? $this->prettyMemberKey($fragmentKey)
+ : $this->getOriginLink($nextFragmentFirstUsageOrigin, $this->prettyMemberKey($fragmentKey));
+
+ $output->writeLineFormatted(sprintf('| %s%s>', $indent, $pathFragment));
+
+ $depth++;
+ }
+ } elseif (!isset($analysedClasses[$typeName])) {
+ $output->writeLineFormatted("|\n| Not defined within analysed files!>");
+
+ } elseif (isset($debugMember['usages'])) {
+ $output->writeLineFormatted("|\n| Dead because:>");
+ $output->writeLineFormatted('| all usages originate in unused code');
+ }
+
+ if (isset($debugMember['usages'])) {
+ $plural = count($debugMember['usages']) > 1 ? 's' : '';
+ $output->writeLineFormatted(sprintf("|\n| Found %d usage%s:>", count($debugMember['usages']), $plural));
+
+ foreach ($debugMember['usages'] as $collectedUsage) {
+ $origin = $collectedUsage->getUsage()->getOrigin();
+ $output->writeFormatted(sprintf('| • %s>', $this->getOriginReference($origin)));
+
+ if ($collectedUsage->isExcluded()) {
+ $output->writeFormatted(sprintf(' - excluded by %s excluder>', $collectedUsage->getExcludedBy()));
+ }
+
+ $output->writeLineFormatted('');
+ }
+ } elseif (isset($debugMember['neverReported'])) {
+ $output->writeLineFormatted(sprintf("|\n| Is never reported as dead: %s>", $debugMember['neverReported']));
+ } else {
+ $output->writeLineFormatted("|\n| No usages found>");
+ }
+
+ $output->writeLineFormatted('');
+ }
+ }
+
+ private function prettyMemberKey(string $memberKey): string
+ {
+ $replaced = preg_replace('/^(m|c)\//', '', $memberKey);
+
+ if ($replaced === null) {
+ throw new LogicException('Failed to pretty member key ' . $memberKey);
+ }
+
+ return $replaced;
+ }
+
+ private function getOriginReference(UsageOrigin $origin, bool $preferFileLine = true): string
+ {
+ $file = $origin->getFile();
+ $line = $origin->getLine();
+
+ if ($file !== null && $line !== null) {
+ $relativeFile = $this->relativePathHelper->getRelativePath($file);
+
+ $title = $origin->getClassName() !== null && $origin->getMethodName() !== null && !$preferFileLine
+ ? sprintf('%s::%s:%d', $origin->getClassName(), $origin->getMethodName(), $line)
+ : sprintf('%s:%s', $relativeFile, $line);
+
+ if ($this->editorUrl === null) {
+ return $title;
+ }
+
+ return sprintf(
+ '%s>',
+ str_replace(['%file%', '%relFile%', '%line%'], [$file, $relativeFile, (string) $line], $this->editorUrl),
+ $title,
+ );
+ }
+
+ if ($origin->getProvider() !== null) {
+ $note = $origin->getNote() !== null ? " ({$origin->getNote()})" : '';
+ return 'virtual usage from ' . $origin->getProvider() . $note;
+ }
+
+ throw new LogicException('Unknown state of usage origin');
+ }
+
+ private function getOriginLink(UsageOrigin $origin, string $title): string
+ {
+ $file = $origin->getFile();
+ $line = $origin->getLine();
+
+ if ($line !== null) {
+ $title = sprintf('%s:%s', $title, $line);
+ }
+
+ if ($this->editorUrl !== null && $file !== null && $line !== null) {
+ $relativeFile = $this->relativePathHelper->getRelativePath($file);
+
+ return sprintf(
+ '%s>',
+ str_replace(['%file%', '%relFile%', '%line%'], [$file, $relativeFile, (string) $line], $this->editorUrl),
+ $title,
+ );
+ }
+
+ return $title;
+ }
+
+ /**
+ * @param list $alternativeKeys
+ */
+ public function recordUsage(CollectedUsage $collectedUsage, array $alternativeKeys = []): void
+ {
+ $memberKeys = array_unique([
+ $collectedUsage->getUsage()->getMemberRef()->toKey(),
+ ...$alternativeKeys,
+ ]);
+
+ foreach ($memberKeys as $memberKey) {
+ if (!isset($this->debugMembers[$memberKey])) {
+ continue;
+ }
+
+ $this->debugMembers[$memberKey]['usages'][] = $collectedUsage;
+ }
+ }
+
+ /**
+ * @param array> $eliminationPath
+ */
+ public function markMemberAsWhite(BlackMember $blackMember, array $eliminationPath): void
+ {
+ $memberKey = $blackMember->getMember()->toKey();
+
+ if (!isset($this->debugMembers[$memberKey])) {
+ return;
+ }
+
+ $this->debugMembers[$memberKey]['eliminationPath'] = $eliminationPath;
+ }
+
+ public function markMemberAsNeverReported(BlackMember $blackMember, string $reason): void
+ {
+ $memberKey = $blackMember->getMember()->toKey();
+
+ if (!isset($this->debugMembers[$memberKey])) {
+ return;
+ }
+
+ $this->debugMembers[$memberKey]['neverReported'] = $reason;
+ }
+
+ /**
+ * @param list $debugMembers
+ * @return array, eliminationPath?: array>, neverReported?: string}>
+ */
+ private function buildDebugMemberKeys(array $debugMembers): array
+ {
+ $result = [];
+
+ foreach ($debugMembers as $debugMember) {
+ if (strpos($debugMember, '::') === false) {
+ throw new LogicException("Invalid debug member format: '$debugMember', expected 'ClassName::memberName'");
+ }
+
+ [$class, $memberName] = explode('::', $debugMember); // @phpstan-ignore offsetAccess.notFound
+ $normalizedClass = ltrim($class, '\\');
+
+ if (!$this->reflectionProvider->hasClass($normalizedClass)) {
+ throw new LogicException("Class '$normalizedClass' does not exist");
+ }
+
+ $classReflection = $this->reflectionProvider->getClass($normalizedClass);
+
+ if ($this->hasOwnMethod($classReflection, $memberName)) {
+ $key = ClassMethodRef::buildKey($normalizedClass, $memberName);
+
+ } elseif ($this->hasOwnConstant($classReflection, $memberName)) {
+ $key = ClassConstantRef::buildKey($normalizedClass, $memberName);
+
+ } elseif ($this->hasOwnProperty($classReflection, $memberName)) {
+ throw new LogicException("Cannot debug '$debugMember', properties are not supported yet");
+
+ } else {
+ throw new LogicException("Member '$memberName' does not exist directly in '$normalizedClass'");
+ }
+
+ $result[$key] = [
+ 'typename' => $normalizedClass,
+ ];
+ }
+
+ return $result;
+ }
+
+ private function hasOwnMethod(ClassReflection $classReflection, string $methodName): bool
+ {
+ if (!$classReflection->hasMethod($methodName)) {
+ return false;
+ }
+
+ try {
+ return $classReflection->getNativeReflection()->getMethod($methodName)->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
+ } catch (ReflectionException $e) {
+ return false;
+ }
+ }
+
+ private function hasOwnConstant(ClassReflection $classReflection, string $constantName): bool
+ {
+ $constantReflection = $classReflection->getNativeReflection()->getReflectionConstant($constantName);
+
+ if ($constantReflection === false) {
+ return false;
+ }
+
+ return $constantReflection->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
+ }
+
+ private function hasOwnProperty(ClassReflection $classReflection, string $propertyName): bool
+ {
+ if (!$classReflection->hasProperty($propertyName)) {
+ return false;
+ }
+
+ try {
+ return $classReflection->getNativeReflection()->getProperty($propertyName)->getBetterReflection()->getDeclaringClass()->getName() === $classReflection->getName();
+ } catch (ReflectionException $e) {
+ return false;
+ }
+ }
+
+}
diff --git a/src/Enum/NeverReportedReason.php b/src/Enum/NeverReportedReason.php
new file mode 100644
index 00000000..4cd3414f
--- /dev/null
+++ b/src/Enum/NeverReportedReason.php
@@ -0,0 +1,12 @@
+isPossibleDescendant()) {
- throw new LogicException('Origin should always be exact place in codebase.');
- }
-
- if ($origin !== null && $origin->getClassName() === null) {
- throw new LogicException('Origin should always be exact place in codebase, thus className should be known.');
- }
-
$this->origin = $origin;
}
- public function getOrigin(): ?ClassMethodRef
+ public function getOrigin(): UsageOrigin
{
return $this->origin;
}
@@ -49,12 +38,4 @@ abstract public function getMemberRef(): ClassMemberRef;
*/
abstract public function concretizeMixedUsage(string $className): self;
- public function toHumanString(): string
- {
- $origin = $this->origin !== null ? $this->origin->toHumanString() : 'unknown';
- $callee = $this->getMemberRef()->toHumanString();
-
- return "$origin -> $callee";
- }
-
}
diff --git a/src/Graph/ClassMethodUsage.php b/src/Graph/ClassMethodUsage.php
index 6630bcb6..e900c019 100644
--- a/src/Graph/ClassMethodUsage.php
+++ b/src/Graph/ClassMethodUsage.php
@@ -14,11 +14,11 @@ final class ClassMethodUsage extends ClassMemberUsage
private ClassMethodRef $callee;
/**
- * @param ClassMethodRef|null $origin The method where the call occurs
+ * @param UsageOrigin $origin The method where the call occurs
* @param ClassMethodRef $callee The method being called
*/
public function __construct(
- ?ClassMethodRef $origin,
+ UsageOrigin $origin,
ClassMethodRef $callee
)
{
diff --git a/src/Graph/CollectedUsage.php b/src/Graph/CollectedUsage.php
index 9a7cfa46..68e58b18 100644
--- a/src/Graph/CollectedUsage.php
+++ b/src/Graph/CollectedUsage.php
@@ -60,12 +60,13 @@ public function serialize(): string
$data = [
'e' => $this->excludedBy,
't' => $this->usage->getMemberType(),
- 'o' => $origin === null
- ? null
- : [
+ 'o' => [
'c' => $origin->getClassName(),
- 'm' => $origin->getMemberName(),
- 'd' => $origin->isPossibleDescendant(),
+ 'm' => $origin->getMethodName(),
+ 'f' => $origin->getFile(),
+ 'l' => $origin->getLine(),
+ 'p' => $origin->getProvider(),
+ 'n' => $origin->getNote(),
],
'm' => [
'c' => $memberRef->getClassName(),
@@ -84,14 +85,14 @@ public function serialize(): string
public static function deserialize(string $data): self
{
try {
- /** @var array{e: string|null, t: MemberType::*, o: array{c: string|null, m: string, d: bool}|null, m: array{c: string|null, m: string, d: bool}} $result */
+ /** @var array{e: string|null, t: MemberType::*, o: array{c: string|null, m: string|null, f: string|null, l: int|null, p: string|null, n: string|null}, m: array{c: string|null, m: string, d: bool}} $result */
$result = json_decode($data, true, 3, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new LogicException('Deserialization failure: ' . $e->getMessage(), 0, $e);
}
$memberType = $result['t'];
- $origin = $result['o'] === null ? null : new ClassMethodRef($result['o']['c'], $result['o']['m'], $result['o']['d']);
+ $origin = new UsageOrigin($result['o']['c'], $result['o']['m'], $result['o']['f'], $result['o']['l'], $result['o']['p'], $result['o']['n']);
$exclusionReason = $result['e'];
$usage = $memberType === MemberType::CONSTANT
diff --git a/src/Graph/UsageOrigin.php b/src/Graph/UsageOrigin.php
new file mode 100644
index 00000000..b06698d0
--- /dev/null
+++ b/src/Graph/UsageOrigin.php
@@ -0,0 +1,141 @@
+className = $className;
+ $this->methodName = $methodName;
+ $this->fileName = $fileName;
+ $this->line = $line;
+ $this->provider = $provider;
+ $this->note = $note;
+ }
+
+ /**
+ * Creates virtual usage origin with no reference to any place in code
+ *
+ * @param ?string $note More detailed identification why provider emitted this virtual usage
+ */
+ public static function createVirtual(MemberUsageProvider $provider, ?string $note = null): self
+ {
+ return new self(
+ null,
+ null,
+ null,
+ null,
+ get_class($provider),
+ $note,
+ );
+ }
+
+ /**
+ * Creates usage origin with reference to file:line
+ */
+ public static function createRegular(Node $node, Scope $scope): self
+ {
+ if (!$scope->isInClass() || !$scope->getFunction() instanceof MethodReflection) {
+ return new self(
+ null,
+ null,
+ $scope->getFile(),
+ $node->getStartLine(),
+ null,
+ null,
+ );
+ }
+
+ return new self(
+ $scope->getClassReflection()->getName(),
+ $scope->getFunction()->getName(),
+ $scope->getFile(),
+ $node->getStartLine(),
+ null,
+ null,
+ );
+ }
+
+ public function getClassName(): ?string
+ {
+ return $this->className;
+ }
+
+ public function getMethodName(): ?string
+ {
+ return $this->methodName;
+ }
+
+ public function getFile(): ?string
+ {
+ return $this->fileName;
+ }
+
+ public function getLine(): ?int
+ {
+ return $this->line;
+ }
+
+ public function getProvider(): ?string
+ {
+ return $this->provider;
+ }
+
+ public function getNote(): ?string
+ {
+ return $this->note;
+ }
+
+ public function hasClassMethodRef(): bool
+ {
+ return $this->className !== null && $this->methodName !== null;
+ }
+
+ public function toClassMethodRef(): ClassMethodRef
+ {
+ if ($this->className === null || $this->methodName === null) {
+ throw new LogicException('Usage origin does not have class method ref');
+ }
+
+ return new ClassMethodRef(
+ $this->className,
+ $this->methodName,
+ false,
+ );
+ }
+
+}
diff --git a/src/Graph/UsageOriginDetector.php b/src/Graph/UsageOriginDetector.php
deleted file mode 100644
index 03911259..00000000
--- a/src/Graph/UsageOriginDetector.php
+++ /dev/null
@@ -1,31 +0,0 @@
-isInClass()) {
- return null;
- }
-
- if (!$scope->getFunction() instanceof MethodReflection) {
- return null;
- }
-
- return new ClassMethodRef(
- $scope->getClassReflection()->getName(),
- $scope->getFunction()->getName(),
- false,
- );
- }
-
-}
diff --git a/src/Provider/DoctrineUsageProvider.php b/src/Provider/DoctrineUsageProvider.php
index e50c9cf7..04f874f1 100644
--- a/src/Provider/DoctrineUsageProvider.php
+++ b/src/Provider/DoctrineUsageProvider.php
@@ -13,6 +13,7 @@
use PHPStan\Reflection\MethodReflection;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
class DoctrineUsageProvider implements MemberUsageProvider
{
@@ -100,12 +101,13 @@ private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
$className = $scope->getClassReflection()->getName();
$usages = [];
+ $usageOrigin = UsageOrigin::createRegular($node, $scope);
foreach ($scope->getType($node->expr)->getConstantArrays() as $rootArray) {
foreach ($rootArray->getValuesArray()->getValueTypes() as $eventConfig) {
foreach ($eventConfig->getConstantStrings() as $subscriberMethodString) {
$usages[] = new ClassMethodUsage(
- null,
+ $usageOrigin,
new ClassMethodRef(
$className,
$subscriberMethodString->getValue(),
@@ -199,7 +201,7 @@ private function isDoctrineInstalled(): bool
private function createMethodUsage(ExtendedMethodReflection $methodReflection): ClassMethodUsage
{
return new ClassMethodUsage(
- null,
+ UsageOrigin::createVirtual($this),
new ClassMethodRef(
$methodReflection->getDeclaringClass()->getName(),
$methodReflection->getName(),
diff --git a/src/Provider/PhpUnitUsageProvider.php b/src/Provider/PhpUnitUsageProvider.php
index 2911bee6..eb02b8ac 100644
--- a/src/Provider/PhpUnitUsageProvider.php
+++ b/src/Provider/PhpUnitUsageProvider.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use function array_merge;
use function is_string;
use function strpos;
@@ -56,12 +57,12 @@ public function getUsages(Node $node, Scope $scope): array
foreach ($dataProviders as $dataProvider) {
if ($classReflection->hasNativeMethod($dataProvider)) {
- $usages[] = $this->createUsage($classReflection->getNativeMethod($dataProvider));
+ $usages[] = $this->createUsage($classReflection->getNativeMethod($dataProvider), 'data provider method');
}
}
if ($this->isTestCaseMethod($method)) {
- $usages[] = $this->createUsage($classReflection->getNativeMethod($method->getName()));
+ $usages[] = $this->createUsage($classReflection->getNativeMethod($method->getName()), 'test method');
}
}
@@ -141,10 +142,10 @@ private function hasAnnotation(ReflectionMethod $method, string $string): bool
return strpos($method->getDocComment(), $string) !== false;
}
- private function createUsage(ExtendedMethodReflection $getNativeMethod): ClassMethodUsage
+ private function createUsage(ExtendedMethodReflection $getNativeMethod, string $reason): ClassMethodUsage
{
return new ClassMethodUsage(
- null,
+ UsageOrigin::createVirtual($this, $reason),
new ClassMethodRef(
$getNativeMethod->getDeclaringClass()->getName(),
$getNativeMethod->getName(),
diff --git a/src/Provider/ReflectionBasedMemberUsageProvider.php b/src/Provider/ReflectionBasedMemberUsageProvider.php
index c7626c21..54c13a1f 100644
--- a/src/Provider/ReflectionBasedMemberUsageProvider.php
+++ b/src/Provider/ReflectionBasedMemberUsageProvider.php
@@ -13,6 +13,7 @@
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use function array_merge;
abstract class ReflectionBasedMemberUsageProvider implements MemberUsageProvider
@@ -92,7 +93,7 @@ private function getConstantUsages(ClassReflection $classReflection): array
private function createConstantUsage(ReflectionClassConstant $constantReflection): ClassConstantUsage
{
return new ClassConstantUsage(
- null,
+ UsageOrigin::createVirtual($this),
new ClassConstantRef(
$constantReflection->getDeclaringClass()->getName(),
$constantReflection->getName(),
@@ -104,7 +105,7 @@ private function createConstantUsage(ReflectionClassConstant $constantReflection
private function createMethodUsage(ReflectionMethod $methodReflection): ClassMethodUsage
{
return new ClassMethodUsage(
- null,
+ UsageOrigin::createVirtual($this),
new ClassMethodRef(
$methodReflection->getDeclaringClass()->getName(),
$methodReflection->getName(),
diff --git a/src/Provider/ReflectionUsageProvider.php b/src/Provider/ReflectionUsageProvider.php
index 3f279aa7..a5f9a211 100644
--- a/src/Provider/ReflectionUsageProvider.php
+++ b/src/Provider/ReflectionUsageProvider.php
@@ -18,7 +18,7 @@
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
-use ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use function array_key_first;
use function count;
use function in_array;
@@ -26,16 +26,12 @@
class ReflectionUsageProvider implements MemberUsageProvider
{
- private UsageOriginDetector $usageOriginDetector;
-
private bool $enabled;
public function __construct(
- UsageOriginDetector $usageOriginDetector,
bool $enabled
)
{
- $this->usageOriginDetector = $usageOriginDetector;
$this->enabled = $enabled;
}
@@ -73,11 +69,11 @@ private function processMethodCall(MethodCall $node, Scope $scope): array
foreach ($genericType->getObjectClassReflections() as $genericReflection) {
$usedConstants = [
...$usedConstants,
- ...$this->extractConstantsUsedByReflection($methodName, $genericReflection, $node->getArgs(), $scope),
+ ...$this->extractConstantsUsedByReflection($methodName, $genericReflection, $node->getArgs(), $node, $scope),
];
$usedMethods = [
...$usedMethods,
- ...$this->extractMethodsUsedByReflection($methodName, $genericReflection, $node->getArgs(), $scope),
+ ...$this->extractMethodsUsedByReflection($methodName, $genericReflection, $node->getArgs(), $node, $scope),
];
}
}
@@ -98,6 +94,7 @@ private function extractConstantsUsedByReflection(
string $methodName,
ClassReflection $genericReflection,
array $args,
+ Node $node,
Scope $scope
): array
{
@@ -105,7 +102,7 @@ private function extractConstantsUsedByReflection(
if ($methodName === 'getConstants' || $methodName === 'getReflectionConstants') {
foreach ($genericReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) {
- $usedConstants[] = $this->createConstantUsage($scope, $reflectionConstant->getDeclaringClass()->getName(), $reflectionConstant->getName());
+ $usedConstants[] = $this->createConstantUsage($node, $scope, $reflectionConstant->getDeclaringClass()->getName(), $reflectionConstant->getName());
}
}
@@ -113,7 +110,7 @@ private function extractConstantsUsedByReflection(
$firstArg = $args[array_key_first($args)]; // @phpstan-ignore offsetAccess.notFound
foreach ($scope->getType($firstArg->value)->getConstantStrings() as $constantString) {
- $usedConstants[] = $this->createConstantUsage($scope, $genericReflection->getName(), $constantString->getValue());
+ $usedConstants[] = $this->createConstantUsage($node, $scope, $genericReflection->getName(), $constantString->getValue());
}
}
@@ -128,6 +125,7 @@ private function extractMethodsUsedByReflection(
string $methodName,
ClassReflection $genericReflection,
array $args,
+ Node $node,
Scope $scope
): array
{
@@ -135,7 +133,7 @@ private function extractMethodsUsedByReflection(
if ($methodName === 'getMethods') {
foreach ($genericReflection->getNativeReflection()->getMethods() as $reflectionMethod) {
- $usedMethods[] = $this->createMethodUsage($scope, $reflectionMethod->getDeclaringClass()->getName(), $reflectionMethod->getName());
+ $usedMethods[] = $this->createMethodUsage($node, $scope, $reflectionMethod->getDeclaringClass()->getName(), $reflectionMethod->getName());
}
}
@@ -143,7 +141,7 @@ private function extractMethodsUsedByReflection(
$firstArg = $args[array_key_first($args)]; // @phpstan-ignore offsetAccess.notFound
foreach ($scope->getType($firstArg->value)->getConstantStrings() as $constantString) {
- $usedMethods[] = $this->createMethodUsage($scope, $genericReflection->getName(), $constantString->getValue());
+ $usedMethods[] = $this->createMethodUsage($node, $scope, $genericReflection->getName(), $constantString->getValue());
}
}
@@ -151,7 +149,7 @@ private function extractMethodsUsedByReflection(
$constructor = $genericReflection->getNativeReflection()->getConstructor();
if ($constructor !== null) {
- $usedMethods[] = $this->createMethodUsage($scope, $constructor->getDeclaringClass()->getName(), '__construct');
+ $usedMethods[] = $this->createMethodUsage($node, $scope, $constructor->getDeclaringClass()->getName(), '__construct');
}
}
@@ -181,10 +179,15 @@ private function getMethodNames(CallLike $call, Scope $scope): array
return [$call->name->toString()];
}
- private function createConstantUsage(Scope $scope, string $className, string $constantName): ClassConstantUsage
+ private function createConstantUsage(
+ Node $node,
+ Scope $scope,
+ string $className,
+ string $constantName
+ ): ClassConstantUsage
{
return new ClassConstantUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($node, $scope),
new ClassConstantRef(
$className,
$constantName,
@@ -193,10 +196,15 @@ private function createConstantUsage(Scope $scope, string $className, string $co
);
}
- private function createMethodUsage(Scope $scope, string $className, string $methodName): ClassMethodUsage
+ private function createMethodUsage(
+ Node $node,
+ Scope $scope,
+ string $className,
+ string $methodName
+ ): ClassMethodUsage
{
return new ClassMethodUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($node, $scope),
new ClassMethodRef(
$className,
$methodName,
diff --git a/src/Provider/SymfonyUsageProvider.php b/src/Provider/SymfonyUsageProvider.php
index 1b6902e6..4297dd03 100644
--- a/src/Provider/SymfonyUsageProvider.php
+++ b/src/Provider/SymfonyUsageProvider.php
@@ -27,7 +27,7 @@
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
-use ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use SimpleXMLElement;
use SplFileInfo;
use UnexpectedValueException;
@@ -64,16 +64,12 @@ class SymfonyUsageProvider implements MemberUsageProvider
*/
private array $dicConstants = [];
- private UsageOriginDetector $usageOriginDetector;
-
public function __construct(
Container $container,
- UsageOriginDetector $usageOriginDetector,
?bool $enabled,
?string $configDir
)
{
- $this->usageOriginDetector = $usageOriginDetector;
$this->enabled = $enabled ?? $this->isSymfonyInstalled();
$resolvedConfigDir = $configDir ?? $this->autodetectConfigDir();
$containerXmlPath = $this->getContainerXmlPath($container);
@@ -148,6 +144,7 @@ private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
$className = $scope->getClassReflection()->getName();
$usages = [];
+ $usageOrigin = UsageOrigin::createRegular($node, $scope);
// phpcs:disable Squiz.PHP.CommentedOutCode.Found
foreach ($scope->getType($node->expr)->getConstantArrays() as $rootArray) {
@@ -155,7 +152,7 @@ private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
// ['eventName' => 'methodName']
foreach ($eventConfig->getConstantStrings() as $subscriberMethodString) {
$usages[] = new ClassMethodUsage(
- null,
+ $usageOrigin,
new ClassMethodRef(
$className,
$subscriberMethodString->getValue(),
@@ -168,7 +165,7 @@ private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
foreach ($eventConfig->getConstantArrays() as $subscriberMethodArray) {
foreach ($subscriberMethodArray->getFirstIterableValueType()->getConstantStrings() as $subscriberMethodString) {
$usages[] = new ClassMethodUsage(
- null,
+ $usageOrigin,
new ClassMethodRef(
$className,
$subscriberMethodString->getValue(),
@@ -183,7 +180,7 @@ private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
foreach ($subscriberMethodArray->getIterableValueType()->getConstantArrays() as $innerArray) {
foreach ($innerArray->getFirstIterableValueType()->getConstantStrings() as $subscriberMethodString) {
$usages[] = new ClassMethodUsage(
- null,
+ $usageOrigin,
new ClassMethodRef(
$className,
$subscriberMethodString->getValue(),
@@ -214,7 +211,7 @@ private function getMethodUsagesFromReflection(InClassNode $node): array
foreach ($nativeReflection->getMethods() as $method) {
if (isset($this->dicCalls[$className][$method->getName()])) {
- $usages[] = $this->createUsage($classReflection->getNativeMethod($method->getName()));
+ $usages[] = $this->createUsage($classReflection->getNativeMethod($method->getName()), 'called via DIC');
}
if ($method->getDeclaringClass()->getName() !== $nativeReflection->getName()) {
@@ -222,7 +219,7 @@ private function getMethodUsagesFromReflection(InClassNode $node): array
}
if ($this->shouldMarkAsUsed($method)) {
- $usages[] = $this->createUsage($classReflection->getNativeMethod($method->getName()));
+ $usages[] = $this->createUsage($classReflection->getNativeMethod($method->getName()), null);
}
}
@@ -235,6 +232,7 @@ private function getMethodUsagesFromReflection(InClassNode $node): array
private function getMethodUsagesFromAttributeReflection(InClassMethodNode $node, Scope $scope): array
{
$usages = [];
+ $usageOrigin = UsageOrigin::createRegular($node, $scope);
foreach ($node->getMethodReflection()->getParameters() as $parameter) {
foreach ($parameter->getAttributes() as $attributeReflection) {
@@ -259,7 +257,7 @@ private function getMethodUsagesFromAttributeReflection(InClassMethodNode $node,
foreach ($classNames as $className) {
$usages[] = new ClassMethodUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ $usageOrigin,
new ClassMethodRef(
$className->getValue(),
$defaultIndexMethod[0]->getValue(),
@@ -283,7 +281,7 @@ private function getMethodUsagesFromAttributeReflection(InClassMethodNode $node,
foreach ($classNames as $className) {
$usages[] = new ClassMethodUsage(
- $this->usageOriginDetector->detectOrigin($scope),
+ UsageOrigin::createRegular($node, $scope),
new ClassMethodRef(
$className->getValue(),
$defaultIndexMethod[0]->getValue(),
@@ -481,10 +479,10 @@ private function isSymfonyInstalled(): bool
|| InstalledVersions::isInstalled('symfony/dependency-injection');
}
- private function createUsage(ExtendedMethodReflection $methodReflection): ClassMethodUsage
+ private function createUsage(ExtendedMethodReflection $methodReflection, ?string $reason): ClassMethodUsage
{
return new ClassMethodUsage(
- null,
+ UsageOrigin::createVirtual($this, $reason),
new ClassMethodRef(
$methodReflection->getDeclaringClass()->getName(),
$methodReflection->getName(),
@@ -570,7 +568,7 @@ private function getConstantUsages(ClassReflection $classReflection): array
}
$usages[] = new ClassConstantUsage(
- null,
+ UsageOrigin::createVirtual($this, 'used in DIC'),
new ClassConstantRef(
$classReflection->getName(),
$constantName,
diff --git a/src/Rule/DeadCodeRule.php b/src/Rule/DeadCodeRule.php
index 31648930..7b21597b 100644
--- a/src/Rule/DeadCodeRule.php
+++ b/src/Rule/DeadCodeRule.php
@@ -16,8 +16,10 @@
use ShipMonk\PHPStan\DeadCode\Collector\MethodCallCollector;
use ShipMonk\PHPStan\DeadCode\Collector\ProvidedUsagesCollector;
use ShipMonk\PHPStan\DeadCode\Compatibility\BackwardCompatibilityChecker;
+use ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter;
use ShipMonk\PHPStan\DeadCode\Enum\ClassLikeKind;
use ShipMonk\PHPStan\DeadCode\Enum\MemberType;
+use ShipMonk\PHPStan\DeadCode\Enum\NeverReportedReason;
use ShipMonk\PHPStan\DeadCode\Enum\Visibility;
use ShipMonk\PHPStan\DeadCode\Error\BlackMember;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
@@ -27,16 +29,16 @@
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
use ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy;
use function array_key_exists;
+use function array_key_last;
use function array_keys;
use function array_map;
use function array_merge;
use function array_merge_recursive;
use function array_slice;
-use function array_sum;
+use function array_unique;
use function array_values;
use function in_array;
use function ksort;
-use function sprintf;
use function strpos;
/**
@@ -66,6 +68,8 @@ class DeadCodeRule implements Rule, DiagnoseExtension
'__debugInfo' => null,
];
+ private DebugUsagePrinter $debugUsagePrinter;
+
private ClassHierarchy $classHierarchy;
/**
@@ -91,8 +95,6 @@ class DeadCodeRule implements Rule, DiagnoseExtension
private bool $reportTransitivelyDeadAsSeparateError;
- private bool $trackMixedAccess;
-
/**
* memberKey => DeadMember
*
@@ -108,20 +110,20 @@ class DeadCodeRule implements Rule, DiagnoseExtension
private array $mixedMemberUsages = [];
/**
- * @var array> callerKey => memberUseKey[]
+ * @var array>> callerKey => array
*/
private array $usageGraph = [];
public function __construct(
+ DebugUsagePrinter $debugUsagePrinter,
ClassHierarchy $classHierarchy,
bool $reportTransitivelyDeadMethodAsSeparateError,
- bool $trackMixedAccess,
BackwardCompatibilityChecker $checker
)
{
+ $this->debugUsagePrinter = $debugUsagePrinter;
$this->classHierarchy = $classHierarchy;
$this->reportTransitivelyDeadAsSeparateError = $reportTransitivelyDeadMethodAsSeparateError;
- $this->trackMixedAccess = $trackMixedAccess;
$checker->check();
}
@@ -224,8 +226,8 @@ public function processNode(
}
}
- /** @var list $whiteMemberKeys */
- $whiteMemberKeys = [];
+ /** @var array> $whiteMembers */
+ $whiteMembers = [];
/** @var list $excludedMemberUsages */
$excludedMemberUsages = [];
@@ -239,30 +241,40 @@ public function processNode(
$isWhite = $this->isConsideredWhite($memberUsage);
$alternativeMemberKeys = $this->getAlternativeMemberKeys($memberUsage->getMemberRef());
- $alternativeOriginKeys = $memberUsage->getOrigin() !== null ? $this->getAlternativeMemberKeys($memberUsage->getOrigin()) : [];
+ $alternativeOriginKeys = $memberUsage->getOrigin()->hasClassMethodRef()
+ ? $this->getAlternativeMemberKeys($memberUsage->getOrigin()->toClassMethodRef())
+ : [];
foreach ($alternativeMemberKeys as $alternativeMemberKey) {
foreach ($alternativeOriginKeys as $alternativeOriginKey) {
- $this->usageGraph[$alternativeOriginKey][] = $alternativeMemberKey;
+ $this->usageGraph[$alternativeOriginKey][$alternativeMemberKey][] = $memberUsage;
}
if ($isWhite) {
- $whiteMemberKeys[] = $alternativeMemberKey;
+ $whiteMembers[$alternativeMemberKey][] = $collectedUsage->getUsage();
}
}
+
+ $this->debugUsagePrinter->recordUsage($collectedUsage, $alternativeMemberKeys);
}
- foreach ($whiteMemberKeys as $whiteCalleeKey) {
- $this->markTransitivesWhite($whiteCalleeKey);
+ foreach ($whiteMembers as $whiteCalleeKey => $usages) {
+ $this->markTransitivesWhite([$whiteCalleeKey => $usages]);
}
foreach ($this->blackMembers as $blackMemberKey => $blackMember) {
- if ($this->isNeverReportedAsDead($blackMember)) {
+ $neverReportedReason = $this->isNeverReportedAsDead($blackMember);
+
+ if ($neverReportedReason !== null) {
+ $this->debugUsagePrinter->markMemberAsNeverReported($blackMember, $neverReportedReason);
+
unset($this->blackMembers[$blackMemberKey]);
}
}
foreach ($excludedMemberUsages as $excludedMemberUsage) {
+ $this->debugUsagePrinter->recordUsage($excludedMemberUsage);
+
$excludedBy = $excludedMemberUsage->getExcludedBy();
$excludedMemberRef = $excludedMemberUsage->getUsage()->getMemberRef();
@@ -407,6 +419,8 @@ private function getAlternativeMemberKeys(ClassMemberRef $member): array
}
}
+ $result = array_values(array_unique($result));
+
$this->memberAlternativesCache[$cacheKey] = $result;
return $result;
@@ -447,17 +461,21 @@ private function findDefinerMemberKey(
}
/**
- * @param array $visitedKeys
+ * @param non-empty-array> $stack callerKey => usages[]
*/
- private function markTransitivesWhite(string $callerKey, array $visitedKeys = []): void
+ private function markTransitivesWhite(array $stack): void
{
- $visitedKeys = $visitedKeys === [] ? [$callerKey => null] : $visitedKeys;
- $calleeKeys = $this->usageGraph[$callerKey] ?? [];
+ $callerKey = array_key_last($stack);
+ $callees = $this->usageGraph[$callerKey] ?? [];
- unset($this->blackMembers[$callerKey]);
+ if (isset($this->blackMembers[$callerKey])) {
+ $this->debugUsagePrinter->markMemberAsWhite($this->blackMembers[$callerKey], $stack);
- foreach ($calleeKeys as $calleeKey) {
- if (array_key_exists($calleeKey, $visitedKeys)) {
+ unset($this->blackMembers[$callerKey]);
+ }
+
+ foreach ($callees as $calleeKey => $usages) {
+ if (array_key_exists($calleeKey, $stack)) {
continue;
}
@@ -465,7 +483,7 @@ private function markTransitivesWhite(string $callerKey, array $visitedKeys = []
continue;
}
- $this->markTransitivesWhite($calleeKey, array_merge($visitedKeys, [$calleeKey => null]));
+ $this->markTransitivesWhite(array_merge($stack, [$calleeKey => $usages]));
}
}
@@ -476,11 +494,11 @@ private function markTransitivesWhite(string $callerKey, array $visitedKeys = []
private function getTransitiveDeadCalls(string $callerKey, array $visitedKeys = []): array
{
$visitedKeys = $visitedKeys === [] ? [$callerKey => null] : $visitedKeys;
- $calleeKeys = $this->usageGraph[$callerKey] ?? [];
+ $callees = $this->usageGraph[$callerKey] ?? [];
$result = [];
- foreach ($calleeKeys as $calleeKey) {
+ foreach ($callees as $calleeKey => $_) {
if (array_key_exists($calleeKey, $visitedKeys)) {
continue;
}
@@ -516,7 +534,7 @@ private function groupDeadMembers(): array
continue;
}
- foreach ($callees as $callee) {
+ foreach ($callees as $callee => $_) {
if (array_key_exists($callee, $this->blackMembers)) {
$deadMethodsWithCaller[$callee] = true;
}
@@ -673,15 +691,18 @@ private function getTraitUsages(string $typeName): array
private function isConsideredWhite(ClassMemberUsage $memberUsage): bool
{
- return $memberUsage->getOrigin() === null
+ return $memberUsage->getOrigin()->getClassName() === null // out-of-class scope
|| $this->isAnonymousClass($memberUsage->getOrigin()->getClassName())
- || (array_key_exists($memberUsage->getOrigin()->getMemberName(), self::UNSUPPORTED_MAGIC_METHODS));
+ || (array_key_exists((string) $memberUsage->getOrigin()->getMethodName(), self::UNSUPPORTED_MAGIC_METHODS));
}
- private function isNeverReportedAsDead(BlackMember $blackMember): bool
+ /**
+ * @return NeverReportedReason::*|null
+ */
+ private function isNeverReportedAsDead(BlackMember $blackMember): ?string
{
if (!$blackMember->getMember() instanceof ClassMethodRef) {
- return false;
+ return null;
}
$typeName = $blackMember->getMember()->getClassName();
@@ -699,73 +720,25 @@ private function isNeverReportedAsDead(BlackMember $blackMember): bool
if ($kind === ClassLikeKind::TRAIT && $abstract) {
// abstract methods in traits make sense (not dead) only when called within the trait itself, but that is hard to detect for now, so lets ignore them completely
// the difference from interface methods (or abstract methods) is that those methods can be called over the interface, but you cannot call method over trait
- return true;
+ return NeverReportedReason::ABSTRACT_TRAIT_METHOD;
}
if ($memberName === '__construct' && ($visibility & Visibility::PRIVATE) !== 0 && $params === 0) {
// private constructors with zero parameters are often used to deny instantiation
- return true;
+ return NeverReportedReason::PRIVATE_CONSTRUCTOR_NO_PARAMS;
}
if (array_key_exists($memberName, self::UNSUPPORTED_MAGIC_METHODS)) {
- return true;
+ return NeverReportedReason::UNSUPPORTED_MAGIC_METHOD;
}
- return false;
+ return null;
}
public function print(Output $output): void
{
- if ($this->mixedMemberUsages === [] || !$output->isDebug() || !$this->trackMixedAccess) {
- return;
- }
-
- $totalCount = array_sum(array_map('count', $this->mixedMemberUsages));
- $maxExamplesToShow = 20;
- $examplesShown = 0;
- $output->writeLineFormatted(sprintf('Found %d usages over unknown type>:', $totalCount));
-
- foreach ($this->mixedMemberUsages as $memberType => $collectedUsages) {
- foreach ($collectedUsages as $memberName => $usages) {
- $examplesShown++;
- $memberTypeString = $memberType === MemberType::METHOD ? 'method' : 'constant';
- $output->writeFormatted(sprintf(' • %s> %s', $memberName, $memberTypeString));
-
- $exampleCaller = $this->getExampleCaller($usages);
-
- if ($exampleCaller !== null) {
- $output->writeFormatted(sprintf(', for example in %s>', $exampleCaller));
- }
-
- $output->writeLineFormatted('');
-
- if ($examplesShown >= $maxExamplesToShow) {
- break 2;
- }
- }
- }
-
- if ($totalCount > $maxExamplesToShow) {
- $output->writeLineFormatted(sprintf('... and %d more', $totalCount - $maxExamplesToShow));
- }
-
- $output->writeLineFormatted('');
- $output->writeLineFormatted('Thus, any member named the same is considered used, no matter its declaring class!');
- $output->writeLineFormatted('');
- }
-
- /**
- * @param list $usages
- */
- private function getExampleCaller(array $usages): ?string
- {
- foreach ($usages as $usage) {
- if ($usage->getUsage()->getOrigin() !== null) {
- return $usage->getUsage()->getOrigin()->toHumanString();
- }
- }
-
- return null;
+ $this->debugUsagePrinter->printMixedMemberUsages($output, $this->mixedMemberUsages);
+ $this->debugUsagePrinter->printDebugMemberUsages($output, $this->typeDefinitions);
}
}
diff --git a/tests/AllServicesInConfigTest.php b/tests/AllServicesInConfigTest.php
index 8d1b4a33..f9c9bb95 100644
--- a/tests/AllServicesInConfigTest.php
+++ b/tests/AllServicesInConfigTest.php
@@ -10,6 +10,7 @@
use ShipMonk\PHPStan\DeadCode\Collector\BufferedUsageCollector;
use ShipMonk\PHPStan\DeadCode\Enum\ClassLikeKind;
use ShipMonk\PHPStan\DeadCode\Enum\MemberType;
+use ShipMonk\PHPStan\DeadCode\Enum\NeverReportedReason;
use ShipMonk\PHPStan\DeadCode\Enum\Visibility;
use ShipMonk\PHPStan\DeadCode\Error\BlackMember;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
@@ -19,6 +20,7 @@
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
+use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
use ShipMonk\PHPStan\DeadCode\Transformer\RemoveClassMemberVisitor;
@@ -54,6 +56,7 @@ public function test(): void
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
$missingClassNames = [];
$excluded = [
+ UsageOrigin::class,
ClassMethodUsage::class,
ClassMethodRef::class,
ClassConstantRef::class,
@@ -70,6 +73,7 @@ public function test(): void
RemoveDeadCodeTransformer::class,
RemoveClassMemberVisitor::class,
MemberType::class,
+ NeverReportedReason::class,
];
/** @var DirectoryIterator $file */
diff --git a/tests/Graph/SerializationTest.php b/tests/Graph/SerializationTest.php
index 63bdbca3..b8fe9a1b 100644
--- a/tests/Graph/SerializationTest.php
+++ b/tests/Graph/SerializationTest.php
@@ -12,10 +12,8 @@ class SerializationTest extends TestCase
*/
public function testSerialization(CollectedUsage $expected, string $serialized): void
{
- $unserialized = CollectedUsage::deserialize($serialized);
-
self::assertSame($serialized, $expected->serialize());
- self::assertEquals($expected, $unserialized);
+ self::assertEquals($expected, CollectedUsage::deserialize($serialized));
}
/**
@@ -23,25 +21,15 @@ public function testSerialization(CollectedUsage $expected, string $serialized):
*/
public static function provideData(): iterable
{
- yield [
- new CollectedUsage(
- new ClassMethodUsage(
- null,
- new ClassMethodRef('Some', 'method', false),
- ),
- null,
- ),
- '{"e":null,"t":1,"o":null,"m":{"c":"Some","m":"method","d":false}}',
- ];
yield [
new CollectedUsage(
new ClassConstantUsage(
- new ClassMethodRef('Clazz', 'method', false),
+ new UsageOrigin('Clazz', 'method', '/app/index.php', 7, null, null),
new ClassConstantRef(null, 'CONSTANT', true),
),
'excluder',
),
- '{"e":"excluder","t":2,"o":{"c":"Clazz","m":"method","d":false},"m":{"c":null,"m":"CONSTANT","d":true}}',
+ '{"e":"excluder","t":2,"o":{"c":"Clazz","m":"method","f":"\/app\/index.php","l":7,"p":null,"n":null},"m":{"c":null,"m":"CONSTANT","d":true}}',
];
}
diff --git a/tests/Rule/DeadCodeRuleTest.php b/tests/Rule/DeadCodeRuleTest.php
index db7ecca7..62dccc09 100644
--- a/tests/Rule/DeadCodeRuleTest.php
+++ b/tests/Rule/DeadCodeRuleTest.php
@@ -4,6 +4,7 @@
use Composer\InstalledVersions;
use Composer\Semver\VersionParser;
+use LogicException;
use PhpParser\Node;
use PHPStan\Analyser\Error;
use PHPStan\Analyser\Scope;
@@ -11,6 +12,7 @@
use PHPStan\Command\AnalysisResult;
use PHPStan\Command\Output;
use PHPStan\DependencyInjection\Container;
+use PHPStan\File\SimpleRelativePathHelper;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\Reflection\ReflectionProvider;
@@ -20,11 +22,11 @@
use ShipMonk\PHPStan\DeadCode\Collector\MethodCallCollector;
use ShipMonk\PHPStan\DeadCode\Collector\ProvidedUsagesCollector;
use ShipMonk\PHPStan\DeadCode\Compatibility\BackwardCompatibilityChecker;
+use ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter;
use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;
use ShipMonk\PHPStan\DeadCode\Excluder\TestsUsageExcluder;
use ShipMonk\PHPStan\DeadCode\Formatter\RemoveDeadCodeFormatter;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
-use ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector;
use ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy;
use ShipMonk\PHPStan\DeadCode\Provider\DoctrineUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
@@ -38,6 +40,7 @@
use ShipMonk\PHPStan\DeadCode\Transformer\FileSystem;
use function file_get_contents;
use function is_array;
+use function preg_replace;
use function str_replace;
use function strpos;
use const PHP_VERSION_ID;
@@ -48,6 +51,11 @@
class DeadCodeRuleTest extends RuleTestCase
{
+ /**
+ * @var list
+ */
+ private array $debugMembers = [];
+
private bool $trackMixedAccess = true;
private bool $emitErrorsInGroups = true;
@@ -58,11 +66,22 @@ class DeadCodeRuleTest extends RuleTestCase
protected function getRule(): DeadCodeRule
{
+ $container = $this->createMock(Container::class);
+ $container->expects(self::any())
+ ->method('getParameter')
+ ->willReturn(['debug' => ['usagesOf' => $this->debugMembers]]);
+
if ($this->rule === null) {
$this->rule = new DeadCodeRule(
+ new DebugUsagePrinter(
+ $container,
+ new SimpleRelativePathHelper(__DIR__), // @phpstan-ignore phpstanApi.constructor
+ self::createReflectionProvider(),
+ null,
+ true,
+ ),
new ClassHierarchy(),
!$this->emitErrorsInGroups,
- true,
new BackwardCompatibilityChecker([]),
);
}
@@ -79,9 +98,9 @@ protected function getCollectors(): array
return [
new ProvidedUsagesCollector($reflectionProvider, $this->getMemberUsageProviders(), $this->getMemberUsageExcluders()),
- new ClassDefinitionCollector(self::createReflectionProvider()),
- new MethodCallCollector($this->createUsageOriginDetector(), $this->trackMixedAccess, $this->getMemberUsageExcluders()),
- new ConstantFetchCollector($this->createUsageOriginDetector(), $reflectionProvider, $this->trackMixedAccess, $this->getMemberUsageExcluders()),
+ new ClassDefinitionCollector($reflectionProvider),
+ new MethodCallCollector($this->trackMixedAccess, $this->getMemberUsageExcluders()),
+ new ConstantFetchCollector($reflectionProvider, $this->trackMixedAccess, $this->getMemberUsageExcluders()),
];
}
@@ -159,10 +178,10 @@ static function (string $message) use (&$actualOutput): void {
$ec = ''; // hack editorconfig checker to ignore wrong indentation
$expectedOutput = <<<"OUTPUT"
Found 4 usages over unknown type>:
- $ec • getter1> method, for example in DeadMixed1\Tester::__construct>
- $ec • getter2> method, for example in DeadMixed1\Tester::__construct>
- $ec • getter3> method, for example in DeadMixed1\Tester::__construct>
- $ec • staticMethod> method, for example in DeadMixed1\Tester::__construct>
+ $ec • getter1> method, for example in data/methods/mixed/tracked.php:46>
+ $ec • getter2> method, for example in data/methods/mixed/tracked.php:49>
+ $ec • getter3> method, for example in data/methods/mixed/tracked.php:52>
+ $ec • staticMethod> method, for example in data/methods/mixed/tracked.php:57>
Thus, any member named the same is considered used, no matter its declaring class!
@@ -172,6 +191,92 @@ static function (string $message) use (&$actualOutput): void {
self::assertSame($expectedOutput, $actualOutput);
}
+ public function testDebugUsage(): void
+ {
+ $this->debugMembers = [
+ 'DateTime::format',
+ 'DebugAlternative\Foo::foo',
+ 'DebugCtor\Foo::__construct',
+ 'DebugExclude\Foo::mixedExcluder',
+ 'DebugNever\Foo::__get',
+ 'DebugVirtual\FooTest::testFoo',
+ 'DebugGlobal\Foo::chain2',
+ 'DebugMixed\Foo::any',
+ 'DebugCycle\Foo::__construct',
+ 'DebugRegular\Another::call',
+ 'DebugUnsupported\Foo::notDead',
+ 'DebugZero\Foo::__construct',
+ ];
+ $this->analyseFiles([
+ __DIR__ . '/data/debug/alternative.php',
+ __DIR__ . '/data/debug/ctor.php',
+ __DIR__ . '/data/debug/exclude.php',
+ __DIR__ . '/data/debug/cycle.php',
+ __DIR__ . '/data/debug/foreign.php',
+ __DIR__ . '/data/debug/global.php',
+ __DIR__ . '/data/debug/mixed.php',
+ __DIR__ . '/data/debug/never.php',
+ __DIR__ . '/data/debug/regular.php',
+ __DIR__ . '/data/debug/unsupported.php',
+ __DIR__ . '/data/debug/virtual.php',
+ __DIR__ . '/data/debug/zero.php',
+ ]);
+ $rule = $this->getRule();
+
+ $actualOutput = '';
+ $output = $this->createMock(Output::class);
+ $output->expects(self::atLeastOnce())
+ ->method('isDebug')
+ ->willReturn(true);
+ $output->expects(self::atLeastOnce())
+ ->method('writeFormatted')
+ ->willReturnCallback(
+ static function (string $message) use (&$actualOutput): void {
+ $actualOutput .= $message;
+ },
+ );
+ $output->expects(self::atLeastOnce())
+ ->method('writeLineFormatted')
+ ->willReturnCallback(
+ static function (string $message) use (&$actualOutput): void {
+ $actualOutput .= $message . "\n";
+ },
+ );
+
+ $rule->print($output);
+
+ $expectedOutput = file_get_contents(__DIR__ . '/data/debug/expected_output.txt');
+ self::assertNotFalse($expectedOutput);
+ self::assertSame($expectedOutput . "\n", $this->trimFgColors($actualOutput));
+ }
+
+ /**
+ * @dataProvider provideDebugUsageInvalidArgs
+ */
+ public function testDebugUsageInvalidArgs(string $member, string $error): void
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage($error);
+
+ $this->debugMembers = [$member];
+ $this->analyseFiles([__DIR__ . '/data/debug/alternative.php']);
+ $this->getRule();
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideDebugUsageInvalidArgs(): array
+ {
+ return [
+ 'method not owned' => ['DebugAlternative\Clazz::foo', "Member 'foo' does not exist directly in 'DebugAlternative\Clazz'"],
+ 'method not declared' => ['DebugAlternative\Clazz::__construct', "Member '__construct' does not exist directly in 'DebugAlternative\Clazz'"],
+ 'no method' => ['DebugAlternative\Clazz::xyz', "Member 'xyz' does not exist directly in 'DebugAlternative\Clazz'"],
+ 'no class' => ['InvalidClass::foo', "Class 'InvalidClass' does not exist"],
+ 'invalid format' => ['InvalidFormat', "Invalid debug member format: 'InvalidFormat', expected 'ClassName::memberName'"],
+ ];
+ }
+
/**
* @dataProvider provideAutoRemoveFiles
*/
@@ -448,7 +553,6 @@ private function getMemberUsageProviders(): array
{
return [
new ReflectionUsageProvider(
- $this->createUsageOriginDetector(),
true,
),
new class extends ReflectionBasedMemberUsageProvider
@@ -481,7 +585,6 @@ public function shouldMarkMethodAsUsed(ReflectionMethod $method): bool
),
new SymfonyUsageProvider(
$this->createContainerMockWithSymfonyConfig(),
- new UsageOriginDetector(),
true,
__DIR__ . '/data/providers/symfony/',
),
@@ -532,18 +635,6 @@ static function (string $type): array {
return $mock;
}
- private function createUsageOriginDetector(): UsageOriginDetector
- {
- /** @var UsageOriginDetector|null $detector */
- static $detector = null;
-
- if ($detector === null) {
- $detector = new UsageOriginDetector();
- }
-
- return $detector;
- }
-
public function gatherAnalyserErrors(array $files): array
{
if (!$this->unwrapGroupedErrors) {
@@ -604,4 +695,19 @@ private static function requiresPackage(string $package, string $constraint): bo
return InstalledVersions::satisfies(new VersionParser(), $package, $constraint);
}
+ private function trimFgColors(string $output): string
+ {
+ $replaced = preg_replace(
+ '/(.*?)<\/>/',
+ '$1',
+ $output,
+ );
+
+ if ($replaced === null) {
+ throw new LogicException('Failed to trim colors');
+ }
+
+ return $replaced;
+ }
+
}
diff --git a/tests/Rule/data/debug/alternative.php b/tests/Rule/data/debug/alternative.php
new file mode 100644
index 00000000..e7672611
--- /dev/null
+++ b/tests/Rule/data/debug/alternative.php
@@ -0,0 +1,13 @@
+foo();
diff --git a/tests/Rule/data/debug/ctor.php b/tests/Rule/data/debug/ctor.php
new file mode 100644
index 00000000..a5ab7d90
--- /dev/null
+++ b/tests/Rule/data/debug/ctor.php
@@ -0,0 +1,13 @@
+format('Y-m-d H:i:s');
diff --git a/tests/Rule/data/debug/global.php b/tests/Rule/data/debug/global.php
new file mode 100644
index 00000000..1e0e8ea8
--- /dev/null
+++ b/tests/Rule/data/debug/global.php
@@ -0,0 +1,20 @@
+chain2();
+ }
+
+ public function chain2()
+ {
+
+ }
+}
+
+(new Foo())->chain1();
diff --git a/tests/Rule/data/debug/mixed.php b/tests/Rule/data/debug/mixed.php
new file mode 100644
index 00000000..d859ba4b
--- /dev/null
+++ b/tests/Rule/data/debug/mixed.php
@@ -0,0 +1,14 @@
+any();
+}
diff --git a/tests/Rule/data/debug/never.php b/tests/Rule/data/debug/never.php
new file mode 100644
index 00000000..185761bd
--- /dev/null
+++ b/tests/Rule/data/debug/never.php
@@ -0,0 +1,9 @@
+call();
+ $another->call();
+ }
+}
+
+class Another
+{
+ public function call(): void
+ {
+ }
+}
diff --git a/tests/Rule/data/debug/unsupported.php b/tests/Rule/data/debug/unsupported.php
new file mode 100644
index 00000000..b170a8f4
--- /dev/null
+++ b/tests/Rule/data/debug/unsupported.php
@@ -0,0 +1,14 @@
+notDead();
+ }
+
+ private function notDead()
+ {
+ }
+}
diff --git a/tests/Rule/data/debug/virtual.php b/tests/Rule/data/debug/virtual.php
new file mode 100644
index 00000000..760bfd59
--- /dev/null
+++ b/tests/Rule/data/debug/virtual.php
@@ -0,0 +1,13 @@
+