Skip to content

Commit

Permalink
Add RelativeUnusedPublicClassMethodRule (#111)
Browse files Browse the repository at this point in the history
* misc

* remove deprecated twig_template_paths

* Add RelativeUnusedPublicClassMethodRule
  • Loading branch information
TomasVotruba authored Apr 12, 2024
1 parent 8e3fd00 commit 8d4fc84
Show file tree
Hide file tree
Showing 13 changed files with 391 additions and 65 deletions.
37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<br>

It's easy to find unused private class elements, because they're not used in the class itself:
It's easy to find unused private class elements, because they're not used in the class itself. But what about public methods/properties/constants?

```diff
final class Book
Expand All @@ -18,29 +18,24 @@ It's easy to find unused private class elements, because they're not used in the
// ...
}

- private function getSubtitle(): string
- public function getSubtitle(): string
- {
- // ...
- }
}
```

But what about public class elements?
**How can we detect unused public element?**

<br>

**How can we detect such element?**

* find a e.g. public method
* find all public method calls
* compare those in simple diff
* find a public method
* find all public method calls in code and templates
* if the public method is not found, it probably unused

That's exactly what this package does.

<br>

This technique is very useful for private projects and to detect accidentally open public API that should be used only locally.
This technique is very useful for private projects and to detect accidentally used `public` modifier that should be changed to `private` as called locally only.

<br>

Expand All @@ -50,7 +45,7 @@ This technique is very useful for private projects and to detect accidentally op
composer require tomasvotruba/unused-public --dev
```

The package is available on PHP 7.2-8.1 versions in tagged releases.
The package is available for PHP 7.2+ version.

<br>

Expand All @@ -71,6 +66,24 @@ parameters:
<br>
Do you have hundreds of reported public method? You don't have time to check them all, but want to handle them gradually?
Set maximum allowed % configuration instead:
```yaml
# phpstan.neon
parameters:
unused_public:
methods: 2.5
```
This means maximum 2.5 % of all public methods is allowed as unused:
* If it's 5 %, you'll be alerted.
* If it's 1 %, it will be skipped as tolerated.
<br>
Do you want to check local-only method calls that should not be removed, but be turned into `private`/`protected` instead?

```yaml
Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
"require-dev": {
"phpstan/extension-installer": "^1.3",
"tracy/tracy": "^2.10",
"symplify/easy-coding-standard": "^12.0",
"symplify/easy-coding-standard": "^12.1",
"rector/rector": "^1.0",
"phpunit/phpunit": "^10.5",
"tomasvotruba/class-leak": "^0.2",
"tomasvotruba/class-leak": "^0.2.11",
"tomasvotruba/cognitive-complexity": "^0.2.2",
"tomasvotruba/type-coverage": "^0.2",
"symplify/easy-ci": "^11.2",
"symplify/easy-ci": "^12.0",
"nikic/php-parser": "^4.19"
},
"autoload": {
Expand Down
7 changes: 3 additions & 4 deletions config/extension.neon
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
parametersSchema:
unused_public: structure([
methods: bool()
methods: anyOf(bool(), int(), float())
local_methods: bool()
properties: bool()
constants: bool()
template_paths: listOf(string())
twig_template_paths: listOf(string())
])

# default parameters
Expand All @@ -16,8 +15,6 @@ parameters:
properties: true
constants: true
template_paths: []
# deprecated
twig_template_paths: []

services:
- TomasVotruba\UnusedPublic\PublicClassMethodMatcher
Expand All @@ -28,6 +25,7 @@ services:
- TomasVotruba\UnusedPublic\ApiDocStmtAnalyzer
- TomasVotruba\UnusedPublic\ClassMethodCallReferenceResolver
- TomasVotruba\UnusedPublic\CollectorMapper\MethodCallCollectorMapper
- TomasVotruba\UnusedPublic\NodeCollectorExtractor
# templates
- TomasVotruba\UnusedPublic\Templates\TemplateMethodCallsProvider
- TomasVotruba\UnusedPublic\Templates\TemplateRegexFinder
Expand Down Expand Up @@ -109,3 +107,4 @@ rules:
- TomasVotruba\UnusedPublic\Rules\UnusedPublicClassConstRule
- TomasVotruba\UnusedPublic\Rules\UnusedPublicPropertyRule
- TomasVotruba\UnusedPublic\Rules\LocalOnlyPublicClassMethodRule
- TomasVotruba\UnusedPublic\Rules\RelativeUnusedPublicClassMethodRule
20 changes: 18 additions & 2 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,23 @@ public function __construct(

public function isUnusedMethodEnabled(): bool
{
return $this->parameters['methods'] ?? false;
$methods = $this->parameters['methods'] ?? false;
if (! is_bool($methods)) {
return false;
}

return $methods;
}

public function isUnusedRelativeMethodEnabled(): bool
{
$methods = $this->parameters['methods'] ?? false;
return is_numeric($methods);
}

public function getMaximumRelativeUnusedPublicMethod(): float|int
{
return $this->parameters['methods'] ?? 0;
}

public function shouldCollectMethods(): bool
Expand Down Expand Up @@ -53,7 +69,7 @@ public function isUnusedConstantsEnabled(): bool
*/
public function getTemplatePaths(): array
{
$templatePaths = $this->parameters['template_paths'] ?? $this->parameters['twig_template_paths'];
$templatePaths = $this->parameters['template_paths'];

Assert::allDirectory($templatePaths);
Assert::allFileExists($templatePaths);
Expand Down
54 changes: 54 additions & 0 deletions src/NodeCollectorExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace TomasVotruba\UnusedPublic;

use PHPStan\Node\CollectedDataNode;
use TomasVotruba\UnusedPublic\CollectorMapper\MethodCallCollectorMapper;
use TomasVotruba\UnusedPublic\Collectors\Callable_\AttributeCallableCollector;
use TomasVotruba\UnusedPublic\Collectors\Callable_\CallUserFuncCollector;
use TomasVotruba\UnusedPublic\Collectors\MethodCall\MethodCallableCollector;
use TomasVotruba\UnusedPublic\Collectors\MethodCall\MethodCallCollector;
use TomasVotruba\UnusedPublic\Collectors\StaticCall\StaticMethodCallableCollector;
use TomasVotruba\UnusedPublic\Collectors\StaticCall\StaticMethodCallCollector;
use TomasVotruba\UnusedPublic\ValueObject\LocalAndExternalMethodCallReferences;

final readonly class NodeCollectorExtractor
{
public function __construct(
private MethodCallCollectorMapper $methodCallCollectorMapper
) {
}

public function extractLocalAndExternalMethodCallReferences(
CollectedDataNode $collectedDataNode
): LocalAndExternalMethodCallReferences {
$collectedDatas = $this->extractCollectedDatas($collectedDataNode);
return $this->methodCallCollectorMapper->mapToLocalAndExternal($collectedDatas);
}

/**
* @return string[]
*/
public function extractMethodCallReferences(CollectedDataNode $collectedDataNode): array
{
$collectedDatas = $this->extractCollectedDatas($collectedDataNode);
return $this->methodCallCollectorMapper->mapToMethodCallReferences($collectedDatas);
}

/**
* @return mixed[]
*/
private function extractCollectedDatas(CollectedDataNode $collectedDataNode): array
{
return [
$collectedDataNode->get(MethodCallCollector::class),
$collectedDataNode->get(MethodCallableCollector::class),
$collectedDataNode->get(StaticMethodCallCollector::class),
$collectedDataNode->get(StaticMethodCallableCollector::class),
$collectedDataNode->get(AttributeCallableCollector::class),
$collectedDataNode->get(CallUserFuncCollector::class),
];
}
}
31 changes: 10 additions & 21 deletions src/Rules/LocalOnlyPublicClassMethodRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,13 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use TomasVotruba\UnusedPublic\CollectorMapper\MethodCallCollectorMapper;
use TomasVotruba\UnusedPublic\Collectors\Callable_\AttributeCallableCollector;
use TomasVotruba\UnusedPublic\Collectors\Callable_\CallUserFuncCollector;
use TomasVotruba\UnusedPublic\Collectors\MethodCall\MethodCallableCollector;
use TomasVotruba\UnusedPublic\Collectors\MethodCall\MethodCallCollector;
use TomasVotruba\UnusedPublic\Collectors\PublicClassMethodCollector;
use TomasVotruba\UnusedPublic\Collectors\StaticCall\StaticMethodCallableCollector;
use TomasVotruba\UnusedPublic\Collectors\StaticCall\StaticMethodCallCollector;
use TomasVotruba\UnusedPublic\Configuration;
use TomasVotruba\UnusedPublic\Enum\RuleTips;
use TomasVotruba\UnusedPublic\NodeCollectorExtractor;
use TomasVotruba\UnusedPublic\Templates\TemplateMethodCallsProvider;
use TomasVotruba\UnusedPublic\Templates\UsedMethodAnalyzer;
use TomasVotruba\UnusedPublic\Utils\Strings;

/**
* @see \TomasVotruba\UnusedPublic\Tests\Rules\LocalOnlyPublicClassMethodRule\LocalOnlyPublicClassMethodRuleTest
Expand All @@ -39,7 +34,7 @@ public function __construct(
private Configuration $configuration,
private UsedMethodAnalyzer $usedMethodAnalyzer,
private TemplateMethodCallsProvider $templateMethodCallsProvider,
private MethodCallCollectorMapper $methodCallCollectorMapper
private NodeCollectorExtractor $nodeCollectorExtractor,
) {
}

Expand All @@ -60,28 +55,22 @@ public function processNode(Node $node, Scope $scope): array

$twigMethodNames = $this->templateMethodCallsProvider->provideTwigMethodCalls();

$localAndExternalMethodCallReferences = $this->methodCallCollectorMapper->mapToLocalAndExternal([
$node->get(MethodCallCollector::class),
$node->get(MethodCallableCollector::class),
$node->get(StaticMethodCallCollector::class),
$node->get(StaticMethodCallableCollector::class),
$node->get(AttributeCallableCollector::class),
$node->get(CallUserFuncCollector::class),
]);
$localAndExternalMethodCallReferences = $this->nodeCollectorExtractor->extractLocalAndExternalMethodCallReferences(
$node
);

$publicClassMethodCollector = $node->get(PublicClassMethodCollector::class);
// php method calls are case-insensitive
$lowerExternalRefs = array_map(
static fn (string $item): string => strtolower($item),
$lowerExternalRefs = Strings::lowercase(
$localAndExternalMethodCallReferences->getExternalMethodCallReferences()
);
$lowerLocalRefs = array_map(
static fn (string $item): string => strtolower($item),

$lowerLocalRefs = Strings::lowercase(
$localAndExternalMethodCallReferences->getLocalMethodCallReferences()
);

$ruleErrors = [];

$publicClassMethodCollector = $node->get(PublicClassMethodCollector::class);
foreach ($publicClassMethodCollector as $filePath => $declarations) {
foreach ($declarations as [$className, $methodName, $line]) {
if (! $this->isUsedOnlyLocally(
Expand Down
Loading

0 comments on commit 8d4fc84

Please sign in to comment.