Skip to content

Unused interface, abstract class and trait detection #71

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions