Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unused interface, abstract class and trait detection #71

Open
vincentchalamon opened this issue Sep 6, 2024 · 2 comments
Open

Unused interface, abstract class and trait detection #71

vincentchalamon opened this issue Sep 6, 2024 · 2 comments

Comments

@vincentchalamon
Copy link

vincentchalamon commented Sep 6, 2024

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.

@janedbal
Copy link
Member

janedbal commented Sep 6, 2024

I have a big list of features to implement in future, and "dead class analysis" is one of them. You just mentioned the easiest part of it, but the problem is more complex in general.

Unused traits are already implemented in native PHPStan. The rest will be much easier once I finalize internal structures refactoring.

Maybe I'll consider adding this easy part after my refactoring is done. Thanks.

@janedbal
Copy link
Member

I just realized we cannot reliably tell that some interface is unused just by checking that it is never implemented / extended in another interface because it can contain used constant. So until we implement dead constants analysis, we cannot implement this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants