* * 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 */ final class ContextSnippetGenerator implements SnippetGenerator { /** * @var string[string] */ private static $proposedMethods = array(); /** * @var string */ private static $templateTemplate = <<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; } }