Initial commit,修正

This commit is contained in:
2025-07-13 19:37:38 +08:00
parent 38ce288e27
commit 65bbd06981
12384 changed files with 3985464 additions and 8 deletions
+47
View File
@@ -0,0 +1,47 @@
name: Build
on:
push:
branches: [master]
pull_request:
release:
types: [created]
jobs:
tests:
runs-on: ubuntu-latest
name: Build and test
strategy:
fail-fast: false
matrix:
php: [7.2, 7.3, 7.4, 8.0, 8.1]
composer-flags: [ "" ]
symfony-version: [ "" ]
include:
- php: 7.2
symfony-version: '3.*'
- php: 7.3
symfony-version: '4.*'
- php: 7.4
symfony-version: '5.*'
- php: 8.0
symfony-version: '5.*'
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php }}"
coverage: none
- name: Update Symfony version
if: matrix.symfony-version != ''
run: composer require --no-update "symfony/symfony:${{ matrix.symfony-version }}"
- name: Install dependencies
run: composer update ${{ matrix.composer-flags }}
- name: Run tests (phpunit)
run: ./vendor/bin/phpunit
+49
View File
@@ -0,0 +1,49 @@
name: Update Cucumber
on:
schedule:
- cron: '0 7 * * *'
jobs:
cucumber-update:
runs-on: ubuntu-latest
name: Upstream cucumber update
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
coverage: none
- uses: actions/checkout@v2
- name: Install dependencies
run: composer update
- name: Update cucumber tag
id: cucumber
run: bin/update_cucumber
- name: Re-install dependencies
run: composer update
if: steps.cucumber.outputs.cucumber_version
- name: Update translations
run: bin/update_i18n
if: steps.cucumber.outputs.cucumber_version
- name: Find changelog
id: changelog
run: bin/cucumber_changelog ${{ steps.cucumber.outputs.cucumber_version }}
if: steps.cucumber.outputs.cucumber_version
- name: Open a PR
uses: peter-evans/create-pull-request@v3
if: steps.cucumber.outputs.cucumber_version
with:
commit-message: Automatic Cucumber tag update to ${{ steps.cucumber.outputs.cucumber_version }}
branch: cucumber-update-${{ steps.cucumber.outputs.cucumber_version }}
delete-branch: true
title: Cucumber update ${{ steps.cucumber.outputs.cucumber_version }}
body: ${{ steps.changelog.outputs.changelog }}
base: 'master'
+442
View File
@@ -0,0 +1,442 @@
4.9.0 / 2021-10-12
==================
* Simplify the boolean condition for the tag matching by @stof in https://github.com/Behat/Gherkin/pull/219
* Remove symfony phpunit bridge by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/220
* Ignore the bin folder in archives by @stof in https://github.com/Behat/Gherkin/pull/226
* Cast table node exceptions into ParserExceptions when throwing by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/216
* Cucumber changelog in PRs and using correct hash by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/225
* Support alternative docstrings format (```) by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/214
* Fix DocBlocks (Boolean -> bool) by @simonhammes in https://github.com/Behat/Gherkin/pull/237
* Tag parsing by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/215
* Remove test - cucumber added an example with Rule which is not supported by @ciaranmcnulty in https://github.com/Behat/Gherkin/pull/239
* Add PHP 8.1 support by @javer in https://github.com/Behat/Gherkin/pull/242
* Fix main branch alias version by @mvorisek in https://github.com/Behat/Gherkin/pull/244
4.8.0 / 2021-02-04
==================
* Drop support for PHP before version 7.2
4.7.3 / 2021-02-04
==================
* Refactored comments parsing to avoid Maximum function nesting level errors
4.7.2 / 2021-02-03
==================
* Issue where Scenario Outline title was not populated into Examples
* Updated translations from cucumber 16.0.0
4.7.1 / 2021-01-26
==================
* Issue parsing comments before scenarios when following an Examples table
4.7.0 / 2021-01-24
==================
* Provides better messages for TableNode construct errors
* Now allows single character steps
* Supports multiple Example Tables with tags
4.6.2 / 2020-03-17
==================
* Fixed issues due to incorrect cache key
4.6.1 / 2020-02-27
==================
* Fix AZ translations
* Correctly filter features, now that the base path is correctly set
4.6.0 / 2019-01-16
==================
* Updated translations (including 'Example' as synonym for 'Scenario' in `en`)
4.5.1 / 2017-08-30
==================
* Fix regression in `PathsFilter`
4.5.0 / 2017-08-30
==================
* Sync i18n with Cucumber Gherkin
* Drop support for HHVM tests on Travis
* Add `TableNode::fromList()` method (thanks @TravisCarden)
* Add `ExampleNode::getOutlineTitle()` method (thanks @duxet)
* Use realpath, so the feature receives the cwd prefixed (thanks @glennunipro)
* Explicitly handle non-two-dimensional arrays in TableNode (thanks @TravisCarden)
* Fix to line/linefilter scenario runs which take relative paths to files (thanks @generalconsensus)
4.4.5 / 2016-10-30
==================
* Fix partial paths matching in `PathsFilter`
4.4.4 / 2016-09-18
==================
* Provide clearer exception for non-writeable cache directories
4.4.3 / 2016-09-18
==================
* Ensure we reset tags between features
4.4.2 / 2016-09-03
==================
* Sync 18n with gherkin 3
4.4.1 / 2015-12-30
==================
* Ensure keywords are trimmed when syncing translations
* Sync 18n with cucumber
4.4.0 / 2015-09-19
==================
* Added validation enforcing that all rows of a `TableNode` have the same number of columns
* Added `TableNode::getColumn` to get a column from the table
* Sync 18n with cucumber
4.3.0 / 2014-06-06
==================
* Added `setFilters(array)` method to `Gherkin` class
* Added `NarrativeFilter` for non-english `RoleFilter` lovers
4.2.1 / 2014-06-06
==================
* Fix parsing of features without line feed at the end
4.2.0 / 2014-05-27
==================
* Added `getKeyword()` and `getKeywordType()` methods to `StepNode`, deprecated `getType()`.
Thanks to @kibao
4.1.3 / 2014-05-25
==================
* Properly handle tables with rows terminating in whitespace
4.1.2 / 2014-05-14
==================
* Handle case where Gherkin cache is broken
4.1.1 / 2014-05-05
==================
* Fixed the compatibility with PHP 5.6-beta by avoiding to use the broken PHP array function
* The YamlFileLoader no longer extend from ArrayLoader but from AbstractFileLoader
4.1.0 / 2014-04-20
==================
* Fixed scenario tag filtering
* Do not allow multiple multiline step arguments
* Sync 18n with cucumber
4.0.0 / 2014-01-05
==================
* Changed the behavior when no loader can be found for the resource. Instead of throwing an exception, the
Gherkin class now returns an empty array.
3.1.3 / 2014-01-04
==================
* Dropped the dependency on the Symfony Finder by using SPL iterators directly
* Added testing on HHVM on Travis. HHVM is officially supported (previous release was actually already compatible)
3.1.2 / 2014-01-01
==================
* All paths passed to PathsFilter are converted using realpath
3.1.1 / 2013-12-31
==================
* Add `ComplexFilterInterace` that has complex behavior for scenarios and requires to pass
feature too
* `TagFilter` is an instance of a `ComplexFilterInterace` now
3.1.0 / 2013-12-31
==================
* Example node is a scenario
* Nodes do not have uprefs (memory usage fix)
* Scenario filters do not depend on feature nodes
3.0.5 / 2014-01-01
==================
* All paths passed to PathsFilter are converted using realpath
3.0.4 / 2013-12-31
==================
* TableNode is now traversable using foreach
* All possibly thrown exceptions implement Gherkin\Exception interface
* Sync i18n with cucumber
3.0.3 / 2013-09-15
==================
* Extend ExampleNode with additional methods
3.0.2 / 2013-09-14
==================
* Extract `KeywordNodeInterface` and `ScenarioLikeInterface`
* Add `getIndex()` methods to scenarios, outlines, steps and examples
* Throw proper exception for fractured node tree
3.0.1 / 2013-09-14
==================
* Use versioned subfolder in FileCache
3.0.0 / 2013-09-14
==================
* A lot of optimizations in Parser and Lexer
* Node tree is now immutable by nature (no setters)
* Example nodes are now part of the node tree. They are lazily generated by Outline node
* Sync with latest cucumber i18n
2.3.4 / 2013-08-11
==================
* Fix leaks in memory cache
2.3.3 / 2013-08-11
==================
* Fix encoding bug introduced with previous release
* Sync i18n with cucumber
2.3.2 / 2013-08-11
==================
* Explicitly use utf8 encoding
2.3.1 / 2013-08-10
==================
* Support `an` prefix with RoleFilter
2.3.0 / 2013-08-04
==================
* Add RoleFilter
* Add PathsFilter
* Add MemoryCache
2.2.9 / 2013-03-02
==================
* Fix dependency version requirement
2.2.8 / 2013-03-02
==================
* Features filtering behavior change. Now emptified (by filtering) features
that do not match filter themselves are removed from resultset.
* Small potential bug fix in TableNode
2.2.7 / 2013-01-27
==================
* Fixed bug in i18n syncing script
* Resynced Gherkin i18n
2.2.6 / 2013-01-26
==================
* Support long row hashes in tables ([see](https://github.com/Behat/Gherkin/issues/40))
* Synced Gherkin i18n
2.2.5 / 2012-09-26
==================
* Fixed issue with loading empty features
* Synced Gherkin i18n
2.2.4 / 2012-08-03
==================
* Fixed exception message for "no loader found"
2.2.3 / 2012-08-03
==================
* Fixed minor loader bug with empty base path
* Synced Gherkin i18n
2.2.2 / 2012-07-01
==================
* Added ability to filter outline scenarios by line and range filters
* Synced Gherkin i18n
* Refactored table parser to read row line numbers too
2.2.1 / 2012-05-04
==================
* Fixed StepNode `getLanguage()` and `getFile()`
2.2.0 / 2012-05-03
==================
* Features freeze after parsing
* Implemented GherkinDumper (@Halleck45)
* Synced i18n with Cucumber
* Updated inline documentation
2.1.1 / 2012-03-09
==================
* Fixed caching bug, where `isFresh()` always returned false
2.1.0 / 2012-03-09
==================
* Added parser caching layer
* Added support for table delimiter escaping (use `\|` for that)
* Added LineRangeFilter (thanks @headrevision)
* Synced i18n dictionary with cucumber/gherkin
2.0.2 / 2012-02-04
==================
* Synced i18n dictionary with cucumber/gherkin
2.0.1 / 2012-01-26
==================
* Fixed issue about parsing features without indentation
2.0.0 / 2012-01-19
==================
* Background titles support
* Correct parsing of titles/descriptions (hirarchy lexing)
* Migration to the cucumber/gherkin i18n dictionary
* Speed optimizations
* Refactored KeywordsDumper
* New loaders
* Bugfixes
1.1.4 / 2012-01-08
==================
* Read feature description even if it looks like a step
1.1.3 / 2011-12-14
==================
* Removed file loading routines from Parser (fixes `is_file()` issue on some systems - thanks
@flodocteurklein)
1.1.2 / 2011-12-01
==================
* Updated spanish trasnaltion (@anbotero)
* Integration with Composer and Travis CI
1.1.1 / 2011-07-29
==================
* Updated pt language step types (@danielcsgomes)
* Updated vendors
1.1.0 / 2011-07-16
==================
* Return all tags, including inherited in `Scenario::getTags()`
* New `Feature::getOwnTags()` and `Scenario::getOwnTags()` method added,
which returns only own tags
1.0.8 / 2011-06-29
==================
* Fixed comments parsing.
You can’t have comments at the end of a line # like this
# But you can still have comments at the beginning of a line
1.0.7 / 2011-06-28
==================
* Added `getRaw()` method to PyStringNode
* Updated vendors
1.0.6 / 2011-06-17
==================
* Updated vendors
1.0.5 / 2011-06-10
==================
* Fixed bug, introduced with 1.0.4 - hash in PyStrings
1.0.4 / 2011-06-10
==================
* Fixed inability to comment pystrings
1.0.3 / 2011-04-21
==================
* Fixed introduced with 1.0.2 pystring parsing bug
1.0.2 / 2011-04-18
==================
* Fixed bugs in text with comments parsing
1.0.1 / 2011-04-01
==================
* Updated vendors
1.0.0 / 2011-03-08
==================
* Updated vendors
1.0.0RC2 / 2011-02-25
=====================
* Windows support
* Missing phpunit config
1.0.0RC1 / 2011-02-15
=====================
* Huge optimizations to Lexer & Parser
* Additional loaders (Yaml, Array, Directory)
* Filters (Tag, Name, Line)
* Code refactoring
* Nodes optimizations
* Additional tests for exceptions and translations
* Keywords dumper
0.2.0 / 2011-01-05
==================
* New Parser & Lexer (based on AST)
* New verbose parsing exception handling
* New translation mechanics
* 47 brand new translations (see i18n)
* Full test suite for everything from AST nodes to translations
+33
View File
@@ -0,0 +1,33 @@
Contributing
------------
Gherkin is an open source, community-driven project. If you'd like to contribute, feel free to do this, but remember to follow this few simple rules:
- Make your feature addition or bug fix,
- Always use the `master` branch as base for your changes (all new development happens in `master`),
- Add tests for those changes (please look into `tests/` folder for some examples). This is important so we don't break it in a future version unintentionally,
- Commit your code, but do not mess with `CHANGES.md`,
- __Remember__: when you create Pull Request, always select `master` branch as target (done by default), otherwise it will be closed.
Running tests
-------------
Make sure that you don't break anything with your changes by running:
```bash
$> phpunit
```
Contributing to Gherkin Translations
------------------------------------
Gherkin supports →40 different languages and you could add more! You might notice
`i18n.php` file in the root of the library. This file is downloaded and **autogenerated**
from original [cucumber/gherkin translations](https://github.com/cucumber/cucumber/blob/master/gherkin/gherkin-languages.json).
So, in order to fix/update/add some translation, you should send Pull Request to the
`cucumber/gherkin` repository. `Behat\Gherkin` will redownload/regenerate translations
from there before each release.
It might sounds difficult, but this way of dictionary sharing gives you ability to
migrate your `*.feature` files from language to language and library to library without
the need to rewrite/modify them - same dictionary (Gherkin) used everywhere.
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2011-2013 Konstantin Kudryashov <ever.zet@gmail.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+64
View File
@@ -0,0 +1,64 @@
Behat Gherkin Parser
====================
This is the php Gherkin parser for Behat. It comes bundled with more than 40 native languages
(see `i18n.php`) support & clean architecture.
Useful Links
------------
- Official Google Group is at [http://groups.google.com/group/behat](http://groups.google.com/group/behat)
- IRC channel on [#freenode](http://freenode.net/) is `#behat`
- [Note on Patches/Pull Requests](CONTRIBUTING.md)
Usage Example
-------------
``` php
<?php
$keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array(
'en' => array(
'feature' => 'Feature',
'background' => 'Background',
'scenario' => 'Scenario',
'scenario_outline' => 'Scenario Outline|Scenario Template',
'examples' => 'Examples|Scenarios',
'given' => 'Given',
'when' => 'When',
'then' => 'Then',
'and' => 'And',
'but' => 'But'
),
'en-pirate' => array(
'feature' => 'Ahoy matey!',
'background' => 'Yo-ho-ho',
'scenario' => 'Heave to',
'scenario_outline' => 'Shiver me timbers',
'examples' => 'Dead men tell no tales',
'given' => 'Gangway!',
'when' => 'Blimey!',
'then' => 'Let go and haul',
'and' => 'Aye',
'but' => 'Avast!'
)
));
$lexer = new Behat\Gherkin\Lexer($keywords);
$parser = new Behat\Gherkin\Parser($lexer);
$feature = $parser->parse(file_get_contents('some.feature'));
```
Installing Dependencies
-----------------------
``` bash
$> curl http://getcomposer.org/installer | php
$> php composer.phar update
```
Contributors
------------
* Konstantin Kudryashov [everzet](http://github.com/everzet) [lead developer]
* Other [awesome developers](https://github.com/Behat/Gherkin/graphs/contributors)
+67
View File
@@ -0,0 +1,67 @@
{
"name": "behat/gherkin",
"description": "Gherkin DSL parser for PHP",
"keywords": ["BDD", "parser", "DSL", "Behat", "Gherkin", "Cucumber"],
"homepage": "http://behat.org/",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
}
],
"require": {
"php": "~7.2|~8.0"
},
"require-dev": {
"symfony/yaml": "~3|~4|~5",
"phpunit/phpunit": "~8|~9",
"cucumber/cucumber": "dev-gherkin-24.0.0"
},
"suggest": {
"symfony/yaml": "If you want to parse features, represented in YAML files"
},
"autoload": {
"psr-0": {
"Behat\\Gherkin": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Behat\\": "tests/Behat/"
}
},
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
}
},
"repositories": [
{
"type": "package",
"package": {
"name": "cucumber/cucumber",
"version": "dev-gherkin-24.0.0",
"source": {
"type": "git",
"url": "https://github.com/cucumber/cucumber.git",
"reference": "effd4cf5273798c74793a63f9c88a14a1047346d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cucumber/cucumber/zipball/effd4cf5273798c74793a63f9c88a14a1047346d",
"reference": "effd4cf5273798c74793a63f9c88a14a1047346d"
}
}
}
]
}
+1245
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
<?php
return __DIR__;
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Cache;
use Behat\Gherkin\Node\FeatureNode;
/**
* Parser cache interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface CacheInterface
{
/**
* Checks that cache for feature exists and is fresh.
*
* @param string $path Feature path
* @param integer $timestamp The last time feature was updated
*
* @return bool
*/
public function isFresh($path, $timestamp);
/**
* Reads feature cache from path.
*
* @param string $path Feature path
*
* @return FeatureNode
*/
public function read($path);
/**
* Caches feature node.
*
* @param string $path Feature path
* @param FeatureNode $feature Feature instance
*/
public function write($path, FeatureNode $feature);
}
@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Cache;
use Behat\Gherkin\Exception\CacheException;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Gherkin;
/**
* File cache.
* Caches feature into a file.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class FileCache implements CacheInterface
{
private $path;
/**
* Initializes file cache.
*
* @param string $path Path to the folder where to store caches.
*
* @throws CacheException
*/
public function __construct($path)
{
$this->path = rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'v'.Gherkin::VERSION;
if (!is_dir($this->path)) {
@mkdir($this->path, 0777, true);
}
if (!is_writeable($this->path)) {
throw new CacheException(sprintf('Cache path "%s" is not writeable. Check your filesystem permissions or disable Gherkin file cache.', $this->path));
}
}
/**
* Checks that cache for feature exists and is fresh.
*
* @param string $path Feature path
* @param integer $timestamp The last time feature was updated
*
* @return bool
*/
public function isFresh($path, $timestamp)
{
$cachePath = $this->getCachePathFor($path);
if (!file_exists($cachePath)) {
return false;
}
return filemtime($cachePath) > $timestamp;
}
/**
* Reads feature cache from path.
*
* @param string $path Feature path
*
* @return FeatureNode
*
* @throws CacheException
*/
public function read($path)
{
$cachePath = $this->getCachePathFor($path);
$feature = unserialize(file_get_contents($cachePath));
if (!$feature instanceof FeatureNode) {
throw new CacheException(sprintf('Can not load cache for a feature "%s" from "%s".', $path, $cachePath ));
}
return $feature;
}
/**
* Caches feature node.
*
* @param string $path Feature path
* @param FeatureNode $feature Feature instance
*/
public function write($path, FeatureNode $feature)
{
file_put_contents($this->getCachePathFor($path), serialize($feature));
}
/**
* Returns feature cache file path from features path.
*
* @param string $path Feature path
*
* @return string
*/
protected function getCachePathFor($path)
{
return $this->path.'/'.md5($path).'.feature.cache';
}
}
@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Cache;
use Behat\Gherkin\Node\FeatureNode;
/**
* Memory cache.
* Caches feature into a memory.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class MemoryCache implements CacheInterface
{
private $features = array();
private $timestamps = array();
/**
* Checks that cache for feature exists and is fresh.
*
* @param string $path Feature path
* @param integer $timestamp The last time feature was updated
*
* @return bool
*/
public function isFresh($path, $timestamp)
{
if (!isset($this->features[$path])) {
return false;
}
return $this->timestamps[$path] > $timestamp;
}
/**
* Reads feature cache from path.
*
* @param string $path Feature path
*
* @return FeatureNode
*/
public function read($path)
{
return $this->features[$path];
}
/**
* Caches feature node.
*
* @param string $path Feature path
* @param FeatureNode $feature Feature instance
*/
public function write($path, FeatureNode $feature)
{
$this->features[$path] = $feature;
$this->timestamps[$path] = time();
}
}
@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Exception;
use RuntimeException;
/**
* Cache exception.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CacheException extends RuntimeException implements Exception
{
}
@@ -0,0 +1,15 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Exception;
interface Exception
{
}
@@ -0,0 +1,17 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Exception;
use RuntimeException;
class LexerException extends RuntimeException implements Exception
{
}
@@ -0,0 +1,17 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Exception;
use RuntimeException;
class NodeException extends RuntimeException implements Exception
{
}
@@ -0,0 +1,17 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Exception;
use RuntimeException;
class ParserException extends RuntimeException implements Exception
{
}
@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\FeatureNode;
/**
* Abstract filter class.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
abstract class ComplexFilter implements ComplexFilterInterface
{
/**
* Filters feature according to the filter.
*
* @param FeatureNode $feature
*
* @return FeatureNode
*/
public function filterFeature(FeatureNode $feature)
{
$scenarios = array();
foreach ($feature->getScenarios() as $scenario) {
if (!$this->isScenarioMatch($feature, $scenario)) {
continue;
}
$scenarios[] = $scenario;
}
return new FeatureNode(
$feature->getTitle(),
$feature->getDescription(),
$feature->getTags(),
$feature->getBackground(),
$scenarios,
$feature->getKeyword(),
$feature->getLanguage(),
$feature->getFile(),
$feature->getLine()
);
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioInterface;
/**
* Filter interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ComplexFilterInterface extends FeatureFilterInterface
{
/**
* Checks if scenario or outline matches specified filter.
*
* @param FeatureNode $feature Feature node instance
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return bool
*/
public function isScenarioMatch(FeatureNode $feature, ScenarioInterface $scenario);
}
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\FeatureNode;
/**
* Feature filter interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface FeatureFilterInterface
{
/**
* Checks if Feature matches specified filter.
*
* @param FeatureNode $feature Feature instance
*
* @return bool
*/
public function isFeatureMatch(FeatureNode $feature);
/**
* Filters feature according to the filter and returns new one.
*
* @param FeatureNode $feature
*
* @return FeatureNode
*/
public function filterFeature(FeatureNode $feature);
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\ScenarioInterface;
/**
* Filter interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface FilterInterface extends FeatureFilterInterface
{
/**
* Checks if scenario or outline matches specified filter.
*
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return bool
*/
public function isScenarioMatch(ScenarioInterface $scenario);
}
@@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\ExampleTableNode;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Gherkin\Node\ScenarioInterface;
/**
* Filters scenarios by definition line number.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class LineFilter implements FilterInterface
{
protected $filterLine;
/**
* Initializes filter.
*
* @param string $filterLine Line of the scenario to filter on
*/
public function __construct($filterLine)
{
$this->filterLine = intval($filterLine);
}
/**
* Checks if Feature matches specified filter.
*
* @param FeatureNode $feature Feature instance
*
* @return bool
*/
public function isFeatureMatch(FeatureNode $feature)
{
return $this->filterLine === $feature->getLine();
}
/**
* Checks if scenario or outline matches specified filter.
*
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return bool
*/
public function isScenarioMatch(ScenarioInterface $scenario)
{
if ($this->filterLine === $scenario->getLine()) {
return true;
}
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
return $this->filterLine === $scenario->getLine()
|| in_array($this->filterLine, $scenario->getExampleTable()->getLines());
}
return false;
}
/**
* Filters feature according to the filter and returns new one.
*
* @param FeatureNode $feature
*
* @return FeatureNode
*/
public function filterFeature(FeatureNode $feature)
{
$scenarios = array();
foreach ($feature->getScenarios() as $scenario) {
if (!$this->isScenarioMatch($scenario)) {
continue;
}
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
foreach ($scenario->getExampleTables() as $exampleTable) {
$table = $exampleTable->getTable();
$lines = array_keys($table);
if (in_array($this->filterLine, $lines)) {
$filteredTable = array($lines[0] => $table[$lines[0]]);
if ($lines[0] !== $this->filterLine) {
$filteredTable[$this->filterLine] = $table[$this->filterLine];
}
$scenario = new OutlineNode(
$scenario->getTitle(),
$scenario->getTags(),
$scenario->getSteps(),
array(new ExampleTableNode($filteredTable, $exampleTable->getKeyword(), $exampleTable->getTags())),
$scenario->getKeyword(),
$scenario->getLine()
);
}
}
}
$scenarios[] = $scenario;
}
return new FeatureNode(
$feature->getTitle(),
$feature->getDescription(),
$feature->getTags(),
$feature->getBackground(),
$scenarios,
$feature->getKeyword(),
$feature->getLanguage(),
$feature->getFile(),
$feature->getLine()
);
}
}
@@ -0,0 +1,143 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\ExampleTableNode;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Gherkin\Node\ScenarioInterface;
/**
* Filters scenarios by definition line number range.
*
* @author Fabian Kiss <headrevision@gmail.com>
*/
class LineRangeFilter implements FilterInterface
{
protected $filterMinLine;
protected $filterMaxLine;
/**
* Initializes filter.
*
* @param string $filterMinLine Minimum line of a scenario to filter on
* @param string $filterMaxLine Maximum line of a scenario to filter on
*/
public function __construct($filterMinLine, $filterMaxLine)
{
$this->filterMinLine = intval($filterMinLine);
if ($filterMaxLine == '*') {
$this->filterMaxLine = PHP_INT_MAX;
} else {
$this->filterMaxLine = intval($filterMaxLine);
}
}
/**
* Checks if Feature matches specified filter.
*
* @param FeatureNode $feature Feature instance
*
* @return bool
*/
public function isFeatureMatch(FeatureNode $feature)
{
return $this->filterMinLine <= $feature->getLine()
&& $this->filterMaxLine >= $feature->getLine();
}
/**
* Checks if scenario or outline matches specified filter.
*
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return bool
*/
public function isScenarioMatch(ScenarioInterface $scenario)
{
if ($this->filterMinLine <= $scenario->getLine() && $this->filterMaxLine >= $scenario->getLine()) {
return true;
}
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
foreach ($scenario->getExampleTable()->getLines() as $line) {
if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) {
return true;
}
}
}
return false;
}
/**
* Filters feature according to the filter.
*
* @param FeatureNode $feature
*
* @return FeatureNode
*/
public function filterFeature(FeatureNode $feature)
{
$scenarios = array();
foreach ($feature->getScenarios() as $scenario) {
if (!$this->isScenarioMatch($scenario)) {
continue;
}
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
// first accumulate examples and then create scenario
$exampleTableNodes = array();
foreach ($scenario->getExampleTables() as $exampleTable) {
$table = $exampleTable->getTable();
$lines = array_keys($table);
$filteredTable = array($lines[0] => $table[$lines[0]]);
unset($table[$lines[0]]);
foreach ($table as $line => $row) {
if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) {
$filteredTable[$line] = $row;
}
}
if (count($filteredTable) > 1) {
$exampleTableNodes[] = new ExampleTableNode($filteredTable, $exampleTable->getKeyword(), $exampleTable->getTags());
}
}
$scenario = new OutlineNode(
$scenario->getTitle(),
$scenario->getTags(),
$scenario->getSteps(),
$exampleTableNodes,
$scenario->getKeyword(),
$scenario->getLine()
);
}
$scenarios[] = $scenario;
}
return new FeatureNode(
$feature->getTitle(),
$feature->getDescription(),
$feature->getTags(),
$feature->getBackground(),
$scenarios,
$feature->getKeyword(),
$feature->getLanguage(),
$feature->getFile(),
$feature->getLine()
);
}
}
@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioInterface;
/**
* Filters scenarios by feature/scenario name.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class NameFilter extends SimpleFilter
{
protected $filterString;
/**
* Initializes filter.
*
* @param string $filterString Name filter string
*/
public function __construct($filterString)
{
$this->filterString = trim($filterString);
}
/**
* Checks if Feature matches specified filter.
*
* @param FeatureNode $feature Feature instance
*
* @return bool
*/
public function isFeatureMatch(FeatureNode $feature)
{
if (null === $feature->getTitle()) {
return false;
}
if ('/' === $this->filterString[0]) {
return 1 === preg_match($this->filterString, $feature->getTitle());
}
return false !== mb_strpos($feature->getTitle(), $this->filterString, 0, 'utf8');
}
/**
* Checks if scenario or outline matches specified filter.
*
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return bool
*/
public function isScenarioMatch(ScenarioInterface $scenario)
{
if (null === $scenario->getTitle()) {
return false;
}
if ('/' === $this->filterString[0] && 1 === preg_match($this->filterString, $scenario->getTitle())) {
return true;
} elseif (false !== mb_strpos($scenario->getTitle(), $this->filterString, 0, 'utf8')) {
return true;
}
return false;
}
}
@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\ScenarioInterface;
use Behat\Gherkin\Node\FeatureNode;
/**
* Filters features by their narrative using regular expression.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class NarrativeFilter extends SimpleFilter
{
/**
* @var string
*/
private $regex;
/**
* Initializes filter.
*
* @param string $regex
*/
public function __construct($regex)
{
$this->regex = $regex;
}
/**
* Checks if Feature matches specified filter.
*
* @param FeatureNode $feature Feature instance
*
* @return bool
*/
public function isFeatureMatch(FeatureNode $feature)
{
return 1 === preg_match($this->regex, $feature->getDescription() ?? '');
}
/**
* Checks if scenario or outline matches specified filter.
*
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return bool
*/
public function isScenarioMatch(ScenarioInterface $scenario)
{
return false;
}
}
@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioInterface;
/**
* Filters features by their paths.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class PathsFilter extends SimpleFilter
{
protected $filterPaths = array();
/**
* Initializes filter.
*
* @param string[] $paths List of approved paths
*/
public function __construct(array $paths)
{
$this->filterPaths = array_map(
function ($realpath) {
return rtrim($realpath, DIRECTORY_SEPARATOR) .
(is_dir($realpath) ? DIRECTORY_SEPARATOR : '');
},
array_filter(
array_map('realpath', $paths)
)
);
}
/**
* Checks if Feature matches specified filter.
*
* @param FeatureNode $feature Feature instance
*
* @return bool
*/
public function isFeatureMatch(FeatureNode $feature)
{
foreach ($this->filterPaths as $path) {
if (0 === strpos(realpath($feature->getFile()), $path)) {
return true;
}
}
return false;
}
/**
* Checks if scenario or outline matches specified filter.
*
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return false This filter is designed to work only with features
*/
public function isScenarioMatch(ScenarioInterface $scenario)
{
return false;
}
}
@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioInterface;
/**
* Filters features by their actors role.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class RoleFilter extends SimpleFilter
{
protected $pattern;
/**
* Initializes filter.
*
* @param string $role Approved role wildcard
*/
public function __construct($role)
{
$this->pattern = '/as an? ' . strtr(preg_quote($role, '/'), array(
'\*' => '.*',
'\?' => '.',
'\[' => '[',
'\]' => ']'
)) . '[$\n]/i';
}
/**
* Checks if Feature matches specified filter.
*
* @param FeatureNode $feature Feature instance
*
* @return bool
*/
public function isFeatureMatch(FeatureNode $feature)
{
return 1 === preg_match($this->pattern, $feature->getDescription() ?? '');
}
/**
* Checks if scenario or outline matches specified filter.
*
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return false This filter is designed to work only with features
*/
public function isScenarioMatch(ScenarioInterface $scenario)
{
return false;
}
}
@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\FeatureNode;
/**
* Abstract filter class.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
abstract class SimpleFilter implements FilterInterface
{
/**
* Filters feature according to the filter.
*
* @param FeatureNode $feature
*
* @return FeatureNode
*/
public function filterFeature(FeatureNode $feature)
{
if ($this->isFeatureMatch($feature)) {
return $feature;
}
$scenarios = array();
foreach ($feature->getScenarios() as $scenario) {
if (!$this->isScenarioMatch($scenario)) {
continue;
}
$scenarios[] = $scenario;
}
return new FeatureNode(
$feature->getTitle(),
$feature->getDescription(),
$feature->getTags(),
$feature->getBackground(),
$scenarios,
$feature->getKeyword(),
$feature->getLanguage(),
$feature->getFile(),
$feature->getLine()
);
}
}
@@ -0,0 +1,163 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Filter;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Gherkin\Node\ScenarioInterface;
/**
* Filters scenarios by feature/scenario tag.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class TagFilter extends ComplexFilter
{
protected $filterString;
/**
* Initializes filter.
*
* @param string $filterString Name filter string
*/
public function __construct($filterString)
{
$this->filterString = trim($filterString);
if(preg_match('/\s/u', $this->filterString)) {
trigger_error(
"Tags with whitespace are deprecated and may be removed in a future version",
E_USER_DEPRECATED
);
}
}
/**
* Filters feature according to the filter.
*
* @param FeatureNode $feature
*
* @return FeatureNode
*/
public function filterFeature(FeatureNode $feature)
{
$scenarios = array();
foreach ($feature->getScenarios() as $scenario) {
if (!$this->isScenarioMatch($feature, $scenario)) {
continue;
}
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
$exampleTables = array();
foreach ($scenario->getExampleTables() as $exampleTable) {
if ($this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags(), $exampleTable->getTags()))) {
$exampleTables[] = $exampleTable;
}
}
$scenario = new OutlineNode(
$scenario->getTitle(),
$scenario->getTags(),
$scenario->getSteps(),
$exampleTables,
$scenario->getKeyword(),
$scenario->getLine()
);
}
$scenarios[] = $scenario;
}
return new FeatureNode(
$feature->getTitle(),
$feature->getDescription(),
$feature->getTags(),
$feature->getBackground(),
$scenarios,
$feature->getKeyword(),
$feature->getLanguage(),
$feature->getFile(),
$feature->getLine()
);
}
/**
* Checks if Feature matches specified filter.
*
* @param FeatureNode $feature Feature instance
*
* @return bool
*/
public function isFeatureMatch(FeatureNode $feature)
{
return $this->isTagsMatchCondition($feature->getTags());
}
/**
* Checks if scenario or outline matches specified filter.
*
* @param FeatureNode $feature Feature node instance
* @param ScenarioInterface $scenario Scenario or Outline node instance
*
* @return bool
*/
public function isScenarioMatch(FeatureNode $feature, ScenarioInterface $scenario)
{
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
foreach ($scenario->getExampleTables() as $example) {
if ($this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags(), $example->getTags()))) {
return true;
}
}
return false;
}
return $this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags()));
}
/**
* Checks that node matches condition.
*
* @param string[] $tags
*
* @return bool
*/
protected function isTagsMatchCondition($tags)
{
$satisfies = true;
if (\strlen($this->filterString) === 0) {
return $satisfies;
}
foreach (explode('&&', $this->filterString) as $andTags) {
$satisfiesComma = false;
foreach (explode(',', $andTags) as $tag) {
$tag = str_replace('@', '', trim($tag));
if ('~' === $tag[0]) {
$tag = mb_substr($tag, 1, mb_strlen($tag, 'utf8') - 1, 'utf8');
$satisfiesComma = !in_array($tag, $tags) || $satisfiesComma;
} else {
$satisfiesComma = in_array($tag, $tags) || $satisfiesComma;
}
}
$satisfies = $satisfiesComma && $satisfies;
}
return $satisfies;
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin;
use Behat\Gherkin\Filter\FeatureFilterInterface;
use Behat\Gherkin\Filter\LineFilter;
use Behat\Gherkin\Filter\LineRangeFilter;
use Behat\Gherkin\Loader\FileLoaderInterface;
use Behat\Gherkin\Loader\LoaderInterface;
/**
* Gherkin manager.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class Gherkin
{
const VERSION = '4.8.0';
/**
* @var LoaderInterface[]
*/
protected $loaders = array();
/**
* @var FeatureFilterInterface[]
*/
protected $filters = array();
/**
* Adds loader to manager.
*
* @param LoaderInterface $loader Feature loader
*/
public function addLoader(LoaderInterface $loader)
{
$this->loaders[] = $loader;
}
/**
* Adds filter to manager.
*
* @param FeatureFilterInterface $filter Feature filter
*/
public function addFilter(FeatureFilterInterface $filter)
{
$this->filters[] = $filter;
}
/**
* Sets filters to the parser.
*
* @param FeatureFilterInterface[] $filters
*/
public function setFilters(array $filters)
{
$this->filters = array();
array_map(array($this, 'addFilter'), $filters);
}
/**
* Sets base features path.
*
* @param string $path Loaders base path
*/
public function setBasePath($path)
{
foreach ($this->loaders as $loader) {
if ($loader instanceof FileLoaderInterface) {
$loader->setBasePath($path);
}
}
}
/**
* Loads & filters resource with added loaders.
*
* @param mixed $resource Resource to load
* @param FeatureFilterInterface[] $filters Additional filters
*
* @return array
*/
public function load($resource, array $filters = array())
{
$filters = array_merge($this->filters, $filters);
$matches = array();
if (preg_match('/^(.*)\:(\d+)-(\d+|\*)$/', $resource, $matches)) {
$resource = $matches[1];
$filters[] = new LineRangeFilter($matches[2], $matches[3]);
} elseif (preg_match('/^(.*)\:(\d+)$/', $resource, $matches)) {
$resource = $matches[1];
$filters[] = new LineFilter($matches[2]);
}
$loader = $this->resolveLoader($resource);
if (null === $loader) {
return array();
}
$features = array();
foreach ($loader->load($resource) as $feature) {
foreach ($filters as $filter) {
$feature = $filter->filterFeature($feature);
if (!$feature->hasScenarios() && !$filter->isFeatureMatch($feature)) {
continue 2;
}
}
$features[] = $feature;
}
return $features;
}
/**
* Resolves loader by resource.
*
* @param mixed $resource Resource to load
*
* @return LoaderInterface
*/
public function resolveLoader($resource)
{
foreach ($this->loaders as $loader) {
if ($loader->supports($resource)) {
return $loader;
}
}
return null;
}
}
@@ -0,0 +1,200 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Keywords;
/**
* Array initializable keywords holder.
*
* $keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array(
* 'en' => array(
* 'feature' => 'Feature',
* 'background' => 'Background',
* 'scenario' => 'Scenario',
* 'scenario_outline' => 'Scenario Outline|Scenario Template',
* 'examples' => 'Examples|Scenarios',
* 'given' => 'Given',
* 'when' => 'When',
* 'then' => 'Then',
* 'and' => 'And',
* 'but' => 'But'
* ),
* 'ru' => array(
* 'feature' => 'Функционал',
* 'background' => 'Предыстория',
* 'scenario' => 'Сценарий',
* 'scenario_outline' => 'Структура сценария',
* 'examples' => 'Примеры',
* 'given' => 'Допустим',
* 'when' => 'Если',
* 'then' => 'То',
* 'and' => 'И',
* 'but' => 'Но'
* )
* ));
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ArrayKeywords implements KeywordsInterface
{
private $keywords = array();
private $keywordString = array();
private $language;
/**
* Initializes holder with keywords.
*
* @param array $keywords Keywords array
*/
public function __construct(array $keywords)
{
$this->keywords = $keywords;
}
/**
* Sets keywords holder language.
*
* @param string $language Language name
*/
public function setLanguage($language)
{
if (!isset($this->keywords[$language])) {
$this->language = 'en';
} else {
$this->language = $language;
}
}
/**
* Returns Feature keywords (splitted by "|").
*
* @return string
*/
public function getFeatureKeywords()
{
return $this->keywords[$this->language]['feature'];
}
/**
* Returns Background keywords (splitted by "|").
*
* @return string
*/
public function getBackgroundKeywords()
{
return $this->keywords[$this->language]['background'];
}
/**
* Returns Scenario keywords (splitted by "|").
*
* @return string
*/
public function getScenarioKeywords()
{
return $this->keywords[$this->language]['scenario'];
}
/**
* Returns Scenario Outline keywords (splitted by "|").
*
* @return string
*/
public function getOutlineKeywords()
{
return $this->keywords[$this->language]['scenario_outline'];
}
/**
* Returns Examples keywords (splitted by "|").
*
* @return string
*/
public function getExamplesKeywords()
{
return $this->keywords[$this->language]['examples'];
}
/**
* Returns Given keywords (splitted by "|").
*
* @return string
*/
public function getGivenKeywords()
{
return $this->keywords[$this->language]['given'];
}
/**
* Returns When keywords (splitted by "|").
*
* @return string
*/
public function getWhenKeywords()
{
return $this->keywords[$this->language]['when'];
}
/**
* Returns Then keywords (splitted by "|").
*
* @return string
*/
public function getThenKeywords()
{
return $this->keywords[$this->language]['then'];
}
/**
* Returns And keywords (splitted by "|").
*
* @return string
*/
public function getAndKeywords()
{
return $this->keywords[$this->language]['and'];
}
/**
* Returns But keywords (splitted by "|").
*
* @return string
*/
public function getButKeywords()
{
return $this->keywords[$this->language]['but'];
}
/**
* Returns all step keywords (Given, When, Then, And, But).
*
* @return string
*/
public function getStepKeywords()
{
if (!isset($this->keywordString[$this->language])) {
$keywords = array_merge(
explode('|', $this->getGivenKeywords()),
explode('|', $this->getWhenKeywords()),
explode('|', $this->getThenKeywords()),
explode('|', $this->getAndKeywords()),
explode('|', $this->getButKeywords())
);
usort($keywords, function ($keyword1, $keyword2) {
return mb_strlen($keyword2, 'utf8') - mb_strlen($keyword1, 'utf8');
});
$this->keywordString[$this->language] = implode('|', $keywords);
}
return $this->keywordString[$this->language];
}
}
@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Keywords;
/**
* File initializable keywords holder.
*
* $keywords = new Behat\Gherkin\Keywords\CachedArrayKeywords($file);
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CachedArrayKeywords extends ArrayKeywords
{
/**
* Initializes holder with file.
*
* @param string $file Cached array path
*/
public function __construct($file)
{
parent::__construct(include($file));
}
}
@@ -0,0 +1,121 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Keywords;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
/**
* Cucumber-translations reader.
*
* $keywords = new Behat\Gherkin\Keywords\CucumberKeywords($i18nYmlPath);
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CucumberKeywords extends ArrayKeywords
{
/**
* Initializes holder with yaml string OR file.
*
* @param string $yaml Yaml string or file path
*/
public function __construct($yaml)
{
// Handle filename explicitly for BC reasons, as Symfony Yaml 3.0 does not do it anymore
$file = null;
if (strpos($yaml, "\n") === false && is_file($yaml)) {
if (false === is_readable($yaml)) {
throw new ParseException(sprintf('Unable to parse "%s" as the file is not readable.', $yaml));
}
$file = $yaml;
$yaml = file_get_contents($file);
}
try {
$content = Yaml::parse($yaml);
} catch (ParseException $e) {
if ($file) {
$e->setParsedFile($file);
}
throw $e;
}
parent::__construct($content);
}
/**
* Returns Feature keywords (splitted by "|").
*
* @return string
*/
public function getGivenKeywords()
{
return $this->prepareStepString(parent::getGivenKeywords());
}
/**
* Returns When keywords (splitted by "|").
*
* @return string
*/
public function getWhenKeywords()
{
return $this->prepareStepString(parent::getWhenKeywords());
}
/**
* Returns Then keywords (splitted by "|").
*
* @return string
*/
public function getThenKeywords()
{
return $this->prepareStepString(parent::getThenKeywords());
}
/**
* Returns And keywords (splitted by "|").
*
* @return string
*/
public function getAndKeywords()
{
return $this->prepareStepString(parent::getAndKeywords());
}
/**
* Returns But keywords (splitted by "|").
*
* @return string
*/
public function getButKeywords()
{
return $this->prepareStepString(parent::getButKeywords());
}
/**
* Trim *| from the begining of the list.
*
* @param string $keywordsString Keywords string
*
* @return string
*/
private function prepareStepString($keywordsString)
{
if (0 === mb_strpos($keywordsString, '*|', 0, 'UTF-8')) {
$keywordsString = mb_substr($keywordsString, 2, mb_strlen($keywordsString, 'utf8') - 2, 'utf8');
}
return $keywordsString;
}
}
@@ -0,0 +1,365 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Keywords;
/**
* Gherkin keywords dumper.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class KeywordsDumper
{
private $keywords;
private $keywordsDumper;
/**
* Initializes dumper.
*
* @param KeywordsInterface $keywords Keywords instance
*/
public function __construct(KeywordsInterface $keywords)
{
$this->keywords = $keywords;
$this->keywordsDumper = array($this, 'dumpKeywords');
}
/**
* Sets keywords mapper function.
*
* Callable should accept 2 arguments (array $keywords and bool $isShort)
*
* @param callable $mapper Mapper function
*/
public function setKeywordsDumperFunction($mapper)
{
$this->keywordsDumper = $mapper;
}
/**
* Defaults keywords dumper.
*
* @param array $keywords Keywords list
* @param bool $isShort Is short version
*
* @return string
*/
public function dumpKeywords(array $keywords, $isShort)
{
if ($isShort) {
return 1 < count($keywords) ? '(' . implode('|', $keywords) . ')' : $keywords[0];
}
return $keywords[0];
}
/**
* Dumps keyworded feature into string.
*
* @param string $language Keywords language
* @param bool $short Dump short version
* @param bool $excludeAsterisk
*
* @return string|array String for short version and array of features for extended
*/
public function dump($language, $short = true, $excludeAsterisk = false)
{
$this->keywords->setLanguage($language);
$languageComment = '';
if ('en' !== $language) {
$languageComment = "# language: $language\n";
}
$keywords = explode('|', $this->keywords->getFeatureKeywords());
if ($short) {
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
return trim($languageComment . $this->dumpFeature($keywords, $short, $excludeAsterisk));
}
$features = array();
foreach ($keywords as $keyword) {
$keyword = call_user_func($this->keywordsDumper, array($keyword), $short);
$features[] = trim($languageComment . $this->dumpFeature($keyword, $short, $excludeAsterisk));
}
return $features;
}
/**
* Dumps feature example.
*
* @param string $keyword Item keyword
* @param bool $short Dump short version?
*
* @return string
*/
protected function dumpFeature($keyword, $short = true, $excludeAsterisk = false)
{
$dump = <<<GHERKIN
{$keyword}: Internal operations
In order to stay secret
As a secret organization
We need to be able to erase past agents' memory
GHERKIN;
// Background
$keywords = explode('|', $this->keywords->getBackgroundKeywords());
if ($short) {
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
$dump .= $this->dumpBackground($keywords, $short, $excludeAsterisk);
} else {
$keyword = call_user_func($this->keywordsDumper, array($keywords[0]), $short);
$dump .= $this->dumpBackground($keyword, $short, $excludeAsterisk);
}
// Scenario
$keywords = explode('|', $this->keywords->getScenarioKeywords());
if ($short) {
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
$dump .= $this->dumpScenario($keywords, $short, $excludeAsterisk);
} else {
foreach ($keywords as $keyword) {
$keyword = call_user_func($this->keywordsDumper, array($keyword), $short);
$dump .= $this->dumpScenario($keyword, $short, $excludeAsterisk);
}
}
// Outline
$keywords = explode('|', $this->keywords->getOutlineKeywords());
if ($short) {
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
$dump .= $this->dumpOutline($keywords, $short, $excludeAsterisk);
} else {
foreach ($keywords as $keyword) {
$keyword = call_user_func($this->keywordsDumper, array($keyword), $short);
$dump .= $this->dumpOutline($keyword, $short, $excludeAsterisk);
}
}
return $dump;
}
/**
* Dumps background example.
*
* @param string $keyword Item keyword
* @param bool $short Dump short version?
*
* @return string
*/
protected function dumpBackground($keyword, $short = true, $excludeAsterisk = false)
{
$dump = <<<GHERKIN
{$keyword}:
GHERKIN;
// Given
$dump .= $this->dumpStep(
$this->keywords->getGivenKeywords(),
'there is agent A',
$short,
$excludeAsterisk
);
// And
$dump .= $this->dumpStep(
$this->keywords->getAndKeywords(),
'there is agent B',
$short,
$excludeAsterisk
);
return $dump . "\n";
}
/**
* Dumps scenario example.
*
* @param string $keyword Item keyword
* @param bool $short Dump short version?
*
* @return string
*/
protected function dumpScenario($keyword, $short = true, $excludeAsterisk = false)
{
$dump = <<<GHERKIN
{$keyword}: Erasing agent memory
GHERKIN;
// Given
$dump .= $this->dumpStep(
$this->keywords->getGivenKeywords(),
'there is agent J',
$short,
$excludeAsterisk
);
// And
$dump .= $this->dumpStep(
$this->keywords->getAndKeywords(),
'there is agent K',
$short,
$excludeAsterisk
);
// When
$dump .= $this->dumpStep(
$this->keywords->getWhenKeywords(),
'I erase agent K\'s memory',
$short,
$excludeAsterisk
);
// Then
$dump .= $this->dumpStep(
$this->keywords->getThenKeywords(),
'there should be agent J',
$short,
$excludeAsterisk
);
// But
$dump .= $this->dumpStep(
$this->keywords->getButKeywords(),
'there should not be agent K',
$short,
$excludeAsterisk
);
return $dump . "\n";
}
/**
* Dumps outline example.
*
* @param string $keyword Item keyword
* @param bool $short Dump short version?
*
* @return string
*/
protected function dumpOutline($keyword, $short = true, $excludeAsterisk = false)
{
$dump = <<<GHERKIN
{$keyword}: Erasing other agents' memory
GHERKIN;
// Given
$dump .= $this->dumpStep(
$this->keywords->getGivenKeywords(),
'there is agent <agent1>',
$short,
$excludeAsterisk
);
// And
$dump .= $this->dumpStep(
$this->keywords->getAndKeywords(),
'there is agent <agent2>',
$short,
$excludeAsterisk
);
// When
$dump .= $this->dumpStep(
$this->keywords->getWhenKeywords(),
'I erase agent <agent2>\'s memory',
$short,
$excludeAsterisk
);
// Then
$dump .= $this->dumpStep(
$this->keywords->getThenKeywords(),
'there should be agent <agent1>',
$short,
$excludeAsterisk
);
// But
$dump .= $this->dumpStep(
$this->keywords->getButKeywords(),
'there should not be agent <agent2>',
$short,
$excludeAsterisk
);
$keywords = explode('|', $this->keywords->getExamplesKeywords());
if ($short) {
$keyword = call_user_func($this->keywordsDumper, $keywords, $short);
} else {
$keyword = call_user_func($this->keywordsDumper, array($keywords[0]), $short);
}
$dump .= <<<GHERKIN
{$keyword}:
| agent1 | agent2 |
| D | M |
GHERKIN;
return $dump . "\n";
}
/**
* Dumps step example.
*
* @param string $keywords Item keyword
* @param string $text Step text
* @param bool $short Dump short version?
*
* @return string
*/
protected function dumpStep($keywords, $text, $short = true, $excludeAsterisk = false)
{
$dump = '';
$keywords = explode('|', $keywords);
if ($short) {
$keywords = array_map(
function ($keyword) {
return str_replace('<', '', $keyword);
},
$keywords
);
$keywords = call_user_func($this->keywordsDumper, $keywords, $short);
$dump .= <<<GHERKIN
{$keywords} {$text}
GHERKIN;
} else {
foreach ($keywords as $keyword) {
if ($excludeAsterisk && '*' === $keyword) {
continue;
}
$indent = ' ';
if (false !== mb_strpos($keyword, '<', 0, 'utf8')) {
$keyword = mb_substr($keyword, 0, -1, 'utf8');
$indent = '';
}
$keyword = call_user_func($this->keywordsDumper, array($keyword), $short);
$dump .= <<<GHERKIN
{$keyword}{$indent}{$text}
GHERKIN;
}
}
return $dump;
}
}
@@ -0,0 +1,103 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Keywords;
/**
* Keywords holder interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface KeywordsInterface
{
/**
* Sets keywords holder language.
*
* @param string $language Language name
*/
public function setLanguage($language);
/**
* Returns Feature keywords (splitted by "|").
*
* @return string
*/
public function getFeatureKeywords();
/**
* Returns Background keywords (splitted by "|").
*
* @return string
*/
public function getBackgroundKeywords();
/**
* Returns Scenario keywords (splitted by "|").
*
* @return string
*/
public function getScenarioKeywords();
/**
* Returns Scenario Outline keywords (splitted by "|").
*
* @return string
*/
public function getOutlineKeywords();
/**
* Returns Examples keywords (splitted by "|").
*
* @return string
*/
public function getExamplesKeywords();
/**
* Returns Given keywords (splitted by "|").
*
* @return string
*/
public function getGivenKeywords();
/**
* Returns When keywords (splitted by "|").
*
* @return string
*/
public function getWhenKeywords();
/**
* Returns Then keywords (splitted by "|").
*
* @return string
*/
public function getThenKeywords();
/**
* Returns And keywords (splitted by "|").
*
* @return string
*/
public function getAndKeywords();
/**
* Returns But keywords (splitted by "|").
*
* @return string
*/
public function getButKeywords();
/**
* Returns all step keywords (splitted by "|").
*
* @return string
*/
public function getStepKeywords();
}
+653
View File
@@ -0,0 +1,653 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin;
use Behat\Gherkin\Exception\LexerException;
use Behat\Gherkin\Keywords\KeywordsInterface;
/**
* Gherkin lexer.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class Lexer
{
private $language;
private $lines;
private $linesCount;
private $line;
private $trimmedLine;
private $lineNumber;
private $eos;
private $keywords;
private $keywordsCache = array();
private $stepKeywordTypesCache = array();
private $deferredObjects = array();
private $deferredObjectsCount = 0;
private $stashedToken;
private $inPyString = false;
private $pyStringSwallow = 0;
private $featureStarted = false;
private $allowMultilineArguments = false;
private $allowSteps = false;
private $pyStringDelimiter = null;
/**
* Initializes lexer.
*
* @param KeywordsInterface $keywords Keywords holder
*/
public function __construct(KeywordsInterface $keywords)
{
$this->keywords = $keywords;
}
/**
* Sets lexer input.
*
* @param string $input Input string
* @param string $language Language name
*
* @throws Exception\LexerException
*/
public function analyse($input, $language = 'en')
{
// try to detect unsupported encoding
if ('UTF-8' !== mb_detect_encoding($input, 'UTF-8', true)) {
throw new LexerException('Feature file is not in UTF8 encoding');
}
$input = strtr($input, array("\r\n" => "\n", "\r" => "\n"));
$this->lines = explode("\n", $input);
$this->linesCount = count($this->lines);
$this->line = $this->lines[0];
$this->lineNumber = 1;
$this->trimmedLine = null;
$this->eos = false;
$this->deferredObjects = array();
$this->deferredObjectsCount = 0;
$this->stashedToken = null;
$this->inPyString = false;
$this->pyStringSwallow = 0;
$this->featureStarted = false;
$this->allowMultilineArguments = false;
$this->allowSteps = false;
$this->keywords->setLanguage($this->language = $language);
$this->keywordsCache = array();
$this->stepKeywordTypesCache = array();
}
/**
* Returns current lexer language.
*
* @return string
*/
public function getLanguage()
{
return $this->language;
}
/**
* Returns next token or previously stashed one.
*
* @return array
*/
public function getAdvancedToken()
{
return $this->getStashedToken() ?: $this->getNextToken();
}
/**
* Defers token.
*
* @param array $token Token to defer
*/
public function deferToken(array $token)
{
$token['deferred'] = true;
$this->deferredObjects[] = $token;
++$this->deferredObjectsCount;
}
/**
* Predicts the upcoming token without passing over it.
*
* @return array
*/
public function predictToken()
{
if (null === $this->stashedToken) {
$this->stashedToken = $this->getNextToken();
}
return $this->stashedToken;
}
/**
* Skips over the currently-predicted token, if any.
*
* @return void
*/
public function skipPredictedToken()
{
$this->stashedToken = null;
}
/**
* Constructs token with specified parameters.
*
* @param string $type Token type
* @param string $value Token value
*
* @return array
*/
public function takeToken($type, $value = null)
{
return array(
'type' => $type,
'line' => $this->lineNumber,
'value' => $value ?: null,
'deferred' => false
);
}
/**
* Consumes line from input & increments line counter.
*/
protected function consumeLine()
{
++$this->lineNumber;
if (($this->lineNumber - 1) === $this->linesCount) {
$this->eos = true;
return;
}
$this->line = $this->lines[$this->lineNumber - 1];
$this->trimmedLine = null;
}
/**
* Consumes first part of line from input without incrementing the line number
*/
protected function consumeLineUntil(int $trimmedOffset)
{
$this->line = mb_substr(ltrim($this->line), $trimmedOffset, null, 'utf-8');
$this->trimmedLine = null;
}
/**
* Returns trimmed version of line.
*
* @return string
*/
protected function getTrimmedLine()
{
return null !== $this->trimmedLine ? $this->trimmedLine : $this->trimmedLine = trim($this->line);
}
/**
* Returns stashed token or null if hasn't.
*
* @return array|null
*/
protected function getStashedToken()
{
$stashedToken = $this->stashedToken;
$this->stashedToken = null;
return $stashedToken;
}
/**
* Returns deferred token or null if hasn't.
*
* @return array|null
*/
protected function getDeferredToken()
{
if (!$this->deferredObjectsCount) {
return null;
}
--$this->deferredObjectsCount;
return array_shift($this->deferredObjects);
}
/**
* Returns next token from input.
*
* @return array
*/
protected function getNextToken()
{
return $this->getDeferredToken()
?: $this->scanEOS()
?: $this->scanLanguage()
?: $this->scanComment()
?: $this->scanPyStringOp()
?: $this->scanPyStringContent()
?: $this->scanStep()
?: $this->scanScenario()
?: $this->scanBackground()
?: $this->scanOutline()
?: $this->scanExamples()
?: $this->scanFeature()
?: $this->scanTags()
?: $this->scanTableRow()
?: $this->scanNewline()
?: $this->scanText();
}
/**
* Scans for token with specified regex.
*
* @param string $regex Regular expression
* @param string $type Expected token type
*
* @return null|array
*/
protected function scanInput($regex, $type)
{
if (!preg_match($regex, $this->line, $matches)) {
return null;
}
$token = $this->takeToken($type, $matches[1]);
$this->consumeLine();
return $token;
}
/**
* Scans for token with specified keywords.
*
* @param string $keywords Keywords (splitted with |)
* @param string $type Expected token type
*
* @return null|array
*/
protected function scanInputForKeywords($keywords, $type)
{
if (!preg_match('/^(\s*)(' . $keywords . '):\s*(.*)/u', $this->line, $matches)) {
return null;
}
$token = $this->takeToken($type, $matches[3]);
$token['keyword'] = $matches[2];
$token['indent'] = mb_strlen($matches[1], 'utf8');
$this->consumeLine();
// turn off language searching
if ('Feature' === $type) {
$this->featureStarted = true;
}
// turn off PyString and Table searching
if ('Feature' === $type || 'Scenario' === $type || 'Outline' === $type) {
$this->allowMultilineArguments = false;
} elseif ('Examples' === $type) {
$this->allowMultilineArguments = true;
}
// turn on steps searching
if ('Scenario' === $type || 'Background' === $type || 'Outline' === $type) {
$this->allowSteps = true;
}
return $token;
}
/**
* Scans EOS from input & returns it if found.
*
* @return null|array
*/
protected function scanEOS()
{
if (!$this->eos) {
return null;
}
return $this->takeToken('EOS');
}
/**
* Returns keywords for provided type.
*
* @param string $type Keyword type
*
* @return string
*/
protected function getKeywords($type)
{
if (!isset($this->keywordsCache[$type])) {
$getter = 'get' . $type . 'Keywords';
$keywords = $this->keywords->$getter();
if ('Step' === $type) {
$padded = array();
foreach (explode('|', $keywords) as $keyword) {
$padded[] = false !== mb_strpos($keyword, '<', 0, 'utf8')
? preg_quote(mb_substr($keyword, 0, -1, 'utf8'), '/') . '\s*'
: preg_quote($keyword, '/') . '\s+';
}
$keywords = implode('|', $padded);
}
$this->keywordsCache[$type] = $keywords;
}
return $this->keywordsCache[$type];
}
/**
* Scans Feature from input & returns it if found.
*
* @return null|array
*/
protected function scanFeature()
{
return $this->scanInputForKeywords($this->getKeywords('Feature'), 'Feature');
}
/**
* Scans Background from input & returns it if found.
*
* @return null|array
*/
protected function scanBackground()
{
return $this->scanInputForKeywords($this->getKeywords('Background'), 'Background');
}
/**
* Scans Scenario from input & returns it if found.
*
* @return null|array
*/
protected function scanScenario()
{
return $this->scanInputForKeywords($this->getKeywords('Scenario'), 'Scenario');
}
/**
* Scans Scenario Outline from input & returns it if found.
*
* @return null|array
*/
protected function scanOutline()
{
return $this->scanInputForKeywords($this->getKeywords('Outline'), 'Outline');
}
/**
* Scans Scenario Outline Examples from input & returns it if found.
*
* @return null|array
*/
protected function scanExamples()
{
return $this->scanInputForKeywords($this->getKeywords('Examples'), 'Examples');
}
/**
* Scans Step from input & returns it if found.
*
* @return null|array
*/
protected function scanStep()
{
if (!$this->allowSteps) {
return null;
}
$keywords = $this->getKeywords('Step');
if (!preg_match('/^\s*(' . $keywords . ')([^\s].*)/u', $this->line, $matches)) {
return null;
}
$keyword = trim($matches[1]);
$token = $this->takeToken('Step', $keyword);
$token['keyword_type'] = $this->getStepKeywordType($keyword);
$token['text'] = $matches[2];
$this->consumeLine();
$this->allowMultilineArguments = true;
return $token;
}
/**
* Scans PyString from input & returns it if found.
*
* @return null|array
*/
protected function scanPyStringOp()
{
if (!$this->allowMultilineArguments) {
return null;
}
if(!preg_match('/^\s*(?<delimiter>"""|```)/u', $this->line, $matches, PREG_OFFSET_CAPTURE)) {
return null;
}
['delimiter' => [0 => $delimiter, 1 => $indent]] = $matches;
if ($this->inPyString) {
if ($this->pyStringDelimiter !== $delimiter) {
return null;
}
$this->pyStringDelimiter = null;
}
else {
$this->pyStringDelimiter= $delimiter;
}
$this->inPyString = !$this->inPyString;
$token = $this->takeToken('PyStringOp');
$this->pyStringSwallow = $indent;
$this->consumeLine();
return $token;
}
/**
* Scans PyString content.
*
* @return null|array
*/
protected function scanPyStringContent()
{
if (!$this->inPyString) {
return null;
}
$token = $this->scanText();
// swallow trailing spaces
$token['value'] = preg_replace('/^\s{0,' . $this->pyStringSwallow . '}/u', '', $token['value'] ?? '');
return $token;
}
/**
* Scans Table Row from input & returns it if found.
*
* @return null|array
*/
protected function scanTableRow()
{
if (!$this->allowMultilineArguments) {
return null;
}
$line = $this->getTrimmedLine();
if (!isset($line[0]) || '|' !== $line[0] || '|' !== substr($line, -1)) {
return null;
}
$token = $this->takeToken('TableRow');
$line = mb_substr($line, 1, mb_strlen($line, 'utf8') - 2, 'utf8');
$columns = array_map(function ($column) {
return trim(str_replace('\\|', '|', $column));
}, preg_split('/(?<!\\\)\|/u', $line));
$token['columns'] = $columns;
$this->consumeLine();
return $token;
}
/**
* Scans Tags from input & returns it if found.
*
* @return null|array
*/
protected function scanTags()
{
$line = $this->getTrimmedLine();
if (!isset($line[0]) || '@' !== $line[0]) {
return null;
}
if(preg_match('/^(?<line>.*)\s+#.*$/', $line, $matches)) {
['line' => $line] = $matches;
$this->consumeLineUntil(mb_strlen($line, 'utf-8'));
} else {
$this->consumeLine();
}
$token = $this->takeToken('Tag');
$tags = explode('@', mb_substr($line, 1, mb_strlen($line, 'utf8') - 1, 'utf8'));
$tags = array_map('trim', $tags);
$token['tags'] = $tags;
return $token;
}
/**
* Scans Language specifier from input & returns it if found.
*
* @return null|array
*/
protected function scanLanguage()
{
if ($this->featureStarted) {
return null;
}
if ($this->inPyString) {
return null;
}
if (0 !== mb_strpos(ltrim($this->line), '#', 0, 'utf8')) {
return null;
}
return $this->scanInput('/^\s*\#\s*language:\s*([\w_\-]+)\s*$/', 'Language');
}
/**
* Scans Comment from input & returns it if found.
*
* @return null|array
*/
protected function scanComment()
{
if ($this->inPyString) {
return null;
}
$line = $this->getTrimmedLine();
if (0 !== mb_strpos($line, '#', 0, 'utf8')) {
return null;
}
$token = $this->takeToken('Comment', $line);
$this->consumeLine();
return $token;
}
/**
* Scans Newline from input & returns it if found.
*
* @return null|array
*/
protected function scanNewline()
{
if ('' !== $this->getTrimmedLine()) {
return null;
}
$token = $this->takeToken('Newline', mb_strlen($this->line, 'utf8'));
$this->consumeLine();
return $token;
}
/**
* Scans text from input & returns it if found.
*
* @return null|array
*/
protected function scanText()
{
$token = $this->takeToken('Text', $this->line);
$this->consumeLine();
return $token;
}
/**
* Returns step type keyword (Given, When, Then, etc.).
*
* @param string $native Step keyword in provided language
* @return string
*/
private function getStepKeywordType($native)
{
// Consider "*" as a AND keyword so that it is normalized to the previous step type
if ('*' === $native) {
return 'And';
}
if (empty($this->stepKeywordTypesCache)) {
$this->stepKeywordTypesCache = array(
'Given' => explode('|', $this->keywords->getGivenKeywords()),
'When' => explode('|', $this->keywords->getWhenKeywords()),
'Then' => explode('|', $this->keywords->getThenKeywords()),
'And' => explode('|', $this->keywords->getAndKeywords()),
'But' => explode('|', $this->keywords->getButKeywords())
);
}
foreach ($this->stepKeywordTypesCache as $type => $keywords) {
if (in_array($native, $keywords) || in_array($native . '<', $keywords)) {
return $type;
}
}
return 'Given';
}
}
@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Loader;
/**
* Abstract filesystem loader.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
abstract class AbstractFileLoader implements FileLoaderInterface
{
protected $basePath;
/**
* Sets base features path.
*
* @param string $path Base loader path
*/
public function setBasePath($path)
{
$this->basePath = realpath($path);
}
/**
* Finds relative path for provided absolute (relative to base features path).
*
* @param string $path Absolute path
*
* @return string
*/
protected function findRelativePath($path)
{
if (null !== $this->basePath) {
return strtr($path, array($this->basePath . DIRECTORY_SEPARATOR => ''));
}
return $path;
}
/**
* Finds absolute path for provided relative (relative to base features path).
*
* @param string $path Relative path
*
* @return string
*/
protected function findAbsolutePath($path)
{
if (is_file($path) || is_dir($path)) {
return realpath($path);
}
if (null === $this->basePath) {
return false;
}
if (is_file($this->basePath . DIRECTORY_SEPARATOR . $path)
|| is_dir($this->basePath . DIRECTORY_SEPARATOR . $path)) {
return realpath($this->basePath . DIRECTORY_SEPARATOR . $path);
}
return false;
}
}
@@ -0,0 +1,312 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Loader;
use Behat\Gherkin\Node\BackgroundNode;
use Behat\Gherkin\Node\ExampleTableNode;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\ScenarioNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Gherkin\Node\TableNode;
/**
* From-array loader.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ArrayLoader implements LoaderInterface
{
/**
* Checks if current loader supports provided resource.
*
* @param mixed $resource Resource to load
*
* @return bool
*/
public function supports($resource)
{
return is_array($resource) && (isset($resource['features']) || isset($resource['feature']));
}
/**
* Loads features from provided resource.
*
* @param mixed $resource Resource to load
*
* @return FeatureNode[]
*/
public function load($resource)
{
$features = array();
if (isset($resource['features'])) {
foreach ($resource['features'] as $iterator => $hash) {
$feature = $this->loadFeatureHash($hash, $iterator);
$features[] = $feature;
}
} elseif (isset($resource['feature'])) {
$feature = $this->loadFeatureHash($resource['feature']);
$features[] = $feature;
}
return $features;
}
/**
* Loads feature from provided feature hash.
*
* @param array $hash Feature hash
* @param integer $line
*
* @return FeatureNode
*/
protected function loadFeatureHash(array $hash, $line = 0)
{
$hash = array_merge(
array(
'title' => null,
'description' => null,
'tags' => array(),
'keyword' => 'Feature',
'language' => 'en',
'line' => $line,
'scenarios' => array(),
),
$hash
);
$background = isset($hash['background']) ? $this->loadBackgroundHash($hash['background']) : null;
$scenarios = array();
foreach ((array) $hash['scenarios'] as $scenarioIterator => $scenarioHash) {
if (isset($scenarioHash['type']) && 'outline' === $scenarioHash['type']) {
$scenarios[] = $this->loadOutlineHash($scenarioHash, $scenarioIterator);
} else {
$scenarios[] = $this->loadScenarioHash($scenarioHash, $scenarioIterator);
}
}
return new FeatureNode($hash['title'], $hash['description'], $hash['tags'], $background, $scenarios, $hash['keyword'], $hash['language'], null, $hash['line']);
}
/**
* Loads background from provided hash.
*
* @param array $hash Background hash
*
* @return BackgroundNode
*/
protected function loadBackgroundHash(array $hash)
{
$hash = array_merge(
array(
'title' => null,
'keyword' => 'Background',
'line' => 0,
'steps' => array(),
),
$hash
);
$steps = $this->loadStepsHash($hash['steps']);
return new BackgroundNode($hash['title'], $steps, $hash['keyword'], $hash['line']);
}
/**
* Loads scenario from provided scenario hash.
*
* @param array $hash Scenario hash
* @param integer $line Scenario definition line
*
* @return ScenarioNode
*/
protected function loadScenarioHash(array $hash, $line = 0)
{
$hash = array_merge(
array(
'title' => null,
'tags' => array(),
'keyword' => 'Scenario',
'line' => $line,
'steps' => array(),
),
$hash
);
$steps = $this->loadStepsHash($hash['steps']);
return new ScenarioNode($hash['title'], $hash['tags'], $steps, $hash['keyword'], $hash['line']);
}
/**
* Loads outline from provided outline hash.
*
* @param array $hash Outline hash
* @param integer $line Outline definition line
*
* @return OutlineNode
*/
protected function loadOutlineHash(array $hash, $line = 0)
{
$hash = array_merge(
array(
'title' => null,
'tags' => array(),
'keyword' => 'Scenario Outline',
'line' => $line,
'steps' => array(),
'examples' => array(),
),
$hash
);
$steps = $this->loadStepsHash($hash['steps']);
if (isset($hash['examples']['keyword'])) {
$examplesKeyword = $hash['examples']['keyword'];
unset($hash['examples']['keyword']);
} else {
$examplesKeyword = 'Examples';
}
$exHash = $hash['examples'];
$examples = array();
if ($this->examplesAreInArray($exHash)) {
$examples = $this->processExamplesArray($exHash, $examplesKeyword, $examples);
} else {
// examples as a single table - we create an array with the only one element
$examples[] = new ExampleTableNode($exHash, $examplesKeyword);;
}
return new OutlineNode($hash['title'], $hash['tags'], $steps, $examples, $hash['keyword'], $hash['line']);
}
/**
* Loads steps from provided hash.
*
* @param array $hash
*
* @return StepNode[]
*/
private function loadStepsHash(array $hash)
{
$steps = array();
foreach ($hash as $stepIterator => $stepHash) {
$steps[] = $this->loadStepHash($stepHash, $stepIterator);
}
return $steps;
}
/**
* Loads step from provided hash.
*
* @param array $hash Step hash
* @param integer $line Step definition line
*
* @return StepNode
*/
protected function loadStepHash(array $hash, $line = 0)
{
$hash = array_merge(
array(
'keyword_type' => 'Given',
'type' => 'Given',
'text' => null,
'keyword' => 'Scenario',
'line' => $line,
'arguments' => array(),
),
$hash
);
$arguments = array();
foreach ($hash['arguments'] as $argumentHash) {
if ('table' === $argumentHash['type']) {
$arguments[] = $this->loadTableHash($argumentHash['rows']);
} elseif ('pystring' === $argumentHash['type']) {
$arguments[] = $this->loadPyStringHash($argumentHash, $hash['line'] + 1);
}
}
return new StepNode($hash['type'], $hash['text'], $arguments, $hash['line'], $hash['keyword_type']);
}
/**
* Loads table from provided hash.
*
* @param array $hash Table hash
*
* @return TableNode
*/
protected function loadTableHash(array $hash)
{
return new TableNode($hash);
}
/**
* Loads PyString from provided hash.
*
* @param array $hash PyString hash
* @param integer $line
*
* @return PyStringNode
*/
protected function loadPyStringHash(array $hash, $line = 0)
{
$line = isset($hash['line']) ? $hash['line'] : $line;
$strings = array();
foreach (explode("\n", $hash['text']) as $string) {
$strings[] = $string;
}
return new PyStringNode($strings, $line);
}
/**
* Checks if examples node is an array
* @param $exHash object hash
* @return bool
*/
private function examplesAreInArray($exHash)
{
return isset($exHash[0]);
}
/**
* Processes cases when examples are in the form of array of arrays
* OR in the form of array of objects
*
* @param $exHash array hash
* @param $examplesKeyword string
* @param $examples array
* @return array
*/
private function processExamplesArray($exHash, $examplesKeyword, $examples)
{
for ($i = 0; $i < count($exHash); $i++) {
if (isset($exHash[$i]['table'])) {
// we have examples as objects, hence there could be tags
$exHashTags = isset($exHash[$i]['tags']) ? $exHash[$i]['tags'] : array();
$examples[] = new ExampleTableNode($exHash[$i]['table'], $examplesKeyword, $exHashTags);
} else {
// we have examples as arrays
$examples[] = new ExampleTableNode($exHash[$i], $examplesKeyword);
}
}
return $examples;
}
}
@@ -0,0 +1,193 @@
<?php
namespace Behat\Gherkin\Loader;
use Behat\Gherkin\Node\BackgroundNode;
use Behat\Gherkin\Node\ExampleTableNode;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Gherkin\Node\ScenarioInterface;
use Behat\Gherkin\Node\ScenarioNode;
use Behat\Gherkin\Node\StepNode;
/**
* Loads a feature from cucumber's protobuf JSON format
*/
class CucumberNDJsonAstLoader implements LoaderInterface
{
public function supports($resource)
{
return is_string($resource);
}
public function load($resource)
{
return array_values(array_filter(array_map(
static function ($line) use ($resource) {
return self::getFeature(json_decode($line, true), $resource);
},
file($resource)
)));
}
/**
* @return FeatureNode|null
*/
private static function getFeature(array $json, $filePath)
{
if (!isset($json['gherkinDocument']['feature'])) {
return null;
}
$featureJson = $json['gherkinDocument']['feature'];
$feature = new FeatureNode(
isset($featureJson['name']) ? $featureJson['name'] : null,
$featureJson['description'] ? trim($featureJson['description']) : null,
self::getTags($featureJson),
self::getBackground($featureJson),
self::getScenarios($featureJson),
$featureJson['keyword'],
$featureJson['language'],
preg_replace('/(?<=\\.feature).*$/', '', $filePath),
$featureJson['location']['line']
);
return $feature;
}
/**
* @return string[]
*/
private static function getTags(array $json)
{
return array_map(
static function(array $tag) { return preg_replace('/^@/', '', $tag['name']); },
isset($json['tags']) ? $json['tags'] : []
);
}
/**
* @return ScenarioInterface[]
*/
private static function getScenarios(array $json)
{
return array_values(
array_map(
static function ($child) {
if ($child['scenario']['examples']) {
return new OutlineNode(
isset($child['scenario']['name']) ? $child['scenario']['name'] : null,
self::getTags($child['scenario']),
self::getSteps(isset($child['scenario']['steps']) ? $child['scenario']['steps'] : []),
self::getTables($child['scenario']['examples']),
$child['scenario']['keyword'],
$child['scenario']['location']['line']
);
}
else {
return new ScenarioNode(
$child['scenario']['name'],
self::getTags($child['scenario']),
self::getSteps(isset($child['scenario']['steps']) ? $child['scenario']['steps'] : []),
$child['scenario']['keyword'],
$child['scenario']['location']['line']
);
}
},
array_filter(
isset($json['children']) ? $json['children'] : [],
static function ($child) {
return isset($child['scenario']);
}
)
)
);
}
/**
* @return BackgroundNode|null
*/
private static function getBackground(array $json)
{
$backgrounds = array_values(
array_map(
static function ($child) {
return new BackgroundNode(
$child['background']['name'],
self::getSteps(isset($child['background']['steps']) ? $child['background']['steps'] : []),
$child['background']['keyword'],
$child['background']['location']['line']
);
},
array_filter(
isset($json['children']) ? $json['children'] : [],
static function ($child) {
return isset($child['background']);
}
)
)
);
return count($backgrounds) == 1 ? $backgrounds[0] : null;
}
/**
* @return StepNode[]
*/
private static function getSteps(array $json)
{
return array_map(
static function(array $json) {
return new StepNode(
trim($json['keyword']),
$json['text'],
[],
$json['location']['line'],
trim($json['keyword'])
);
},
$json
);
}
/**
* @return ExampleTableNode[]
*/
private static function getTables(array $json)
{
return array_map(
static function($tableJson) {
$table = [];
$table[$tableJson['tableHeader']['location']['line']] = array_map(
static function($cell) {
return $cell['value'];
},
$tableJson['tableHeader']['cells']
);
foreach ($tableJson['tableBody'] as $bodyRow) {
$table[$bodyRow['location']['line']] = array_map(
static function($cell) {
return $cell['value'];
},
$bodyRow['cells']
);
}
return new ExampleTableNode(
$table,
$tableJson['keyword'],
self::getTags($tableJson)
);
},
$json
);
}
}
@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Loader;
use Behat\Gherkin\Gherkin;
use Behat\Gherkin\Node\FeatureNode;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* Directory contents loader.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class DirectoryLoader extends AbstractFileLoader
{
protected $gherkin;
/**
* Initializes loader.
*
* @param Gherkin $gherkin Gherkin manager
*/
public function __construct(Gherkin $gherkin)
{
$this->gherkin = $gherkin;
}
/**
* Checks if current loader supports provided resource.
*
* @param mixed $path Resource to load
*
* @return bool
*/
public function supports($path)
{
return is_string($path)
&& is_dir($this->findAbsolutePath($path));
}
/**
* Loads features from provided resource.
*
* @param string $path Resource to load
*
* @return FeatureNode[]
*/
public function load($path)
{
$path = $this->findAbsolutePath($path);
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
);
$paths = array_map('strval', iterator_to_array($iterator));
uasort($paths, 'strnatcasecmp');
$features = array();
foreach ($paths as $path) {
$path = (string) $path;
$loader = $this->gherkin->resolveLoader($path);
if (null !== $loader) {
$features = array_merge($features, $loader->load($path));
}
}
return $features;
}
}
@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Loader;
/**
* File Loader interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface FileLoaderInterface extends LoaderInterface
{
/**
* Sets base features path.
*
* @param string $path Base loader path
*/
public function setBasePath($path);
}
@@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Loader;
use Behat\Gherkin\Cache\CacheInterface;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Parser;
/**
* Gherkin *.feature files loader.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class GherkinFileLoader extends AbstractFileLoader
{
protected $parser;
protected $cache;
/**
* Initializes loader.
*
* @param Parser $parser Parser
* @param CacheInterface $cache Cache layer
*/
public function __construct(Parser $parser, CacheInterface $cache = null)
{
$this->parser = $parser;
$this->cache = $cache;
}
/**
* Sets cache layer.
*
* @param CacheInterface $cache Cache layer
*/
public function setCache(CacheInterface $cache)
{
$this->cache = $cache;
}
/**
* Checks if current loader supports provided resource.
*
* @param mixed $path Resource to load
*
* @return bool
*/
public function supports($path)
{
return is_string($path)
&& is_file($absolute = $this->findAbsolutePath($path))
&& 'feature' === pathinfo($absolute, PATHINFO_EXTENSION);
}
/**
* Loads features from provided resource.
*
* @param string $path Resource to load
*
* @return FeatureNode[]
*/
public function load($path)
{
$path = $this->findAbsolutePath($path);
if ($this->cache) {
if ($this->cache->isFresh($path, filemtime($path))) {
$feature = $this->cache->read($path);
} elseif (null !== $feature = $this->parseFeature($path)) {
$this->cache->write($path, $feature);
}
} else {
$feature = $this->parseFeature($path);
}
return null !== $feature ? array($feature) : array();
}
/**
* Parses feature at provided absolute path.
*
* @param string $path Feature path
*
* @return FeatureNode
*/
protected function parseFeature($path)
{
$content = file_get_contents($path);
$feature = $this->parser->parse($content, $path);
return $feature;
}
}
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Loader;
use Behat\Gherkin\Node\FeatureNode;
/**
* Loader interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface LoaderInterface
{
/**
* Checks if current loader supports provided resource.
*
* @param mixed $resource Resource to load
*
* @return bool
*/
public function supports($resource);
/**
* Loads features from provided resource.
*
* @param mixed $resource Resource to load
*
* @return FeatureNode[]
*/
public function load($resource);
}
@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Loader;
use Behat\Gherkin\Node\FeatureNode;
use Symfony\Component\Yaml\Yaml;
/**
* Yaml files loader.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class YamlFileLoader extends AbstractFileLoader
{
private $loader;
public function __construct()
{
$this->loader = new ArrayLoader();
}
/**
* Checks if current loader supports provided resource.
*
* @param mixed $path Resource to load
*
* @return bool
*/
public function supports($path)
{
return is_string($path)
&& is_file($absolute = $this->findAbsolutePath($path))
&& 'yml' === pathinfo($absolute, PATHINFO_EXTENSION);
}
/**
* Loads features from provided resource.
*
* @param string $path Resource to load
*
* @return FeatureNode[]
*/
public function load($path)
{
$path = $this->findAbsolutePath($path);
$hash = Yaml::parse(file_get_contents($path));
$features = $this->loader->load($hash);
return array_map(function (FeatureNode $feature) use ($path) {
return new FeatureNode(
$feature->getTitle(),
$feature->getDescription(),
$feature->getTags(),
$feature->getBackground(),
$feature->getScenarios(),
$feature->getKeyword(),
$feature->getLanguage(),
$path,
$feature->getLine()
);
}, $features);
}
}
@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Gherkin arguments interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ArgumentInterface extends NodeInterface
{
}
@@ -0,0 +1,112 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Represents Gherkin Background.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class BackgroundNode implements ScenarioLikeInterface
{
/**
* @var string
*/
private $title;
/**
* @var StepNode[]
*/
private $steps = array();
/**
* @var string
*/
private $keyword;
/**
* @var integer
*/
private $line;
/**
* Initializes background.
*
* @param null|string $title
* @param StepNode[] $steps
* @param string $keyword
* @param integer $line
*/
public function __construct($title, array $steps, $keyword, $line)
{
$this->title = $title;
$this->steps = $steps;
$this->keyword = $keyword;
$this->line = $line;
}
/**
* Returns node type string
*
* @return string
*/
public function getNodeType()
{
return 'Background';
}
/**
* Returns background title.
*
* @return null|string
*/
public function getTitle()
{
return $this->title;
}
/**
* Checks if background has steps.
*
* @return bool
*/
public function hasSteps()
{
return 0 < count($this->steps);
}
/**
* Returns background steps.
*
* @return StepNode[]
*/
public function getSteps()
{
return $this->steps;
}
/**
* Returns background keyword.
*
* @return string
*/
public function getKeyword()
{
return $this->keyword;
}
/**
* Returns background declaration line number.
*
* @return integer
*/
public function getLine()
{
return $this->line;
}
}
@@ -0,0 +1,274 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Represents Gherkin Outline Example.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ExampleNode implements ScenarioInterface
{
/**
* @var string
*/
private $title;
/**
* @var string[]
*/
private $tags;
/**
* @var StepNode[]
*/
private $outlineSteps;
/**
* @var string[]
*/
private $tokens;
/**
* @var integer
*/
private $line;
/**
* @var null|StepNode[]
*/
private $steps;
/**
* @var string
*/
private $outlineTitle;
/**
* Initializes outline.
*
* @param string $title
* @param string[] $tags
* @param StepNode[] $outlineSteps
* @param string[] $tokens
* @param integer $line
* @param string|null $outlineTitle
*/
public function __construct($title, array $tags, $outlineSteps, array $tokens, $line, $outlineTitle = null)
{
$this->title = $title;
$this->tags = $tags;
$this->outlineSteps = $outlineSteps;
$this->tokens = $tokens;
$this->line = $line;
$this->outlineTitle = $outlineTitle;
}
/**
* Returns node type string
*
* @return string
*/
public function getNodeType()
{
return 'Example';
}
/**
* Returns node keyword.
*
* @return string
*/
public function getKeyword()
{
return $this->getNodeType();
}
/**
* Returns example title.
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Checks if outline is tagged with tag.
*
* @param string $tag
*
* @return bool
*/
public function hasTag($tag)
{
return in_array($tag, $this->getTags());
}
/**
* Checks if outline has tags (both inherited from feature and own).
*
* @return bool
*/
public function hasTags()
{
return 0 < count($this->getTags());
}
/**
* Returns outline tags (including inherited from feature).
*
* @return string[]
*/
public function getTags()
{
return $this->tags;
}
/**
* Checks if outline has steps.
*
* @return bool
*/
public function hasSteps()
{
return 0 < count($this->outlineSteps);
}
/**
* Returns outline steps.
*
* @return StepNode[]
*/
public function getSteps()
{
return $this->steps = $this->steps ? : $this->createExampleSteps();
}
/**
* Returns example tokens.
*
* @return string[]
*/
public function getTokens()
{
return $this->tokens;
}
/**
* Returns outline declaration line number.
*
* @return integer
*/
public function getLine()
{
return $this->line;
}
/**
* Returns outline title.
*
* @return string
*/
public function getOutlineTitle()
{
return $this->outlineTitle;
}
/**
* Creates steps for this example from abstract outline steps.
*
* @return StepNode[]
*/
protected function createExampleSteps()
{
$steps = array();
foreach ($this->outlineSteps as $outlineStep) {
$keyword = $outlineStep->getKeyword();
$keywordType = $outlineStep->getKeywordType();
$text = $this->replaceTextTokens($outlineStep->getText());
$args = $this->replaceArgumentsTokens($outlineStep->getArguments());
$line = $outlineStep->getLine();
$steps[] = new StepNode($keyword, $text, $args, $line, $keywordType);
}
return $steps;
}
/**
* Replaces tokens in arguments with row values.
*
* @param ArgumentInterface[] $arguments
*
* @return ArgumentInterface[]
*/
protected function replaceArgumentsTokens(array $arguments)
{
foreach ($arguments as $num => $argument) {
if ($argument instanceof TableNode) {
$arguments[$num] = $this->replaceTableArgumentTokens($argument);
}
if ($argument instanceof PyStringNode) {
$arguments[$num] = $this->replacePyStringArgumentTokens($argument);
}
}
return $arguments;
}
/**
* Replaces tokens in table with row values.
*
* @param TableNode $argument
*
* @return TableNode
*/
protected function replaceTableArgumentTokens(TableNode $argument)
{
$table = $argument->getTable();
foreach ($table as $line => $row) {
foreach (array_keys($row) as $col) {
$table[$line][$col] = $this->replaceTextTokens($table[$line][$col]);
}
}
return new TableNode($table);
}
/**
* Replaces tokens in PyString with row values.
*
* @param PyStringNode $argument
*
* @return PyStringNode
*/
protected function replacePyStringArgumentTokens(PyStringNode $argument)
{
$strings = $argument->getStrings();
foreach ($strings as $line => $string) {
$strings[$line] = $this->replaceTextTokens($strings[$line]);
}
return new PyStringNode($strings, $argument->getLine());
}
/**
* Replaces tokens in text with row values.
*
* @param string $text
*
* @return string
*/
protected function replaceTextTokens($text)
{
foreach ($this->tokens as $key => $val) {
$text = str_replace('<' . $key . '>', $val, $text);
}
return $text;
}
}
@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Represents Gherkin Outline Example Table.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ExampleTableNode extends TableNode
{
/**
* @var string[]
*/
private $tags;
/**
* @var string
*/
private $keyword;
/**
* Initializes example table.
*
* @param array $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
* @param string $keyword
* @param string[] $tags
*/
public function __construct(array $table, $keyword, array $tags = array())
{
$this->keyword = $keyword;
$this->tags = $tags;
parent::__construct($table);
}
/**
* Returns node type string
*
* @return string
*/
public function getNodeType()
{
return 'ExampleTable';
}
/**
* Returns attached tags
* @return \string[]
*/
public function getTags()
{
return $this->tags;
}
/**
* Returns example table keyword.
*
* @return string
*/
public function getKeyword()
{
return $this->keyword;
}
}
@@ -0,0 +1,271 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Represents Gherkin Feature.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class FeatureNode implements KeywordNodeInterface, TaggedNodeInterface
{
/**
* @var null|string
*/
private $title;
/**
* @var null|string
*/
private $description;
/**
* @var string[]
*/
private $tags = array();
/**
* @var null|BackgroundNode
*/
private $background;
/**
* @var ScenarioInterface[]
*/
private $scenarios = array();
/**
* @var string
*/
private $keyword;
/**
* @var string
*/
private $language;
/**
* @var null|string
*/
private $file;
/**
* @var integer
*/
private $line;
/**
* Initializes feature.
*
* @param null|string $title
* @param null|string $description
* @param string[] $tags
* @param null|BackgroundNode $background
* @param ScenarioInterface[] $scenarios
* @param string $keyword
* @param string $language
* @param null|string $file The absolute path to the feature file.
* @param integer $line
*/
public function __construct(
$title,
$description,
array $tags,
BackgroundNode $background = null,
array $scenarios,
$keyword,
$language,
$file,
$line
) {
// Verify that the feature file is an absolute path.
if (!empty($file) && !$this->isAbsolutePath($file)) {
throw new \InvalidArgumentException('The file should be an absolute path.');
}
$this->title = $title;
$this->description = $description;
$this->tags = $tags;
$this->background = $background;
$this->scenarios = $scenarios;
$this->keyword = $keyword;
$this->language = $language;
$this->file = $file;
$this->line = $line;
}
/**
* Returns node type string
*
* @return string
*/
public function getNodeType()
{
return 'Feature';
}
/**
* Returns feature title.
*
* @return null|string
*/
public function getTitle()
{
return $this->title;
}
/**
* Checks if feature has a description.
*
* @return bool
*/
public function hasDescription()
{
return !empty($this->description);
}
/**
* Returns feature description.
*
* @return null|string
*/
public function getDescription()
{
return $this->description;
}
/**
* Checks if feature is tagged with tag.
*
* @param string $tag
*
* @return bool
*/
public function hasTag($tag)
{
return in_array($tag, $this->tags);
}
/**
* Checks if feature has tags.
*
* @return bool
*/
public function hasTags()
{
return 0 < count($this->tags);
}
/**
* Returns feature tags.
*
* @return string[]
*/
public function getTags()
{
return $this->tags;
}
/**
* Checks if feature has background.
*
* @return bool
*/
public function hasBackground()
{
return null !== $this->background;
}
/**
* Returns feature background.
*
* @return null|BackgroundNode
*/
public function getBackground()
{
return $this->background;
}
/**
* Checks if feature has scenarios.
*
* @return bool
*/
public function hasScenarios()
{
return 0 < count($this->scenarios);
}
/**
* Returns feature scenarios.
*
* @return ScenarioInterface[]
*/
public function getScenarios()
{
return $this->scenarios;
}
/**
* Returns feature keyword.
*
* @return string
*/
public function getKeyword()
{
return $this->keyword;
}
/**
* Returns feature language.
*
* @return string
*/
public function getLanguage()
{
return $this->language;
}
/**
* Returns feature file as an absolute path.
*
* @return null|string
*/
public function getFile()
{
return $this->file;
}
/**
* Returns feature declaration line number.
*
* @return integer
*/
public function getLine()
{
return $this->line;
}
/**
* Returns whether the file path is an absolute path.
*
* @param string $file A file path
*
* @return bool
*
* @see https://github.com/symfony/filesystem/blob/master/Filesystem.php
*/
protected function isAbsolutePath($file)
{
if (null === $file) {
@trigger_error(sprintf('Calling "%s()" with a null in the $file argument is deprecated since Symfony 4.4.', __METHOD__), E_USER_DEPRECATED);
}
return strspn($file, '/\\', 0, 1)
|| (\strlen($file) > 3 && ctype_alpha($file[0])
&& ':' === $file[1]
&& strspn($file, '/\\', 2, 1)
)
|| null !== parse_url($file, PHP_URL_SCHEME)
;
}
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Gherkin keyword node interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface KeywordNodeInterface extends NodeInterface
{
/**
* Returns node keyword.
*
* @return string
*/
public function getKeyword();
/**
* Returns node title.
*
* @return null|string
*/
public function getTitle();
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Gherkin node interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface NodeInterface
{
/**
* Returns node type string
*
* @return string
*/
public function getNodeType();
/**
* Returns feature declaration line number.
*
* @return integer
*/
public function getLine();
}
@@ -0,0 +1,246 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Represents Gherkin Outline.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class OutlineNode implements ScenarioInterface
{
/**
* @var string
*/
private $title;
/**
* @var string[]
*/
private $tags;
/**
* @var StepNode[]
*/
private $steps;
/**
* @var ExampleTableNode|ExampleTableNode[]
*/
private $tables;
/**
* @var string
*/
private $keyword;
/**
* @var integer
*/
private $line;
/**
* @var null|ExampleNode[]
*/
private $examples;
/**
* Initializes outline.
*
* @param null|string $title
* @param string[] $tags
* @param StepNode[] $steps
* @param ExampleTableNode|ExampleTableNode[] $tables
* @param string $keyword
* @param integer $line
*/
public function __construct(
$title,
array $tags,
array $steps,
$tables,
$keyword,
$line
) {
$this->title = $title;
$this->tags = $tags;
$this->steps = $steps;
$this->keyword = $keyword;
$this->line = $line;
if (!is_array($tables)) {
$this->tables = array($tables);
} else {
$this->tables = $tables;
}
}
/**
* Returns node type string
*
* @return string
*/
public function getNodeType()
{
return 'Outline';
}
/**
* Returns outline title.
*
* @return null|string
*/
public function getTitle()
{
return $this->title;
}
/**
* Checks if outline is tagged with tag.
*
* @param string $tag
*
* @return bool
*/
public function hasTag($tag)
{
return in_array($tag, $this->getTags());
}
/**
* Checks if outline has tags (both inherited from feature and own).
*
* @return bool
*/
public function hasTags()
{
return 0 < count($this->getTags());
}
/**
* Returns outline tags (including inherited from feature).
*
* @return string[]
*/
public function getTags()
{
return $this->tags;
}
/**
* Checks if outline has steps.
*
* @return bool
*/
public function hasSteps()
{
return 0 < count($this->steps);
}
/**
* Returns outline steps.
*
* @return StepNode[]
*/
public function getSteps()
{
return $this->steps;
}
/**
* Checks if outline has examples.
*
* @return bool
*/
public function hasExamples()
{
return 0 < count($this->tables);
}
/**
* Builds and returns examples table for the outline.
*
* WARNING: it returns a merged table with tags lost.
*
* @deprecated use getExampleTables instead
* @return ExampleTableNode
*/
public function getExampleTable()
{
$table = array();
foreach ($this->tables[0]->getTable() as $k => $v) {
$table[$k] = $v;
}
/** @var ExampleTableNode $exampleTableNode */
$exampleTableNode = new ExampleTableNode($table, $this->tables[0]->getKeyword());
for ($i = 1; $i < count($this->tables); $i++) {
$exampleTableNode->mergeRowsFromTable($this->tables[$i]);
}
return $exampleTableNode;
}
/**
* Returns list of examples for the outline.
* @return ExampleNode[]
*/
public function getExamples()
{
return $this->examples = $this->examples ?: $this->createExamples();
}
/**
* Returns examples tables array for the outline.
* @return ExampleTableNode[]
*/
public function getExampleTables()
{
return $this->tables;
}
/**
* Returns outline keyword.
*
* @return string
*/
public function getKeyword()
{
return $this->keyword;
}
/**
* Returns outline declaration line number.
*
* @return integer
*/
public function getLine()
{
return $this->line;
}
/**
* Creates examples for this outline using examples table.
*
* @return ExampleNode[]
*/
protected function createExamples()
{
$examples = array();
foreach ($this->getExampleTables() as $exampleTable) {
foreach ($exampleTable->getColumnsHash() as $rowNum => $row) {
$examples[] = new ExampleNode(
$exampleTable->getRowAsString($rowNum + 1),
array_merge($this->tags, $exampleTable->getTags()),
$this->getSteps(),
$row,
$exampleTable->getRowLine($rowNum + 1),
$this->getTitle()
);
}
}
return $examples;
}
}
@@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Represents Gherkin PyString argument.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class PyStringNode implements ArgumentInterface
{
/**
* @var array
*/
private $strings = array();
/**
* @var integer
*/
private $line;
/**
* Initializes PyString.
*
* @param array $strings String in form of [$stringLine]
* @param integer $line Line number where string been started
*/
public function __construct(array $strings, $line)
{
$this->strings = $strings;
$this->line = $line;
}
/**
* Returns node type.
*
* @return string
*/
public function getNodeType()
{
return 'PyString';
}
/**
* Returns entire PyString lines set.
*
* @return array
*/
public function getStrings()
{
return $this->strings;
}
/**
* Returns raw string.
*
* @return string
*/
public function getRaw()
{
return implode("\n", $this->strings);
}
/**
* Converts PyString into string.
*
* @return string
*/
public function __toString()
{
return $this->getRaw();
}
/**
* Returns line number at which PyString was started.
*
* @return integer
*/
public function getLine()
{
return $this->line;
}
}
@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Gherkin scenario interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ScenarioInterface extends ScenarioLikeInterface, TaggedNodeInterface
{
}
@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Gherkin scenario-like interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ScenarioLikeInterface extends KeywordNodeInterface, StepContainerInterface
{
}
@@ -0,0 +1,150 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Represents Gherkin Scenario.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ScenarioNode implements ScenarioInterface
{
/**
* @var string
*/
private $title;
/**
* @var array
*/
private $tags = array();
/**
* @var StepNode[]
*/
private $steps = array();
/**
* @var string
*/
private $keyword;
/**
* @var integer
*/
private $line;
/**
* Initializes scenario.
*
* @param null|string $title
* @param array $tags
* @param StepNode[] $steps
* @param string $keyword
* @param integer $line
*/
public function __construct($title, array $tags, array $steps, $keyword, $line)
{
$this->title = $title;
$this->tags = $tags;
$this->steps = $steps;
$this->keyword = $keyword;
$this->line = $line;
}
/**
* Returns node type string
*
* @return string
*/
public function getNodeType()
{
return 'Scenario';
}
/**
* Returns scenario title.
*
* @return null|string
*/
public function getTitle()
{
return $this->title;
}
/**
* Checks if scenario is tagged with tag.
*
* @param string $tag
*
* @return bool
*/
public function hasTag($tag)
{
return in_array($tag, $this->getTags());
}
/**
* Checks if scenario has tags (both inherited from feature and own).
*
* @return bool
*/
public function hasTags()
{
return 0 < count($this->getTags());
}
/**
* Returns scenario tags (including inherited from feature).
*
* @return array
*/
public function getTags()
{
return $this->tags;
}
/**
* Checks if scenario has steps.
*
* @return bool
*/
public function hasSteps()
{
return 0 < count($this->steps);
}
/**
* Returns scenario steps.
*
* @return StepNode[]
*/
public function getSteps()
{
return $this->steps;
}
/**
* Returns scenario keyword.
*
* @return string
*/
public function getKeyword()
{
return $this->keyword;
}
/**
* Returns scenario declaration line number.
*
* @return integer
*/
public function getLine()
{
return $this->line;
}
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Gherkin step container interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface StepContainerInterface extends NodeInterface
{
/**
* Checks if container has steps.
*
* @return bool
*/
public function hasSteps();
/**
* Returns container steps.
*
* @return StepNode[]
*/
public function getSteps();
}
+152
View File
@@ -0,0 +1,152 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
use Behat\Gherkin\Exception\NodeException;
/**
* Represents Gherkin Step.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class StepNode implements NodeInterface
{
/**
* @var string
*/
private $keyword;
/**
* @var string
*/
private $keywordType;
/**
* @var string
*/
private $text;
/**
* @var ArgumentInterface[]
*/
private $arguments = array();
/**
* @var integer
*/
private $line;
/**
* Initializes step.
*
* @param string $keyword
* @param string $text
* @param ArgumentInterface[] $arguments
* @param integer $line
* @param string $keywordType
*/
public function __construct($keyword, $text, array $arguments, $line, $keywordType = null)
{
if (count($arguments) > 1) {
throw new NodeException(sprintf(
'Steps could have only one argument, but `%s %s` have %d.',
$keyword,
$text,
count($arguments)
));
}
$this->keyword = $keyword;
$this->text = $text;
$this->arguments = $arguments;
$this->line = $line;
$this->keywordType = $keywordType ?: 'Given';
}
/**
* Returns node type string
*
* @return string
*/
public function getNodeType()
{
return 'Step';
}
/**
* Returns step keyword in provided language (Given, When, Then, etc.).
*
* @return string
*
* @deprecated use getKeyword() instead
*/
public function getType()
{
return $this->getKeyword();
}
/**
* Returns step keyword in provided language (Given, When, Then, etc.).
*
* @return string
*
*/
public function getKeyword()
{
return $this->keyword;
}
/**
* Returns step type keyword (Given, When, Then, etc.).
*
* @return string
*/
public function getKeywordType()
{
return $this->keywordType;
}
/**
* Returns step text.
*
* @return string
*/
public function getText()
{
return $this->text;
}
/**
* Checks if step has arguments.
*
* @return bool
*/
public function hasArguments()
{
return 0 < count($this->arguments);
}
/**
* Returns step arguments.
*
* @return ArgumentInterface[]
*/
public function getArguments()
{
return $this->arguments;
}
/**
* Returns step declaration line number.
*
* @return integer
*/
public function getLine()
{
return $this->line;
}
}
@@ -0,0 +1,383 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
use ArrayIterator;
use Behat\Gherkin\Exception\NodeException;
use Iterator;
use IteratorAggregate;
use ReturnTypeWillChange;
/**
* Represents Gherkin Table argument.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class TableNode implements ArgumentInterface, IteratorAggregate
{
/**
* @var array
*/
private $table;
/**
* @var integer
*/
private $maxLineLength = array();
/**
* Initializes table.
*
* @param array $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
*
* @throws NodeException If the given table is invalid
*/
public function __construct(array $table)
{
$this->table = $table;
$columnCount = null;
foreach ($this->getRows() as $ridx => $row) {
if (!is_array($row)) {
throw new NodeException(sprintf(
"Table row '%s' is expected to be array, got %s",
$ridx,
gettype($row)
));
}
if ($columnCount === null) {
$columnCount = count($row);
}
if (count($row) !== $columnCount) {
throw new NodeException(sprintf(
"Table row '%s' is expected to have %s columns, got %s",
$ridx,
$columnCount,
count($row)
));
}
foreach ($row as $column => $string) {
if (!isset($this->maxLineLength[$column])) {
$this->maxLineLength[$column] = 0;
}
if (!is_scalar($string)) {
throw new NodeException(sprintf(
"Table cell at row '%s', col '%s' is expected to be scalar, got %s",
$ridx,
$column,
gettype($string)
));
}
$this->maxLineLength[$column] = max($this->maxLineLength[$column], mb_strlen($string, 'utf8'));
}
}
}
/**
* Creates a table from a given list.
*
* @param array $list One-dimensional array
*
* @return TableNode
*
* @throws NodeException If the given list is not a one-dimensional array
*/
public static function fromList(array $list)
{
if (count($list) !== count($list, COUNT_RECURSIVE)) {
throw new NodeException('List is not a one-dimensional array.');
}
array_walk($list, function (&$item) {
$item = array($item);
});
return new self($list);
}
/**
* Returns node type.
*
* @return string
*/
public function getNodeType()
{
return 'Table';
}
/**
* Returns table hash, formed by columns (ColumnsHash).
*
* @return array
*/
public function getHash()
{
return $this->getColumnsHash();
}
/**
* Returns table hash, formed by columns.
*
* @return array
*/
public function getColumnsHash()
{
$rows = $this->getRows();
$keys = array_shift($rows);
$hash = array();
foreach ($rows as $row) {
$hash[] = array_combine($keys, $row);
}
return $hash;
}
/**
* Returns table hash, formed by rows.
*
* @return array
*/
public function getRowsHash()
{
$hash = array();
foreach ($this->getRows() as $row) {
$hash[array_shift($row)] = (1 == count($row)) ? $row[0] : $row;
}
return $hash;
}
/**
* Returns numerated table lines.
* Line numbers are keys, lines are values.
*
* @return array
*/
public function getTable()
{
return $this->table;
}
/**
* Returns table rows.
*
* @return array
*/
public function getRows()
{
return array_values($this->table);
}
/**
* Returns table definition lines.
*
* @return array
*/
public function getLines()
{
return array_keys($this->table);
}
/**
* Returns specific row in a table.
*
* @param integer $index Row number
*
* @return array
*
* @throws NodeException If row with specified index does not exist
*/
public function getRow($index)
{
$rows = $this->getRows();
if (!isset($rows[$index])) {
throw new NodeException(sprintf('Rows #%d does not exist in table.', $index));
}
return $rows[$index];
}
/**
* Returns specific column in a table.
*
* @param integer $index Column number
*
* @return array
*
* @throws NodeException If column with specified index does not exist
*/
public function getColumn($index)
{
if ($index >= count($this->getRow(0))) {
throw new NodeException(sprintf('Column #%d does not exist in table.', $index));
}
$rows = $this->getRows();
$column = array();
foreach ($rows as $row) {
$column[] = $row[$index];
}
return $column;
}
/**
* Returns line number at which specific row was defined.
*
* @param integer $index
*
* @return integer
*
* @throws NodeException If row with specified index does not exist
*/
public function getRowLine($index)
{
$lines = array_keys($this->table);
if (!isset($lines[$index])) {
throw new NodeException(sprintf('Rows #%d does not exist in table.', $index));
}
return $lines[$index];
}
/**
* Converts row into delimited string.
*
* @param integer $rowNum Row number
*
* @return string
*/
public function getRowAsString($rowNum)
{
$values = array();
foreach ($this->getRow($rowNum) as $column => $value) {
$values[] = $this->padRight(' ' . $value . ' ', $this->maxLineLength[$column] + 2);
}
return sprintf('|%s|', implode('|', $values));
}
/**
* Converts row into delimited string.
*
* @param integer $rowNum Row number
* @param callable $wrapper Wrapper function
*
* @return string
*/
public function getRowAsStringWithWrappedValues($rowNum, $wrapper)
{
$values = array();
foreach ($this->getRow($rowNum) as $column => $value) {
$value = $this->padRight(' ' . $value . ' ', $this->maxLineLength[$column] + 2);
$values[] = call_user_func($wrapper, $value, $column);
}
return sprintf('|%s|', implode('|', $values));
}
/**
* Converts entire table into string
*
* @return string
*/
public function getTableAsString()
{
$lines = array();
for ($i = 0; $i < count($this->getRows()); $i++) {
$lines[] = $this->getRowAsString($i);
}
return implode("\n", $lines);
}
/**
* Returns line number at which table was started.
*
* @return integer
*/
public function getLine()
{
return $this->getRowLine(0);
}
/**
* Converts table into string
*
* @return string
*/
public function __toString()
{
return $this->getTableAsString();
}
/**
* Retrieves a hash iterator.
*
* @return Iterator
*/
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->getHash());
}
/**
* Obtains and adds rows from another table to the current table.
* The second table should have the same structure as the current one.
* @param TableNode $node
*
* @deprecated remove together with OutlineNode::getExampleTable
*/
public function mergeRowsFromTable(TableNode $node)
{
// check structure
if ($this->getRow(0) !== $node->getRow(0)) {
throw new NodeException("Tables have different structure. Cannot merge one into another");
}
$firstLine = $node->getLine();
foreach ($node->getTable() as $line => $value) {
if ($line === $firstLine) {
continue;
}
$this->table[$line] = $value;
}
}
/**
* Pads string right.
*
* @param string $text Text to pad
* @param integer $length Length
*
* @return string
*/
protected function padRight($text, $length)
{
while ($length > mb_strlen($text, 'utf8')) {
$text = $text . ' ';
}
return $text;
}
}
@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin\Node;
/**
* Gherkin tagged node interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface TaggedNodeInterface extends NodeInterface
{
/**
* Checks if node is tagged with tag.
*
* @param string $tag
*
* @return bool
*/
public function hasTag($tag);
/**
* Checks if node has tags (both inherited from feature and own).
*
* @return bool
*/
public function hasTags();
/**
* Returns node tags (including inherited from feature).
*
* @return string[]
*/
public function getTags();
}
+771
View File
@@ -0,0 +1,771 @@
<?php
/*
* This file is part of the Behat Gherkin.
* (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\Gherkin;
use Behat\Gherkin\Exception\LexerException;
use Behat\Gherkin\Exception\NodeException;
use Behat\Gherkin\Exception\ParserException;
use Behat\Gherkin\Node\BackgroundNode;
use Behat\Gherkin\Node\ExampleTableNode;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\ScenarioInterface;
use Behat\Gherkin\Node\ScenarioNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Gherkin\Node\TableNode;
/**
* Gherkin parser.
*
* $lexer = new Behat\Gherkin\Lexer($keywords);
* $parser = new Behat\Gherkin\Parser($lexer);
* $featuresArray = $parser->parse('/path/to/feature.feature');
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class Parser
{
private $lexer;
private $input;
private $file;
private $tags = array();
private $languageSpecifierLine;
private $passedNodesStack = array();
/**
* Initializes parser.
*
* @param Lexer $lexer Lexer instance
*/
public function __construct(Lexer $lexer)
{
$this->lexer = $lexer;
}
/**
* Parses input & returns features array.
*
* @param string $input Gherkin string document
* @param string $file File name
*
* @return FeatureNode|null
*
* @throws ParserException
*/
public function parse($input, $file = null)
{
$this->languageSpecifierLine = null;
$this->input = $input;
$this->file = $file;
$this->tags = array();
try {
$this->lexer->analyse($this->input, 'en');
} catch (LexerException $e) {
throw new ParserException(
sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
0,
$e
);
}
$feature = null;
while ('EOS' !== ($predicted = $this->predictTokenType())) {
$node = $this->parseExpression();
if (null === $node || "\n" === $node) {
continue;
}
if (!$feature && $node instanceof FeatureNode) {
$feature = $node;
continue;
}
if ($feature && $node instanceof FeatureNode) {
throw new ParserException(sprintf(
'Only one feature is allowed per feature file. But %s got multiple.',
$this->file
));
}
if (is_string($node)) {
throw new ParserException(sprintf(
'Expected Feature, but got text: "%s"%s',
$node,
$this->file ? ' in file: ' . $this->file : ''
));
}
if (!$node instanceof FeatureNode) {
throw new ParserException(sprintf(
'Expected Feature, but got %s on line: %d%s',
$node->getKeyword(),
$node->getLine(),
$this->file ? ' in file: ' . $this->file : ''
));
}
}
return $feature;
}
/**
* Returns next token if it's type equals to expected.
*
* @param string $type Token type
*
* @return array
*
* @throws Exception\ParserException
*/
protected function expectTokenType($type)
{
$types = (array) $type;
if (in_array($this->predictTokenType(), $types)) {
return $this->lexer->getAdvancedToken();
}
$token = $this->lexer->predictToken();
throw new ParserException(sprintf(
'Expected %s token, but got %s on line: %d%s',
implode(' or ', $types),
$this->predictTokenType(),
$token['line'],
$this->file ? ' in file: ' . $this->file : ''
));
}
/**
* Returns next token if it's type equals to expected.
*
* @param string $type Token type
*
* @return null|array
*/
protected function acceptTokenType($type)
{
if ($type !== $this->predictTokenType()) {
return null;
}
return $this->lexer->getAdvancedToken();
}
/**
* Returns next token type without real input reading (prediction).
*
* @return string
*/
protected function predictTokenType()
{
$token = $this->lexer->predictToken();
return $token['type'];
}
/**
* Parses current expression & returns Node.
*
* @return string|FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|TableNode|StepNode
*
* @throws ParserException
*/
protected function parseExpression()
{
$type = $this->predictTokenType();
while ($type === 'Comment') {
$this->expectTokenType('Comment');
$type = $this->predictTokenType();
}
switch ($type) {
case 'Feature':
return $this->parseFeature();
case 'Background':
return $this->parseBackground();
case 'Scenario':
return $this->parseScenario();
case 'Outline':
return $this->parseOutline();
case 'Examples':
return $this->parseExamples();
case 'TableRow':
return $this->parseTable();
case 'PyStringOp':
return $this->parsePyString();
case 'Step':
return $this->parseStep();
case 'Text':
return $this->parseText();
case 'Newline':
return $this->parseNewline();
case 'Tag':
return $this->parseTags();
case 'Language':
return $this->parseLanguage();
case 'EOS':
return '';
}
throw new ParserException(sprintf('Unknown token type: %s', $type));
}
/**
* Parses feature token & returns it's node.
*
* @return FeatureNode
*
* @throws ParserException
*/
protected function parseFeature()
{
$token = $this->expectTokenType('Feature');
$title = trim($token['value'] ?? '');
$description = null;
$tags = $this->popTags();
$background = null;
$scenarios = array();
$keyword = $token['keyword'];
$language = $this->lexer->getLanguage();
$file = $this->file;
$line = $token['line'];
array_push($this->passedNodesStack, 'Feature');
// Parse description, background, scenarios & outlines
while ('EOS' !== $this->predictTokenType()) {
$node = $this->parseExpression();
if (is_string($node)) {
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
$description .= (null !== $description ? "\n" : '') . $text;
continue;
}
if (!$background && $node instanceof BackgroundNode) {
$background = $node;
continue;
}
if ($node instanceof ScenarioInterface) {
$scenarios[] = $node;
continue;
}
if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) {
throw new ParserException(sprintf(
'Each Feature could have only one Background, but found multiple on lines %d and %d%s',
$background->getLine(),
$node->getLine(),
$this->file ? ' in file: ' . $this->file : ''
));
}
if (!$node instanceof ScenarioNode) {
throw new ParserException(sprintf(
'Expected Scenario, Outline or Background, but got %s on line: %d%s',
$node->getNodeType(),
$node->getLine(),
$this->file ? ' in file: ' . $this->file : ''
));
}
}
return new FeatureNode(
rtrim($title) ?: null,
rtrim($description ?? '') ?: null,
$tags,
$background,
$scenarios,
$keyword,
$language,
$file,
$line
);
}
/**
* Parses background token & returns it's node.
*
* @return BackgroundNode
*
* @throws ParserException
*/
protected function parseBackground()
{
$token = $this->expectTokenType('Background');
$title = trim($token['value'] ?? '');
$keyword = $token['keyword'];
$line = $token['line'];
if (count($this->popTags())) {
throw new ParserException(sprintf(
'Background can not be tagged, but it is on line: %d%s',
$line,
$this->file ? ' in file: ' . $this->file : ''
));
}
// Parse description and steps
$steps = array();
$allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment');
while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
$node = $this->parseExpression();
if ($node instanceof StepNode) {
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
continue;
}
if (!count($steps) && is_string($node)) {
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
$title .= "\n" . $text;
continue;
}
if ("\n" === $node) {
continue;
}
if (is_string($node)) {
throw new ParserException(sprintf(
'Expected Step, but got text: "%s"%s',
$node,
$this->file ? ' in file: ' . $this->file : ''
));
}
if (!$node instanceof StepNode) {
throw new ParserException(sprintf(
'Expected Step, but got %s on line: %d%s',
$node->getNodeType(),
$node->getLine(),
$this->file ? ' in file: ' . $this->file : ''
));
}
}
return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line);
}
/**
* Parses scenario token & returns it's node.
*
* @return ScenarioNode
*
* @throws ParserException
*/
protected function parseScenario()
{
$token = $this->expectTokenType('Scenario');
$title = trim($token['value'] ?? '');
$tags = $this->popTags();
$keyword = $token['keyword'];
$line = $token['line'];
array_push($this->passedNodesStack, 'Scenario');
// Parse description and steps
$steps = array();
while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) {
$node = $this->parseExpression();
if ($node instanceof StepNode) {
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
continue;
}
if (!count($steps) && is_string($node)) {
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
$title .= "\n" . $text;
continue;
}
if ("\n" === $node) {
continue;
}
if (is_string($node)) {
throw new ParserException(sprintf(
'Expected Step, but got text: "%s"%s',
$node,
$this->file ? ' in file: ' . $this->file : ''
));
}
if (!$node instanceof StepNode) {
throw new ParserException(sprintf(
'Expected Step, but got %s on line: %d%s',
$node->getNodeType(),
$node->getLine(),
$this->file ? ' in file: ' . $this->file : ''
));
}
}
array_pop($this->passedNodesStack);
return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line);
}
/**
* Parses scenario outline token & returns it's node.
*
* @return OutlineNode
*
* @throws ParserException
*/
protected function parseOutline()
{
$token = $this->expectTokenType('Outline');
$title = trim($token['value'] ?? '');
$tags = $this->popTags();
$keyword = $token['keyword'];
/** @var ExampleTableNode $examples */
$examples = array();
$line = $token['line'];
// Parse description, steps and examples
$steps = array();
array_push($this->passedNodesStack, 'Outline');
while (in_array($nextTokenType = $this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment', 'Tag'))) {
if ($nextTokenType === 'Comment') {
$this->lexer->skipPredictedToken();
continue;
}
$node = $this->parseExpression();
if ($node instanceof StepNode) {
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
continue;
}
if ($node instanceof ExampleTableNode) {
$examples[] = $node;
continue;
}
if (!count($steps) && is_string($node)) {
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
$title .= "\n" . $text;
continue;
}
if ("\n" === $node) {
continue;
}
if (is_string($node)) {
throw new ParserException(sprintf(
'Expected Step or Examples table, but got text: "%s"%s',
$node,
$this->file ? ' in file: ' . $this->file : ''
));
}
if (!$node instanceof StepNode) {
throw new ParserException(sprintf(
'Expected Step or Examples table, but got %s on line: %d%s',
$node->getNodeType(),
$node->getLine(),
$this->file ? ' in file: ' . $this->file : ''
));
}
}
if (empty($examples)) {
throw new ParserException(sprintf(
'Outline should have examples table, but got none for outline "%s" on line: %d%s',
rtrim($title),
$line,
$this->file ? ' in file: ' . $this->file : ''
));
}
return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line);
}
/**
* Parses step token & returns it's node.
*
* @return StepNode
*/
protected function parseStep()
{
$token = $this->expectTokenType('Step');
$keyword = $token['value'];
$keywordType = $token['keyword_type'];
$text = trim($token['text']);
$line = $token['line'];
array_push($this->passedNodesStack, 'Step');
$arguments = array();
while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) {
if ('Comment' === $predicted || 'Newline' === $predicted) {
$this->acceptTokenType($predicted);
continue;
}
$node = $this->parseExpression();
if ($node instanceof PyStringNode || $node instanceof TableNode) {
$arguments[] = $node;
}
}
array_pop($this->passedNodesStack);
return new StepNode($keyword, $text, $arguments, $line, $keywordType);
}
/**
* Parses examples table node.
*
* @return ExampleTableNode
*/
protected function parseExamples()
{
$keyword = ($this->expectTokenType('Examples'))['keyword'];
$tags = empty($this->tags) ? array() : $this->popTags();
$table = $this->parseTableRows();
try {
return new ExampleTableNode($table, $keyword, $tags);
} catch(NodeException $e) {
$this->rethrowNodeException($e);
}
}
/**
* Parses table token & returns it's node.
*
* @return TableNode
*/
protected function parseTable()
{
$table = $this->parseTableRows();
try {
return new TableNode($table);
} catch(NodeException $e) {
$this->rethrowNodeException($e);
}
}
/**
* Parses PyString token & returns it's node.
*
* @return PyStringNode
*/
protected function parsePyString()
{
$token = $this->expectTokenType('PyStringOp');
$line = $token['line'];
$strings = array();
while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) {
$token = $this->expectTokenType('Text');
$strings[] = $token['value'];
}
$this->expectTokenType('PyStringOp');
return new PyStringNode($strings, $line);
}
/**
* Parses tags.
*
* @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
*/
protected function parseTags()
{
$token = $this->expectTokenType('Tag');
$this->guardTags($token['tags']);
$this->tags = array_merge($this->tags, $token['tags']);
$possibleTransitions = array(
'Outline' => array(
'Examples',
'Step'
)
);
$currentType = '-1';
// check if that is ok to go inside:
if (!empty($this->passedNodesStack)) {
$currentType = $this->passedNodesStack[count($this->passedNodesStack) - 1];
}
$nextType = $this->predictTokenType();
if (!isset($possibleTransitions[$currentType]) || in_array($nextType, $possibleTransitions[$currentType])) {
return $this->parseExpression();
}
return "\n";
}
/**
* Returns current set of tags and clears tag buffer.
*
* @return array
*/
protected function popTags()
{
$tags = $this->tags;
$this->tags = array();
return $tags;
}
/**
* Checks the tags fit the required format
*
* @param string[] $tags
*/
protected function guardTags(array $tags)
{
foreach ($tags as $tag) {
if (preg_match('/\s/', $tag)) {
trigger_error(sprintf('Whitespace in tags is deprecated, found "%s"', $tag), E_USER_DEPRECATED);
}
}
}
/**
* Parses next text line & returns it.
*
* @return string
*/
protected function parseText()
{
$token = $this->expectTokenType('Text');
return $token['value'];
}
/**
* Parses next newline & returns \n.
*
* @return string
*/
protected function parseNewline()
{
$this->expectTokenType('Newline');
return "\n";
}
/**
* Parses language block and updates lexer configuration based on it.
*
* @return BackgroundNode|FeatureNode|OutlineNode|ScenarioNode|StepNode|TableNode|string
*
* @throws ParserException
*/
protected function parseLanguage()
{
$token = $this->expectTokenType('Language');
if (null === $this->languageSpecifierLine) {
$this->lexer->analyse($this->input, $token['value']);
$this->languageSpecifierLine = $token['line'];
} elseif ($token['line'] !== $this->languageSpecifierLine) {
throw new ParserException(sprintf(
'Ambiguous language specifiers on lines: %d and %d%s',
$this->languageSpecifierLine,
$token['line'],
$this->file ? ' in file: ' . $this->file : ''
));
}
return $this->parseExpression();
}
/**
* Parses the rows of a table
*
* @return string[][]
*/
private function parseTableRows()
{
$table = array();
while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) {
if ('Comment' === $predicted || 'Newline' === $predicted) {
$this->acceptTokenType($predicted);
continue;
}
$token = $this->expectTokenType('TableRow');
$table[$token['line']] = $token['columns'];
}
return $table;
}
/**
* Changes step node type for types But, And to type of previous step if it exists else sets to Given
*
* @param StepNode $node
* @param StepNode[] $steps
* @return StepNode
*/
private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array())
{
if (in_array($node->getKeywordType(), array('And', 'But'))) {
if (($prev = end($steps))) {
$keywordType = $prev->getKeywordType();
} else {
$keywordType = 'Given';
}
$node = new StepNode(
$node->getKeyword(),
$node->getText(),
$node->getArguments(),
$node->getLine(),
$keywordType
);
}
return $node;
}
private function rethrowNodeException(NodeException $e): void
{
throw new ParserException(
$e->getMessage() . ($this->file ? ' in file ' . $this->file : ''),
0,
$e
);
}
}