--- /dev/null
+<?php
+
+/*
+ * This file is part of the Behat.
+ * (c) Konstantin Kudryashov <ever.zet@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Behat\Behat\Context\Snippet\Generator;
+
+use Behat\Behat\Context\Environment\ContextEnvironment;
+use Behat\Behat\Context\Snippet\ContextSnippet;
+use Behat\Behat\Definition\Pattern\PatternTransformer;
+use Behat\Behat\Snippet\Exception\EnvironmentSnippetGenerationException;
+use Behat\Behat\Snippet\Generator\SnippetGenerator;
+use Behat\Gherkin\Node\PyStringNode;
+use Behat\Gherkin\Node\StepNode;
+use Behat\Gherkin\Node\TableNode;
+use Behat\Testwork\Environment\Environment;
+use ReflectionClass;
+
+/**
+ * Generates snippets for a context class.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+final class ContextSnippetGenerator implements SnippetGenerator
+{
+ /**
+ * @var string[string]
+ */
+ private static $proposedMethods = array();
+ /**
+ * @var string
+ */
+ private static $templateTemplate = <<<TPL
+ /**
+ * @%%s %s
+ */
+ public function %s(%s)
+ {
+ throw new PendingException();
+ }
+TPL;
+ /**
+ * @var PatternTransformer
+ */
+ private $patternTransformer;
+ /**
+ * @var TargetContextIdentifier
+ */
+ private $contextIdentifier;
+ /**
+ * @var PatternIdentifier
+ */
+ private $patternIdentifier;
+
+ /**
+ * Initializes snippet generator.
+ *
+ * @param PatternTransformer $patternTransformer
+ */
+ public function __construct(PatternTransformer $patternTransformer)
+ {
+ $this->patternTransformer = $patternTransformer;
+
+ $this->setContextIdentifier(new FixedContextIdentifier(null));
+ $this->setPatternIdentifier(new FixedPatternIdentifier(null));
+ }
+
+ /**
+ * Sets target context identifier.
+ *
+ * @param TargetContextIdentifier $identifier
+ */
+ public function setContextIdentifier(TargetContextIdentifier $identifier)
+ {
+ $this->contextIdentifier = new CachedContextIdentifier($identifier);
+ }
+
+ /**
+ * Sets target pattern type identifier.
+ *
+ * @param PatternIdentifier $identifier
+ */
+ public function setPatternIdentifier(PatternIdentifier $identifier)
+ {
+ $this->patternIdentifier = $identifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsEnvironmentAndStep(Environment $environment, StepNode $step)
+ {
+ if (!$environment instanceof ContextEnvironment) {
+ return false;
+ }
+
+ if (!$environment->hasContexts()) {
+ return false;
+ }
+
+ return null !== $this->contextIdentifier->guessTargetContextClass($environment);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generateSnippet(Environment $environment, StepNode $step)
+ {
+ if (!$environment instanceof ContextEnvironment) {
+ throw new EnvironmentSnippetGenerationException(sprintf(
+ 'ContextSnippetGenerator does not support `%s` environment.',
+ get_class($environment)
+ ), $environment);
+ }
+
+ $contextClass = $this->contextIdentifier->guessTargetContextClass($environment);
+ $patternType = $this->patternIdentifier->guessPatternType($contextClass);
+ $stepText = $step->getText();
+ $pattern = $this->patternTransformer->generatePattern($patternType, $stepText);
+
+ $methodName = $this->getMethodName($contextClass, $pattern->getCanonicalText(), $pattern->getPattern());
+ $methodArguments = $this->getMethodArguments($step, $pattern->getPlaceholderCount());
+ $snippetTemplate = $this->getSnippetTemplate($pattern->getPattern(), $methodName, $methodArguments);
+
+ $usedClasses = $this->getUsedClasses($step);
+
+ return new ContextSnippet($step, $snippetTemplate, $contextClass, $usedClasses);
+ }
+
+ /**
+ * Generates method name using step text and regex.
+ *
+ * @param string $contextClass
+ * @param string $canonicalText
+ * @param string $pattern
+ *
+ * @return string
+ */
+ private function getMethodName($contextClass, $canonicalText, $pattern)
+ {
+ $methodName = $this->deduceMethodName($canonicalText);
+ $methodName = $this->getUniqueMethodName($contextClass, $pattern, $methodName);
+
+ return $methodName;
+ }
+
+ /**
+ * Returns an array of method argument names from step and token count.
+ *
+ * @param StepNode $step
+ * @param integer $tokenCount
+ *
+ * @return string[]
+ */
+ private function getMethodArguments(StepNode $step, $tokenCount)
+ {
+ $args = array();
+ for ($i = 0; $i < $tokenCount; $i++) {
+ $args[] = '$arg' . ($i + 1);
+ }
+
+ foreach ($step->getArguments() as $argument) {
+ $args[] = $this->getMethodArgument($argument);
+ }
+
+ return $args;
+ }
+
+ /**
+ * Returns an array of classes used by the snippet template
+ *
+ * @param StepNode $step
+ *
+ * @return string[]
+ */
+ private function getUsedClasses(StepNode $step)
+ {
+ $usedClasses = array('Behat\Behat\Tester\Exception\PendingException');
+
+ foreach ($step->getArguments() as $argument) {
+ if ($argument instanceof TableNode) {
+ $usedClasses[] = 'Behat\Gherkin\Node\TableNode';
+ } elseif ($argument instanceof PyStringNode) {
+ $usedClasses[] = 'Behat\Gherkin\Node\PyStringNode';
+ }
+ }
+
+ return $usedClasses;
+ }
+
+ /**
+ * Generates snippet template using regex, method name and arguments.
+ *
+ * @param string $pattern
+ * @param string $methodName
+ * @param string[] $methodArguments
+ *
+ * @return string
+ */
+ private function getSnippetTemplate($pattern, $methodName, array $methodArguments)
+ {
+ return sprintf(
+ self::$templateTemplate,
+ str_replace('%', '%%', $pattern),
+ $methodName,
+ implode(', ', $methodArguments)
+ );
+ }
+
+ /**
+ * Generates definition method name based on the step text.
+ *
+ * @param string $canonicalText
+ *
+ * @return string
+ */
+ private function deduceMethodName($canonicalText)
+ {
+ // check that method name is not empty
+ if (0 !== strlen($canonicalText)) {
+ $canonicalText[0] = strtolower($canonicalText[0]);
+
+ return $canonicalText;
+ }
+
+ return 'stepDefinition1';
+ }
+
+ /**
+ * Ensures uniqueness of the method name in the context.
+ *
+ * @param string $contextClass
+ * @param string $stepPattern
+ * @param string $name
+ *
+ * @return string
+ */
+ private function getUniqueMethodName($contextClass, $stepPattern, $name)
+ {
+ $reflection = new ReflectionClass($contextClass);
+
+ $number = $this->getMethodNumberFromTheMethodName($name);
+ list($name, $number) = $this->getMethodNameNotExistentInContext($reflection, $name, $number);
+ $name = $this->getMethodNameNotProposedEarlier($contextClass, $stepPattern, $name, $number);
+
+ return $name;
+ }
+
+ /**
+ * Tries to deduct method number from the provided method name.
+ *
+ * @param string $methodName
+ *
+ * @return integer
+ */
+ private function getMethodNumberFromTheMethodName($methodName)
+ {
+ $methodNumber = 2;
+ if (preg_match('/(\d+)$/', $methodName, $matches)) {
+ $methodNumber = intval($matches[1]);
+ }
+
+ return $methodNumber;
+ }
+
+ /**
+ * Tries to guess method name that is not yet defined in the context class.
+ *
+ * @param ReflectionClass $reflection
+ * @param string $methodName
+ * @param integer $methodNumber
+ *
+ * @return array
+ */
+ private function getMethodNameNotExistentInContext(ReflectionClass $reflection, $methodName, $methodNumber)
+ {
+ while ($reflection->hasMethod($methodName)) {
+ $methodName = preg_replace('/\d+$/', '', $methodName);
+ $methodName .= $methodNumber++;
+ }
+
+ return array($methodName, $methodNumber);
+ }
+
+ /**
+ * Tries to guess method name that is not yet proposed to the context class.
+ *
+ * @param string $contextClass
+ * @param string $stepPattern
+ * @param string $name
+ * @param integer $number
+ *
+ * @return string
+ */
+ private function getMethodNameNotProposedEarlier($contextClass, $stepPattern, $name, $number)
+ {
+ foreach ($this->getAlreadyProposedMethods($contextClass) as $proposedPattern => $proposedMethod) {
+ if ($proposedPattern === $stepPattern) {
+ continue;
+ }
+
+ while ($proposedMethod === $name) {
+ $name = preg_replace('/\d+$/', '', $name);
+ $name .= $number++;
+ }
+ }
+
+ $this->markMethodAsAlreadyProposed($contextClass, $stepPattern, $name);
+
+ return $name;
+ }
+
+ /**
+ * Returns already proposed method names.
+ *
+ * @param string $contextClass
+ *
+ * @return string[]
+ */
+ private function getAlreadyProposedMethods($contextClass)
+ {
+ return isset(self::$proposedMethods[$contextClass]) ? self::$proposedMethods[$contextClass] : array();
+ }
+
+ /**
+ * Marks method as proposed one.
+ *
+ * @param string $contextClass
+ * @param string $stepPattern
+ * @param string $methodName
+ */
+ private function markMethodAsAlreadyProposed($contextClass, $stepPattern, $methodName)
+ {
+ self::$proposedMethods[$contextClass][$stepPattern] = $methodName;
+ }
+
+ /**
+ * Returns method argument.
+ *
+ * @param string $argument
+ *
+ * @return string
+ */
+ private function getMethodArgument($argument)
+ {
+ $arg = '__unknown__';
+ if ($argument instanceof PyStringNode) {
+ $arg = 'PyStringNode $string';
+ } elseif ($argument instanceof TableNode) {
+ $arg = 'TableNode $table';
+ }
+
+ return $arg;
+ }
+}