diff --git a/README.md b/README.md index e5b27fb..0ce8846 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,191 @@ return $config; # Fixers +## PedroTroller/order_behat_steps + +Step definition methods in Behat contexts MUST BE ordered by annotation and method name. + + +### Available options + + - `instanceof` (*optional*): Parent class or interface of your behat context classes. + - default: `Behat\Behat\Context\Context` + +### Configuration examples + +```php +// .php_cs.dist +setRules([ + // ... + 'PedroTroller/order_behat_steps' => true, + // ... + ]) + // ... + ->registerCustomFixers(new PedroTroller\CS\Fixer\Fixers()) +; + +return $config; +``` +**OR** using my [rule list builder](doc/rule-set-factory.md). +```php +// .php_cs.dist +setRules(PedroTroller\CS\Fixer\RuleSetFactory::create() + ->enable('PedroTroller/order_behat_steps') + ->getRules() + ]) + // ... + ->registerCustomFixers(new PedroTroller\CS\Fixer\Fixers()) +; + +return $config; +``` + +### Fixes + +```diff +--- Original // 80 chars ++++ New // +@@ @@ // + } // + // + /** // +- * @Then the response should be received // ++ * @BeforeScenario // + */ // +- public function theResponseShouldBeReceived() // ++ public function reset() // + { // + // ... // + } // + // + /** // +- * @When a demo scenario sends a request to :path // ++ * @Given I am on the homepage // + */ // +- public function aDemoScenarioSendsARequestTo($path) // ++ public function iAmOnTheHomepage() // + { // + // ... // + } // + // + /** // +- * @Given I am on the homepage // ++ * @When a demo scenario sends a request to :path // + */ // +- public function iAmOnTheHomepage() // ++ public function aDemoScenarioSendsARequestTo($path) // + { // + // ... // + } // + // + /** // +- * @BeforeScenario // ++ * @Then the response should be received // + */ // +- public function reset() // ++ public function theResponseShouldBeReceived() // + { // + // ... // + } // + } // + // +``` +### Configuration examples + +```php +// .php_cs.dist +setRules([ + // ... + 'PedroTroller/order_behat_steps' => [ 'instanceof' => [ 'Behat\Behat\Context\Context' ] ], + // ... + ]) + // ... + ->registerCustomFixers(new PedroTroller\CS\Fixer\Fixers()) +; + +return $config; +``` +**OR** using my [rule list builder](doc/rule-set-factory.md). +```php +// .php_cs.dist +setRules(PedroTroller\CS\Fixer\RuleSetFactory::create() + ->enable('PedroTroller/order_behat_steps', [ 'instanceof' => [ 'Behat\Behat\Context\Context' ] ]) + ->getRules() + ]) + // ... + ->registerCustomFixers(new PedroTroller\CS\Fixer\Fixers()) +; + +return $config; +``` + +### Fixes + +```diff +--- Original // 80 chars ++++ New // +@@ @@ // + } // + // + /** // +- * @Then the response should be received // ++ * @BeforeScenario // + */ // +- public function theResponseShouldBeReceived() // ++ public function reset() // + { // + // ... // + } // + // + /** // +- * @When a demo scenario sends a request to :path // ++ * @Given I am on the homepage // + */ // +- public function aDemoScenarioSendsARequestTo($path) // ++ public function iAmOnTheHomepage() // + { // + // ... // + } // + // + /** // +- * @Given I am on the homepage // ++ * @When a demo scenario sends a request to :path // + */ // +- public function iAmOnTheHomepage() // ++ public function aDemoScenarioSendsARequestTo($path) // + { // + // ... // + } // + // + /** // +- * @BeforeScenario // ++ * @Then the response should be received // + */ // +- public function reset() // ++ public function theResponseShouldBeReceived() // + { // + // ... // + } // + } // + // +``` + ## PedroTroller/ordered_with_getter_and_setter_first Class/interface/trait methods MUST BE ordered (accessors at the beginning of the class, ordered following properties order). diff --git a/src/PedroTroller/CS/Fixer/AbstractFixer.php b/src/PedroTroller/CS/Fixer/AbstractFixer.php index d0d7b5f..10183e7 100644 --- a/src/PedroTroller/CS/Fixer/AbstractFixer.php +++ b/src/PedroTroller/CS/Fixer/AbstractFixer.php @@ -13,7 +13,9 @@ abstract class AbstractFixer extends PhpCsFixer { /** - * {@inheritdoc} + * @param Tokens $tokens + * + * @return bool */ public function isCandidate(Tokens $tokens) { @@ -29,7 +31,7 @@ public function getName() } /** - * @return array[] + * @return array */ public function getSampleConfigurations() { diff --git a/src/PedroTroller/CS/Fixer/AbstractOrderedClassElementsFixer.php b/src/PedroTroller/CS/Fixer/AbstractOrderedClassElementsFixer.php index 4ab3fb1..d554efb 100644 --- a/src/PedroTroller/CS/Fixer/AbstractOrderedClassElementsFixer.php +++ b/src/PedroTroller/CS/Fixer/AbstractOrderedClassElementsFixer.php @@ -105,6 +105,14 @@ private function getElements(Tokens $tokens, $startIndex) break; } + $possibleCommentIndex = $startIndex + 1; + + if (isset($tokens[$possibleCommentIndex]) && $tokens[$possibleCommentIndex]->isComment()) { + $element['comment'] = $tokens[$possibleCommentIndex]->getContent(); + } else { + $element['comment'] = null; + } + $elements[] = $element; $startIndex = $element['end'] + 1; } diff --git a/src/PedroTroller/CS/Fixer/Behat/OrderBehatStepsFixer.php b/src/PedroTroller/CS/Fixer/Behat/OrderBehatStepsFixer.php new file mode 100644 index 0000000..00405c4 --- /dev/null +++ b/src/PedroTroller/CS/Fixer/Behat/OrderBehatStepsFixer.php @@ -0,0 +1,207 @@ + ['Behat\Behat\Context\Context']], + ]; + } + + public function isCandidate(Tokens $tokens) + { + foreach ($this->configuration['instanceof'] as $parent) { + if ($this->extendsClass($tokens, $parent)) { + return true; + } + + if ($this->implementsInterface($tokens, $parent)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getSampleCode() + { + return <<<'SPEC' +kernel = $kernel; + } + + /** + * @Then the response should be received + */ + public function theResponseShouldBeReceived() + { + // ... + } + + /** + * @When a demo scenario sends a request to :path + */ + public function aDemoScenarioSendsARequestTo($path) + { + // ... + } + + /** + * @Given I am on the homepage + */ + public function iAmOnTheHomepage() + { + // ... + } + + /** + * @BeforeScenario + */ + public function reset() + { + // ... + } +} +SPEC; + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return Priority::before(OrderedClassElementsFixer::class); + } + + public function getDocumentation() + { + return 'Step definition methods in Behat contexts MUST BE ordered by annotation and method name.'; + } + + /** + * {@inheritdoc} + */ + protected function createConfigurationDefinition() + { + return new FixerConfigurationResolver([ + (new FixerOptionBuilder('instanceof', 'Parent class or interface of your behat context classes.')) + ->setDefault(['Behat\Behat\Context\Context']) + ->getOption(), + ]); + } + + /** + * {@inheritdoc} + */ + protected function sortElements(array $elements) + { + $ordered = []; + + foreach (self::ANNOTATION_PRIORITIES as $annotation) { + $ordered[$annotation] = []; + } + + foreach ($elements as $index => $element) { + if ('method' !== $element['type']) { + continue; + } + + if (empty($element['comment'])) { + continue; + } + + foreach (self::ANNOTATION_PRIORITIES as $search) { + $regex = "/^ *(\\/\\/|\\*).* {$search}( .+|$)/m"; + + if (!preg_match($regex, $element['comment'])) { + continue; + } + + $ordered[$search][$element['methodName']] = $element; + unset($elements[$index]); + + continue 2; + } + } + + foreach ($ordered as $annotation => $methods) { + ksort($ordered[$annotation]); + } + + $result = []; + + foreach ($elements as $element) { + if ('method' === $element['type'] && '__construct' !== $element['methodName']) { + foreach ($ordered as $methods) { + foreach ($methods as $method) { + $result[] = $method; + } + } + $ordered = []; + } + + $result[] = $element; + } + + foreach ($ordered as $methods) { + foreach ($methods as $method) { + $result[] = $method; + } + } + + return $result; + } +} diff --git a/tests/UseCase/Behat.php b/tests/UseCase/Behat.php new file mode 100644 index 0000000..99266c2 --- /dev/null +++ b/tests/UseCase/Behat.php @@ -0,0 +1,234 @@ +kernel = $kernel; + } + + /** + * @BeforeScenario + */ + public function resetDatabase() + { + // ... + } + + /** + * @BeforeScenario + * @AfterStep + */ + public function resetLogs() + { + // ... + } + + public function doSomething() + { + // ... + } + + /** + * @When a demo scenario sends a request to :path + */ + public function aDemoScenarioSendsARequestTo($path) + { + // ... + } + + /** + * @Given I have a user account + * @When I create a user account + */ + public function theResponseShouldBeReceived() + { + // ... + } + + /** + * @BeforeScenario + */ + public function resetDatabase() + { + // ... + } + + /** + * @BeforeScenario + * @AfterStep + */ + public function resetLogs() + { + // ... + } + + /** + * @BeforeScenario + * @Given I am anonymous + */ + public function iAmAnnon() + { + // ... + } +} +CONTEXT; + } + + public function getExpectation() + { + return <<<'CONTEXT' +kernel = $kernel; + } + + /** + * @BeforeScenario + * @Given I am anonymous + */ + public function iAmAnnon() + { + // ... + } + + /** + * @BeforeScenario + */ + public function resetDatabase() + { + // ... + } + + /** + * @BeforeScenario + * @AfterStep + */ + public function resetLogs() + { + // ... + } + + /** + * @Given I am on the homepage + */ + public function iAmOnTheHomepage() + { + // ... + } + + /** + * @Given I have a user account + * @When I create a user account + */ + public function theResponseShouldBeReceived() + { + // ... + } + + /** + * @When a demo scenario sends a request to :path + */ + public function aDemoScenarioSendsARequestTo($path) + { + // ... + } + + /** + * @Then the response should be received + */ + public function theResponseShouldBeReceived() + { + // ... + } + + public function doSomething() + { + // ... + } +} +CONTEXT; + } + + public function getMinSupportedPhpVersion() + { + return 0; + } +}