Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,34 @@ parameters:

<br>

### PreferInlineTraitRule

A trait with a handful of lines used in a single service is better inlined into the class. It empowers IDE, Rector and PHPStan to understand the code. This rule spots traits used only a few times across the project, so you can inline them:

```php
trait NameTrait
{
public function getName(): string
{
return $this->name;
}
}
```

:x:

<br>

The rule reports traits used at most `maxUsage` times (2 by default). It is disabled by default. Enable it with the `inlineTrait` parameter:

```yaml
parameters:
symplify:
inlineTrait: true
```

<br>

### ParamNameToTypeConventionRule

By convention, we can define parameter type by its name. If we know the "userId" is always an `int`, PHPStan can warn us about it and let us know to fill the type.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"config/doctrine-rules.neon",
"config/phpunit-rules.neon",
"config/ctor-rules.neon",
"config/inline-trait-rules.neon",
"config/mock-rules.neon",
"config/phpstan-extensions.neon"
]
Expand Down
22 changes: 22 additions & 0 deletions config/inline-trait-rules.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
services:
-
class: Symplify\PHPStanRules\Collector\TraitDefinitionCollector
tags:
- phpstan.collector
arguments:
isEnabled: %symplify.inlineTrait%

-
class: Symplify\PHPStanRules\Collector\TraitUsageCollector
tags:
- phpstan.collector
arguments:
isEnabled: %symplify.inlineTrait%

-
class: Symplify\PHPStanRules\Rules\PreferInlineTraitRule
tags:
- phpstan.rules.rule
arguments:
maxUsage: 2
isEnabled: %symplify.inlineTrait%
2 changes: 2 additions & 0 deletions config/phpstan-extensions.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ parameters:
phpunit: true
mocks: false
ctor: false
inlineTrait: false
symfonyReturnType: false
laravelReturnType: false
pathStrings: false
Expand All @@ -31,6 +32,7 @@ parametersSchema:
phpunit: bool()
mocks: bool()
ctor: bool()
inlineTrait: bool()
symfonyReturnType: bool()
laravelReturnType: bool()
pathStrings: bool()
Expand Down
53 changes: 53 additions & 0 deletions src/Collector/TraitDefinitionCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Collector;

use PhpParser\Node;
use PhpParser\Node\Stmt\Trait_;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;

/**
* Collect every trait definition with its FQN, file, line and line count,
* so the rule can decide which traits are used too rarely to justify existence.
*
* @implements Collector<Trait_, array{traitName: string, file: string, line: int, lineCount: int}>
*/
final readonly class TraitDefinitionCollector implements Collector
{
public function __construct(
private bool $isEnabled
) {
}

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

/**
* @param Trait_ $node
* @return array{traitName: string, file: string, line: int, lineCount: int}|null
*/
public function processNode(Node $node, Scope $scope): ?array
{
// enable with "inlineTrait: true" parameter
if (! $this->isEnabled) {
return null;
}

$traitName = $node->namespacedName?->toString() ?? $node->name?->toString();
if ($traitName === null) {
return null;
}

return [
'traitName' => $traitName,
'file' => $scope->getFile(),
'line' => $node->getStartLine(),
'lineCount' => $node->getEndLine() - $node->getStartLine() + 1,
];
}
}
48 changes: 48 additions & 0 deletions src/Collector/TraitUsageCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Collector;

use PhpParser\Node;
use PhpParser\Node\Stmt\TraitUse;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;

/**
* Collect every "use SomeTrait;" occurrence as a trait FQN,
* so the rule can count how many times each trait is used across the project.
*
* @implements Collector<TraitUse, string[]>
*/
final readonly class TraitUsageCollector implements Collector
{
public function __construct(
private bool $isEnabled
) {
}

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

/**
* @param TraitUse $node
* @return string[]|null
*/
public function processNode(Node $node, Scope $scope): ?array
{
// enable with "inlineTrait: true" parameter
if (! $this->isEnabled) {
return null;
}

$traitNames = [];
foreach ($node->traits as $traitName) {
$traitNames[] = $traitName->toString();
}

return $traitNames;
}
}
2 changes: 2 additions & 0 deletions src/Enum/RuleIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,6 @@ final class RuleIdentifier
public const string NO_SERVICE_JUGGLING = 'symplify.noServiceJuggling';

public const string NO_NULLABLE_SERVICE_IN_CONSTRUCTOR = 'symplify.noNullableServiceInConstructor';

public const string PREFER_INLINE_TRAIT = 'symplify.preferInlineTrait';
}
99 changes: 99 additions & 0 deletions src/Rules/PreferInlineTraitRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Rules;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\CollectedDataNode;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Symplify\PHPStanRules\Collector\TraitDefinitionCollector;
use Symplify\PHPStanRules\Collector\TraitUsageCollector;
use Symplify\PHPStanRules\Enum\RuleIdentifier;

/**
* @see TraitDefinitionCollector
* @see TraitUsageCollector
* @see \Symplify\PHPStanRules\Tests\Rules\PreferInlineTraitRule\PreferInlineTraitRuleTest
*
* @implements Rule<CollectedDataNode>
*/
final readonly class PreferInlineTraitRule implements Rule
{
public const string ERROR_MESSAGE = 'Trait "%s" (%d lines) is used only %d-time(s).%sInline it into its user(s) to empower IDE, Rector and PHPStan';

public function __construct(
private int $maxUsage,
private bool $isEnabled
) {
}

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

/**
* @param CollectedDataNode $node
* @return IdentifierRuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
// enable with "inlineTrait: true" parameter
if (! $this->isEnabled) {
return [];
}

$usageCountByTraitName = $this->countUsagesByTraitName($node);

$ruleErrors = [];

foreach ($node->get(TraitDefinitionCollector::class) as $traitDefinitions) {
foreach ($traitDefinitions as $traitDefinition) {
$traitName = $traitDefinition['traitName'];
$usageCount = $usageCountByTraitName[$traitName] ?? 0;

if ($usageCount > $this->maxUsage) {
continue;
}

$errorMessage = sprintf(
self::ERROR_MESSAGE,
$traitName,
$traitDefinition['lineCount'],
$usageCount,
PHP_EOL
);

$ruleErrors[] = RuleErrorBuilder::message($errorMessage)
->identifier(RuleIdentifier::PREFER_INLINE_TRAIT)
->file($traitDefinition['file'])
->line($traitDefinition['line'])
->build();
}
}

return $ruleErrors;
}

/**
* @return array<string, int>
*/
private function countUsagesByTraitName(CollectedDataNode $collectedDataNode): array
{
$usageCountByTraitName = [];

foreach ($collectedDataNode->get(TraitUsageCollector::class) as $traitUsagesInFiles) {
foreach ($traitUsagesInFiles as $traitUsageInFile) {
foreach ($traitUsageInFile as $traitName) {
$usageCountByTraitName[$traitName] = ($usageCountByTraitName[$traitName] ?? 0) + 1;
}
}
}

return $usageCountByTraitName;
}
}
12 changes: 12 additions & 0 deletions tests/Rules/PreferInlineTraitRule/Fixture/InlineCandidateTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\PreferInlineTraitRule\Fixture;

trait InlineCandidateTrait
{
public function someMethod(): void
{
}
}
12 changes: 12 additions & 0 deletions tests/Rules/PreferInlineTraitRule/Fixture/PopularTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\PreferInlineTraitRule\Fixture;

trait PopularTrait
{
public function popularMethod(): void
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\PreferInlineTraitRule\Fixture;

final class PopularTraitFirstUser
{
use PopularTrait;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\PreferInlineTraitRule\Fixture;

final class PopularTraitSecondUser
{
use PopularTrait;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\PreferInlineTraitRule\Fixture;

final class PopularTraitThirdUser
{
use PopularTrait;
}
10 changes: 10 additions & 0 deletions tests/Rules/PreferInlineTraitRule/Fixture/SingleTraitUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Symplify\PHPStanRules\Tests\Rules\PreferInlineTraitRule\Fixture;

final class SingleTraitUser
{
use InlineCandidateTrait;
}
Loading
Loading