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();