Skip to content

Commit

Permalink
Use AST to collect class hierarchy definitions (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal authored Sep 11, 2024
1 parent 4068ca8 commit 86608ff
Show file tree
Hide file tree
Showing 21 changed files with 530 additions and 274 deletions.
4 changes: 3 additions & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,9 @@
<rule ref="SlevomatCodingStandard.ControlStructures.AssignmentInCondition"/>
<rule ref="SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing"/>
<rule ref="SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch"/>
<rule ref="SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn"/>
<rule ref="SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn">
<exclude name="SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn.UselessIfCondition"/>
</rule>
<rule ref="SlevomatCodingStandard.ControlStructures.UselessTernaryOperator"/>
<rule ref="SlevomatCodingStandard.ControlStructures.EarlyExit">
<exclude name="SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed"/> <!-- leads to less readable code in some cases -->
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ parameters:
analyseAndScan:
- tests/*/data/*
tmpDir: cache/phpstan/
internalErrorsCountLimit: 1
checkMissingCallableSignature: true
checkUninitializedProperties: true
checkBenevolentUnionTypes: true
Expand Down
7 changes: 1 addition & 6 deletions rules.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
-
class: ShipMonk\PHPStan\DeadCode\Reflection\ClassHierarchy
class: ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy

-
class: ShipMonk\PHPStan\DeadCode\Provider\VendorEntrypointProvider
Expand Down Expand Up @@ -47,11 +47,6 @@ services:
tags:
- phpstan.collector

-
class: ShipMonk\PHPStan\DeadCode\Collector\MethodDefinitionCollector
tags:
- phpstan.collector

-
class: ShipMonk\PHPStan\DeadCode\Rule\DeadMethodRule
arguments:
Expand Down
185 changes: 173 additions & 12 deletions src/Collector/ClassDefinitionCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<InClassNode, array<string, string>>
* @implements Collector<ClassLike, array{
* kind: string,
* name: string,
* methods: array<string, array{line: int}>,
* parents: array<string, null>,
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
* interfaces: array<string, null>,
* }>
*/
class ClassDefinitionCollector implements Collector
{

public function getNodeType(): string
{
return InClassNode::class;
return ClassLike::class;
}

/**
* @param InClassNode $node
* @return array<string, string>
* @param ClassLike $node
* @return array{
* kind: string,
* name: string,
* methods: array<string, array{line: int}>,
* parents: array<string, null>,
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
* interfaces: array<string, null>,
* }|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<string, null>
*/
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<string, null>
*/
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<string, array{excluded?: list<string>, aliases?: array<string, string>}>
*/
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');
}

}
Loading

0 comments on commit 86608ff

Please sign in to comment.