Skip to content

Commit db6a186

Browse files
authored
[behastan] Add class + method name to mask metadata (#81)
* [behastan] Add class + method name to mask metadata * add php-parser approach, more cleaner, include class + method name * improve test
1 parent d411ee6 commit db6a186

21 files changed

+281
-77
lines changed

scoper.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
return [
1515
'prefix' => 'SwissKnife' . $timestamp,
1616
'expose-constants' => ['#^SYMFONY\_[\p{L}_]+$#'],
17-
'exclude-classes' => [
18-
\Symfony\Component\Finder\SplFileInfo::class,
19-
],
17+
'exclude-classes' => [\Symfony\Component\Finder\SplFileInfo::class],
2018
'exclude-namespaces' => [
2119
'#^Rector\\\\SwissKnife#',
2220
'#^Symfony\\\\Polyfill#',

src/Behastan/Behastan.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
namespace Rector\SwissKnife\Behastan;
66

77
use Nette\Utils\Strings;
8-
use Rector\SwissKnife\Behastan\ValueObject\AbstractMask;
9-
use Rector\SwissKnife\Behastan\ValueObject\ExactMask;
8+
use Rector\SwissKnife\Behastan\ValueObject\Mask\AbstractMask;
9+
use Rector\SwissKnife\Behastan\ValueObject\Mask\ExactMask;
10+
use Rector\SwissKnife\Behastan\ValueObject\Mask\NamedMask;
11+
use Rector\SwissKnife\Behastan\ValueObject\Mask\RegexMask;
12+
use Rector\SwissKnife\Behastan\ValueObject\Mask\SkippedMask;
1013
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;
1414
use Symfony\Component\Console\Style\SymfonyStyle;
1515
use Symfony\Component\Finder\SplFileInfo;
1616

src/Behastan/Command/BehastanCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use Rector\SwissKnife\Behastan\Behastan;
88
use Rector\SwissKnife\Behastan\Finder\BehatMetafilesFinder;
9-
use Rector\SwissKnife\Behastan\ValueObject\AbstractMask;
9+
use Rector\SwissKnife\Behastan\ValueObject\Mask\AbstractMask;
1010
use Symfony\Component\Console\Command\Command;
1111
use Symfony\Component\Console\Input\InputArgument;
1212
use Symfony\Component\Console\Input\InputInterface;

src/Behastan/DefinitionMasksResolver.php

Lines changed: 133 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@
44

55
namespace Rector\SwissKnife\Behastan;
66

7-
use Nette\Utils\Strings;
8-
use Rector\SwissKnife\Behastan\ValueObject\ExactMask;
7+
use PhpParser\Comment\Doc;
8+
use PhpParser\Node\Name;
9+
use PhpParser\Node\Scalar\String_;
10+
use PhpParser\Node\Stmt;
11+
use PhpParser\Node\Stmt\Class_;
12+
use PhpParser\NodeFinder;
13+
use PhpParser\NodeTraverser;
14+
use PhpParser\NodeVisitor\NameResolver;
15+
use PhpParser\ParserFactory;
16+
use Rector\SwissKnife\Behastan\ValueObject\ClassMethodContextDefinition;
17+
use Rector\SwissKnife\Behastan\ValueObject\Mask\ExactMask;
18+
use Rector\SwissKnife\Behastan\ValueObject\Mask\NamedMask;
19+
use Rector\SwissKnife\Behastan\ValueObject\Mask\RegexMask;
20+
use Rector\SwissKnife\Behastan\ValueObject\Mask\SkippedMask;
921
use Rector\SwissKnife\Behastan\ValueObject\MaskCollection;
10-
use Rector\SwissKnife\Behastan\ValueObject\NamedMask;
11-
use Rector\SwissKnife\Behastan\ValueObject\RegexMask;
12-
use Rector\SwissKnife\Behastan\ValueObject\SkippedMask;
13-
use Symfony\Component\Finder\SplFileInfo;
22+
use SplFileInfo;
1423

1524
final class DefinitionMasksResolver
1625
{
@@ -20,9 +29,9 @@ final class DefinitionMasksResolver
2029
private const INSTRUCTION_DOCBLOCK_REGEX = '#\@(Given|Then|When)\s+(?<instruction>.*?)\n#m';
2130

2231
/**
23-
* @var string
32+
* @var string[]
2433
*/
25-
private const INSTRUCTION_ATTRIBUTE_REGEX = '#\#\[(Given|Then|When)\(\'(?<instruction>.*?)\'\)#sm';
34+
private const ATTRIBUTE_NAMES = ['Behat\Step\Then', 'Behat\Step\Given', 'Behat\Step\And'];
2635

2736
/**
2837
* @param SplFileInfo[] $contextFiles
@@ -31,31 +40,53 @@ public function resolve(array $contextFiles): MaskCollection
3140
{
3241
$masks = [];
3342

34-
$rawMasksByFilePath = $this->resolveMasksFromFiles($contextFiles);
43+
$classMethodContextDefinitions = $this->resolveMasksFromFiles($contextFiles);
3544

36-
foreach ($rawMasksByFilePath as $filePath => $rawMasks) {
37-
foreach ($rawMasks as $rawMask) {
38-
// @todo edge case - handle next
39-
if (str_contains($rawMask, ' [:')) {
40-
$masks[] = new SkippedMask($rawMask, $filePath);
41-
continue;
42-
}
45+
foreach ($classMethodContextDefinitions as $classMethodContextDefinition) {
46+
$rawMask = $classMethodContextDefinition->getMask();
4347

44-
// regex pattern, handled else-where
45-
if (str_starts_with($rawMask, '/')) {
46-
$masks[] = new RegexMask($rawMask, $filePath);
47-
continue;
48-
}
48+
// @todo edge case - handle next
49+
if (str_contains($rawMask, ' [:')) {
50+
$masks[] = new SkippedMask(
51+
$rawMask,
52+
$classMethodContextDefinition->getFilePath(),
53+
$classMethodContextDefinition->getClass(),
54+
$classMethodContextDefinition->getMethodName()
55+
);
56+
continue;
57+
}
4958

50-
// handled in mask one
51-
if (Strings::match($rawMask, '#(\:[\W\w]+)#')) {
52-
// if (str_contains($rawMask, ':')) {
53-
$masks[] = new NamedMask($rawMask, $filePath);
54-
continue;
55-
}
59+
// regex pattern, handled else-where
60+
if (str_starts_with($rawMask, '/')) {
61+
$masks[] = new RegexMask(
62+
$rawMask,
63+
$classMethodContextDefinition->getFilePath(),
64+
$classMethodContextDefinition->getClass(),
65+
$classMethodContextDefinition->getMethodName()
66+
);
67+
continue;
68+
}
5669

57-
$masks[] = new ExactMask($rawMask, $filePath);
70+
// handled in mask one
71+
preg_match('#(\:[\W\w]+)#', $rawMask, $match);
72+
73+
if ($match !== []) {
74+
// if (str_contains($rawMask, ':')) {
75+
$masks[] = new NamedMask(
76+
$rawMask,
77+
$classMethodContextDefinition->getFilePath(),
78+
$classMethodContextDefinition->getClass(),
79+
$classMethodContextDefinition->getMethodName()
80+
);
81+
continue;
5882
}
83+
84+
$masks[] = new ExactMask(
85+
$rawMask,
86+
$classMethodContextDefinition->getFilePath(),
87+
$classMethodContextDefinition->getClass(),
88+
$classMethodContextDefinition->getMethodName()
89+
);
5990
}
6091

6192
return new MaskCollection($masks);
@@ -64,37 +95,94 @@ public function resolve(array $contextFiles): MaskCollection
6495
/**
6596
* @param SplFileInfo[] $fileInfos
6697
*
67-
* @return array<string, string[]>
98+
* @return ClassMethodContextDefinition[]
6899
*/
69100
private function resolveMasksFromFiles(array $fileInfos): array
70101
{
71-
$masksByFilePath = [];
102+
$classMethodContextDefinitions = [];
103+
104+
$parserFactory = new ParserFactory();
105+
$nodeFinder = new NodeFinder();
106+
107+
$phpParser = $parserFactory->createForHostVersion();
108+
$nodeTraverser = new NodeTraverser();
109+
$nodeTraverser->addVisitor(new NameResolver());
72110

73111
foreach ($fileInfos as $fileInfo) {
74-
$matches = $this->matchDocblockAndAttributeDefinitions($fileInfo);
112+
/** @var string $fileContents */
113+
$fileContents = file_get_contents($fileInfo->getRealPath());
75114

76-
foreach ($matches as $match) {
77-
$mask = trim((string) $match['instruction']);
115+
/** @var Stmt[] $stmts */
116+
$stmts = $phpParser->parse($fileContents);
117+
$nodeTraverser->traverse($stmts);
78118

79-
// clear extra quote escaping that would cause miss-match with feature masks
80-
$mask = str_replace('\\\'', "'", $mask);
81-
$mask = str_replace('\\/', '/', $mask);
119+
// 1. get class name
120+
$class = $nodeFinder->findFirstInstanceOf($stmts, Class_::class);
121+
if (! $class instanceof Class_) {
122+
continue;
123+
}
124+
if ($class->isAnonymous()) {
125+
continue;
126+
}
127+
if (! $class->namespacedName instanceof Name) {
128+
continue;
129+
}
130+
131+
$className = $class->namespacedName->toString();
132+
133+
foreach ($class->getMethods() as $classMethod) {
134+
$methodName = $classMethod->name->toString();
135+
136+
// 1. collect from docblock
137+
if ($classMethod->getDocComment() instanceof Doc) {
138+
preg_match_all(self::INSTRUCTION_DOCBLOCK_REGEX, $classMethod->getDocComment()->getText(), $match);
139+
140+
foreach ($match['instruction'] as $instruction) {
141+
$mask = $this->clearMask($instruction);
82142

83-
$masksByFilePath[$fileInfo->getRealPath()][] = $mask;
143+
$classMethodContextDefinitions[] = new ClassMethodContextDefinition(
144+
$fileInfo->getRealPath(),
145+
$className,
146+
$methodName,
147+
$mask
148+
);
149+
}
150+
}
151+
152+
// 2. collect from attributes
153+
foreach ($classMethod->attrGroups as $attrGroup) {
154+
foreach ($attrGroup->attrs as $attr) {
155+
$attributeName = $attr->name->toString();
156+
if (! in_array($attributeName, self::ATTRIBUTE_NAMES)) {
157+
continue;
158+
}
159+
160+
$firstArgValue = $attr->args[0]->value;
161+
162+
if (! $firstArgValue instanceof String_) {
163+
continue;
164+
}
165+
166+
$classMethodContextDefinitions[] = new ClassMethodContextDefinition(
167+
$fileInfo->getRealPath(),
168+
$className,
169+
$methodName,
170+
$firstArgValue->value
171+
);
172+
}
173+
}
84174
}
85175
}
86176

87-
return $masksByFilePath;
177+
return $classMethodContextDefinitions;
88178
}
89179

90-
/**
91-
* @return mixed[]
92-
*/
93-
private function matchDocblockAndAttributeDefinitions(SplFileInfo $contextFileInfo): array
180+
private function clearMask(string $mask): string
94181
{
95-
$attributeMatches = Strings::matchAll($contextFileInfo->getContents(), self::INSTRUCTION_ATTRIBUTE_REGEX);
96-
$docblockMatches = Strings::matchAll($contextFileInfo->getContents(), self::INSTRUCTION_DOCBLOCK_REGEX);
182+
$mask = trim($mask);
97183

98-
return array_merge($attributeMatches, $docblockMatches);
184+
// clear extra quote escaping that would cause miss-match with feature masks
185+
$mask = str_replace('\\\'', "'", $mask);
186+
return str_replace('\\/', '/', $mask);
99187
}
100188
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\SwissKnife\Behastan\ValueObject;
6+
7+
final readonly class ClassMethodContextDefinition
8+
{
9+
public function __construct(
10+
private string $filePath,
11+
private string $class,
12+
private string $methodName,
13+
private string $mask
14+
) {
15+
}
16+
17+
public function getFilePath(): string
18+
{
19+
return $this->filePath;
20+
}
21+
22+
public function getClass(): string
23+
{
24+
return $this->class;
25+
}
26+
27+
public function getMethodName(): string
28+
{
29+
return $this->methodName;
30+
}
31+
32+
public function getMask(): string
33+
{
34+
return $this->mask;
35+
}
36+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace Rector\SwissKnife\Behastan\ValueObject;
3+
namespace Rector\SwissKnife\Behastan\ValueObject\Mask;
44

55
use Rector\SwissKnife\Behastan\Contract\MaskInterface;
66

@@ -9,6 +9,8 @@ abstract class AbstractMask implements MaskInterface
99
public function __construct(
1010
public readonly string $mask,
1111
public readonly string $filePath,
12+
public readonly string $className,
13+
public readonly string $methodName,
1214
) {
1315
}
1416
}

src/Behastan/ValueObject/ExactMask.php renamed to src/Behastan/ValueObject/Mask/ExactMask.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Rector\SwissKnife\Behastan\ValueObject;
5+
namespace Rector\SwissKnife\Behastan\ValueObject\Mask;
66

77
final class ExactMask extends AbstractMask
88
{

src/Behastan/ValueObject/NamedMask.php renamed to src/Behastan/ValueObject/Mask/NamedMask.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Rector\SwissKnife\Behastan\ValueObject;
5+
namespace Rector\SwissKnife\Behastan\ValueObject\Mask;
66

77
final class NamedMask extends AbstractMask
88
{

src/Behastan/ValueObject/RegexMask.php renamed to src/Behastan/ValueObject/Mask/RegexMask.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Rector\SwissKnife\Behastan\ValueObject;
5+
namespace Rector\SwissKnife\Behastan\ValueObject\Mask;
66

77
final class RegexMask extends AbstractMask
88
{

src/Behastan/ValueObject/SkippedMask.php renamed to src/Behastan/ValueObject/Mask/SkippedMask.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Rector\SwissKnife\Behastan\ValueObject;
5+
namespace Rector\SwissKnife\Behastan\ValueObject\Mask;
66

77
final class SkippedMask extends AbstractMask
88
{

0 commit comments

Comments
 (0)