Skip to content

Unused interface, abstract class and trait detection #71

@vincentchalamon

Description

@vincentchalamon

I recently worked on a rule to detect unused interfaces, abstract classes and traits from the source code, highly inspired from your plugin (using collectors).

In case you might be interested, here is a shot:

namespace App\Utils\PHPStan\Rules;

use App\Utils\PHPStan\Collector\DeadCode\ClassCollector;
use App\Utils\PHPStan\Collector\DeadCode\PossiblyUnusedClassCollector;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\CollectedDataNode;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
 * Checks unimplemented classes.
 */
final class DeadClassRule implements Rule
{
    /**
     * @var array<string, IdentifierRuleError>
     */
    private array $errors = [];

    #[\Override]
    public function getNodeType(): string
    {
        return CollectedDataNode::class;
    }

    /**
     * @param CollectedDataNode $node
     */
    #[\Override]
    public function processNode(Node $node, Scope $scope): array
    {
        if ($node->isOnlyFilesAnalysis()) {
            return [];
        }

        $classDeclarationData = $node->get(ClassCollector::class);
        $possiblyUnusedClasses = array_flip(array_map(static fn (array $values): string => $values[0], $node->get(PossiblyUnusedClassCollector::class)));

        foreach ($classDeclarationData as $classesInFile) {
            foreach ($classesInFile as $classPairs) {
                foreach ($classPairs as $ancestor => $descendant) {
                    if (\array_key_exists($ancestor, $possiblyUnusedClasses)) {
                        // ancestor is used, remove it from collection
                        unset($possiblyUnusedClasses[$ancestor]);
                    }
                }
            }
        }

        foreach ($possiblyUnusedClasses as $className => $file) {
            $this->errors[$className] = RuleErrorBuilder::message("Unused {$className}")
                ->file($file)
                ->line((new \ReflectionClass($className))->getStartLine())
                ->identifier('shipmonk.deadMethod')
                ->build();
        }

        return array_values($this->errors);
    }
}

#########################################

namespace App\Utils\PHPStan\Collector\DeadCode;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Enum_;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;

/**
 * @implements Collector<ClassLike, string>
 */
class PossiblyUnusedClassCollector implements Collector
{
    #[\Override]
    public function getNodeType(): string
    {
        return ClassLike::class;
    }

    /**
     * @param ClassLike $node
     */
    #[\Override]
    public function processNode(Node $node, Scope $scope): ?string
    {
        // can't determine if a class is unused due to framework (except for abstract classes)
        if ($node instanceof Class_ && !$node->isAbstract()) {
            return null;
        }

        // can't determine if an enum is unused
        if ($node instanceof Enum_) {
            return null;
        }

        // node should be an interface, a trait or an abstract class
        return $node->namespacedName?->toString();
    }
}

#########################################

namespace App\Utils\PHPStan\Collector\DeadCode;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Node\InClassNode;

/**
 * @implements Collector<InClassNode, <string, string>>
 */
class ClassCollector implements Collector
{
    #[\Override]
    public function getNodeType(): string
    {
        return InClassNode::class;
    }

    /**
     * @param InClassNode $node
     *
     * @return array<string, string>
     */
    #[\Override]
    public function processNode(Node $node, Scope $scope): array
    {
        $pairs = [];
        $origin = $node->getClassReflection();

        foreach ($origin->getAncestors() as $ancestor) {
            // ignore self
            if ($ancestor === $origin) {
                continue;
            }

            // ignore ancestors from PHP global namespace
            if ($ancestor->isInternal()) {
                continue;
            }

            // ignore ancestors from vendor
            if (str_contains((string) $ancestor->getFileName(), '/vendor/')) {
                continue;
            }

            $pairs[$ancestor->getName()] = $ancestor->getFileName();
        }

        return $pairs;
    }
}

I'm also wondering if I missed a use-case which could lead to an error or a false-alert. Any opinion about it?

Of course, if you're interested about it, I would be happy to contribute or help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions