Skip to content

Commit 0a1beed

Browse files
committed
misc
1 parent 684b3c3 commit 0a1beed

File tree

5 files changed

+275
-84
lines changed

5 files changed

+275
-84
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Behastan\Command;
6+
7+
use Behastan\Finder\BehatMetafilesFinder;
8+
use Behastan\PhpParser\SimplePhpParser;
9+
use Behastan\Resolver\ClassMethodMasksResolver;
10+
use Behastan\ValueObject\ClassMethodContextDefinition;
11+
use PhpParser\Node\Name;
12+
use PhpParser\Node\Stmt\Class_;
13+
use PhpParser\Node\Stmt\ClassMethod;
14+
use PhpParser\NodeFinder;
15+
use PhpParser\PrettyPrinter\Standard;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Style\SymfonyStyle;
21+
use Webmozart\Assert\Assert;
22+
23+
final class DuplicatedDefinitionsCommand extends Command
24+
{
25+
public function __construct(
26+
private readonly SymfonyStyle $symfonyStyle,
27+
private readonly BehatMetafilesFinder $behatMetafilesFinder,
28+
private readonly SimplePhpParser $simplePhpParser,
29+
private readonly NodeFinder $nodeFinder,
30+
private readonly Standard $printerStandard,
31+
private readonly ClassMethodMasksResolver $classMethodMasksResolver,
32+
) {
33+
parent::__construct();
34+
}
35+
36+
protected function configure(): void
37+
{
38+
$this->setName('duplicated-definitions');
39+
40+
$this->setDescription(
41+
'Find duplicated definitions in *Context.php, use just one to keep definitions clear and to the point'
42+
);
43+
44+
$this->addArgument(
45+
'test-directory',
46+
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
47+
'One or more paths to check or *.Context.php and feature.yml files'
48+
);
49+
}
50+
51+
protected function execute(InputInterface $input, OutputInterface $output): int
52+
{
53+
$testDirectories = (array) $input->getArgument('test-directory');
54+
Assert::allDirectory($testDirectories);
55+
56+
$contextFileInfos = $this->behatMetafilesFinder->findContextFiles($testDirectories);
57+
58+
if ($contextFileInfos === []) {
59+
$this->symfonyStyle->error('No *.Context files found. Please provide correct test directory');
60+
return self::FAILURE;
61+
}
62+
63+
$classMethodContextDefinitionByClassMethodHash = [];
64+
65+
foreach ($contextFileInfos as $contextFileInfo) {
66+
$contextClassStmts = $this->simplePhpParser->parseFilePath($contextFileInfo->getRealPath());
67+
68+
$class = $this->nodeFinder->findFirstInstanceOf($contextClassStmts, Class_::class);
69+
if (! $class instanceof Class_ || ! $class->namespacedName instanceof Name) {
70+
continue;
71+
}
72+
73+
$className = $class->namespacedName->toString();
74+
75+
foreach ($class->getMethods() as $classMethod) {
76+
if (! $classMethod->isPublic() || $classMethod->isMagic()) {
77+
continue;
78+
}
79+
80+
$classMethodHash = $this->createClassMethodHash($classMethod);
81+
82+
$rawMasks = $this->classMethodMasksResolver->resolve($classMethod);
83+
84+
// no masks :(
85+
if (count($rawMasks) === 0) {
86+
continue;
87+
}
88+
89+
$classMethodContextDefinition = new ClassMethodContextDefinition(
90+
$contextFileInfo->getRealPath(),
91+
$className,
92+
$classMethod->name->toString(),
93+
// @todo what about multiple masks?
94+
$rawMasks[0],
95+
$classMethod->getStartLine()
96+
);
97+
98+
$classMethodContextDefinitionByClassMethodHash[$classMethodHash][] = $classMethodContextDefinition;
99+
}
100+
}
101+
102+
// keep only duplicated
103+
foreach ($classMethodContextDefinitionByClassMethodHash as $hash => $classAndMethods) {
104+
if (count($classAndMethods) < 2) {
105+
unset($classMethodContextDefinitionByClassMethodHash[$hash]);
106+
}
107+
108+
}
109+
110+
foreach ($classMethodContextDefinitionByClassMethodHash as $classAndMethods) {
111+
$this->symfonyStyle->warning('Found duplicated class classMethod contents');
112+
113+
foreach ($classAndMethods as $classMethodContextDefinition) {
114+
/** @var ClassMethodContextDefinition $classMethodContextDefinition */
115+
$this->symfonyStyle->writeln(
116+
' * ' . $classMethodContextDefinition->getClass() . '::' . $classMethodContextDefinition->getMethodName()
117+
);
118+
$this->symfonyStyle->writeln(
119+
'in ' . $classMethodContextDefinition->getFilePath() . ':' . $classMethodContextDefinition->getMethodLine()
120+
);
121+
122+
$this->symfonyStyle->writeln('Mask: ' . $classMethodContextDefinition->getMask());
123+
$this->symfonyStyle->newLine();
124+
}
125+
}
126+
127+
// $this->symfonyStyle->error(sprintf('Found %d duplicated class classMethod contents', count($classMethodContextDefinitionByClassMethodHash)));
128+
129+
return Command::FAILURE;
130+
}
131+
132+
private function createClassMethodHash(ClassMethod $classMethod): string
133+
{
134+
$printedClassMethod = $this->printerStandard->prettyPrint((array) $classMethod->stmts);
135+
return sha1($printedClassMethod);
136+
}
137+
}

src/DefinitionMasksResolver.php

Lines changed: 21 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,27 @@
44

55
namespace Behastan;
66

7+
use Behastan\PhpParser\SimplePhpParser;
8+
use Behastan\Resolver\ClassMethodMasksResolver;
79
use Behastan\ValueObject\ClassMethodContextDefinition;
810
use Behastan\ValueObject\Mask\ExactMask;
911
use Behastan\ValueObject\Mask\NamedMask;
1012
use Behastan\ValueObject\Mask\RegexMask;
1113
use Behastan\ValueObject\Mask\SkippedMask;
1214
use Behastan\ValueObject\MaskCollection;
13-
use PhpParser\Comment\Doc;
1415
use PhpParser\Node\Name;
15-
use PhpParser\Node\Scalar\String_;
16-
use PhpParser\Node\Stmt;
1716
use PhpParser\Node\Stmt\Class_;
1817
use PhpParser\NodeFinder;
19-
use PhpParser\NodeTraverser;
20-
use PhpParser\NodeVisitor\NameResolver;
21-
use PhpParser\ParserFactory;
2218
use SplFileInfo;
2319

2420
final class DefinitionMasksResolver
2521
{
26-
/**
27-
* @var string
28-
*/
29-
private const INSTRUCTION_DOCBLOCK_REGEX = '#\@(Given|Then|When)\s+(?<instruction>.*?)\n#m';
30-
31-
/**
32-
* @var string[]
33-
*/
34-
private const ATTRIBUTE_NAMES = ['Behat\Step\Then', 'Behat\Step\Given', 'Behat\Step\And'];
22+
public function __construct(
23+
private readonly SimplePhpParser $simplePhpParser,
24+
private readonly NodeFinder $nodeFinder,
25+
private readonly ClassMethodMasksResolver $classMethodMasksResolver,
26+
) {
27+
}
3528

3629
/**
3730
* @param SplFileInfo[] $contextFiles
@@ -94,97 +87,42 @@ public function resolve(array $contextFiles): MaskCollection
9487

9588
/**
9689
* @param SplFileInfo[] $fileInfos
97-
*
9890
* @return ClassMethodContextDefinition[]
9991
*/
10092
private function resolveMasksFromFiles(array $fileInfos): array
10193
{
10294
$classMethodContextDefinitions = [];
10395

104-
$parserFactory = new ParserFactory();
105-
$nodeFinder = new NodeFinder();
106-
107-
$phpParser = $parserFactory->createForHostVersion();
108-
$nodeTraverser = new NodeTraverser();
109-
$nodeTraverser->addVisitor(new NameResolver());
110-
11196
foreach ($fileInfos as $fileInfo) {
112-
/** @var string $fileContents */
113-
$fileContents = file_get_contents($fileInfo->getRealPath());
114-
115-
/** @var Stmt[] $stmts */
116-
$stmts = $phpParser->parse($fileContents);
117-
$nodeTraverser->traverse($stmts);
97+
$stmts = $this->simplePhpParser->parseFilePath($fileInfo->getRealPath());
11898

11999
// 1. get class name
120-
$class = $nodeFinder->findFirstInstanceOf($stmts, Class_::class);
100+
$class = $this->nodeFinder->findFirstInstanceOf($stmts, Class_::class);
121101
if (! $class instanceof Class_) {
122102
continue;
123103
}
124104

125-
if ($class->isAnonymous()) {
126-
continue;
127-
}
128-
129-
if (! $class->namespacedName instanceof Name) {
105+
// is magic class?
106+
if ($class->isAnonymous() || ! $class->namespacedName instanceof Name) {
130107
continue;
131108
}
132109

133110
$className = $class->namespacedName->toString();
134111

135112
foreach ($class->getMethods() as $classMethod) {
136-
$methodName = $classMethod->name->toString();
137-
138-
// 1. collect from docblock
139-
if ($classMethod->getDocComment() instanceof Doc) {
140-
preg_match_all(self::INSTRUCTION_DOCBLOCK_REGEX, $classMethod->getDocComment()->getText(), $match);
141-
142-
foreach ($match['instruction'] as $instruction) {
143-
$mask = $this->clearMask($instruction);
144-
145-
$classMethodContextDefinitions[] = new ClassMethodContextDefinition(
146-
$fileInfo->getRealPath(),
147-
$className,
148-
$methodName,
149-
$mask
150-
);
151-
}
152-
}
153-
154-
// 2. collect from attributes
155-
foreach ($classMethod->attrGroups as $attrGroup) {
156-
foreach ($attrGroup->attrs as $attr) {
157-
$attributeName = $attr->name->toString();
158-
if (! in_array($attributeName, self::ATTRIBUTE_NAMES)) {
159-
continue;
160-
}
161-
162-
$firstArgValue = $attr->args[0]->value;
163-
164-
if (! $firstArgValue instanceof String_) {
165-
continue;
166-
}
167-
168-
$classMethodContextDefinitions[] = new ClassMethodContextDefinition(
169-
$fileInfo->getRealPath(),
170-
$className,
171-
$methodName,
172-
$firstArgValue->value
173-
);
174-
}
113+
$rawMasks = $this->classMethodMasksResolver->resolve($classMethod);
114+
115+
foreach ($rawMasks as $rawMask) {
116+
$classMethodContextDefinitions[] = new ClassMethodContextDefinition(
117+
$fileInfo->getRealPath(),
118+
$className,
119+
$classMethod->name->toString(),
120+
$rawMask
121+
);
175122
}
176123
}
177124
}
178125

179126
return $classMethodContextDefinitions;
180127
}
181-
182-
private function clearMask(string $mask): string
183-
{
184-
$mask = trim($mask);
185-
186-
// clear extra quote escaping that would cause miss-match with feature masks
187-
$mask = str_replace('\\\'', "'", $mask);
188-
return str_replace('\\/', '/', $mask);
189-
}
190128
}

src/PhpParser/SimplePhpParser.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Behastan\PhpParser;
6+
7+
use PhpParser\Node\Stmt;
8+
use PhpParser\NodeTraverser;
9+
use PhpParser\NodeVisitor\NameResolver;
10+
use PhpParser\Parser;
11+
use PhpParser\ParserFactory;
12+
use Webmozart\Assert\Assert;
13+
14+
final class SimplePhpParser
15+
{
16+
private Parser $phpParser;
17+
18+
public function __construct()
19+
{
20+
$this->phpParser = (new ParserFactory())->createForHostVersion();
21+
}
22+
23+
/**
24+
* @return Stmt[]
25+
*/
26+
public function parseFilePath(string $filePath): array
27+
{
28+
Assert::fileExists($filePath);
29+
30+
$fileContents = file_get_contents($filePath);
31+
Assert::string($fileContents);
32+
33+
$stmts = $this->phpParser->parse($fileContents);
34+
Assert::isArray($stmts);
35+
36+
$nameNodeTraverser = new NodeTraverser();
37+
$nameNodeTraverser->addVisitor(new NameResolver());
38+
$nameNodeTraverser->traverse($stmts);
39+
40+
return $stmts;
41+
}
42+
}

0 commit comments

Comments
 (0)