edb2227f5b96114c40838dd816340e2ce85b66b7
[yaffs-website] / vendor / behat / behat / src / Behat / Behat / Context / Snippet / Generator / ContextSnippetGenerator.php
1 <?php
2
3 /*
4  * This file is part of the Behat.
5  * (c) Konstantin Kudryashov <ever.zet@gmail.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 namespace Behat\Behat\Context\Snippet\Generator;
12
13 use Behat\Behat\Context\Environment\ContextEnvironment;
14 use Behat\Behat\Context\Snippet\ContextSnippet;
15 use Behat\Behat\Definition\Pattern\PatternTransformer;
16 use Behat\Behat\Snippet\Exception\EnvironmentSnippetGenerationException;
17 use Behat\Behat\Snippet\Generator\SnippetGenerator;
18 use Behat\Gherkin\Node\PyStringNode;
19 use Behat\Gherkin\Node\StepNode;
20 use Behat\Gherkin\Node\TableNode;
21 use Behat\Testwork\Environment\Environment;
22 use ReflectionClass;
23
24 /**
25  * Generates snippets for a context class.
26  *
27  * @author Konstantin Kudryashov <ever.zet@gmail.com>
28  */
29 final class ContextSnippetGenerator implements SnippetGenerator
30 {
31     /**
32      * @var string[string]
33      */
34     private static $proposedMethods = array();
35     /**
36      * @var string
37      */
38     private static $templateTemplate = <<<TPL
39     /**
40      * @%%s %s
41      */
42     public function %s(%s)
43     {
44         throw new PendingException();
45     }
46 TPL;
47     /**
48      * @var PatternTransformer
49      */
50     private $patternTransformer;
51     /**
52      * @var TargetContextIdentifier
53      */
54     private $contextIdentifier;
55     /**
56      * @var PatternIdentifier
57      */
58     private $patternIdentifier;
59
60     /**
61      * Initializes snippet generator.
62      *
63      * @param PatternTransformer $patternTransformer
64      */
65     public function __construct(PatternTransformer $patternTransformer)
66     {
67         $this->patternTransformer = $patternTransformer;
68
69         $this->setContextIdentifier(new FixedContextIdentifier(null));
70         $this->setPatternIdentifier(new FixedPatternIdentifier(null));
71     }
72
73     /**
74      * Sets target context identifier.
75      *
76      * @param TargetContextIdentifier $identifier
77      */
78     public function setContextIdentifier(TargetContextIdentifier $identifier)
79     {
80         $this->contextIdentifier = new CachedContextIdentifier($identifier);
81     }
82
83     /**
84      * Sets target pattern type identifier.
85      *
86      * @param PatternIdentifier $identifier
87      */
88     public function setPatternIdentifier(PatternIdentifier $identifier)
89     {
90         $this->patternIdentifier = $identifier;
91     }
92
93     /**
94      * {@inheritdoc}
95      */
96     public function supportsEnvironmentAndStep(Environment $environment, StepNode $step)
97     {
98         if (!$environment instanceof ContextEnvironment) {
99             return false;
100         }
101
102         if (!$environment->hasContexts()) {
103             return false;
104         }
105
106         return null !== $this->contextIdentifier->guessTargetContextClass($environment);
107     }
108
109     /**
110      * {@inheritdoc}
111      */
112     public function generateSnippet(Environment $environment, StepNode $step)
113     {
114         if (!$environment instanceof ContextEnvironment) {
115             throw new EnvironmentSnippetGenerationException(sprintf(
116                 'ContextSnippetGenerator does not support `%s` environment.',
117                 get_class($environment)
118             ), $environment);
119         }
120
121         $contextClass = $this->contextIdentifier->guessTargetContextClass($environment);
122         $patternType = $this->patternIdentifier->guessPatternType($contextClass);
123         $stepText = $step->getText();
124         $pattern = $this->patternTransformer->generatePattern($patternType, $stepText);
125
126         $methodName = $this->getMethodName($contextClass, $pattern->getCanonicalText(), $pattern->getPattern());
127         $methodArguments = $this->getMethodArguments($step, $pattern->getPlaceholderCount());
128         $snippetTemplate = $this->getSnippetTemplate($pattern->getPattern(), $methodName, $methodArguments);
129
130         $usedClasses = $this->getUsedClasses($step);
131
132         return new ContextSnippet($step, $snippetTemplate, $contextClass, $usedClasses);
133     }
134
135     /**
136      * Generates method name using step text and regex.
137      *
138      * @param string $contextClass
139      * @param string $canonicalText
140      * @param string $pattern
141      *
142      * @return string
143      */
144     private function getMethodName($contextClass, $canonicalText, $pattern)
145     {
146         $methodName = $this->deduceMethodName($canonicalText);
147         $methodName = $this->getUniqueMethodName($contextClass, $pattern, $methodName);
148
149         return $methodName;
150     }
151
152     /**
153      * Returns an array of method argument names from step and token count.
154      *
155      * @param StepNode $step
156      * @param integer  $tokenCount
157      *
158      * @return string[]
159      */
160     private function getMethodArguments(StepNode $step, $tokenCount)
161     {
162         $args = array();
163         for ($i = 0; $i < $tokenCount; $i++) {
164             $args[] = '$arg' . ($i + 1);
165         }
166
167         foreach ($step->getArguments() as $argument) {
168             $args[] = $this->getMethodArgument($argument);
169         }
170
171         return $args;
172     }
173
174     /**
175      * Returns an array of classes used by the snippet template
176      *
177      * @param StepNode $step
178      *
179      * @return string[]
180      */
181     private function getUsedClasses(StepNode $step)
182     {
183         $usedClasses = array('Behat\Behat\Tester\Exception\PendingException');
184
185         foreach ($step->getArguments() as $argument) {
186             if ($argument instanceof TableNode) {
187                 $usedClasses[] = 'Behat\Gherkin\Node\TableNode';
188             } elseif ($argument instanceof PyStringNode) {
189                 $usedClasses[] = 'Behat\Gherkin\Node\PyStringNode';
190             }
191         }
192
193         return $usedClasses;
194     }
195
196     /**
197      * Generates snippet template using regex, method name and arguments.
198      *
199      * @param string   $pattern
200      * @param string   $methodName
201      * @param string[] $methodArguments
202      *
203      * @return string
204      */
205     private function getSnippetTemplate($pattern, $methodName, array $methodArguments)
206     {
207         return sprintf(
208             self::$templateTemplate,
209             str_replace('%', '%%', $pattern),
210             $methodName,
211             implode(', ', $methodArguments)
212         );
213     }
214
215     /**
216      * Generates definition method name based on the step text.
217      *
218      * @param string $canonicalText
219      *
220      * @return string
221      */
222     private function deduceMethodName($canonicalText)
223     {
224         // check that method name is not empty
225         if (0 !== strlen($canonicalText)) {
226             $canonicalText[0] = strtolower($canonicalText[0]);
227
228             return $canonicalText;
229         }
230
231         return 'stepDefinition1';
232     }
233
234     /**
235      * Ensures uniqueness of the method name in the context.
236      *
237      * @param string $contextClass
238      * @param string $stepPattern
239      * @param string $name
240      *
241      * @return string
242      */
243     private function getUniqueMethodName($contextClass, $stepPattern, $name)
244     {
245         $reflection = new ReflectionClass($contextClass);
246
247         $number = $this->getMethodNumberFromTheMethodName($name);
248         list($name, $number) = $this->getMethodNameNotExistentInContext($reflection, $name, $number);
249         $name = $this->getMethodNameNotProposedEarlier($contextClass, $stepPattern, $name, $number);
250
251         return $name;
252     }
253
254     /**
255      * Tries to deduct method number from the provided method name.
256      *
257      * @param string $methodName
258      *
259      * @return integer
260      */
261     private function getMethodNumberFromTheMethodName($methodName)
262     {
263         $methodNumber = 2;
264         if (preg_match('/(\d+)$/', $methodName, $matches)) {
265             $methodNumber = intval($matches[1]);
266         }
267
268         return $methodNumber;
269     }
270
271     /**
272      * Tries to guess method name that is not yet defined in the context class.
273      *
274      * @param ReflectionClass $reflection
275      * @param string          $methodName
276      * @param integer         $methodNumber
277      *
278      * @return array
279      */
280     private function getMethodNameNotExistentInContext(ReflectionClass $reflection, $methodName, $methodNumber)
281     {
282         while ($reflection->hasMethod($methodName)) {
283             $methodName = preg_replace('/\d+$/', '', $methodName);
284             $methodName .= $methodNumber++;
285         }
286
287         return array($methodName, $methodNumber);
288     }
289
290     /**
291      * Tries to guess method name that is not yet proposed to the context class.
292      *
293      * @param string  $contextClass
294      * @param string  $stepPattern
295      * @param string  $name
296      * @param integer $number
297      *
298      * @return string
299      */
300     private function getMethodNameNotProposedEarlier($contextClass, $stepPattern, $name, $number)
301     {
302         foreach ($this->getAlreadyProposedMethods($contextClass) as $proposedPattern => $proposedMethod) {
303             if ($proposedPattern === $stepPattern) {
304                 continue;
305             }
306
307             while ($proposedMethod === $name) {
308                 $name = preg_replace('/\d+$/', '', $name);
309                 $name .= $number++;
310             }
311         }
312
313         $this->markMethodAsAlreadyProposed($contextClass, $stepPattern, $name);
314
315         return $name;
316     }
317
318     /**
319      * Returns already proposed method names.
320      *
321      * @param string $contextClass
322      *
323      * @return string[]
324      */
325     private function getAlreadyProposedMethods($contextClass)
326     {
327         return isset(self::$proposedMethods[$contextClass]) ? self::$proposedMethods[$contextClass] : array();
328     }
329
330     /**
331      * Marks method as proposed one.
332      *
333      * @param string $contextClass
334      * @param string $stepPattern
335      * @param string $methodName
336      */
337     private function markMethodAsAlreadyProposed($contextClass, $stepPattern, $methodName)
338     {
339         self::$proposedMethods[$contextClass][$stepPattern] = $methodName;
340     }
341
342     /**
343      * Returns method argument.
344      *
345      * @param string $argument
346      *
347      * @return string
348      */
349     private function getMethodArgument($argument)
350     {
351         $arg = '__unknown__';
352         if ($argument instanceof PyStringNode) {
353             $arg = 'PyStringNode $string';
354         } elseif ($argument instanceof TableNode) {
355             $arg = 'TableNode $table';
356         }
357
358         return $arg;
359     }
360 }