Skip to content

Commit c8faf2e

Browse files
authored
Switch to single "analyze" command (#8)
* remove stats command, no pratical use * misc intro analyse command
1 parent 092b044 commit c8faf2e

23 files changed

+533
-324
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ composer require behastan/behastan --dev
1919
Some definitions have very similar masks, but even identical contents. Better use a one definitions with exact mask, to make your tests more precise and easier to maintain:
2020

2121
```bash
22-
vendor/bin/behastan duplicated-definitions tests
22+
vendor/bin/behastan analyze
2323
```
2424

2525

phpstan.neon

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,3 @@ parameters:
1111
excludePaths:
1212
- */Fixture/*
1313
- */Source/*
14-
15-
ignoreErrors:

src/Analyzer/ClassMethodContextDefinitionsAnalyzer.php renamed to src/Analyzer/ContextDefinitionsAnalyzer.php

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,45 +11,54 @@
1111
use PhpParser\PrettyPrinter\Standard;
1212
use Rector\Behastan\PhpParser\SimplePhpParser;
1313
use Rector\Behastan\Resolver\ClassMethodMasksResolver;
14-
use Rector\Behastan\ValueObject\ClassMethodContextDefinition;
14+
use Rector\Behastan\ValueObject\ContextDefinition;
1515
use Symfony\Component\Finder\SplFileInfo;
1616

17-
final readonly class ClassMethodContextDefinitionsAnalyzer
17+
final class ContextDefinitionsAnalyzer
1818
{
19+
/**
20+
* @var array<string, array<string, ContextDefinition[]>>
21+
*/
22+
private array $contextDefinitionsByContentHash = [];
23+
1924
public function __construct(
20-
private SimplePhpParser $simplePhpParser,
21-
private NodeFinder $nodeFinder,
22-
private Standard $printerStandard,
23-
private ClassMethodMasksResolver $classMethodMasksResolver,
25+
private readonly SimplePhpParser $simplePhpParser,
26+
private readonly NodeFinder $nodeFinder,
27+
private readonly Standard $printerStandard,
28+
private readonly ClassMethodMasksResolver $classMethodMasksResolver,
2429
) {
2530
}
2631

2732
/**
2833
* @param SplFileInfo[] $contextFileInfos
29-
* @return ClassMethodContextDefinition[]
34+
* @return ContextDefinition[]
3035
*/
3136
public function resolve(array $contextFileInfos): array
3237
{
33-
$classMethodContextDefinitionByClassMethodHash = $this->resolveAndGroupByContentHash($contextFileInfos);
34-
35-
$classMethodContextDefinitions = [];
36-
foreach ($classMethodContextDefinitionByClassMethodHash as $classMethodContextDefinition) {
37-
$classMethodContextDefinitions = array_merge(
38-
$classMethodContextDefinitions,
39-
$classMethodContextDefinition
40-
);
38+
$contextDefinitionByClassMethodHash = $this->resolveAndGroupByContentHash($contextFileInfos);
39+
40+
$allContextDefinitions = [];
41+
foreach ($contextDefinitionByClassMethodHash as $contextDefinition) {
42+
$allContextDefinitions = array_merge($allContextDefinitions, $contextDefinition);
4143
}
4244

43-
return $classMethodContextDefinitions;
45+
return $allContextDefinitions;
4446
}
4547

4648
/**
4749
* @param SplFileInfo[] $contextFileInfos
48-
* @return array<string, ClassMethodContextDefinition[]>
50+
* @return array<string, ContextDefinition[]>
4951
*/
5052
public function resolveAndGroupByContentHash(array $contextFileInfos): array
5153
{
52-
$classMethodContextDefinitionByClassMethodHash = [];
54+
// re-use cached result if already done
55+
$cacheKey = sha1((string) json_encode($contextFileInfos));
56+
57+
if (isset($this->contextDefinitionsByContentHash[$cacheKey])) {
58+
return $this->contextDefinitionsByContentHash[$cacheKey];
59+
}
60+
61+
$contextDefinitionByContentsHash = [];
5362

5463
foreach ($contextFileInfos as $contextFileInfo) {
5564
$contextClassStmts = $this->simplePhpParser->parseFilePath($contextFileInfo->getRealPath());
@@ -58,6 +67,7 @@ public function resolveAndGroupByContentHash(array $contextFileInfos): array
5867
if (! $class instanceof Class_) {
5968
continue;
6069
}
70+
6171
if (! $class->namespacedName instanceof Name) {
6272
continue;
6373
}
@@ -68,19 +78,20 @@ public function resolveAndGroupByContentHash(array $contextFileInfos): array
6878
if (! $classMethod->isPublic()) {
6979
continue;
7080
}
81+
7182
if ($classMethod->isMagic()) {
7283
continue;
7384
}
74-
$classMethodHash = $this->createClassMethodHash($classMethod);
7585

86+
$classMethodHash = $this->createClassMethodHash($classMethod);
7687
$rawMasks = $this->classMethodMasksResolver->resolve($classMethod);
7788

7889
// no masks :(
7990
if ($rawMasks === []) {
8091
continue;
8192
}
8293

83-
$classMethodContextDefinition = new ClassMethodContextDefinition(
94+
$contextDefinition = new ContextDefinition(
8495
$contextFileInfo->getRealPath(),
8596
$className,
8697
$classMethod->name->toString(),
@@ -89,11 +100,13 @@ public function resolveAndGroupByContentHash(array $contextFileInfos): array
89100
$classMethod->getStartLine()
90101
);
91102

92-
$classMethodContextDefinitionByClassMethodHash[$classMethodHash][] = $classMethodContextDefinition;
103+
$contextDefinitionByContentsHash[$classMethodHash][] = $contextDefinition;
93104
}
94105
}
95106

96-
return $classMethodContextDefinitionByClassMethodHash;
107+
$this->contextDefinitionsByContentHash[$cacheKey] = $contextDefinitionByContentsHash;
108+
109+
return $contextDefinitionByContentsHash;
97110
}
98111

99112
private function createClassMethodHash(ClassMethod $classMethod): string

src/Analyzer/UnusedDefinitionsAnalyzer.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
namespace Rector\Behastan\Analyzer;
66

77
use Nette\Utils\Strings;
8-
use Rector\Behastan\DefinitionMasksResolver;
8+
use Rector\Behastan\DefinitionMasksExtractor;
99
use Rector\Behastan\Reporting\MaskCollectionStatsPrinter;
1010
use Rector\Behastan\UsedInstructionResolver;
1111
use Rector\Behastan\ValueObject\Mask\AbstractMask;
1212
use Rector\Behastan\ValueObject\Mask\ExactMask;
1313
use Rector\Behastan\ValueObject\Mask\NamedMask;
1414
use Rector\Behastan\ValueObject\Mask\RegexMask;
1515
use Rector\Behastan\ValueObject\Mask\SkippedMask;
16+
use Rector\Behastan\ValueObject\MaskCollection;
1617
use Symfony\Component\Console\Style\SymfonyStyle;
1718
use Symfony\Component\Finder\SplFileInfo;
1819
use Webmozart\Assert\Assert;
@@ -29,8 +30,8 @@
2930

3031
public function __construct(
3132
private SymfonyStyle $symfonyStyle,
32-
private DefinitionMasksResolver $definitionMasksResolver,
3333
private UsedInstructionResolver $usedInstructionResolver,
34+
private DefinitionMasksExtractor $definitionMasksExtractor,
3435
private MaskCollectionStatsPrinter $maskCollectionStatsPrinter,
3536
) {
3637
}
@@ -41,7 +42,7 @@ public function __construct(
4142
*
4243
* @return AbstractMask[]
4344
*/
44-
public function analyse(array $contextFiles, array $featureFiles): array
45+
public function analyse(array $contextFiles, array $featureFiles, MaskCollection $maskCollection): array
4546
{
4647
Assert::allIsInstanceOf($contextFiles, SplFileInfo::class);
4748
foreach ($contextFiles as $contextFile) {
@@ -53,9 +54,8 @@ public function analyse(array $contextFiles, array $featureFiles): array
5354
Assert::endsWith($featureFile->getFilename(), '.feature');
5455
}
5556

56-
$maskCollection = $this->definitionMasksResolver->resolve($contextFiles);
57-
58-
$this->maskCollectionStatsPrinter->printStats($maskCollection);
57+
$maskCollection = $this->definitionMasksExtractor->extract($contextFiles);
58+
$this->maskCollectionStatsPrinter->print($maskCollection);
5959

6060
$featureInstructions = $this->usedInstructionResolver->resolveInstructionsFromFeatureFiles($featureFiles);
6161
$maskProgressBar = $this->symfonyStyle->createProgressBar($maskCollection->count());

src/Command/AnalyzeCommand.php

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Behastan\Command;
6+
7+
use Rector\Behastan\Contract\RuleInterface;
8+
use Rector\Behastan\DefinitionMasksExtractor;
9+
use Rector\Behastan\Enum\Option;
10+
use Rector\Behastan\Finder\BehatMetafilesFinder;
11+
use Rector\Behastan\Reporting\MaskCollectionStatsPrinter;
12+
use Rector\Behastan\ValueObject\RuleError;
13+
use Symfony\Component\Console\Command\Command;
14+
use Symfony\Component\Console\Input\InputArgument;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Input\InputOption;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Console\Style\SymfonyStyle;
19+
use Webmozart\Assert\Assert;
20+
21+
final class AnalyzeCommand extends Command
22+
{
23+
/**
24+
* @param RuleInterface[] $rules
25+
*/
26+
public function __construct(
27+
private readonly SymfonyStyle $symfonyStyle,
28+
private readonly DefinitionMasksExtractor $definitionMasksExtractor,
29+
private readonly MaskCollectionStatsPrinter $maskCollectionStatsPrinter,
30+
private readonly array $rules
31+
) {
32+
parent::__construct();
33+
34+
Assert::allObject($rules);
35+
Assert::allIsInstanceOf($rules, RuleInterface::class);
36+
Assert::notEmpty($rules);
37+
Assert::greaterThan(count($rules), 2);
38+
}
39+
40+
protected function configure(): void
41+
{
42+
$this->setName('analyze');
43+
$this->setDescription('Run complete static analysis on Behat definitions and features');
44+
45+
$this->addArgument(
46+
Option::PROJECT_DIRECTORY,
47+
InputArgument::OPTIONAL,
48+
'Project directory (we find *.Context.php definition files and *.feature script files there)',
49+
getcwd()
50+
);
51+
52+
$this->addOption(
53+
'skip',
54+
null,
55+
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
56+
'Skip a rule by identifier'
57+
);
58+
}
59+
60+
protected function execute(InputInterface $input, OutputInterface $output): int
61+
{
62+
$testDirectory = $input->getArgument(Option::PROJECT_DIRECTORY);
63+
Assert::directory($testDirectory);
64+
65+
$contextFileInfos = BehatMetafilesFinder::findContextFiles([$testDirectory]);
66+
if ($contextFileInfos === []) {
67+
$this->symfonyStyle->error(sprintf(
68+
'No *.Context files found in "%s". Please provide correct test directory',
69+
$testDirectory
70+
));
71+
return self::FAILURE;
72+
}
73+
74+
$featureFileInfos = BehatMetafilesFinder::findFeatureFiles([$testDirectory]);
75+
if ($featureFileInfos === []) {
76+
$this->symfonyStyle->error(sprintf(
77+
'No *.feature files found in "%s". Please provide correct test directory',
78+
$testDirectory
79+
));
80+
return self::FAILURE;
81+
}
82+
83+
$this->symfonyStyle->writeln(sprintf(
84+
'<fg=green>Found %d Context and %d feature files</>',
85+
count($contextFileInfos),
86+
count($featureFileInfos)
87+
));
88+
$this->symfonyStyle->writeln('<fg=yellow>Extracting definitions masks...</>');
89+
90+
$maskCollection = $this->definitionMasksExtractor->extract($contextFileInfos);
91+
$this->symfonyStyle->newLine();
92+
93+
$this->maskCollectionStatsPrinter->print($maskCollection);
94+
95+
$this->symfonyStyle->newLine();
96+
97+
// @todo skip by "--skip" option
98+
99+
$this->symfonyStyle->writeln('<fg=yellow>Running analysis...</>');
100+
101+
/** @var RuleError[] $allRuleErrors */
102+
$allRuleErrors = [];
103+
foreach ($this->rules as $rule) {
104+
$ruleErrors = $rule->process($contextFileInfos, $featureFileInfos, $maskCollection, $testDirectory);
105+
$allRuleErrors = array_merge($allRuleErrors, $ruleErrors);
106+
}
107+
108+
if ($allRuleErrors === []) {
109+
$this->symfonyStyle->success('No errors found. Good job!');
110+
111+
return self::SUCCESS;
112+
}
113+
114+
$this->symfonyStyle->newLine(2);
115+
116+
$i = 1;
117+
foreach ($allRuleErrors as $allRuleError) {
118+
$this->symfonyStyle->writeln(sprintf('<fg=yellow>%d) %s</>', $i, $allRuleError->getMessage()));
119+
foreach ($allRuleError->getLineFilePaths() as $lineFilePath) {
120+
// compared to listing() this allow to make paths clickable in IDE
121+
$this->symfonyStyle->writeln($lineFilePath);
122+
}
123+
124+
$this->symfonyStyle->newLine(2);
125+
126+
++$i;
127+
}
128+
129+
$this->symfonyStyle->newLine();
130+
$this->symfonyStyle->error(sprintf(
131+
'Found %d error%s',
132+
count($allRuleErrors),
133+
count($allRuleErrors) > 1 ? 's' : ''
134+
));
135+
136+
return self::FAILURE;
137+
}
138+
}

0 commit comments

Comments
 (0)