44
55namespace 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 ;
921use 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
1524final 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}
0 commit comments