Skip to content

Commit c4598bc

Browse files
authored
improve trait spotting to use single object (#77)
1 parent a4a9a89 commit c4598bc

File tree

9 files changed

+231
-133
lines changed

9 files changed

+231
-133
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,27 @@ vendor/bin/swiss-knife generate-symfony-config-builders
271271
272272
That way IDE, PHPStan after adding those paths and Rector can understand your config files better.
273273
274+
<br>
275+
276+
## 11. Spots Fake Traits
277+
278+
What is trait has 5 lines and used in single service? We know it's better to e inlined, to empower IDE, Rector and PHPStan. But don't have time to worry about these details.
279+
280+
We made a command to automate this process and spot the traits most likely to be inlined:
281+
282+
```bash
283+
vendor/bin/swiss-knife spot-lazy-traits src
284+
```
285+
286+
By default, the commands look for traits used max 2-times. To change that:
287+
288+
```bash
289+
vendor/bin/swiss-knife spot-lazy-traits src --max-used 4
290+
```
291+
292+
That's it! Run this command once upon a time or run it in CI to eliminate traits with low value to exists. Your code will be more robuts and easier to work with.
293+
294+
274295
<br>
275296
276297
Happy coding!
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\SwissKnife\Command;
6+
7+
use Rector\SwissKnife\Traits\TraitSpotter;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Input\InputArgument;
10+
use Symfony\Component\Console\Input\InputInterface;
11+
use Symfony\Component\Console\Input\InputOption;
12+
use Symfony\Component\Console\Output\OutputInterface;
13+
use Symfony\Component\Console\Style\SymfonyStyle;
14+
15+
final class SpotLazyTraitsCommand extends Command
16+
{
17+
public function __construct(
18+
private readonly SymfonyStyle $symfonyStyle,
19+
private readonly TraitSpotter $traitSpotter,
20+
) {
21+
parent::__construct();
22+
}
23+
24+
protected function configure(): void
25+
{
26+
$this->setName('spot-lazy-traits');
27+
28+
$this->addArgument(
29+
'sources',
30+
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
31+
'One or more paths to check'
32+
);
33+
34+
$this->addOption('max-used', null, InputOption::VALUE_REQUIRED, 'Maximum count the trait is used', 2);
35+
36+
$this->setDescription(
37+
'Spot traits that are use only once, to potentially inline them and make code more robust and readable'
38+
);
39+
}
40+
41+
protected function execute(InputInterface $input, OutputInterface $output): int
42+
{
43+
$sources = (array) $input->getArgument('sources');
44+
$maxUsedCount = (int) $input->getOption('max-used');
45+
46+
$this->symfonyStyle->title('Looking for trait definitions');
47+
$traitSpottingResult = $this->traitSpotter->analyse($sources);
48+
49+
if ($traitSpottingResult->getTraitCount() === 0) {
50+
$this->symfonyStyle->success('No traits were found in your project, nothing to worry about');
51+
return self::SUCCESS;
52+
}
53+
54+
$this->symfonyStyle->writeln(
55+
sprintf(
56+
'Found %d trait%s in the whole project',
57+
$traitSpottingResult->getTraitCount(),
58+
$traitSpottingResult->getTraitCount() === 1 ? '' : 's'
59+
)
60+
);
61+
$this->symfonyStyle->listing($traitSpottingResult->getTraitFilePaths());
62+
63+
$this->symfonyStyle->newLine();
64+
65+
$this->symfonyStyle->title(sprintf('Looking for traits used less than %d-times', $maxUsedCount));
66+
67+
$leastUsedTraitsMetadatas = $traitSpottingResult->getTraitMaximumUsedTimes($maxUsedCount);
68+
69+
foreach ($leastUsedTraitsMetadatas as $leastUsedTraitMetadata) {
70+
$this->symfonyStyle->writeln(sprintf(
71+
'Trait "%s" (%d lines) is used only in %d file%s',
72+
$leastUsedTraitMetadata->getShortTraitName(),
73+
$leastUsedTraitMetadata->getLineCount(),
74+
$leastUsedTraitMetadata->getUsedInCount(),
75+
$leastUsedTraitMetadata->getUsedInCount() === 1 ? '' : 's'
76+
));
77+
78+
$this->symfonyStyle->listing($leastUsedTraitMetadata->getUsedIn());
79+
$this->symfonyStyle->newLine();
80+
}
81+
82+
$this->symfonyStyle->warning(sprintf(
83+
'Inline these traits or refactor them to a service if meaningful.%sChange "--max-used" to different number to get more result',
84+
PHP_EOL
85+
));
86+
87+
return self::SUCCESS;
88+
}
89+
}

src/Command/TraitSpottingCommand.php

Lines changed: 0 additions & 75 deletions
This file was deleted.

src/Finder/TraitFilesFinder.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Rector\SwissKnife\Finder;
66

7+
use Nette\Utils\Strings;
78
use Symfony\Component\Finder\Finder;
89
use Symfony\Component\Finder\SplFileInfo;
910
use Webmozart\Assert\Assert;
@@ -48,7 +49,7 @@ public function find(array $directories): array
4849
->sortByName()
4950
->filter(function (SplFileInfo $fileInfo): bool {
5051
$fileContent = $fileInfo->getContents();
51-
return str_contains($fileContent, 'trait ');
52+
return (bool) Strings::match($fileContent, '#^trait\s#m');
5253
});
5354

5455
return iterator_to_array($traitFinder->getIterator());

src/Traits/TraitSpotter.php

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
use Nette\Utils\Strings;
88
use Rector\SwissKnife\Finder\TraitFilesFinder;
9+
use Rector\SwissKnife\ValueObject\Traits\TraitMetadata;
910
use Rector\SwissKnife\ValueObject\Traits\TraitSpottingResult;
10-
use Symfony\Component\Finder\SplFileInfo;
1111

1212
/**
1313
* @see \Rector\SwissKnife\Tests\Traits\TraitSpotterTest
@@ -26,33 +26,46 @@ public function analyse(array $directories): TraitSpottingResult
2626
{
2727
$traitFiles = $this->traitFilesFinder->find($directories);
2828

29-
$shortNameToLineCount = [];
29+
$traitsMetadatas = [];
30+
3031
foreach ($traitFiles as $traitFile) {
3132
$traitShortName = $traitFile->getBasename('.php');
32-
$shortNameToLineCount[$traitShortName] = substr_count($traitFile->getContents(), PHP_EOL);
33-
}
3433

35-
$shortTraitNamesToLineCount = $shortNameToLineCount;
34+
$traitsMetadatas[] = new TraitMetadata($traitFile->getRealPath(), $traitShortName);
35+
}
3636

3737
$traitUsageFiles = $this->traitFilesFinder->findTraitUsages($directories);
3838

39-
$usagesToFiles = [];
4039
foreach ($traitUsageFiles as $traitUsageFile) {
41-
$matches = Strings::matchAll($traitUsageFile->getContents(), '# use (?<short_trait_name>[\w]+);#');
40+
$matches = Strings::matchAll($traitUsageFile->getContents(), '#^ use (?<short_trait_name>[\w]+);#m');
4241

4342
foreach ($matches as $match) {
4443
$shortTraitName = $match['short_trait_name'];
45-
$usagesToFiles[$shortTraitName][] = $this->getRelativeFilePath($traitUsageFile);
44+
45+
// fuzzy in exchange for speed
46+
$currentTraitMetadata = $this->matchTraitByShortName($traitsMetadatas, $shortTraitName);
47+
if (! $currentTraitMetadata instanceof TraitMetadata) {
48+
continue;
49+
}
50+
51+
$currentTraitMetadata->markUsedIn($traitUsageFile->getRealPath());
4652
}
4753
}
4854

49-
$traitUsagesToFiles = $usagesToFiles;
50-
51-
return new TraitSpottingResult($shortTraitNamesToLineCount, $traitUsagesToFiles);
55+
return new TraitSpottingResult($traitsMetadatas);
5256
}
5357

54-
private function getRelativeFilePath(SplFileInfo $fileInfo): string
58+
/**
59+
* @param TraitMetadata[] $traitsMetadatas
60+
*/
61+
private function matchTraitByShortName(array $traitsMetadatas, string $shortTraitName): ?TraitMetadata
5562
{
56-
return substr($fileInfo->getRealPath(), strlen((string) getcwd()) + 1);
63+
foreach ($traitsMetadatas as $traitMetadata) {
64+
if ($traitMetadata->getShortTraitName() === $shortTraitName) {
65+
return $traitMetadata;
66+
}
67+
}
68+
69+
return null;
5770
}
5871
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\SwissKnife\ValueObject\Traits;
6+
7+
use Nette\Utils\FileSystem;
8+
9+
final class TraitMetadata
10+
{
11+
private int $lineCount;
12+
13+
/**
14+
* @var string[]
15+
*/
16+
private array $usedIn = [];
17+
18+
public function __construct(
19+
private readonly string $filePath,
20+
private string $shortTraitName
21+
) {
22+
$this->lineCount = substr_count(FileSystem::read($filePath), PHP_EOL);
23+
}
24+
25+
public function getShortTraitName(): string
26+
{
27+
return $this->shortTraitName;
28+
}
29+
30+
public function getFilePath(): string
31+
{
32+
return $this->filePath;
33+
}
34+
35+
public function markUsedIn(string $filePath): void
36+
{
37+
$this->usedIn[] = $filePath;
38+
}
39+
40+
/**
41+
* @return string[]
42+
*/
43+
public function getUsedIn(): array
44+
{
45+
return $this->usedIn;
46+
}
47+
48+
public function getUsedInCount(): int
49+
{
50+
return count($this->usedIn);
51+
}
52+
53+
public function getLineCount(): int
54+
{
55+
return $this->lineCount;
56+
}
57+
}

0 commit comments

Comments
 (0)