Skip to content

Commit 501d565

Browse files
committed
Add BehastanTest
1 parent c122450 commit 501d565

File tree

7 files changed

+214
-90
lines changed

7 files changed

+214
-90
lines changed

src/Behastan/Behastan.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\SwissKnife\Behastan;
6+
7+
use Nette\Utils\Strings;
8+
use Rector\SwissKnife\Behastan\ValueObject\AbstractMask;
9+
use Rector\SwissKnife\Behastan\ValueObject\ExactMask;
10+
use Rector\SwissKnife\Behastan\ValueObject\MaskCollection;
11+
use Rector\SwissKnife\Behastan\ValueObject\NamedMask;
12+
use Rector\SwissKnife\Behastan\ValueObject\RegexMask;
13+
use Rector\SwissKnife\Behastan\ValueObject\SkippedMask;
14+
use Symfony\Component\Console\Style\SymfonyStyle;
15+
use Symfony\Component\Finder\SplFileInfo;
16+
17+
/**
18+
* @see \Rector\SwissKnife\Tests\Behastan\Behastan\BehastanTest
19+
*/
20+
final class Behastan
21+
{
22+
public function __construct(
23+
private readonly SymfonyStyle $symfonyStyle,
24+
private readonly DefinitionMasksResolver $definitionMasksResolver,
25+
private readonly UsedInstructionResolver $usedInstructionResolver
26+
) {
27+
}
28+
29+
/**
30+
* @param SplFileInfo[] $contextFiles
31+
* @param SplFileInfo[] $featureFiles
32+
*
33+
* @return AbstractMask[]
34+
*/
35+
public function analyse(array $contextFiles, array $featureFiles): array
36+
{
37+
$maskCollection = $this->definitionMasksResolver->resolve($contextFiles);
38+
$this->printStats($maskCollection);
39+
40+
$featureInstructions = $this->usedInstructionResolver->resolveInstructionsFromFeatureFiles($featureFiles);
41+
42+
$maskProgressBar = $this->symfonyStyle->createProgressBar($maskCollection->count());
43+
44+
$unusedMasks = [];
45+
foreach ($maskCollection->all() as $mask) {
46+
$maskProgressBar->advance();
47+
48+
if ($this->isMaskUsed($mask, $featureInstructions)) {
49+
continue;
50+
}
51+
52+
$unusedMasks[] = $mask;
53+
}
54+
55+
$maskProgressBar->finish();
56+
57+
return $unusedMasks;
58+
}
59+
60+
private function printStats(MaskCollection $maskCollection): void
61+
{
62+
$this->symfonyStyle->writeln(sprintf('Found %d masks:', $maskCollection->count()));
63+
$this->symfonyStyle->newLine();
64+
65+
$this->symfonyStyle->writeln(sprintf(' * %d exact', $maskCollection->countByType(ExactMask::class)));
66+
$this->symfonyStyle->writeln(sprintf(' * %d /regex/', $maskCollection->countByType(RegexMask::class)));
67+
$this->symfonyStyle->writeln(sprintf(' * %d :named', $maskCollection->countByType(NamedMask::class)));
68+
$this->symfonyStyle->writeln(sprintf(' * %d skipped', $maskCollection->countByType(SkippedMask::class)));
69+
70+
$skippedMasks = $maskCollection->byType(SkippedMask::class);
71+
if ($skippedMasks !== []) {
72+
$this->symfonyStyle->newLine();
73+
74+
foreach ($skippedMasks as $skippedMask) {
75+
$this->printMask($skippedMask);
76+
}
77+
78+
$this->symfonyStyle->newLine();
79+
}
80+
}
81+
82+
/**
83+
* @param string[] $featureInstructions
84+
*/
85+
private function isRegexDefinitionUsed(string $regexBehatDefinition, array $featureInstructions): bool
86+
{
87+
foreach ($featureInstructions as $featureInstruction) {
88+
if (Strings::match($featureInstruction, $regexBehatDefinition)) {
89+
// it is used!
90+
return true;
91+
}
92+
}
93+
94+
return false;
95+
}
96+
97+
private function printMask(AbstractMask $unusedMask): void
98+
{
99+
$this->symfonyStyle->writeln($unusedMask->mask);
100+
101+
// make path relative
102+
$relativeFilePath = str_replace(getcwd() . '/', '', $unusedMask->filePath);
103+
$this->symfonyStyle->writeln($relativeFilePath);
104+
$this->symfonyStyle->newLine();
105+
}
106+
107+
/**
108+
* @param string[] $featureInstructions
109+
*/
110+
private function isMaskUsed(AbstractMask $mask, array $featureInstructions): bool
111+
{
112+
if ($mask instanceof SkippedMask) {
113+
return true;
114+
}
115+
116+
// is used?
117+
if ($mask instanceof ExactMask && in_array($mask->mask, $featureInstructions, true)) {
118+
return true;
119+
}
120+
121+
// is used?
122+
if ($mask instanceof RegexMask && $this->isRegexDefinitionUsed($mask->mask, $featureInstructions)) {
123+
return true;
124+
}
125+
126+
if ($mask instanceof NamedMask) {
127+
// normalize :mask definition to regex
128+
$regexMask = '#' . Strings::replace($mask->mask, '#(\:[\W\w]+)#', '(.*?)') . '#';
129+
if ($this->isRegexDefinitionUsed($regexMask, $featureInstructions)) {
130+
return true;
131+
}
132+
}
133+
134+
return false;
135+
}
136+
}

src/Behastan/Command/BehastanCommand.php

Lines changed: 3 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,9 @@
44

55
namespace Rector\SwissKnife\Behastan\Command;
66

7-
use Nette\Utils\Strings;
8-
use Rector\SwissKnife\Behastan\DefinitionMasksResolver;
7+
use Rector\SwissKnife\Behastan\Behastan;
98
use Rector\SwissKnife\Behastan\Finder\BehatMetafilesFinder;
10-
use Rector\SwissKnife\Behastan\UsedInstructionResolver;
119
use Rector\SwissKnife\Behastan\ValueObject\AbstractMask;
12-
use Rector\SwissKnife\Behastan\ValueObject\ExactMask;
13-
use Rector\SwissKnife\Behastan\ValueObject\MaskCollection;
14-
use Rector\SwissKnife\Behastan\ValueObject\NamedMask;
15-
use Rector\SwissKnife\Behastan\ValueObject\RegexMask;
16-
use Rector\SwissKnife\Behastan\ValueObject\SkippedMask;
1710
use Symfony\Component\Console\Command\Command;
1811
use Symfony\Component\Console\Input\InputArgument;
1912
use Symfony\Component\Console\Input\InputInterface;
@@ -26,8 +19,7 @@ final class BehastanCommand extends Command
2619
public function __construct(
2720
private readonly SymfonyStyle $symfonyStyle,
2821
private readonly BehatMetafilesFinder $behatMetafilesFinder,
29-
private readonly DefinitionMasksResolver $definitionMasksResolver,
30-
private readonly UsedInstructionResolver $usedInstructionResolver,
22+
private readonly Behastan $behastan,
3123
) {
3224
parent::__construct();
3325
}
@@ -66,45 +58,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6658
sprintf('Checking static, named and regex masks from %d *Feature files', count($featureFiles))
6759
);
6860

69-
$maskCollection = $this->definitionMasksResolver->resolve($contextFiles);
70-
$this->printStats($maskCollection);
61+
$unusedMasks = $this->behastan->analyse($contextFiles, $featureFiles);
7162

72-
$featureInstructions = $this->usedInstructionResolver->resolveInstructionsFromFeatureFiles($featureFiles);
73-
74-
$maskProgressBar = $this->symfonyStyle->createProgressBar($maskCollection->count());
75-
76-
$unusedMasks = [];
77-
foreach ($maskCollection->all() as $mask) {
78-
$maskProgressBar->advance();
79-
80-
if ($mask instanceof SkippedMask) {
81-
continue;
82-
}
83-
84-
// is used?
85-
if ($mask instanceof ExactMask && in_array($mask->mask, $featureInstructions, true)) {
86-
continue;
87-
}
88-
89-
// is used?
90-
if ($mask instanceof RegexMask && $this->isRegexDefinitionUsed($mask->mask, $featureInstructions)) {
91-
continue;
92-
}
93-
94-
if ($mask instanceof NamedMask) {
95-
// normalize :mask definition to regex
96-
$regexMask = '#' . Strings::replace($mask->mask, '#(\:[\W\w]+)#', '(.*?)') . '#';
97-
if ($this->isRegexDefinitionUsed($regexMask, $featureInstructions)) {
98-
continue;
99-
}
100-
}
101-
102-
if ($mask instanceof AbstractMask) {
103-
$unusedMasks[] = $mask;
104-
}
105-
}
106-
107-
$maskProgressBar->finish();
10863
$this->symfonyStyle->newLine(2);
10964

11065
if ($unusedMasks === []) {
@@ -117,21 +72,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
11772
return Command::FAILURE;
11873
}
11974

120-
/**
121-
* @param string[] $featureInstructions
122-
*/
123-
private function isRegexDefinitionUsed(string $regexBehatDefinition, array $featureInstructions): bool
124-
{
125-
foreach ($featureInstructions as $featureInstruction) {
126-
if (Strings::match($featureInstruction, $regexBehatDefinition)) {
127-
// it is used!
128-
return true;
129-
}
130-
}
131-
132-
return false;
133-
}
134-
13575
/**
13676
* @param AbstractMask[] $unusedMasks
13777
*/
@@ -144,28 +84,6 @@ private function reportUnusedDefinitions(array $unusedMasks): void
14484
$this->symfonyStyle->error(sprintf('Found %d unused definitions', count($unusedMasks)));
14585
}
14686

147-
private function printStats(MaskCollection $maskCollection): void
148-
{
149-
$this->symfonyStyle->writeln(sprintf('Found %d masks:', $maskCollection->count()));
150-
$this->symfonyStyle->newLine();
151-
152-
$this->symfonyStyle->writeln(sprintf(' * %d exact', $maskCollection->countByType(ExactMask::class)));
153-
$this->symfonyStyle->writeln(sprintf(' * %d /regex/', $maskCollection->countByType(RegexMask::class)));
154-
$this->symfonyStyle->writeln(sprintf(' * %d :named', $maskCollection->countByType(NamedMask::class)));
155-
$this->symfonyStyle->writeln(sprintf(' * %d skipped', $maskCollection->countByType(SkippedMask::class)));
156-
157-
$skippedMasks = $maskCollection->byType(SkippedMask::class);
158-
if ($skippedMasks !== []) {
159-
$this->symfonyStyle->newLine();
160-
161-
foreach ($skippedMasks as $skippedMask) {
162-
$this->printMask($skippedMask);
163-
}
164-
165-
$this->symfonyStyle->newLine();
166-
}
167-
}
168-
16987
private function printMask(AbstractMask $unusedMask): void
17088
{
17189
$this->symfonyStyle->writeln($unusedMask->mask);

src/Behastan/UsedInstructionResolver.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public function resolveInstructionsFromFeatureFiles(array $featureFileInfos): ar
2626
$featureFileInfo->getContents(),
2727
'#\s+(Given|When|And|Then)\s+(?<instruction>.*?)\n#m'
2828
);
29+
2930
if ($matches === []) {
3031
// there should be at least one instruction in each feature file
3132
throw new RuntimeException(sprintf(

src/Behastan/ValueObject/MaskCollection.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44

55
namespace Rector\SwissKnife\Behastan\ValueObject;
66

7-
use Rector\SwissKnife\Behastan\Contract\MaskInterface;
8-
97
final class MaskCollection
108
{
119
/**
12-
* @param MaskInterface[] $masks
10+
* @param AbstractMask[] $masks
1311
*/
1412
public function __construct(
1513
private readonly array $masks
@@ -31,7 +29,7 @@ public function count(): int
3129
}
3230

3331
/**
34-
* @return MaskInterface[]
32+
* @return AbstractMask[]
3533
*/
3634
public function all(): array
3735
{
@@ -46,6 +44,6 @@ public function all(): array
4644
*/
4745
public function byType(string $type): array
4846
{
49-
return array_filter($this->masks, fn (MaskInterface $mask): bool => $mask instanceof $type);
47+
return array_filter($this->masks, fn (AbstractMask $mask): bool => $mask instanceof $type);
5048
}
5149
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace Rector\SwissKnife\Tests\Behastan\Behastan;
4+
5+
use Rector\SwissKnife\Behastan\Behastan;
6+
use Rector\SwissKnife\Behastan\Finder\BehatMetafilesFinder;
7+
use Rector\SwissKnife\Behastan\ValueObject\AbstractMask;
8+
use Rector\SwissKnife\Tests\AbstractTestCase;
9+
use Symfony\Component\Console\Output\Output;
10+
use Symfony\Component\Console\Style\SymfonyStyle;
11+
12+
final class BehastanTest extends AbstractTestCase
13+
{
14+
private Behastan $behastan;
15+
16+
private BehatMetafilesFinder $behatMetafilesFinder;
17+
18+
protected function setUp(): void
19+
{
20+
parent::setUp();
21+
22+
$this->behastan = $this->make(Behastan::class);
23+
$this->behatMetafilesFinder = $this->make(BehatMetafilesFinder::class);
24+
25+
// silence output in tests
26+
$symfonyStyle = $this->make(SymfonyStyle::class);
27+
$symfonyStyle->setVerbosity(Output::VERBOSITY_QUIET);
28+
}
29+
30+
public function test(): void
31+
{
32+
$featureFiles = $this->behatMetafilesFinder->findFeatureFiles([__DIR__ . '/Fixture']);
33+
$contextFiles = $this->behatMetafilesFinder->findContextFiles([__DIR__ . '/Fixture']);
34+
35+
$this->assertCount(1, $featureFiles);
36+
$this->assertCount(1, $contextFiles);
37+
38+
$unusedMasks = $this->behastan->analyse($contextFiles, $featureFiles);
39+
$this->assertCount(1, $unusedMasks);
40+
$this->assertContainsOnlyInstancesOf(AbstractMask::class, $unusedMasks);
41+
42+
/** @var AbstractMask $unusedMask */
43+
$unusedMask = $unusedMasks[0];
44+
$this->assertSame(__DIR__ . '/Fixture/BehatContext.php', $unusedMask->filePath);
45+
$this->assertSame('never used', $unusedMask->mask);
46+
}
47+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\SwissKnife\Tests\Behastan\Behastan\Fixture;
6+
7+
final class BehatContext
8+
{
9+
/**
10+
* @When I click homepage
11+
*/
12+
public function action(): void
13+
{
14+
}
15+
16+
/**
17+
* @Then never used
18+
*/
19+
public function deadAction(): void
20+
{
21+
}
22+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Scenario: Find unused method
2+
When I click homepage

0 commit comments

Comments
 (0)