diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 08e47df..9d1223c 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -425,7 +425,9 @@
-
+
+
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 0f95a7d..96293f7 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -15,6 +15,7 @@ parameters:
analyseAndScan:
- tests/*/data/*
tmpDir: cache/phpstan/
+ internalErrorsCountLimit: 1
checkMissingCallableSignature: true
checkUninitializedProperties: true
checkBenevolentUnionTypes: true
diff --git a/rules.neon b/rules.neon
index 8735348..d618f83 100644
--- a/rules.neon
+++ b/rules.neon
@@ -1,6 +1,6 @@
services:
-
- class: ShipMonk\PHPStan\DeadCode\Reflection\ClassHierarchy
+ class: ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy
-
class: ShipMonk\PHPStan\DeadCode\Provider\VendorEntrypointProvider
@@ -47,11 +47,6 @@ services:
tags:
- phpstan.collector
- -
- class: ShipMonk\PHPStan\DeadCode\Collector\MethodDefinitionCollector
- tags:
- - phpstan.collector
-
-
class: ShipMonk\PHPStan\DeadCode\Rule\DeadMethodRule
arguments:
diff --git a/src/Collector/ClassDefinitionCollector.php b/src/Collector/ClassDefinitionCollector.php
index 00b51eb..16bad26 100644
--- a/src/Collector/ClassDefinitionCollector.php
+++ b/src/Collector/ClassDefinitionCollector.php
@@ -2,43 +2,204 @@
namespace ShipMonk\PHPStan\DeadCode\Collector;
+use LogicException;
use PhpParser\Node;
+use PhpParser\Node\Name;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassLike;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Enum_;
+use PhpParser\Node\Stmt\Interface_;
+use PhpParser\Node\Stmt\Trait_;
+use PhpParser\Node\Stmt\TraitUseAdaptation\Alias;
+use PhpParser\Node\Stmt\TraitUseAdaptation\Precedence;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
-use PHPStan\Node\InClassNode;
+use ShipMonk\PHPStan\DeadCode\Crate\Kind;
+use function array_fill_keys;
+use function array_map;
+use function strpos;
/**
- * @implements Collector>
+ * @implements Collector,
+ * parents: array,
+ * traits: array, aliases?: array}>,
+ * interfaces: array,
+ * }>
*/
class ClassDefinitionCollector implements Collector
{
public function getNodeType(): string
{
- return InClassNode::class;
+ return ClassLike::class;
}
/**
- * @param InClassNode $node
- * @return array
+ * @param ClassLike $node
+ * @return array{
+ * kind: string,
+ * name: string,
+ * methods: array,
+ * parents: array,
+ * traits: array, aliases?: array}>,
+ * interfaces: array,
+ * }|null
*/
public function processNode(
Node $node,
Scope $scope
- ): array
+ ): ?array
{
- $pairs = [];
- $origin = $node->getClassReflection();
+ if ($node->namespacedName === null) {
+ return null;
+ }
+
+ $kind = $this->getKind($node);
+ $typeName = $node->namespacedName->toString();
- foreach ($origin->getAncestors() as $ancestor) {
- if ($ancestor->isTrait() || $ancestor === $origin) {
+ $methods = [];
+
+ foreach ($node->getMethods() as $method) {
+ if ($this->isUnsupportedMethod($method)) {
continue;
}
- $pairs[$ancestor->getName()] = $origin->getName();
+ $methods[$method->name->toString()] = [
+ 'line' => $method->getStartLine(),
+ ];
+ }
+
+ return [
+ 'kind' => $kind,
+ 'name' => $typeName,
+ 'methods' => $methods,
+ 'parents' => $this->getParents($node),
+ 'traits' => $this->getTraits($node),
+ 'interfaces' => $this->getInterfaces($node),
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private function getParents(ClassLike $node): array
+ {
+ if ($node instanceof Class_) {
+ if ($node->extends === null) {
+ return [];
+ }
+
+ return [$node->extends->toString() => null];
+ }
+
+ if ($node instanceof Interface_) {
+ return array_fill_keys(
+ array_map(
+ static fn(Name $name) => $name->toString(),
+ $node->extends,
+ ),
+ null,
+ );
+ }
+
+ return [];
+ }
+
+ /**
+ * @return array
+ */
+ private function getInterfaces(ClassLike $node): array
+ {
+ if ($node instanceof Class_ || $node instanceof Enum_) {
+ return array_fill_keys(
+ array_map(
+ static fn(Name $name) => $name->toString(),
+ $node->implements,
+ ),
+ null,
+ );
+ }
+
+ return [];
+ }
+
+ /**
+ * @return array, aliases?: array}>
+ */
+ private function getTraits(ClassLike $node): array
+ {
+ $traits = [];
+
+ foreach ($node->getTraitUses() as $traitUse) {
+ foreach ($traitUse->traits as $trait) {
+ $traits[$trait->toString()] = [];
+ }
+
+ foreach ($traitUse->adaptations as $adaptation) {
+ if ($adaptation instanceof Precedence) {
+ foreach ($adaptation->insteadof as $insteadof) {
+ $traits[$insteadof->toString()]['excluded'][] = $adaptation->method->toString();
+ }
+ }
+
+ if ($adaptation instanceof Alias && $adaptation->newName !== null) {
+ if ($adaptation->trait === null) {
+ // assign alias to all traits, wrong ones are eliminated in Rule logic
+ foreach ($traitUse->traits as $trait) {
+ $traits[$trait->toString()]['aliases'][$adaptation->method->toString()] = $adaptation->newName->toString();
+ }
+ } else {
+ $traits[$adaptation->trait->toString()]['aliases'][$adaptation->method->toString()] = $adaptation->newName->toString();
+ }
+ }
+ }
+ }
+
+ return $traits;
+ }
+
+ private function isUnsupportedMethod(ClassMethod $method): bool
+ {
+ $methodName = $method->name->toString();
+
+ if ($methodName === '__destruct') {
+ return true;
+ }
+
+ if ($methodName !== '__construct' && strpos($methodName, '__') === 0) { // magic methods like __toString, __clone, __get, __set etc
+ return true;
+ }
+
+ if ($methodName === '__construct' && $method->isPrivate()) { // e.g. classes with "denied" instantiation
+ return true;
+ }
+
+ return false;
+ }
+
+ private function getKind(ClassLike $node): string
+ {
+ if ($node instanceof Class_) {
+ return Kind::CLASSS;
+ }
+
+ if ($node instanceof Interface_) {
+ return Kind::INTERFACE;
+ }
+
+ if ($node instanceof Trait_) {
+ return Kind::TRAIT;
+ }
+
+ if ($node instanceof Enum_) {
+ return Kind::ENUM;
}
- return $pairs;
+ throw new LogicException('Unknown class-like node');
}
}
diff --git a/src/Collector/MethodDefinitionCollector.php b/src/Collector/MethodDefinitionCollector.php
deleted file mode 100644
index ca41e16..0000000
--- a/src/Collector/MethodDefinitionCollector.php
+++ /dev/null
@@ -1,170 +0,0 @@
-, traitOriginDefinition: ?string}>>
- */
-class MethodDefinitionCollector implements Collector
-{
-
- public function getNodeType(): string
- {
- return InClassNode::class;
- }
-
- /**
- * @param InClassNode $node
- * @return list, traitOriginDefinition: ?string}>|null
- */
- public function processNode(
- Node $node,
- Scope $scope
- ): ?array
- {
- $reflection = $node->getClassReflection();
- $nativeReflection = $reflection->getNativeReflection();
- $result = [];
-
- if ($reflection->isAnonymous()) {
- return null; // https://github.com/phpstan/phpstan/issues/8410
- }
-
- // we need to collect even methods of traits that are always overridden
- foreach ($reflection->getTraits(true) as $trait) {
- foreach ($trait->getNativeReflection()->getMethods() as $traitMethod) {
- if ($this->isUnsupportedMethod($traitMethod)) {
- continue;
- }
-
- $traitLine = $traitMethod->getStartLine();
- $traitFile = $traitMethod->getFileName();
- $traitName = $trait->getName();
- $traitMethodName = $traitMethod->getName();
- $declaringTraitDefinition = $this->getDeclaringTraitDefinition($trait, $traitMethodName);
-
- if ($traitLine === false || $traitFile === false) {
- continue;
- }
-
- $result[] = [
- 'line' => $traitLine,
- 'file' => $traitFile,
- 'definition' => (new MethodDefinition($traitName, $traitMethodName))->toString(),
- 'overriddenDefinitions' => [],
- 'traitOriginDefinition' => $declaringTraitDefinition !== null ? $declaringTraitDefinition->toString() : null,
- ];
- }
- }
-
- foreach ($nativeReflection->getMethods() as $method) {
- if ($this->isUnsupportedMethod($method)) {
- continue;
- }
-
- if ($scope->getFile() !== $method->getDeclaringClass()->getFileName()) { // method in parent class
- continue;
- }
-
- $line = $method->getStartLine();
-
- if ($line === false) {
- continue;
- }
-
- $className = $method->getDeclaringClass()->getName();
- $methodName = $method->getName();
- $definition = new MethodDefinition($className, $methodName);
-
- $declaringTraitDefinition = $this->getDeclaringTraitDefinition($reflection, $methodName);
-
- $overriddenDefinitions = [];
-
- foreach ($reflection->getAncestors() as $ancestor) {
- if ($ancestor === $reflection) {
- continue;
- }
-
- if (!$ancestor->hasMethod($methodName)) {
- continue;
- }
-
- $overriddenDefinitions[] = new MethodDefinition($ancestor->getName(), $methodName);
- }
-
- $result[] = [
- 'line' => $line,
- 'file' => $scope->getFile(),
- 'definition' => $definition->toString(),
- 'overriddenDefinitions' => array_map(static fn (MethodDefinition $definition) => $definition->toString(), $overriddenDefinitions),
- 'traitOriginDefinition' => $declaringTraitDefinition !== null ? $declaringTraitDefinition->toString() : null,
- ];
- }
-
- return $result !== [] ? $result : null;
- }
-
- private function getDeclaringTraitDefinition(
- ClassReflection $classReflection,
- string $methodName
- ): ?MethodDefinition
- {
- try {
- $nativeReflectionMethod = $classReflection->getNativeReflection()->getMethod($methodName);
- $betterReflectionMethod = $nativeReflectionMethod->getBetterReflection();
- $realDeclaringClass = $betterReflectionMethod->getDeclaringClass();
-
- // when trait method name is aliased, we need the original name
- $realName = Closure::bind(function (): string {
- return $this->name;
- }, $betterReflectionMethod, BetterReflectionMethod::class)();
-
- } catch (ReflectionException $e) {
- return null;
- }
-
- if ($realDeclaringClass->isTrait() && $realDeclaringClass->getName() !== $classReflection->getName()) {
- return new MethodDefinition(
- $realDeclaringClass->getName(),
- $realName,
- );
- }
-
- return null;
- }
-
- private function isUnsupportedMethod(ReflectionMethod $method): bool
- {
- if ($method->isDestructor()) {
- return true;
- }
-
- if (!$method->isConstructor() && strpos($method->getName(), '__') === 0) { // magic methods like __toString, __clone, __get, __set etc
- return true;
- }
-
- if ($method->isConstructor() && $method->isPrivate()) { // e.g. classes with "denied" instantiation
- return true;
- }
-
- if ($method->getFileName() === false) { // e.g. php core
- return true;
- }
-
- return strpos($method->getFileName(), '/vendor/') !== false;
- }
-
-}
diff --git a/src/Crate/Kind.php b/src/Crate/Kind.php
new file mode 100644
index 0000000..30f3bd8
--- /dev/null
+++ b/src/Crate/Kind.php
@@ -0,0 +1,13 @@
+ childrenMethodKey[]
- *
- * @var array>
- */
- private array $methodDescendants = [];
-
/**
* traitMethodKey => traitUserMethodKey[]
*
@@ -41,11 +34,6 @@ public function registerClassPair(string $ancestorName, string $descendantName):
$this->classDescendants[$ancestorName][$descendantName] = true;
}
- public function registerMethodPair(MethodDefinition $ancestor, MethodDefinition $descendant): void
- {
- $this->methodDescendants[$ancestor->toString()][] = $descendant;
- }
-
public function registerMethodTraitUsage(
MethodDefinition $declaringTraitMethodKey,
MethodDefinition $traitUsageMethodKey
@@ -65,14 +53,6 @@ public function getClassDescendants(string $className): array
: [];
}
- /**
- * @return list
- */
- public function getMethodDescendants(MethodDefinition $definition): array
- {
- return $this->methodDescendants[$definition->toString()] ?? [];
- }
-
/**
* @return list
*/
diff --git a/src/Provider/SymfonyEntrypointProvider.php b/src/Provider/SymfonyEntrypointProvider.php
index 520c2eb..4463515 100644
--- a/src/Provider/SymfonyEntrypointProvider.php
+++ b/src/Provider/SymfonyEntrypointProvider.php
@@ -10,7 +10,7 @@
use ReflectionClass;
use ReflectionMethod;
use Reflector;
-use ShipMonk\PHPStan\DeadCode\Reflection\ClassHierarchy;
+use ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy;
use const PHP_VERSION_ID;
class SymfonyEntrypointProvider implements EntrypointProvider
diff --git a/src/Rule/DeadMethodRule.php b/src/Rule/DeadMethodRule.php
index 6d1dc3a..bf0d9ab 100644
--- a/src/Rule/DeadMethodRule.php
+++ b/src/Rule/DeadMethodRule.php
@@ -9,16 +9,17 @@
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
+use ReflectionException;
use ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector;
use ShipMonk\PHPStan\DeadCode\Collector\MethodCallCollector;
-use ShipMonk\PHPStan\DeadCode\Collector\MethodDefinitionCollector;
use ShipMonk\PHPStan\DeadCode\Crate\Call;
use ShipMonk\PHPStan\DeadCode\Crate\MethodDefinition;
+use ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy;
use ShipMonk\PHPStan\DeadCode\Provider\EntrypointProvider;
-use ShipMonk\PHPStan\DeadCode\Reflection\ClassHierarchy;
-use function array_map;
+use function array_keys;
use function array_merge;
use function array_values;
+use function in_array;
use function strpos;
/**
@@ -36,6 +37,26 @@ class DeadMethodRule implements Rule
*/
private array $errors = [];
+ /**
+ * typename => data
+ *
+ * @var array,
+ * parents: array,
+ * traits: array, aliases?: array}>,
+ * interfaces: array
+ * }>
+ */
+ private array $typeDefinitions = [];
+
+ /**
+ * @var array>
+ */
+ private array $methodsToMarkAsUsedCache = [];
+
/**
* @var list
*/
@@ -73,43 +94,38 @@ public function processNode(
return [];
}
- $classDeclarationData = $node->get(ClassDefinitionCollector::class);
- $methodDeclarationData = $node->get(MethodDefinitionCollector::class);
+ $methodDeclarationData = $node->get(ClassDefinitionCollector::class);
$methodCallData = $node->get(MethodCallCollector::class);
$declaredMethods = [];
- foreach ($classDeclarationData as $file => $classesInFile) {
- foreach ($classesInFile as $classPairs) {
- foreach ($classPairs as $ancestor => $descendant) {
- $this->classHierarchy->registerClassPair($ancestor, $descendant);
- }
+ foreach ($methodDeclarationData as $file => $data) {
+ foreach ($data as $typeData) {
+ $typeName = $typeData['name'];
+ $this->typeDefinitions[$typeName] = [
+ 'kind' => $typeData['kind'],
+ 'name' => $typeName,
+ 'file' => $file,
+ 'methods' => $typeData['methods'],
+ 'parents' => $typeData['parents'],
+ 'traits' => $typeData['traits'],
+ 'interfaces' => $typeData['interfaces'],
+ ];
}
}
- unset($classDeclarationData);
-
- foreach ($methodDeclarationData as $methodsInFile) {
- foreach ($methodsInFile as $declared) {
- foreach ($declared as $serializedMethodDeclaration) {
- [
- 'line' => $line,
- 'file' => $file,
- 'definition' => $definition,
- 'overriddenDefinitions' => $overriddenDefinitions,
- 'traitOriginDefinition' => $declaringTraitMethodKey,
- ] = $this->deserializeMethodDeclaration($serializedMethodDeclaration);
+ foreach ($this->typeDefinitions as $typeName => $typeDefinition) {
+ $methods = $typeDefinition['methods'];
+ $file = $typeDefinition['file'];
- $declaredMethods[$definition->toString()] = [$file, $line];
+ $ancestorNames = $this->getAncestorNames($typeName);
- if ($declaringTraitMethodKey !== null) {
- $this->classHierarchy->registerMethodTraitUsage($declaringTraitMethodKey, $definition);
- }
+ $this->fillTraitUsages($typeName, $this->getTraitUsages($typeName));
+ $this->fillClassHierarchy($typeName, $ancestorNames);
- foreach ($overriddenDefinitions as $ancestor) {
- $this->classHierarchy->registerMethodPair($ancestor, $definition);
- }
- }
+ foreach ($methods as $methodName => $methodData) {
+ $definition = new MethodDefinition($typeName, $methodName);
+ $declaredMethods[$definition->toString()] = [$file, $methodData['line']];
}
}
@@ -146,6 +162,54 @@ public function processNode(
return array_values($this->errors);
}
+ /**
+ * @param array, aliases?: array}> $usedTraits
+ */
+ private function fillTraitUsages(string $typeName, array $usedTraits): void
+ {
+ foreach ($usedTraits as $traitName => $adaptations) {
+ $traitMethods = array_keys($this->typeDefinitions[$traitName]['methods'] ?? []);
+
+ $excludedMethods = $adaptations['excluded'] ?? [];
+
+ foreach ($traitMethods as $traitMethod) {
+ if (isset($this->typeDefinitions[$typeName]['methods'][$traitMethod])) {
+ continue; // overridden trait method, thus not used
+ }
+
+ $declaringTraitMethodDefinition = new MethodDefinition($traitName, $traitMethod);
+ $aliasMethodName = $adaptations['aliases'][$traitMethod] ?? null;
+
+ // both method names need to work
+ if ($aliasMethodName !== null) {
+ $aliasMethodDefinition = new MethodDefinition($typeName, $aliasMethodName);
+ $this->classHierarchy->registerMethodTraitUsage($declaringTraitMethodDefinition, $aliasMethodDefinition);
+ }
+
+ if (in_array($traitMethod, $excludedMethods, true)) {
+ continue; // was replaced by insteadof
+ }
+
+ $usedTraitMethodDefinition = new MethodDefinition($typeName, $traitMethod);
+ $this->classHierarchy->registerMethodTraitUsage($declaringTraitMethodDefinition, $usedTraitMethodDefinition);
+ }
+
+ $this->fillTraitUsages($typeName, $this->getTraitUsages($traitName));
+ }
+ }
+
+ /**
+ * @param list $ancestorNames
+ */
+ private function fillClassHierarchy(string $typeName, array $ancestorNames): void
+ {
+ foreach ($ancestorNames as $ancestorName) {
+ $this->classHierarchy->registerClassPair($ancestorName, $typeName);
+
+ $this->fillClassHierarchy($typeName, $this->getAncestorNames($ancestorName));
+ }
+ }
+
private function isAnonymousClass(Call $call): bool
{
// https://github.com/phpstan/phpstan/issues/8410 workaround, ideally this should not be ignored
@@ -157,13 +221,17 @@ private function isAnonymousClass(Call $call): bool
*/
private function getMethodsToMarkAsUsed(Call $call): array
{
+ if (isset($this->methodsToMarkAsUsedCache[$call->toString()])) {
+ return $this->methodsToMarkAsUsedCache[$call->toString()];
+ }
+
$definition = $call->getDefinition();
$result = [$definition];
if ($call->possibleDescendantCall) {
- foreach ($this->classHierarchy->getMethodDescendants($definition) as $descendant) {
- $result[] = $descendant;
+ foreach ($this->classHierarchy->getClassDescendants($definition->className) as $descendantName) {
+ $result[] = new MethodDefinition($descendantName, $definition->methodName);
}
}
@@ -180,6 +248,8 @@ private function getMethodsToMarkAsUsed(Call $call): array
}
}
+ $this->methodsToMarkAsUsedCache[$call->toString()] = $result;
+
return $result;
}
@@ -227,10 +297,13 @@ private function isEntryPoint(MethodDefinition $methodDefinition): bool
}
}
- // @phpstan-ignore missingType.checkedException (method should exist)
- $methodReflection = $reflection
- ->getNativeReflection()
- ->getMethod($methodDefinition->methodName);
+ try {
+ $methodReflection = $reflection
+ ->getNativeReflection()
+ ->getMethod($methodDefinition->methodName);
+ } catch (ReflectionException $e) {
+ return false; // to be removed once https://github.com/Roave/BetterReflection/pull/1453 is fixed
+ }
foreach ($this->entrypointProviders as $entrypointProvider) {
if ($entrypointProvider->isEntrypoint($methodReflection)) {
@@ -242,23 +315,23 @@ private function isEntryPoint(MethodDefinition $methodDefinition): bool
}
/**
- * @param array{line: int, file: string, definition: string, overriddenDefinitions: list, traitOriginDefinition: string|null} $serializedMethodDeclaration
- * @return array{line: int, file: string, definition: MethodDefinition, overriddenDefinitions: list, traitOriginDefinition: MethodDefinition|null}
+ * @return list
+ */
+ private function getAncestorNames(string $typeName): array
+ {
+ return array_merge(
+ array_keys($this->typeDefinitions[$typeName]['parents'] ?? []),
+ array_keys($this->typeDefinitions[$typeName]['traits'] ?? []),
+ array_keys($this->typeDefinitions[$typeName]['interfaces'] ?? []),
+ );
+ }
+
+ /**
+ * @return array, aliases?: array}>
*/
- private function deserializeMethodDeclaration(array $serializedMethodDeclaration): array
+ private function getTraitUsages(string $typeName): array
{
- return [
- 'line' => $serializedMethodDeclaration['line'],
- 'file' => $serializedMethodDeclaration['file'],
- 'definition' => MethodDefinition::fromString($serializedMethodDeclaration['definition']),
- 'overriddenDefinitions' => array_map(
- static fn (string $definition) => MethodDefinition::fromString($definition),
- $serializedMethodDeclaration['overriddenDefinitions'],
- ),
- 'traitOriginDefinition' => $serializedMethodDeclaration['traitOriginDefinition'] !== null
- ? MethodDefinition::fromString($serializedMethodDeclaration['traitOriginDefinition'])
- : null,
- ];
+ return $this->typeDefinitions[$typeName]['traits'] ?? [];
}
}
diff --git a/tests/Rule/DeadMethodRuleTest.php b/tests/Rule/DeadMethodRuleTest.php
index 02d1544..ec492a7 100644
--- a/tests/Rule/DeadMethodRuleTest.php
+++ b/tests/Rule/DeadMethodRuleTest.php
@@ -14,7 +14,7 @@
use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector;
use ShipMonk\PHPStan\DeadCode\Collector\MethodCallCollector;
-use ShipMonk\PHPStan\DeadCode\Collector\MethodDefinitionCollector;
+use ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy;
use ShipMonk\PHPStan\DeadCode\Provider\DoctrineEntrypointProvider;
use ShipMonk\PHPStan\DeadCode\Provider\EntrypointProvider;
use ShipMonk\PHPStan\DeadCode\Provider\NetteEntrypointProvider;
@@ -22,7 +22,6 @@
use ShipMonk\PHPStan\DeadCode\Provider\PhpUnitEntrypointProvider;
use ShipMonk\PHPStan\DeadCode\Provider\SymfonyEntrypointProvider;
use ShipMonk\PHPStan\DeadCode\Provider\VendorEntrypointProvider;
-use ShipMonk\PHPStan\DeadCode\Reflection\ClassHierarchy;
use function is_array;
use const PHP_VERSION_ID;
@@ -55,13 +54,12 @@ protected function getCollectors(): array
{
return [
new ClassDefinitionCollector(),
- new MethodDefinitionCollector(),
new MethodCallCollector(),
];
}
/**
- * @param string|list $files
+ * @param string|non-empty-list $files
* @dataProvider provideFiles
*/
public function testDead($files, ?int $lowestPhpVersion = null): void
@@ -82,6 +80,7 @@ public static function provideFiles(): iterable
yield 'code' => [__DIR__ . '/data/DeadMethodRule/basic.php'];
yield 'ctor' => [__DIR__ . '/data/DeadMethodRule/ctor.php'];
yield 'ctor-interface' => [__DIR__ . '/data/DeadMethodRule/ctor-interface.php'];
+ yield 'abstract-1' => [__DIR__ . '/data/DeadMethodRule/abstract-1.php'];
yield 'entrypoint' => [__DIR__ . '/data/DeadMethodRule/entrypoint.php'];
yield 'first-class-callable' => [__DIR__ . '/data/DeadMethodRule/first-class-callable.php'];
yield 'overwriting-1' => [__DIR__ . '/data/DeadMethodRule/overwriting-methods-1.php'];
@@ -100,6 +99,13 @@ public static function provideFiles(): iterable
yield 'trait-9' => [__DIR__ . '/data/DeadMethodRule/traits-9.php'];
yield 'trait-10' => [__DIR__ . '/data/DeadMethodRule/traits-10.php'];
yield 'trait-11' => [[__DIR__ . '/data/DeadMethodRule/traits-11-a.php', __DIR__ . '/data/DeadMethodRule/traits-11-b.php']];
+ yield 'trait-12' => [__DIR__ . '/data/DeadMethodRule/traits-12.php'];
+ yield 'trait-13' => [__DIR__ . '/data/DeadMethodRule/traits-13.php'];
+ yield 'trait-14' => [__DIR__ . '/data/DeadMethodRule/traits-14.php'];
+ yield 'trait-15' => [__DIR__ . '/data/DeadMethodRule/traits-15.php'];
+ yield 'trait-16' => [__DIR__ . '/data/DeadMethodRule/traits-16.php'];
+ yield 'trait-17' => [__DIR__ . '/data/DeadMethodRule/traits-17.php'];
+ yield 'trait-18' => [__DIR__ . '/data/DeadMethodRule/traits-18.php'];
yield 'nullsafe' => [__DIR__ . '/data/DeadMethodRule/nullsafe.php'];
yield 'dead-in-parent-1' => [__DIR__ . '/data/DeadMethodRule/dead-in-parent-1.php'];
yield 'indirect-interface' => [__DIR__ . '/data/DeadMethodRule/indirect-interface.php'];
@@ -107,6 +113,8 @@ public static function provideFiles(): iterable
yield 'parent-call-2' => [__DIR__ . '/data/DeadMethodRule/parent-call-2.php'];
yield 'parent-call-3' => [__DIR__ . '/data/DeadMethodRule/parent-call-3.php'];
yield 'parent-call-4' => [__DIR__ . '/data/DeadMethodRule/parent-call-4.php'];
+ yield 'parent-call-5' => [__DIR__ . '/data/DeadMethodRule/parent-call-5.php'];
+ yield 'parent-call-6' => [__DIR__ . '/data/DeadMethodRule/parent-call-6.php'];
yield 'attribute' => [__DIR__ . '/data/DeadMethodRule/attribute.php'];
yield 'dynamic-method' => [__DIR__ . '/data/DeadMethodRule/dynamic-method.php'];
yield 'call-on-class-string' => [__DIR__ . '/data/DeadMethodRule/class-string.php'];
diff --git a/tests/Rule/RuleTestCase.php b/tests/Rule/RuleTestCase.php
index bf65517..9dd37dd 100644
--- a/tests/Rule/RuleTestCase.php
+++ b/tests/Rule/RuleTestCase.php
@@ -29,7 +29,7 @@ abstract class RuleTestCase extends OriginalRuleTestCase
{
/**
- * @param list $files
+ * @param non-empty-list $files
*/
protected function analyseFiles(array $files, bool $autofix = false): void
{
@@ -45,20 +45,17 @@ protected function analyseFiles(array $files, bool $autofix = false): void
self::fail('Autofixed. This setup should never remain in the codebase.');
}
- if ($analyserErrors === []) {
- $this->expectNotToPerformAssertions();
- }
-
$actualErrorsByFile = $this->processActualErrors($analyserErrors);
- foreach ($actualErrorsByFile as $file => $actualErrors) {
+ foreach ($files as $file) {
+ $actualErrors = $actualErrorsByFile[$file] ?? [];
$expectedErrors = $this->parseExpectedErrors($file);
$extraErrors = array_diff($expectedErrors, $actualErrors);
$missingErrors = array_diff($actualErrors, $expectedErrors);
- $extraErrorsString = $extraErrors === [] ? '' : "\n - Extra errors: " . implode("\n", $extraErrors);
- $missingErrorsString = $missingErrors === [] ? '' : "\n - Missing errors: " . implode("\n", $missingErrors);
+ $extraErrorsString = $extraErrors === [] ? '' : "\nExtra errors:\n" . implode("\n", $extraErrors);
+ $missingErrorsString = $missingErrors === [] ? '' : "\nMissing errors:\n" . implode("\n", $missingErrors);
self::assertSame(
implode("\n", $expectedErrors) . "\n",
diff --git a/tests/Rule/data/DeadMethodRule/abstract-1.php b/tests/Rule/data/DeadMethodRule/abstract-1.php
new file mode 100644
index 0000000..e4533c6
--- /dev/null
+++ b/tests/Rule/data/DeadMethodRule/abstract-1.php
@@ -0,0 +1,22 @@
+method();
diff --git a/tests/Rule/data/DeadMethodRule/parent-call-6.php b/tests/Rule/data/DeadMethodRule/parent-call-6.php
new file mode 100644
index 0000000..9448189
--- /dev/null
+++ b/tests/Rule/data/DeadMethodRule/parent-call-6.php
@@ -0,0 +1,23 @@
+method();
+
diff --git a/tests/Rule/data/DeadMethodRule/traits-13.php b/tests/Rule/data/DeadMethodRule/traits-13.php
new file mode 100644
index 0000000..a50127a
--- /dev/null
+++ b/tests/Rule/data/DeadMethodRule/traits-13.php
@@ -0,0 +1,7 @@
+method();
+
diff --git a/tests/Rule/data/DeadMethodRule/traits-15.php b/tests/Rule/data/DeadMethodRule/traits-15.php
new file mode 100644
index 0000000..adc21e8
--- /dev/null
+++ b/tests/Rule/data/DeadMethodRule/traits-15.php
@@ -0,0 +1,22 @@
+method1(); // is valid call
+$o->method2();
+$o->aliased3();
diff --git a/tests/Rule/data/DeadMethodRule/traits-16.php b/tests/Rule/data/DeadMethodRule/traits-16.php
new file mode 100644
index 0000000..1c3775d
--- /dev/null
+++ b/tests/Rule/data/DeadMethodRule/traits-16.php
@@ -0,0 +1,16 @@
+aliased();
diff --git a/tests/Rule/data/DeadMethodRule/traits-17.php b/tests/Rule/data/DeadMethodRule/traits-17.php
new file mode 100644
index 0000000..5a567a9
--- /dev/null
+++ b/tests/Rule/data/DeadMethodRule/traits-17.php
@@ -0,0 +1,24 @@
+collission1();
+$o->collission2();
diff --git a/tests/Rule/data/DeadMethodRule/traits-18.php b/tests/Rule/data/DeadMethodRule/traits-18.php
new file mode 100644
index 0000000..64a3275
--- /dev/null
+++ b/tests/Rule/data/DeadMethodRule/traits-18.php
@@ -0,0 +1,25 @@
+alias1();
+$o->alias3();