first commit
This commit is contained in:
605
vendor/behat/gherkin/CHANGELOG.md
vendored
Normal file
605
vendor/behat/gherkin/CHANGELOG.md
vendored
Normal file
@ -0,0 +1,605 @@
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
This project follows the [Behat release and version support policies]
|
||||
(https://docs.behat.org/en/latest/releases.html).
|
||||
|
||||
# [4.16.1] - 2025-12-08
|
||||
|
||||
### Fixed
|
||||
|
||||
* Reinstate support for tag filter expressions without a leading `@` (e.g. `wip&&~slow` instead of `@wip&&~@slow`).
|
||||
This syntax was never officially supported, but previously worked and was broken by 4.16.0. We have temporarily
|
||||
fixed this, but it is deprecated and will be removed in the next major version.
|
||||
|
||||
# [4.16.0] - 2025-12-05
|
||||
|
||||
### Changed
|
||||
|
||||
* Further improvements to parser parity when the experimental `gherkin-32` compatibility mode is enabled:
|
||||
- Parse descriptions (instead of multiline titles) for all describable nodes by @acoulton in [#361](https://github.com/Behat/Gherkin/pull/361)
|
||||
- Unescape escaped delimiters within doc strings by @stof in [#393](https://github.com/Behat/Gherkin/pull/393)
|
||||
- Retain the `@` prefix when parsing tags by @acoulton in [#400](https://github.com/Behat/Gherkin/pull/400)
|
||||
- Trim unicode padding from table cells by @stof in [#405](https://github.com/Behat/Gherkin/pull/405)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix the implementation of the default dialect for the keywords provider by @stof in [#404](https://github.com/Behat/Gherkin/pull/404)
|
||||
|
||||
### Internal
|
||||
|
||||
* Add `Stringable` to classes implementing __toString() by @acoulton in [#402](https://github.com/Behat/Gherkin/pull/402)
|
||||
* Fix cucumber variant assertions to include inherited properties by @acoulton in [#394](https://github.com/Behat/Gherkin/pull/394)
|
||||
* Update cucumber/gherkin parity tests to 37.0.0 by @behat-gherkin-updater[bot] in [#397](https://github.com/Behat/Gherkin/pull/397) and [#398](https://github.com/Behat/Gherkin/pull/398)
|
||||
* update_cucumber script should not fail on manually created releases by @acoulton in [#396](https://github.com/Behat/Gherkin/pull/396)
|
||||
* Add funding links and information by @acoulton in [#401](https://github.com/Behat/Gherkin/pull/401)
|
||||
|
||||
# [4.15.0] - 2025-11-05
|
||||
|
||||
### Changed
|
||||
|
||||
* Added a new ParserInterface and deprecated extending the core Lexer, Parser and Node classes by @acoulton in [#354](https://github.com/Behat/Gherkin/pull/354)
|
||||
* Deprecate the CucumberNDJsonAstLoader (which was only intended for internal use by our tests) by @stof in [#356](https://github.com/Behat/Gherkin/pull/356)
|
||||
* By default, the parser ignores invalid language tags (e.g. `#language:no-such`) and falls back to the default language
|
||||
(e.g. `en`). Previously, the resultant `FeatureNode::getLanguage()` would return the original invalid value from the
|
||||
feature file - it will now return the language that was actually used for parsing. By @stof in [#350](https://github.com/Behat/Gherkin/pull/350)
|
||||
|
||||
### Added
|
||||
|
||||
* Introduce a DialectProviderInterface matching the modern cucumber API. This will replace the existing Keywords API in
|
||||
a future major release. By @stof in [#350](https://github.com/Behat/Gherkin/pull/350)
|
||||
* Introduce configurable `GherkinCompatibilityMode` to control how gherkin files are parsed. In the default `legacy` mode,
|
||||
there is no change to parsing. In the new **experimental** `gherkin-32` mode, files will in future be parsed
|
||||
consistently with the official cucumber/gherkin parsers. This mode is not yet complete - in this first release:
|
||||
- Whitespace within description nodes will not be trimmed by @acoulton in [#349](https://github.com/Behat/Gherkin/pull/349)
|
||||
- Invalid language tags will cause an exception by @stof in [#357](https://github.com/Behat/Gherkin/pull/357)
|
||||
- Step keywords will not be trimmed by @stof in [#360](https://github.com/Behat/Gherkin/pull/360)
|
||||
- Language tags can include whitespace by @acoulton in [#358](https://github.com/Behat/Gherkin/pull/358)
|
||||
- `\n` literals in table cells will be parsed as newlines by @stof in [#359](https://github.com/Behat/Gherkin/pull/359)
|
||||
and [#391](https://github.com/Behat/Gherkin/pull/391)
|
||||
* Improved translations for `ru` (Russian) and `af` (Afrikaans) from cucumber/gherkin in [#381](https://github.com/Behat/Gherkin/pull/381)
|
||||
and [#386](https://github.com/Behat/Gherkin/pull/386)
|
||||
* Support PHP 8.5 by @acoulton in [#388](https://github.com/Behat/Gherkin/pull/388)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Improve phpdoc / phpstan type-hinting of the lexer and parser by @uuf6429 in [#344](https://github.com/Behat/Gherkin/pull/344)
|
||||
and @stof in [#363](https://github.com/Behat/Gherkin/pull/363)
|
||||
* Handle race conditions when creating cache directory by @uuf6429 in [#373](https://github.com/Behat/Gherkin/pull/373)
|
||||
* Throw if Loader->load() called with unsupported resource by @uuf6429 in [#372](https://github.com/Behat/Gherkin/pull/372)
|
||||
* Use default file cache key if `behat/gherkin` version is unknown by @uuf6429 in [#370](https://github.com/Behat/Gherkin/pull/370)
|
||||
|
||||
### Internal
|
||||
|
||||
* Enable PHPStan level 10 and resolve remaining warnings by @uuf6429 in [#368](https://github.com/Behat/Gherkin/pull/368)
|
||||
* Remove duplication and improve robustness in filesystem operations by @uuf6429 in [#365](https://github.com/Behat/Gherkin/pull/365)
|
||||
and [#367](https://github.com/Behat/Gherkin/pull/367)
|
||||
* Explicitly cover expected departures from cucumber gherkin parsing with tests by @acoulton in [#392](https://github.com/Behat/Gherkin/pull/392)
|
||||
* Update cucumber/gherkin parity tests to v36.0.0 in [#355](https://github.com/Behat/Gherkin/pull/355), [#376](https://github.com/Behat/Gherkin/pull/376)
|
||||
[#378](https://github.com/Behat/Gherkin/pull/378), [#381](https://github.com/Behat/Gherkin/pull/381), [#385](https://github.com/Behat/Gherkin/pull/385)
|
||||
[#386](https://github.com/Behat/Gherkin/pull/386) and [#387](https://github.com/Behat/Gherkin/pull/387)
|
||||
* Fixes and improvements to the cucumber update CI job by @acoulton in [#374](https://github.com/Behat/Gherkin/pull/374),
|
||||
[#375](https://github.com/Behat/Gherkin/pull/375), [#379](https://github.com/Behat/Gherkin/pull/379)
|
||||
and [#380](https://github.com/Behat/Gherkin/pull/380)
|
||||
* Minor coding style fixes by @acoulton in [#377](https://github.com/Behat/Gherkin/pull/377) and [#383](https://github.com/Behat/Gherkin/pull/383)
|
||||
* Minor code improvements to Lexer/Parser implementation by @uuf6429 in [#352](https://github.com/Behat/Gherkin/pull/352)
|
||||
* Minor code improvements to TableNode by @uuf6429 in [#366](https://github.com/Behat/Gherkin/pull/366)
|
||||
* Add native typehints where this does not break BC by @stof in [#353](https://github.com/Behat/Gherkin/pull/353)
|
||||
* Fix typo of a PHPStan alias type by @uuf6429 in [#371](https://github.com/Behat/Gherkin/pull/371)
|
||||
* Fix github actions workflow job name by @uuf6429 in [#369](https://github.com/Behat/Gherkin/pull/369)
|
||||
|
||||
# [4.14.0] - 2025-05-23
|
||||
|
||||
### Changed
|
||||
|
||||
* Throw ParserException if file ends with tags by @acoulton in [#313](https://github.com/Behat/Gherkin/pull/313)
|
||||
* Throw ParserException if Background comes after first Scenario by @acoulton in [#343](https://github.com/Behat/Gherkin/pull/343)
|
||||
* For compatibility with the official cucumber/gherkin parsers, we now accept some gherkin syntax that would previously
|
||||
have triggered a ParserException. Users may wish to consider running a tool like gherkin-lint in CI to detect
|
||||
incomplete feature files or valid-but-unusual gherkin syntax. The specific changes are:
|
||||
- Parse `Scenario` and `Scenario Outline` as synonyms depending on the presence (or not) of an `Examples:` keyword.
|
||||
by @acoulton in [#316](https://github.com/Behat/Gherkin/pull/316) and [#324](https://github.com/Behat/Gherkin/pull/324)
|
||||
- Do not throw on some unexpected Feature / Language tags by @acoulton in [#323](https://github.com/Behat/Gherkin/pull/323)
|
||||
- Do not throw on `.feature` file that does not contain a Feature by @acoulton in [#340](https://github.com/Behat/Gherkin/pull/340)
|
||||
- Ignore content after table right-hand `|` (instead of throwing) by @acoulton in [#341](https://github.com/Behat/Gherkin/pull/341)
|
||||
* Remove the line length from the NewLine token value by @stof in [#338](https://github.com/Behat/Gherkin/pull/338)
|
||||
* Added precise PHPStan type information by @stof in [#332](https://github.com/Behat/Gherkin/pull/332),
|
||||
[#333](https://github.com/Behat/Gherkin/pull/333), [#339](https://github.com/Behat/Gherkin/pull/339)
|
||||
and [#334](https://github.com/Behat/Gherkin/pull/334)
|
||||
|
||||
### Internal
|
||||
|
||||
* Make private props readonly; fix tests by @uuf6429 in [#319](https://github.com/Behat/Gherkin/pull/319)
|
||||
* Use the `Yaml::parseFile` API to handle Yaml files by @stof in [#335](https://github.com/Behat/Gherkin/pull/335)
|
||||
* test: Make CucumberND name reading consistent by @uuf6429 in [#309](https://github.com/Behat/Gherkin/pull/309)
|
||||
* test: Use vfsStream to simplify / improve filesystem-related tests by @uuf6429 in [#298](https://github.com/Behat/Gherkin/pull/298)
|
||||
* test: Handle optional tableHeader when loading NDJson examples by @uuf6429 in [#294](https://github.com/Behat/Gherkin/pull/294)
|
||||
* test: Refactor valid ParserExceptionsTest examples into cucumber/gherkin testdata by @acoulton in [#322](https://github.com/Behat/Gherkin/pull/322)
|
||||
* test: Compare step arguments when checking gherkin parity by @acoulton in [#325](https://github.com/Behat/Gherkin/pull/325)
|
||||
* test: Use a custom object comparator to ignore the keywordType of StepNode by @stof in [#331](https://github.com/Behat/Gherkin/pull/331)
|
||||
* ci: Add conventional title to gherkin update, error on missing asserts by @acoulton in [#314](https://github.com/Behat/Gherkin/pull/314)
|
||||
* Assert that preg_split does not fail when splitting a table row by @stof in [#337](https://github.com/Behat/Gherkin/pull/337)
|
||||
* Add assertions in the parser to reflect the structure of tokens by @stof in [#342](https://github.com/Behat/Gherkin/pull/342)
|
||||
* style: Define and change phpdoc order coding style by @uuf6429 in [#345](https://github.com/Behat/Gherkin/pull/345)
|
||||
|
||||
|
||||
# [4.13.0] - 2025-05-06
|
||||
|
||||
### Changed
|
||||
|
||||
* Files have been moved to flatten paths into a PSR-4 structure (instead of the previous PSR-0). This may affect users
|
||||
who are requiring files directly rather than using the composer autoloader as expected.
|
||||
See the 4.12.0 release for the new `CachedArrayKeywords::withDefaultKeywords()` to use the `i18n.php` file without
|
||||
depending on paths to other files in this repo. By @uuf6429 in [#288](https://github.com/Behat/Gherkin/pull/288)
|
||||
|
||||
### Added
|
||||
|
||||
* ExampleTableNode now implements TaggedNodeInterface. Also refactored node tag handling methods. By @uuf6429 in
|
||||
[#289](https://github.com/Behat/Gherkin/pull/289)
|
||||
* Improve some exceptions thrown when parsing invalid feature files. Also increased test coverage. By @uuf6429 in
|
||||
[#295](https://github.com/Behat/Gherkin/pull/295)
|
||||
* New translations for `amh` (Amharic), `be` (Belarusian) and `ml` (Malayalam) from cucumber/gherkin in [#306](https://github.com/Behat/Gherkin/pull/306)
|
||||
* Improved translations / whitespace for `ga` (Irish), `it` (Italian), `ja` (Japanese), `ka` (Georgian) and `ko` (Korean)
|
||||
from cucumber/gherkin in [#306](https://github.com/Behat/Gherkin/pull/306)
|
||||
|
||||
### Internal
|
||||
|
||||
* Fix & improve automatic CI updates to newer cucumber/gherkin test data and translations. By @acoulton in
|
||||
[#300](https://github.com/Behat/Gherkin/pull/300), [#302](https://github.com/Behat/Gherkin/pull/302),
|
||||
[#304](https://github.com/Behat/Gherkin/pull/304), [#305](https://github.com/Behat/Gherkin/pull/305)
|
||||
* Update code style and resolve PHPStan warnings (up to level 9) in tests and CI scripts. By @uuf6429 in
|
||||
[#296](https://github.com/Behat/Gherkin/pull/296), [#297](https://github.com/Behat/Gherkin/pull/297)
|
||||
and [#307](https://github.com/Behat/Gherkin/pull/307)
|
||||
* Make tests that expect exceptions more explicit by @uuf6429 in [#310](https://github.com/Behat/Gherkin/pull/310)
|
||||
* Improve CI workflows and integrate Codecov reporting by @uuf6429 in [#299](https://github.com/Behat/Gherkin/pull/299)
|
||||
and [#301](https://github.com/Behat/Gherkin/pull/301)
|
||||
* Refactor tag filtering implementation by @uuf6429 in [#308](https://github.com/Behat/Gherkin/pull/308)
|
||||
* Update cucumber/gherkin parity tests to v32.1.1 in [#306](https://github.com/Behat/Gherkin/pull/306)
|
||||
|
||||
# [4.12.0] - 2025-02-26
|
||||
|
||||
### Changed
|
||||
* Gherkin::VERSION is deprecated and will not be updated, use the composer runtime API if you need to identify the
|
||||
running version. This also changes the value used to namespace cached feature files.
|
||||
by @acoulton in [#279](https://github.com/Behat/Gherkin/pull/279)
|
||||
|
||||
### Added
|
||||
|
||||
* Provide `CachedArrayKeywords::withDefaultKeywords()` to create an instance without an external dependency on the path
|
||||
to the `i18n.php` file in this repo. **NOTE** that paths to source files will change in the next Gherkin release -
|
||||
use the new constructor to avoid any impact.
|
||||
by @carlos-granados in [#290](https://github.com/Behat/Gherkin/pull/290)
|
||||
|
||||
### Internal
|
||||
|
||||
* Upgrade to phpunit 10 by @uuf6429 in [#275](https://github.com/Behat/Gherkin/pull/275)
|
||||
* Remove redundant files by @uuf6429 in [#278](https://github.com/Behat/Gherkin/pull/278)
|
||||
* Update documentation by @uuf6429 in [#274](https://github.com/Behat/Gherkin/pull/274)
|
||||
* Adopt PHP CS Fixer and apply code styles by @uuf6429 in [#277](https://github.com/Behat/Gherkin/pull/277)
|
||||
* Add PHPStan and improve / fix docblock annotations and type-safety within methods to achieve level 5 by
|
||||
@uuf6429 in [#276](https://github.com/Behat/Gherkin/pull/276), [#281](https://github.com/Behat/Gherkin/pull/281),
|
||||
[#282](https://github.com/Behat/Gherkin/pull/282), and [#287](https://github.com/Behat/Gherkin/pull/287)
|
||||
|
||||
# [4.11.0] - 2024-12-06
|
||||
|
||||
### Changed
|
||||
|
||||
* Drop support for PHP < 8.1, Symfony < 5.4 and Symfony 6.0 - 6.3. In future we will drop support for PHP and symfony
|
||||
versions as they reach EOL. by @acoulton in [#272](https://github.com/Behat/Gherkin/pull/272)
|
||||
* Deprecated `ExampleNode::getTitle()` and `ScenarioNode::getTitle()` in favour of new methods with clearer meaning.
|
||||
by @uuf6429 in [#271](https://github.com/Behat/Gherkin/pull/271)
|
||||
|
||||
### Added
|
||||
|
||||
* Added `(ExampleNode|ScenarioNode)::getName()` to access human-readable names for examples and scenarios,
|
||||
and `ExampleNode::getExampleText()` for the string content of the example table row.
|
||||
by @uuf6429 in [#271](https://github.com/Behat/Gherkin/pull/271)
|
||||
|
||||
### Internal
|
||||
|
||||
* Enable dependabot for github actions workflows by @jrfnl in [#261](https://github.com/Behat/Gherkin/pull/261)
|
||||
|
||||
# 4.10.0 / 2024-10-19
|
||||
|
||||
### Changed
|
||||
|
||||
- **⚠ Backslashes in feature files must now be escaped**\
|
||||
Gherkin syntax treats `\` as an escape character, which must be escaped (`\\`) to use it as a
|
||||
literal value. Historically, this was not being parsed correctly. This release fixes that bug,
|
||||
but means that if your scenarios currently use unescaped `\` you will need to replace each one
|
||||
with `\\` to achieve the same parsed result.
|
||||
By @everzet in 5a0836d.
|
||||
|
||||
### Added
|
||||
- Symfony 6 and 7 thanks to @tacman in #257
|
||||
- PHP 8.4 support thanks to @heiglandreas in #258 and @jrfnl in #262
|
||||
|
||||
### Fixed
|
||||
- Fix exception when filter string is empty thanks to @magikid in #251
|
||||
|
||||
### Internal
|
||||
- Sync teststuite with Cucumber 24.1.0
|
||||
- Fix PHPUnit 10 deprecation messages
|
||||
- A lot of great CI work by @heiglandreas and @jrfnl
|
||||
|
||||
# 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
|
||||
|
||||
[4.16.1]: https://github.com/Behat/Gherkin/compare/v4.16.0...v4.16.1
|
||||
[4.16.0]: https://github.com/Behat/Gherkin/compare/v4.15.0...v4.16.0
|
||||
[4.15.0]: https://github.com/Behat/Gherkin/compare/v4.14.0...v4.15.0
|
||||
[4.14.0]: https://github.com/Behat/Gherkin/compare/v4.13.0...v4.14.0
|
||||
[4.13.0]: https://github.com/Behat/Gherkin/compare/v4.12.0...v4.13.0
|
||||
[4.12.0]: https://github.com/Behat/Gherkin/compare/v4.11.0...v4.12.0
|
||||
[4.11.0]: https://github.com/Behat/Gherkin/compare/v4.10.0...v4.11.0
|
||||
22
vendor/behat/gherkin/LICENSE
vendored
Normal file
22
vendor/behat/gherkin/LICENSE
vendored
Normal 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.
|
||||
81
vendor/behat/gherkin/README.md
vendored
Normal file
81
vendor/behat/gherkin/README.md
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
# Behat Gherkin Parser
|
||||
|
||||
This is the php Gherkin parser for Behat. It comes bundled with more than 40 native languages (see `i18n.php`) support
|
||||
and clean architecture.
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [Behat Site](https://behat.org)
|
||||
- [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
|
||||
|
||||
```shell
|
||||
curl https://getcomposer.org/installer | php
|
||||
php composer.phar update
|
||||
```
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
- Konstantin Kudryashov [everzet](https://github.com/everzet) [original developer]
|
||||
- Andrew Coulton [acoulton](https://github.com/acoulton) [current maintainer]
|
||||
- Carlos Granados [carlos-granados](https://github.com/carlos-granados) [current maintainer]
|
||||
- Christophe Coevoet [stof](https://github.com/stof) [current maintainer]
|
||||
- Other [awesome developers](https://github.com/Behat/Gherkin/graphs/contributors)
|
||||
|
||||
Support the project
|
||||
-------------------
|
||||
|
||||
Behat is free software, maintained by volunteers as a gift for users. If you'd like to see
|
||||
the project continue to thrive, and particularly if you use it for work, we'd encourage you
|
||||
to contribute.
|
||||
|
||||
Contributions of time - whether code, documentation, or support reviewing PRs and triaging
|
||||
issues - are very welcome and valued by the maintainers and the wider Behat community.
|
||||
|
||||
But we also believe that [financial sponsorship is an important part of a healthy Open Source
|
||||
ecosystem](https://opensourcepledge.com/about/). Maintaining a project like Behat requires a
|
||||
significant commitment from the core team: your support will help us to keep making that time
|
||||
available over the long term. Even small contributions make a big difference.
|
||||
|
||||
You can support [@acoulton](https://github.com/acoulton), [@carlos-granados](https://github.com/carlos-granados) and
|
||||
[@stof](https://github.com/stof) on GitHub sponsors. If you'd like to discuss supporting us in a different way, please
|
||||
get in touch!
|
||||
95
vendor/behat/gherkin/composer.json
vendored
Normal file
95
vendor/behat/gherkin/composer.json
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "behat/gherkin",
|
||||
"description": "Gherkin DSL parser for PHP",
|
||||
"keywords": ["BDD", "parser", "DSL", "Behat", "Gherkin", "Cucumber"],
|
||||
"homepage": "https://behat.org/",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Kudryashov",
|
||||
"email": "ever.zet@gmail.com",
|
||||
"homepage": "https://everzet.com"
|
||||
}
|
||||
],
|
||||
|
||||
"require": {
|
||||
"php": ">=8.1 <8.6",
|
||||
"composer-runtime-api": "^2.2"
|
||||
},
|
||||
|
||||
"require-dev": {
|
||||
"symfony/yaml": "^5.4 || ^6.4 || ^7.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"cucumber/gherkin-monorepo": "dev-gherkin-v37.0.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.77",
|
||||
"phpstan/phpstan": "^2",
|
||||
"phpstan/extension-installer": "^1",
|
||||
"phpstan/phpstan-phpunit": "^2",
|
||||
"mikey179/vfsstream": "^1.6"
|
||||
},
|
||||
|
||||
"suggest": {
|
||||
"symfony/yaml": "If you want to parse features, represented in YAML files"
|
||||
},
|
||||
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Behat\\Gherkin\\": "src/"
|
||||
}
|
||||
},
|
||||
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\Behat\\Gherkin\\": "tests/"
|
||||
}
|
||||
},
|
||||
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.x-dev"
|
||||
}
|
||||
},
|
||||
|
||||
"repositories": [
|
||||
{
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "cucumber/gherkin-monorepo",
|
||||
"version": "dev-gherkin-v37.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cucumber/gherkin.git",
|
||||
"reference": "1e49335524c384694fe9faa843d74b550fb330c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cucumber/gherkin/zipball/1e49335524c384694fe9faa843d74b550fb330c5",
|
||||
"reference": "1e49335524c384694fe9faa843d74b550fb330c5"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"scripts": {
|
||||
"lint": [
|
||||
"phpstan analyze --ansi --no-progress --memory-limit=-1",
|
||||
"phpstan analyze bin/update_cucumber --ansi --no-progress --memory-limit=-1",
|
||||
"phpstan analyze bin/update_i18n --ansi --no-progress --memory-limit=-1",
|
||||
"php-cs-fixer check --diff --ansi --show-progress=dots --verbose"
|
||||
],
|
||||
"test": [
|
||||
"phpunit --colors=always"
|
||||
],
|
||||
"fix": [
|
||||
"php-cs-fixer fix --diff --ansi --show-progress=dots"
|
||||
]
|
||||
},
|
||||
|
||||
"config": {
|
||||
"process-timeout": 0,
|
||||
"allow-plugins": {
|
||||
"phpstan/extension-installer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
1293
vendor/behat/gherkin/i18n.php
vendored
Normal file
1293
vendor/behat/gherkin/i18n.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3833
vendor/behat/gherkin/resources/gherkin-languages.json
vendored
Normal file
3833
vendor/behat/gherkin/resources/gherkin-languages.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
49
vendor/behat/gherkin/src/Cache/CacheInterface.php
vendored
Normal file
49
vendor/behat/gherkin/src/Cache/CacheInterface.php
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 int $timestamp The last time feature was updated
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFresh(string $path, int $timestamp);
|
||||
|
||||
/**
|
||||
* Reads feature cache from path.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function read(string $path);
|
||||
|
||||
/**
|
||||
* Caches feature node.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function write(string $path, FeatureNode $feature);
|
||||
}
|
||||
136
vendor/behat/gherkin/src/Cache/FileCache.php
vendored
Normal file
136
vendor/behat/gherkin/src/Cache/FileCache.php
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Exception\FilesystemException;
|
||||
use Behat\Gherkin\Filesystem;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Composer\InstalledVersions;
|
||||
|
||||
/**
|
||||
* File cache.
|
||||
* Caches feature into a file.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class FileCache implements CacheInterface
|
||||
{
|
||||
private readonly string $path;
|
||||
|
||||
/**
|
||||
* Used as part of the cache directory path to invalidate cache if the installed package version changes.
|
||||
*/
|
||||
private static function getGherkinVersionHash(): string
|
||||
{
|
||||
$version = InstalledVersions::getVersion('behat/gherkin') ?? 'unknown';
|
||||
|
||||
// Composer version strings can contain arbitrary content so hash for filesystem safety
|
||||
return md5($version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes file cache.
|
||||
*
|
||||
* @param string $path path to the folder where to store caches
|
||||
*
|
||||
* @throws CacheException
|
||||
*/
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . self::getGherkinVersionHash();
|
||||
|
||||
try {
|
||||
Filesystem::ensureDirectoryExists($this->path);
|
||||
} catch (FilesystemException $ex) {
|
||||
throw new CacheException(
|
||||
sprintf(
|
||||
'Cache path "%s" cannot be created or is not a directory: %s',
|
||||
$this->path,
|
||||
$ex->getMessage(),
|
||||
),
|
||||
previous: $ex
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_writable($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 int $timestamp The last time feature was updated
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFresh(string $path, int $timestamp)
|
||||
{
|
||||
$cachePath = $this->getCachePathFor($path);
|
||||
|
||||
if (!file_exists($cachePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Filesystem::getLastModified($cachePath) > $timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads feature cache from path.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return FeatureNode
|
||||
*
|
||||
* @throws CacheException
|
||||
*/
|
||||
public function read(string $path)
|
||||
{
|
||||
$cachePath = $this->getCachePathFor($path);
|
||||
try {
|
||||
$feature = unserialize(Filesystem::readFile($cachePath), ['allowed_classes' => true]);
|
||||
} catch (FilesystemException $ex) {
|
||||
throw new CacheException("Can not load cache: {$ex->getMessage()}", previous: $ex);
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function write(string $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(string $path)
|
||||
{
|
||||
return $this->path . '/' . md5($path) . '.feature.cache';
|
||||
}
|
||||
}
|
||||
73
vendor/behat/gherkin/src/Cache/MemoryCache.php
vendored
Normal file
73
vendor/behat/gherkin/src/Cache/MemoryCache.php
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
/**
|
||||
* @var array<string, FeatureNode>
|
||||
*/
|
||||
private array $features = [];
|
||||
/**
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $timestamps = [];
|
||||
|
||||
/**
|
||||
* Checks that cache for feature exists and is fresh.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
* @param int $timestamp The last time feature was updated
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFresh(string $path, int $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(string $path)
|
||||
{
|
||||
return $this->features[$path];
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches feature node.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function write(string $path, FeatureNode $feature)
|
||||
{
|
||||
$this->features[$path] = $feature;
|
||||
$this->timestamps[$path] = time();
|
||||
}
|
||||
}
|
||||
58
vendor/behat/gherkin/src/Dialect/CucumberDialectProvider.php
vendored
Normal file
58
vendor/behat/gherkin/src/Dialect/CucumberDialectProvider.php
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Dialect;
|
||||
|
||||
use Behat\Gherkin\Exception\NoSuchLanguageException;
|
||||
use Behat\Gherkin\Filesystem;
|
||||
|
||||
/**
|
||||
* A dialect provider that loads the dialects based on the gherkin-languages.json file copied from the Cucumber project.
|
||||
*
|
||||
* @phpstan-import-type TDialectData from GherkinDialect
|
||||
*/
|
||||
final class CucumberDialectProvider implements DialectProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var non-empty-array<non-empty-string, TDialectData>
|
||||
*/
|
||||
private readonly array $dialects;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
/**
|
||||
* Here we force the type checker to assume the decoded JSON has the correct
|
||||
* structure, rather than validating it. This is safe because it's not dynamic.
|
||||
*
|
||||
* @var non-empty-array<non-empty-string, TDialectData> $data
|
||||
*/
|
||||
$data = Filesystem::readJsonFileHash(__DIR__ . '/../../resources/gherkin-languages.json');
|
||||
$this->dialects = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $language
|
||||
*
|
||||
* @throws NoSuchLanguageException
|
||||
*/
|
||||
public function getDialect(string $language): GherkinDialect
|
||||
{
|
||||
if (!isset($this->dialects[$language])) {
|
||||
throw new NoSuchLanguageException($language);
|
||||
}
|
||||
|
||||
return new GherkinDialect($language, $this->dialects[$language]);
|
||||
}
|
||||
|
||||
public function getDefaultDialect(): GherkinDialect
|
||||
{
|
||||
return $this->getDialect('en');
|
||||
}
|
||||
}
|
||||
25
vendor/behat/gherkin/src/Dialect/DialectProviderInterface.php
vendored
Normal file
25
vendor/behat/gherkin/src/Dialect/DialectProviderInterface.php
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Dialect;
|
||||
|
||||
use Behat\Gherkin\Exception\NoSuchLanguageException;
|
||||
|
||||
interface DialectProviderInterface
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $language
|
||||
*
|
||||
* @throws NoSuchLanguageException when the language is not supported
|
||||
*/
|
||||
public function getDialect(string $language): GherkinDialect;
|
||||
|
||||
public function getDefaultDialect(): GherkinDialect;
|
||||
}
|
||||
159
vendor/behat/gherkin/src/Dialect/GherkinDialect.php
vendored
Normal file
159
vendor/behat/gherkin/src/Dialect/GherkinDialect.php
vendored
Normal file
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Dialect;
|
||||
|
||||
/**
|
||||
* @phpstan-type TDialectData array{
|
||||
* feature: non-empty-list<non-empty-string>,
|
||||
* background: non-empty-list<non-empty-string>,
|
||||
* scenario: non-empty-list<non-empty-string>,
|
||||
* scenarioOutline: non-empty-list<non-empty-string>,
|
||||
* examples: non-empty-list<non-empty-string>,
|
||||
* rule: non-empty-list<non-empty-string>,
|
||||
* given: non-empty-list<non-empty-string>,
|
||||
* when: non-empty-list<non-empty-string>,
|
||||
* then: non-empty-list<non-empty-string>,
|
||||
* and: non-empty-list<non-empty-string>,
|
||||
* but: non-empty-list<non-empty-string>,
|
||||
* }
|
||||
*/
|
||||
final class GherkinDialect
|
||||
{
|
||||
/**
|
||||
* @var non-empty-list<non-empty-string>|null
|
||||
*/
|
||||
private ?array $stepKeywordsCache = null;
|
||||
|
||||
/**
|
||||
* @phpstan-param TDialectData $dialect
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $language,
|
||||
private readonly array $dialect,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getFeatureKeywords(): array
|
||||
{
|
||||
return $this->dialect['feature'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getBackgroundKeywords(): array
|
||||
{
|
||||
return $this->dialect['background'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getScenarioKeywords(): array
|
||||
{
|
||||
return $this->dialect['scenario'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getScenarioOutlineKeywords(): array
|
||||
{
|
||||
return $this->dialect['scenarioOutline'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getRuleKeywords(): array
|
||||
{
|
||||
return $this->dialect['rule'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getGivenKeywords(): array
|
||||
{
|
||||
return $this->dialect['given'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getWhenKeywords(): array
|
||||
{
|
||||
return $this->dialect['when'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getThenKeywords(): array
|
||||
{
|
||||
return $this->dialect['then'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getAndKeywords(): array
|
||||
{
|
||||
return $this->dialect['and'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getButKeywords(): array
|
||||
{
|
||||
return $this->dialect['but'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getStepKeywords(): array
|
||||
{
|
||||
if ($this->stepKeywordsCache !== null) {
|
||||
return $this->stepKeywordsCache;
|
||||
}
|
||||
|
||||
$stepKeywords = [
|
||||
...$this->getGivenKeywords(),
|
||||
...$this->getWhenKeywords(),
|
||||
...$this->getThenKeywords(),
|
||||
...$this->getAndKeywords(),
|
||||
...$this->getButKeywords(),
|
||||
];
|
||||
|
||||
// Sort longer keywords before shorter keywords being their prefix
|
||||
rsort($stepKeywords);
|
||||
|
||||
return $this->stepKeywordsCache = $stepKeywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
public function getExamplesKeywords(): array
|
||||
{
|
||||
return $this->dialect['examples'];
|
||||
}
|
||||
}
|
||||
103
vendor/behat/gherkin/src/Dialect/KeywordsDialectProvider.php
vendored
Normal file
103
vendor/behat/gherkin/src/Dialect/KeywordsDialectProvider.php
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Dialect;
|
||||
|
||||
use Behat\Gherkin\Exception\NoSuchLanguageException;
|
||||
use Behat\Gherkin\Keywords\ArrayKeywords;
|
||||
use Behat\Gherkin\Keywords\KeywordsInterface;
|
||||
|
||||
/**
|
||||
* Adapter for the legacy keywords interface.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class KeywordsDialectProvider implements DialectProviderInterface
|
||||
{
|
||||
private readonly string $defaultLanguage;
|
||||
|
||||
public function __construct(
|
||||
private readonly KeywordsInterface $keywords,
|
||||
) {
|
||||
// Assume a default dialect of `en` as the KeywordsInterface does not allow reading its language but returns the current data
|
||||
$this->defaultLanguage = $this->keywords instanceof ArrayKeywords ? $this->keywords->getLanguage() : 'en';
|
||||
}
|
||||
|
||||
public function getDialect(string $language): GherkinDialect
|
||||
{
|
||||
// The legacy keywords interface doesn't support detecting whether changing the language worked or no.
|
||||
$this->keywords->setLanguage($language);
|
||||
|
||||
if ($this->keywords instanceof ArrayKeywords && $this->keywords->getLanguage() !== $language) {
|
||||
throw new NoSuchLanguageException($language);
|
||||
}
|
||||
|
||||
return $this->buildDialect($language);
|
||||
}
|
||||
|
||||
public function getDefaultDialect(): GherkinDialect
|
||||
{
|
||||
$this->keywords->setLanguage($this->defaultLanguage);
|
||||
|
||||
return $this->buildDialect($this->defaultLanguage);
|
||||
}
|
||||
|
||||
private function buildDialect(string $language): GherkinDialect
|
||||
{
|
||||
return new GherkinDialect($language, [
|
||||
'feature' => self::parseKeywords($this->keywords->getFeatureKeywords()),
|
||||
'background' => self::parseKeywords($this->keywords->getBackgroundKeywords()),
|
||||
'scenario' => self::parseKeywords($this->keywords->getScenarioKeywords()),
|
||||
'scenarioOutline' => self::parseKeywords($this->keywords->getOutlineKeywords()),
|
||||
'examples' => self::parseKeywords($this->keywords->getExamplesKeywords()),
|
||||
'rule' => ['Rule'], // Hardcoded value as our old keywords interface doesn't support rules.
|
||||
'given' => self::parseStepKeywords($this->keywords->getGivenKeywords()),
|
||||
'when' => self::parseStepKeywords($this->keywords->getWhenKeywords()),
|
||||
'then' => self::parseStepKeywords($this->keywords->getThenKeywords()),
|
||||
'and' => self::parseStepKeywords($this->keywords->getAndKeywords()),
|
||||
'but' => self::parseStepKeywords($this->keywords->getButKeywords()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
private static function parseKeywords(string $keywordString): array
|
||||
{
|
||||
$keywords = array_values(array_filter(explode('|', $keywordString)));
|
||||
|
||||
if ($keywords === []) {
|
||||
throw new \LogicException('A keyword string must contain at least one keyword.');
|
||||
}
|
||||
|
||||
return $keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<non-empty-string>
|
||||
*/
|
||||
private static function parseStepKeywords(string $keywordString): array
|
||||
{
|
||||
$legacyKeywords = explode('|', $keywordString);
|
||||
$keywords = [];
|
||||
|
||||
foreach ($legacyKeywords as $legacyKeyword) {
|
||||
if (\strlen($legacyKeyword) >= 2 && str_ends_with($legacyKeyword, '<')) {
|
||||
$keyword = substr($legacyKeyword, 0, -1);
|
||||
\assert($keyword !== ''); // phpstan is not smart enough to detect that the length check above guarantees this invariant
|
||||
$keywords[] = $keyword;
|
||||
} else {
|
||||
$keywords[] = $legacyKeyword . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return $keywords;
|
||||
}
|
||||
}
|
||||
22
vendor/behat/gherkin/src/Exception/CacheException.php
vendored
Normal file
22
vendor/behat/gherkin/src/Exception/CacheException.php
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
}
|
||||
15
vendor/behat/gherkin/src/Exception/Exception.php
vendored
Normal file
15
vendor/behat/gherkin/src/Exception/Exception.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
}
|
||||
17
vendor/behat/gherkin/src/Exception/FilesystemException.php
vendored
Normal file
17
vendor/behat/gherkin/src/Exception/FilesystemException.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 FilesystemException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
27
vendor/behat/gherkin/src/Exception/InvalidTagContentException.php
vendored
Normal file
27
vendor/behat/gherkin/src/Exception/InvalidTagContentException.php
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
|
||||
class InvalidTagContentException extends ParserException
|
||||
{
|
||||
public function __construct(string $tag, ?string $file)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf(
|
||||
'Tags cannot include whitespace, found "%s"%s',
|
||||
$tag,
|
||||
is_string($file)
|
||||
? "in file {$file}"
|
||||
: ''
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
17
vendor/behat/gherkin/src/Exception/LexerException.php
vendored
Normal file
17
vendor/behat/gherkin/src/Exception/LexerException.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
}
|
||||
19
vendor/behat/gherkin/src/Exception/NoSuchLanguageException.php
vendored
Normal file
19
vendor/behat/gherkin/src/Exception/NoSuchLanguageException.php
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
|
||||
final class NoSuchLanguageException extends ParserException
|
||||
{
|
||||
public function __construct(public readonly string $language)
|
||||
{
|
||||
parent::__construct('Language not supported: ' . $language);
|
||||
}
|
||||
}
|
||||
17
vendor/behat/gherkin/src/Exception/NodeException.php
vendored
Normal file
17
vendor/behat/gherkin/src/Exception/NodeException.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
}
|
||||
17
vendor/behat/gherkin/src/Exception/ParserException.php
vendored
Normal file
17
vendor/behat/gherkin/src/Exception/ParserException.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
}
|
||||
33
vendor/behat/gherkin/src/Exception/UnexpectedParserNodeException.php
vendored
Normal file
33
vendor/behat/gherkin/src/Exception/UnexpectedParserNodeException.php
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 Behat\Gherkin\Node\NodeInterface;
|
||||
|
||||
class UnexpectedParserNodeException extends ParserException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $expectation,
|
||||
public readonly string|NodeInterface $node,
|
||||
public readonly ?string $sourceFile,
|
||||
) {
|
||||
parent::__construct(
|
||||
sprintf(
|
||||
'Expected %s, but got %s%s',
|
||||
$expectation,
|
||||
is_string($node)
|
||||
? "text: \"{$node}\""
|
||||
: "{$node->getNodeType()} on line: {$node->getLine()}",
|
||||
$sourceFile ? " in file: {$sourceFile}" : ''
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
vendor/behat/gherkin/src/Exception/UnexpectedTaggedNodeException.php
vendored
Normal file
44
vendor/behat/gherkin/src/Exception/UnexpectedTaggedNodeException.php
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 Behat\Gherkin\Lexer;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type TToken from Lexer
|
||||
*/
|
||||
class UnexpectedTaggedNodeException extends ParserException
|
||||
{
|
||||
/**
|
||||
* @phpstan-param TToken $taggedToken
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $taggedToken,
|
||||
public readonly ?string $sourceFile,
|
||||
) {
|
||||
$msg = match ($this->taggedToken['type']) {
|
||||
'EOS' => 'Unexpected end of file after tags',
|
||||
default => sprintf(
|
||||
'%s can not be tagged, but it is',
|
||||
$taggedToken['type'],
|
||||
),
|
||||
};
|
||||
|
||||
parent::__construct(
|
||||
sprintf(
|
||||
'%s on line: %d%s',
|
||||
$msg,
|
||||
$taggedToken['line'],
|
||||
$this->sourceFile ? " in file: {$this->sourceFile}" : '',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
184
vendor/behat/gherkin/src/Filesystem.php
vendored
Normal file
184
vendor/behat/gherkin/src/Filesystem.php
vendored
Normal file
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\FilesystemException;
|
||||
use ErrorException;
|
||||
use JsonException;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
use function assert;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Filesystem
|
||||
{
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
public static function readFile(string $fileName): string
|
||||
{
|
||||
try {
|
||||
$result = self::callSafely(static fn () => file_get_contents($fileName));
|
||||
} catch (ErrorException $e) {
|
||||
throw new FilesystemException(
|
||||
sprintf('File "%s" cannot be read: %s', $fileName, $e->getMessage()),
|
||||
previous: $e,
|
||||
);
|
||||
}
|
||||
|
||||
assert($result !== false, 'file_get_contents() should not return false without emitting a PHP warning');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function writeFile(string $fileName, string $content): void
|
||||
{
|
||||
self::ensureDirectoryExists(dirname($fileName));
|
||||
try {
|
||||
$result = self::callSafely(static fn () => file_put_contents($fileName, $content));
|
||||
} catch (ErrorException $e) {
|
||||
throw new FilesystemException(
|
||||
sprintf('File "%s" cannot be written: %s', $fileName, $e->getMessage()),
|
||||
previous: $e,
|
||||
);
|
||||
}
|
||||
|
||||
assert($result !== false, 'file_put_contents() should not return false without emitting a PHP warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*
|
||||
* @throws JsonException|FilesystemException
|
||||
*/
|
||||
public static function readJsonFileArray(string $fileName): array
|
||||
{
|
||||
$result = json_decode(self::readFile($fileName), true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
assert(is_array($result), 'File must contain JSON with an array or object at its root');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws JsonException|FilesystemException
|
||||
*/
|
||||
public static function readJsonFileHash(string $fileName): array
|
||||
{
|
||||
$result = self::readJsonFileArray($fileName);
|
||||
assert(
|
||||
$result === array_filter($result, is_string(...), ARRAY_FILTER_USE_KEY),
|
||||
'File must contain a JSON object at its root',
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function findFilesRecursively(string $path, string $pattern): array
|
||||
{
|
||||
/**
|
||||
* @var iterable<string, SplFileInfo> $fileIterator
|
||||
*/
|
||||
$fileIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::CHILD_FIRST);
|
||||
|
||||
$found = [];
|
||||
foreach ($fileIterator as $file) {
|
||||
if ($file->isFile() && fnmatch($pattern, $file->getFilename())) {
|
||||
$found[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public static function getLastModified(string $fileName): int
|
||||
{
|
||||
try {
|
||||
$result = self::callSafely(static fn () => filemtime($fileName));
|
||||
} catch (ErrorException $e) {
|
||||
throw new FilesystemException(
|
||||
sprintf('Last modification time of file "%s" cannot be found: %s', $fileName, $e->getMessage()),
|
||||
previous: $e,
|
||||
);
|
||||
}
|
||||
|
||||
assert($result !== false, 'filemtime() should not return false without emitting a PHP warning');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function getRealPath(string $path): string
|
||||
{
|
||||
$result = realpath($path);
|
||||
|
||||
if ($result === false) {
|
||||
throw new FilesystemException("Cannot retrieve the real path of $path");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function ensureDirectoryExists(string $path): void
|
||||
{
|
||||
if (is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = self::callSafely(static fn () => mkdir($path, 0777, true));
|
||||
|
||||
assert($result !== false, 'mkdir() should not return false without emitting a PHP warning');
|
||||
} catch (ErrorException $e) {
|
||||
// @codeCoverageIgnoreStart
|
||||
if (is_dir($path)) {
|
||||
// Some other concurrent process created the directory.
|
||||
return;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
throw new FilesystemException(
|
||||
sprintf('Path at "%s" cannot be created: %s', $path, $e->getMessage()),
|
||||
previous: $e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TResult
|
||||
*
|
||||
* @param (callable(): TResult) $callback
|
||||
*
|
||||
* @return TResult
|
||||
*
|
||||
* @throws ErrorException
|
||||
*/
|
||||
private static function callSafely(callable $callback): mixed
|
||||
{
|
||||
set_error_handler(
|
||||
static fn (int $severity, string $message, string $file, int $line) => throw new ErrorException($message, 0, $severity, $file, $line)
|
||||
);
|
||||
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
}
|
||||
38
vendor/behat/gherkin/src/Filter/ComplexFilter.php
vendored
Normal file
38
vendor/behat/gherkin/src/Filter/ComplexFilter.php
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
|
||||
/**
|
||||
* Abstract filter class.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
abstract class ComplexFilter implements ComplexFilterInterface
|
||||
{
|
||||
/**
|
||||
* Filters feature according to the filter.
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
$scenarios = $feature->getScenarios();
|
||||
$filteredScenarios = array_filter(
|
||||
$scenarios,
|
||||
fn (ScenarioInterface $scenario) => $this->isScenarioMatch($feature, $scenario)
|
||||
);
|
||||
|
||||
return $scenarios === $filteredScenarios ? $feature : $feature->withScenarios($filteredScenarios);
|
||||
}
|
||||
}
|
||||
32
vendor/behat/gherkin/src/Filter/ComplexFilterInterface.php
vendored
Normal file
32
vendor/behat/gherkin/src/Filter/ComplexFilterInterface.php
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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);
|
||||
}
|
||||
37
vendor/behat/gherkin/src/Filter/FeatureFilterInterface.php
vendored
Normal file
37
vendor/behat/gherkin/src/Filter/FeatureFilterInterface.php
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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.
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature);
|
||||
}
|
||||
30
vendor/behat/gherkin/src/Filter/FilterInterface.php
vendored
Normal file
30
vendor/behat/gherkin/src/Filter/FilterInterface.php
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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);
|
||||
}
|
||||
107
vendor/behat/gherkin/src/Filter/LineFilter.php
vendored
Normal file
107
vendor/behat/gherkin/src/Filter/LineFilter.php
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 definition line number.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class LineFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $filterLine;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param int|numeric-string $filterLine Line of the scenario to filter on
|
||||
*/
|
||||
public function __construct(int|string $filterLine)
|
||||
{
|
||||
$this->filterLine = (int) $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.
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
$scenarios = [];
|
||||
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 = [$lines[0] => $table[$lines[0]]];
|
||||
|
||||
if ($lines[0] !== $this->filterLine) {
|
||||
$filteredTable[$this->filterLine] = $table[$this->filterLine];
|
||||
}
|
||||
|
||||
$scenario = $scenario->withTables([$exampleTable->withTable($filteredTable)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
|
||||
return $feature->withScenarios($scenarios);
|
||||
}
|
||||
}
|
||||
125
vendor/behat/gherkin/src/Filter/LineRangeFilter.php
vendored
Normal file
125
vendor/behat/gherkin/src/Filter/LineRangeFilter.php
vendored
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 definition line number range.
|
||||
*
|
||||
* @author Fabian Kiss <headrevision@gmail.com>
|
||||
*/
|
||||
class LineRangeFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $filterMinLine;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $filterMaxLine;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param int|numeric-string $filterMinLine Minimum line of a scenario to filter on
|
||||
* @param int|numeric-string|'*' $filterMaxLine Maximum line of a scenario to filter on
|
||||
*/
|
||||
public function __construct(int|string $filterMinLine, int|string $filterMaxLine)
|
||||
{
|
||||
$this->filterMinLine = (int) $filterMinLine;
|
||||
$this->filterMaxLine = $filterMaxLine === '*' ? PHP_INT_MAX : (int) $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.
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
$scenarios = [];
|
||||
foreach ($feature->getScenarios() as $scenario) {
|
||||
if (!$this->isScenarioMatch($scenario)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||
// first accumulate examples and then create scenario
|
||||
$exampleTableNodes = [];
|
||||
|
||||
foreach ($scenario->getExampleTables() as $exampleTable) {
|
||||
$table = $exampleTable->getTable();
|
||||
$lines = array_keys($table);
|
||||
|
||||
$filteredTable = [$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[] = $exampleTable->withTable($filteredTable);
|
||||
}
|
||||
}
|
||||
|
||||
$scenario = $scenario->withTables($exampleTableNodes);
|
||||
}
|
||||
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
|
||||
return $feature->withScenarios($scenarios);
|
||||
}
|
||||
}
|
||||
87
vendor/behat/gherkin/src/Filter/NameFilter.php
vendored
Normal file
87
vendor/behat/gherkin/src/Filter/NameFilter.php
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\DescribableNodeInterface;
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filterString;
|
||||
|
||||
public function __construct(string $filterString)
|
||||
{
|
||||
$this->filterString = trim($filterString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Feature matches specified filter.
|
||||
*
|
||||
* @param FeatureNode $feature Feature instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
if ($feature->getTitle() === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->filterString[0] === '/') {
|
||||
return (bool) preg_match($this->filterString, $feature->getTitle());
|
||||
}
|
||||
|
||||
return str_contains($feature->getTitle(), $this->filterString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario or outline matches specified filter.
|
||||
*
|
||||
* @param ScenarioInterface $scenario Scenario or Outline node instance
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
// Historically (and in legacy GherkinCompatibilityMode), multiline scenario text was all part of the title.
|
||||
// In new GherkinCompatibilityMode the text will be split into a single-line title & multiline description.
|
||||
// For BC, this filter should continue to match on the complete multiline text value.
|
||||
$textParts = array_filter([
|
||||
$scenario->getTitle(),
|
||||
$scenario instanceof DescribableNodeInterface ? $scenario->getDescription() : null,
|
||||
]);
|
||||
|
||||
if ($textParts === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$textToMatch = implode("\n", $textParts);
|
||||
|
||||
if ($this->filterString[0] === '/' && preg_match($this->filterString, $textToMatch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_contains($textToMatch, $this->filterString)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
38
vendor/behat/gherkin/src/Filter/NarrativeFilter.php
vendored
Normal file
38
vendor/behat/gherkin/src/Filter/NarrativeFilter.php
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 narrative using regular expression.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class NarrativeFilter extends SimpleFilter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $regex,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
return (bool) preg_match($this->regex, $feature->getDescription() ?? '');
|
||||
}
|
||||
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
// This filter does not apply to scenarios.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
71
vendor/behat/gherkin/src/Filter/PathsFilter.php
vendored
Normal file
71
vendor/behat/gherkin/src/Filter/PathsFilter.php
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Exception\FilesystemException;
|
||||
use Behat\Gherkin\Filesystem;
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $filterPaths = [];
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param array<array-key, string> $paths List of approved paths
|
||||
*/
|
||||
public function __construct(array $paths)
|
||||
{
|
||||
foreach ($paths as $path) {
|
||||
try {
|
||||
$realpath = Filesystem::getRealPath($path);
|
||||
} catch (FilesystemException) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->filterPaths[] = rtrim($realpath, DIRECTORY_SEPARATOR)
|
||||
. (is_dir($realpath) ? DIRECTORY_SEPARATOR : '');
|
||||
}
|
||||
}
|
||||
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
if (($filePath = $feature->getFile()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$realFeatureFilePath = Filesystem::getRealPath($filePath);
|
||||
|
||||
foreach ($this->filterPaths as $filterPath) {
|
||||
if (str_starts_with($realFeatureFilePath, $filterPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
// This filter does not apply to scenarios.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
59
vendor/behat/gherkin/src/Filter/RoleFilter.php
vendored
Normal file
59
vendor/behat/gherkin/src/Filter/RoleFilter.php
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $pattern;
|
||||
|
||||
/**
|
||||
* Initializes filter.
|
||||
*
|
||||
* @param string $role Approved role wildcard
|
||||
*/
|
||||
public function __construct(string $role)
|
||||
{
|
||||
$this->pattern = sprintf(
|
||||
'/as an? %s[$\n]/i',
|
||||
strtr(
|
||||
preg_quote($role, '/'),
|
||||
[
|
||||
'\*' => '.*',
|
||||
'\?' => '.',
|
||||
'\[' => '[',
|
||||
'\]' => ']',
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function isFeatureMatch(FeatureNode $feature)
|
||||
{
|
||||
return (bool) preg_match($this->pattern, $feature->getDescription() ?? '');
|
||||
}
|
||||
|
||||
public function isScenarioMatch(ScenarioInterface $scenario)
|
||||
{
|
||||
// This filter does not apply to scenarios.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
40
vendor/behat/gherkin/src/Filter/SimpleFilter.php
vendored
Normal file
40
vendor/behat/gherkin/src/Filter/SimpleFilter.php
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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.
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
if ($this->isFeatureMatch($feature)) {
|
||||
return $feature;
|
||||
}
|
||||
|
||||
return $feature->withScenarios(
|
||||
array_filter(
|
||||
$feature->getScenarios(),
|
||||
$this->isScenarioMatch(...)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
184
vendor/behat/gherkin/src/Filter/TagFilter.php
vendored
Normal file
184
vendor/behat/gherkin/src/Filter/TagFilter.php
vendored
Normal file
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filterString;
|
||||
|
||||
public function __construct(string $filterString)
|
||||
{
|
||||
$filterString = trim($filterString);
|
||||
$fixedFilterString = $this->fixLegacyFilterStringWithoutPrefixes($filterString);
|
||||
// @todo trigger a deprecation here $filterString !== $fixedFilterString
|
||||
$this->filterString = $fixedFilterString;
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix tag expressions where the filter string does not include the `@` prefixes.
|
||||
*
|
||||
* e.g. `new TagFilter('wip&&~slow')` rather than `new TagFilter('@wip&&~@slow')`. These were historically
|
||||
* supported, although not officially, and have been reinstated to solve a BC issue. This syntax will be deprecated
|
||||
* and removed in future.
|
||||
*/
|
||||
private function fixLegacyFilterStringWithoutPrefixes(string $filterString): string
|
||||
{
|
||||
if ($filterString === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$allParts = [];
|
||||
foreach (explode('&&', $filterString) as $andTags) {
|
||||
$allParts[] = implode(
|
||||
',',
|
||||
array_map(
|
||||
fn (string $tag): string => match (true) {
|
||||
// Valid - tag filter contains the `@` prefix
|
||||
str_starts_with($tag, '@'),
|
||||
str_starts_with($tag, '~@') => $tag,
|
||||
// Invalid / legacy cases - insert the missing `@` prefix in the right place
|
||||
str_starts_with($tag, '~') => '~@' . substr($tag, 1),
|
||||
default => '@' . $tag,
|
||||
},
|
||||
explode(',', $andTags),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return implode('&&', $allParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters feature according to the filter.
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
public function filterFeature(FeatureNode $feature)
|
||||
{
|
||||
$scenarios = [];
|
||||
foreach ($feature->getScenarios() as $scenario) {
|
||||
if (!$this->isScenarioMatch($feature, $scenario)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($scenario instanceof OutlineNode && $scenario->hasExamples()) {
|
||||
$exampleTables = [];
|
||||
|
||||
foreach ($scenario->getExampleTables() as $exampleTable) {
|
||||
if ($this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags(), $exampleTable->getTags()))) {
|
||||
$exampleTables[] = $exampleTable;
|
||||
}
|
||||
}
|
||||
|
||||
$scenario = $scenario->withTables($exampleTables);
|
||||
}
|
||||
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
|
||||
return $feature->withScenarios($scenarios);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 array<array-key, string> $tags
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isTagsMatchCondition(array $tags)
|
||||
{
|
||||
if ($this->filterString === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the file was parsed in legacy mode, the `@` prefix will have been removed from the individual tags on the
|
||||
// parsed node. The tags in the filter expression still have their @ so we add the prefix back here if required.
|
||||
// This can be removed once legacy parsing mode is removed.
|
||||
$tags = array_map(
|
||||
static fn (string $tag) => str_starts_with($tag, '@') ? $tag : '@' . $tag,
|
||||
$tags
|
||||
);
|
||||
|
||||
foreach (explode('&&', $this->filterString) as $andTags) {
|
||||
$satisfiesComma = false;
|
||||
|
||||
foreach (explode(',', $andTags) as $tag) {
|
||||
if ($tag[0] === '~') {
|
||||
$tag = mb_substr($tag, 1, mb_strlen($tag, 'utf8') - 1, 'utf8');
|
||||
$satisfiesComma = !in_array($tag, $tags, true) || $satisfiesComma;
|
||||
} else {
|
||||
$satisfiesComma = in_array($tag, $tags, true) || $satisfiesComma;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$satisfiesComma) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
160
vendor/behat/gherkin/src/Gherkin.php
vendored
Normal file
160
vendor/behat/gherkin/src/Gherkin.php
vendored
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
/**
|
||||
* Gherkin manager.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*/
|
||||
class Gherkin
|
||||
{
|
||||
/**
|
||||
* @deprecated this constant will not be updated for releases after 4.8.0 and will be removed in the next major.
|
||||
* You can use composer's runtime API to get the behat version if you need it. Note that composer's versions will
|
||||
* not always be simple numeric values.
|
||||
*/
|
||||
public const VERSION = '4.8.0';
|
||||
|
||||
/**
|
||||
* @var list<LoaderInterface<*>>
|
||||
*/
|
||||
protected $loaders = [];
|
||||
/**
|
||||
* @var list<FeatureFilterInterface>
|
||||
*/
|
||||
protected $filters = [];
|
||||
|
||||
/**
|
||||
* Adds loader to manager.
|
||||
*
|
||||
* @param LoaderInterface<*> $loader Feature loader
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addLoader(LoaderInterface $loader)
|
||||
{
|
||||
$this->loaders[] = $loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds filter to manager.
|
||||
*
|
||||
* @param FeatureFilterInterface $filter Feature filter
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addFilter(FeatureFilterInterface $filter)
|
||||
{
|
||||
$this->filters[] = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets filters to the parser.
|
||||
*
|
||||
* @param array<array-key, FeatureFilterInterface> $filters
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setFilters(array $filters)
|
||||
{
|
||||
$this->filters = [];
|
||||
array_map($this->addFilter(...), $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets base features path.
|
||||
*
|
||||
* @param string $path Loaders base path
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setBasePath(string $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 array<array-key, FeatureFilterInterface> $filters Additional filters
|
||||
*
|
||||
* @return list<FeatureNode>
|
||||
*/
|
||||
public function load($resource, array $filters = [])
|
||||
{
|
||||
$filters = array_merge($this->filters, $filters);
|
||||
|
||||
$matches = [];
|
||||
if (is_scalar($resource) || $resource instanceof \Stringable) {
|
||||
if (preg_match('/^(.*):(\d+)-(\d+|\*)$/', (string) $resource, $matches)) {
|
||||
$resource = $matches[1];
|
||||
$filters[] = new LineRangeFilter($matches[2], $matches[3]);
|
||||
} elseif (preg_match('/^(.*):(\d+)$/', (string) $resource, $matches)) {
|
||||
$resource = $matches[1];
|
||||
$filters[] = new LineFilter($matches[2]);
|
||||
}
|
||||
}
|
||||
|
||||
$loader = $this->resolveLoader($resource);
|
||||
|
||||
if ($loader === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$features = [];
|
||||
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.
|
||||
*
|
||||
* @template TResourceType
|
||||
*
|
||||
* @param TResourceType $resource Resource to load
|
||||
*
|
||||
* @return LoaderInterface<TResourceType>|null
|
||||
*/
|
||||
public function resolveLoader(mixed $resource)
|
||||
{
|
||||
foreach ($this->loaders as $loader) {
|
||||
if ($loader->supports($resource)) {
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
124
vendor/behat/gherkin/src/GherkinCompatibilityMode.php
vendored
Normal file
124
vendor/behat/gherkin/src/GherkinCompatibilityMode.php
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
|
||||
enum GherkinCompatibilityMode: string
|
||||
{
|
||||
case LEGACY = 'legacy';
|
||||
|
||||
/**
|
||||
* Note: The gherkin-32 parsing mode is not yet complete, and further behaviour changes are expected.
|
||||
*
|
||||
* @see https://github.com/Behat/Gherkin/issues?q=is%3Aissue%20state%3Aopen%20label%3Acucumber-parity
|
||||
*/
|
||||
case GHERKIN_32 = 'gherkin-32';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldRemoveStepKeywordSpace(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LEGACY => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldRemoveDescriptionPadding(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LEGACY => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function allowAllNodeDescriptions(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LEGACY => false,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldUseNewTableCellParsing(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LEGACY => false,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldUnespaceDocStringDelimiters(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LEGACY => false,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldIgnoreInvalidLanguage(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LEGACY => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function allowWhitespaceInLanguageTag(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LEGACY => false,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldRemoveTagPrefixChar(): bool
|
||||
{
|
||||
// Note: When this is removed we can also remove the code in TagFilter that handles tags with no leading @
|
||||
return match ($this) {
|
||||
self::LEGACY => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function shouldThrowOnWhitespaceInTag(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
// Note, although we don't throw we have triggered an E_USER_DEPRECATED in Parser::guardTags since v4.9.0
|
||||
self::LEGACY => false,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
226
vendor/behat/gherkin/src/Keywords/ArrayKeywords.php
vendored
Normal file
226
vendor/behat/gherkin/src/Keywords/ArrayKeywords.php
vendored
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @phpstan-type TKeywordsArray array{
|
||||
* feature: string,
|
||||
* background: string,
|
||||
* scenario: string,
|
||||
* scenario_outline: string,
|
||||
* examples: string,
|
||||
* given: string,
|
||||
* when: string,
|
||||
* then: string,
|
||||
* and: string,
|
||||
* but: string,
|
||||
* }
|
||||
* @phpstan-type TMultiLanguageKeywords array<string, TKeywordsArray>
|
||||
*/
|
||||
class ArrayKeywords implements KeywordsInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $keywordString = [];
|
||||
private string $language = 'en';
|
||||
|
||||
/**
|
||||
* Initializes holder with keywords.
|
||||
*
|
||||
* @phpstan-param TMultiLanguageKeywords $keywords Keywords array
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $keywords,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets keywords holder language.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLanguage(string $language)
|
||||
{
|
||||
if (!isset($this->keywords[$language])) {
|
||||
$this->language = 'en';
|
||||
} else {
|
||||
$this->language = $language;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Feature keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFeatureKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['feature'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Background keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBackgroundKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['background'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Scenario keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getScenarioKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['scenario'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Scenario Outline keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOutlineKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['scenario_outline'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Examples keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExamplesKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['examples'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Given keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getGivenKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['given'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns When keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWhenKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['when'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Then keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getThenKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['then'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns And keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAndKeywords()
|
||||
{
|
||||
return $this->keywords[$this->language]['and'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns But keywords (separated 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];
|
||||
}
|
||||
}
|
||||
37
vendor/behat/gherkin/src/Keywords/CachedArrayKeywords.php
vendored
Normal file
37
vendor/behat/gherkin/src/Keywords/CachedArrayKeywords.php
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
public static function withDefaultKeywords(): self
|
||||
{
|
||||
return new self(__DIR__ . '/../../i18n.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes holder with file.
|
||||
*
|
||||
* @param string $file Cached array path
|
||||
*/
|
||||
public function __construct(string $file)
|
||||
{
|
||||
// @phpstan-ignore argument.type
|
||||
parent::__construct(require $file);
|
||||
}
|
||||
}
|
||||
107
vendor/behat/gherkin/src/Keywords/CucumberKeywords.php
vendored
Normal file
107
vendor/behat/gherkin/src/Keywords/CucumberKeywords.php
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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(string $yaml)
|
||||
{
|
||||
if (!str_contains($yaml, "\n") && is_file($yaml)) {
|
||||
$content = Yaml::parseFile($yaml);
|
||||
} else {
|
||||
$content = Yaml::parse($yaml);
|
||||
}
|
||||
|
||||
if (!is_array($content)) {
|
||||
throw new ParseException(sprintf('Root element must be an array, but %s found.', get_debug_type($content)));
|
||||
}
|
||||
|
||||
// @phpstan-ignore argument.type
|
||||
parent::__construct($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Feature keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getGivenKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getGivenKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns When keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWhenKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getWhenKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Then keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getThenKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getThenKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns And keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAndKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getAndKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns But keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getButKeywords()
|
||||
{
|
||||
return $this->prepareStepString(parent::getButKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim *| from the beginning of the list.
|
||||
*/
|
||||
private function prepareStepString(string $keywordsString): string
|
||||
{
|
||||
if (str_starts_with($keywordsString, '*|')) {
|
||||
$keywordsString = mb_substr($keywordsString, 2, mb_strlen($keywordsString, 'utf8') - 2, 'utf8');
|
||||
}
|
||||
|
||||
return $keywordsString;
|
||||
}
|
||||
}
|
||||
121
vendor/behat/gherkin/src/Keywords/DialectKeywords.php
vendored
Normal file
121
vendor/behat/gherkin/src/Keywords/DialectKeywords.php
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 Behat\Gherkin\Dialect\DialectProviderInterface;
|
||||
use Behat\Gherkin\Dialect\GherkinDialect;
|
||||
|
||||
/**
|
||||
* An adapter around a DialectProviderInterface to be able to use it with the KeywordsDumper.
|
||||
*
|
||||
* TODO add support for dumping an example feature for a dialect directly instead.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DialectKeywords implements KeywordsInterface
|
||||
{
|
||||
private GherkinDialect $currentDialect;
|
||||
|
||||
public function __construct(
|
||||
private readonly DialectProviderInterface $dialectProvider,
|
||||
) {
|
||||
$this->currentDialect = $this->dialectProvider->getDefaultDialect();
|
||||
}
|
||||
|
||||
public function setLanguage(string $language): void
|
||||
{
|
||||
if ($language === '') {
|
||||
throw new \InvalidArgumentException('Language cannot be empty');
|
||||
}
|
||||
|
||||
$this->currentDialect = $this->dialectProvider->getDialect($language);
|
||||
}
|
||||
|
||||
public function getFeatureKeywords(): string
|
||||
{
|
||||
return $this->getKeywordString($this->currentDialect->getFeatureKeywords());
|
||||
}
|
||||
|
||||
public function getBackgroundKeywords(): string
|
||||
{
|
||||
return $this->getKeywordString($this->currentDialect->getBackgroundKeywords());
|
||||
}
|
||||
|
||||
public function getScenarioKeywords(): string
|
||||
{
|
||||
return $this->getKeywordString($this->currentDialect->getScenarioKeywords());
|
||||
}
|
||||
|
||||
public function getOutlineKeywords(): string
|
||||
{
|
||||
return $this->getKeywordString($this->currentDialect->getScenarioOutlineKeywords());
|
||||
}
|
||||
|
||||
public function getExamplesKeywords(): string
|
||||
{
|
||||
return $this->getKeywordString($this->currentDialect->getExamplesKeywords());
|
||||
}
|
||||
|
||||
public function getGivenKeywords(): string
|
||||
{
|
||||
return $this->getStepKeywordString($this->currentDialect->getGivenKeywords());
|
||||
}
|
||||
|
||||
public function getWhenKeywords(): string
|
||||
{
|
||||
return $this->getStepKeywordString($this->currentDialect->getWhenKeywords());
|
||||
}
|
||||
|
||||
public function getThenKeywords(): string
|
||||
{
|
||||
return $this->getStepKeywordString($this->currentDialect->getThenKeywords());
|
||||
}
|
||||
|
||||
public function getAndKeywords(): string
|
||||
{
|
||||
return $this->getStepKeywordString($this->currentDialect->getAndKeywords());
|
||||
}
|
||||
|
||||
public function getButKeywords(): string
|
||||
{
|
||||
return $this->getStepKeywordString($this->currentDialect->getButKeywords());
|
||||
}
|
||||
|
||||
public function getStepKeywords(): string
|
||||
{
|
||||
return $this->getStepKeywordString($this->currentDialect->getStepKeywords());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $keywords
|
||||
*/
|
||||
private function getKeywordString(array $keywords): string
|
||||
{
|
||||
return implode('|', $keywords);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $keywords
|
||||
*/
|
||||
private function getStepKeywordString(array $keywords): string
|
||||
{
|
||||
$legacyKeywords = [];
|
||||
foreach ($keywords as $keyword) {
|
||||
if (str_ends_with($keyword, ' ')) {
|
||||
$legacyKeywords[] = substr($keyword, 0, -1);
|
||||
} else {
|
||||
$legacyKeywords[] = $keyword . '<';
|
||||
}
|
||||
}
|
||||
|
||||
return implode('|', $legacyKeywords);
|
||||
}
|
||||
}
|
||||
367
vendor/behat/gherkin/src/Keywords/KeywordsDumper.php
vendored
Normal file
367
vendor/behat/gherkin/src/Keywords/KeywordsDumper.php
vendored
Normal file
@ -0,0 +1,367 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
/**
|
||||
* @var callable(list<string>, bool): string
|
||||
*/
|
||||
private $keywordsDumper;
|
||||
|
||||
public function __construct(
|
||||
private readonly KeywordsInterface $keywords,
|
||||
) {
|
||||
$this->keywordsDumper = [$this, 'dumpKeywords'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets keywords mapper function.
|
||||
*
|
||||
* Callable should accept 2 arguments (array $keywords and bool $isShort)
|
||||
*
|
||||
* @param callable(list<string>, bool): string $mapper Mapper function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setKeywordsDumperFunction(callable $mapper)
|
||||
{
|
||||
$this->keywordsDumper = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults keywords dumper.
|
||||
*
|
||||
* @param list<string> $keywords Keywords list
|
||||
* @param bool $isShort Is short version
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dumpKeywords(array $keywords, bool $isShort)
|
||||
{
|
||||
if ($isShort) {
|
||||
return count($keywords) > 1
|
||||
? '(' . implode('|', $keywords) . ')'
|
||||
: $keywords[0];
|
||||
}
|
||||
|
||||
return $keywords[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps keyworded feature into string.
|
||||
*
|
||||
* @param string $language Keywords language
|
||||
* @param bool $short Dump short version
|
||||
*
|
||||
* @return string|array String for short version and array of features for extended
|
||||
*
|
||||
* @phpstan-return ($short is true ? string : list<string>)
|
||||
*/
|
||||
public function dump(string $language, bool $short = true, bool $excludeAsterisk = false)
|
||||
{
|
||||
$this->keywords->setLanguage($language);
|
||||
$languageComment = '';
|
||||
if ($language !== 'en') {
|
||||
$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 = [];
|
||||
foreach ($keywords as $keyword) {
|
||||
$keyword = call_user_func($this->keywordsDumper, [$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(string $keyword, bool $short = true, bool $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, [$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, [$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, [$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(string $keyword, bool $short = true, bool $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(string $keyword, bool $short = true, bool $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(string $keyword, bool $short = true, bool $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, [$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(string $keywords, string $text, bool $short = true, bool $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 (str_contains($keyword, '<')) {
|
||||
$keyword = mb_substr($keyword, 0, -1, 'utf8');
|
||||
$indent = '';
|
||||
}
|
||||
$keyword = call_user_func($this->keywordsDumper, [$keyword], $short);
|
||||
$dump .= <<<GHERKIN
|
||||
{$keyword}{$indent}{$text}
|
||||
|
||||
GHERKIN;
|
||||
}
|
||||
}
|
||||
|
||||
return $dump;
|
||||
}
|
||||
}
|
||||
103
vendor/behat/gherkin/src/Keywords/KeywordsInterface.php
vendored
Normal file
103
vendor/behat/gherkin/src/Keywords/KeywordsInterface.php
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLanguage(string $language);
|
||||
|
||||
/**
|
||||
* Returns Feature keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFeatureKeywords();
|
||||
|
||||
/**
|
||||
* Returns Background keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBackgroundKeywords();
|
||||
|
||||
/**
|
||||
* Returns Scenario keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getScenarioKeywords();
|
||||
|
||||
/**
|
||||
* Returns Scenario Outline keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOutlineKeywords();
|
||||
|
||||
/**
|
||||
* Returns Examples keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExamplesKeywords();
|
||||
|
||||
/**
|
||||
* Returns Given keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getGivenKeywords();
|
||||
|
||||
/**
|
||||
* Returns When keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWhenKeywords();
|
||||
|
||||
/**
|
||||
* Returns Then keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getThenKeywords();
|
||||
|
||||
/**
|
||||
* Returns And keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAndKeywords();
|
||||
|
||||
/**
|
||||
* Returns But keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getButKeywords();
|
||||
|
||||
/**
|
||||
* Returns all step keywords (separated by "|").
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStepKeywords();
|
||||
}
|
||||
990
vendor/behat/gherkin/src/Lexer.php
vendored
Normal file
990
vendor/behat/gherkin/src/Lexer.php
vendored
Normal file
@ -0,0 +1,990 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Dialect\DialectProviderInterface;
|
||||
use Behat\Gherkin\Dialect\GherkinDialect;
|
||||
use Behat\Gherkin\Dialect\KeywordsDialectProvider;
|
||||
use Behat\Gherkin\Exception\LexerException;
|
||||
use Behat\Gherkin\Exception\NoSuchLanguageException;
|
||||
use Behat\Gherkin\Keywords\KeywordsInterface;
|
||||
use LogicException;
|
||||
|
||||
use function assert;
|
||||
|
||||
/**
|
||||
* Gherkin lexer.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*
|
||||
* @phpstan-type TStepKeyword 'Given'|'When'|'Then'|'And'|'But'
|
||||
* @phpstan-type TTitleKeyword 'Feature'|'Background'|'Scenario'|'Outline'|'Examples'
|
||||
* @phpstan-type TTokenType 'Text'|'Comment'|'EOS'|'Newline'|'PyStringOp'|'TableRow'|'Tag'|'Language'|'Step'|TTitleKeyword
|
||||
* @phpstan-type TToken TStringValueToken|TNullValueToken|TTitleToken|TStepToken|TTagToken|TTableRowToken
|
||||
* @phpstan-type TStringValueToken array{type: TTokenType, value: string, line: int, deferred: bool}
|
||||
* @phpstan-type TNullValueToken array{type: TTokenType, value: null, line: int, deferred: bool}
|
||||
* @phpstan-type TTitleToken array{type: TTitleKeyword, value: null|non-empty-string, line: int, deferred: bool, keyword: string, indent: int}
|
||||
* @phpstan-type TStepToken array{type: 'Step', value: string, line: int, deferred: bool, keyword_type: string, text: string}
|
||||
* @phpstan-type TTagToken array{type: 'Tag', value: null, line: int, deferred: bool, tags: list<string>}
|
||||
* @phpstan-type TTableRowToken array{type: 'TableRow', value: null, line: int, deferred: bool, columns: list<string>}
|
||||
* @phpstan-type TDocStringSeparator '"""'|'```'
|
||||
*/
|
||||
class Lexer
|
||||
{
|
||||
/**
|
||||
* Splits a string around | char, only if it's not preceded by an odd number of \.
|
||||
*
|
||||
* @see https://github.com/cucumber/gherkin/blob/679a87e21263699c15ea635159c6cda60f64af3b/php/src/StringGherkinLine.php#L14
|
||||
*/
|
||||
private const CELL_PATTERN = '/(?<!\\\\)(?:\\\\{2})*\K\\|/u';
|
||||
|
||||
private readonly DialectProviderInterface $dialectProvider;
|
||||
private GherkinDialect $currentDialect;
|
||||
private GherkinCompatibilityMode $compatibilityMode = GherkinCompatibilityMode::LEGACY;
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private array $lines;
|
||||
private int $linesCount;
|
||||
private string $line;
|
||||
private ?string $trimmedLine = null;
|
||||
private int $lineNumber;
|
||||
private bool $eos;
|
||||
/**
|
||||
* A cache of keyword types associated with each keyword.
|
||||
*
|
||||
* @phpstan-var array<string, non-empty-list<TStepKeyword>>|null
|
||||
*/
|
||||
private ?array $stepKeywordTypesCache = null;
|
||||
/**
|
||||
* @phpstan-var list<TToken>
|
||||
*/
|
||||
private array $deferredObjects = [];
|
||||
private int $deferredObjectsCount = 0;
|
||||
/**
|
||||
* @phpstan-var TToken|null
|
||||
*/
|
||||
private ?array $stashedToken = null;
|
||||
private bool $inPyString = false;
|
||||
private int $pyStringSwallow = 0;
|
||||
private bool $allowLanguageTag = true;
|
||||
private bool $allowFeature = true;
|
||||
private bool $allowMultilineArguments = false;
|
||||
private bool $allowExamples = false;
|
||||
private bool $allowSteps = false;
|
||||
/**
|
||||
* @phpstan-var TDocStringSeparator|null
|
||||
*/
|
||||
private ?string $pyStringDelimiter = null;
|
||||
|
||||
public function __construct(
|
||||
DialectProviderInterface|KeywordsInterface $dialectProvider,
|
||||
) {
|
||||
if ($dialectProvider instanceof KeywordsInterface) {
|
||||
// TODO trigger deprecation
|
||||
$dialectProvider = new KeywordsDialectProvider($dialectProvider);
|
||||
}
|
||||
|
||||
$this->dialectProvider = $dialectProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setCompatibilityMode(GherkinCompatibilityMode $compatibilityMode): void
|
||||
{
|
||||
$this->compatibilityMode = $compatibilityMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets lexer input.
|
||||
*
|
||||
* @param string $input Input string
|
||||
* @param string $language Language name
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws LexerException
|
||||
*/
|
||||
public function analyse(string $input, string $language = 'en')
|
||||
{
|
||||
// try to detect unsupported encoding
|
||||
if (mb_detect_encoding($input, 'UTF-8', true) !== 'UTF-8') {
|
||||
throw new LexerException('Feature file is not in UTF8 encoding');
|
||||
}
|
||||
|
||||
$input = strtr($input, ["\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 = [];
|
||||
$this->deferredObjectsCount = 0;
|
||||
$this->stashedToken = null;
|
||||
$this->inPyString = false;
|
||||
$this->pyStringSwallow = 0;
|
||||
|
||||
$this->allowLanguageTag = true;
|
||||
$this->allowFeature = true;
|
||||
$this->allowMultilineArguments = false;
|
||||
$this->allowSteps = false;
|
||||
$this->allowExamples = false;
|
||||
|
||||
if (\func_num_args() > 1) {
|
||||
// @codeCoverageIgnoreStart
|
||||
\assert($language !== '');
|
||||
// TODO trigger deprecation (the Parser does not use this code path)
|
||||
$this->setLanguage($language);
|
||||
// @codeCoverageIgnoreEnd
|
||||
} else {
|
||||
$this->currentDialect = $this->dialectProvider->getDefaultDialect();
|
||||
$this->stepKeywordTypesCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $language
|
||||
*/
|
||||
private function setLanguage(string $language): void
|
||||
{
|
||||
if (($this->stashedToken !== null) || ($this->deferredObjects !== [])) {
|
||||
// @codeCoverageIgnoreStart
|
||||
// It is not possible to trigger this condition using the public interface of this class.
|
||||
// It may be possible if the end-user has extended the Lexer with custom functionality.
|
||||
throw new LogicException(
|
||||
<<<'STRING'
|
||||
Cannot set gherkin language due to unexpected Lexer state.
|
||||
|
||||
Please open an issue at https://github.com/Behat/Gherkin with a copy of the current
|
||||
feature file. If you are using a Lexer or Parser class that extends the ones provided
|
||||
in behat/gherkin, please also provide details of these.
|
||||
STRING,
|
||||
);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
try {
|
||||
$this->currentDialect = $this->dialectProvider->getDialect($language);
|
||||
} catch (NoSuchLanguageException $e) {
|
||||
if (!$this->compatibilityMode->shouldIgnoreInvalidLanguage()) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$this->stepKeywordTypesCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current lexer language.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
return $this->currentDialect->getLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token or previously stashed one.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @phpstan-return TToken
|
||||
*/
|
||||
public function getAdvancedToken()
|
||||
{
|
||||
return $this->getStashedToken() ?? $this->getNextToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers token.
|
||||
*
|
||||
* @phpstan-param TToken $token Token to defer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deferToken(array $token)
|
||||
{
|
||||
$token['deferred'] = true;
|
||||
$this->deferredObjects[] = $token;
|
||||
++$this->deferredObjectsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicts the upcoming token without passing over it.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @phpstan-return TToken
|
||||
*/
|
||||
public function predictToken()
|
||||
{
|
||||
return $this->stashedToken ??= $this->getNextToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips over the currently-predicted token, if any.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function skipPredictedToken()
|
||||
{
|
||||
$this->stashedToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a token with specified parameters.
|
||||
*
|
||||
* @template T of TTokenType
|
||||
*
|
||||
* @param string|null $value Token value
|
||||
*
|
||||
* @phpstan-param T $type Token type
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @phpstan-return ($value is non-empty-string ? array{type: T, value: non-empty-string, line: int, deferred: bool} : array{type: T, value: null, line: int, deferred: bool})
|
||||
*/
|
||||
public function takeToken(string $type, ?string $value = null)
|
||||
{
|
||||
return [
|
||||
'type' => $type,
|
||||
'line' => $this->lineNumber,
|
||||
'value' => $value ?: null,
|
||||
'deferred' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes line from input & increments line counter.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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 $this->trimmedLine ??= trim($this->line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stashed token or null if there isn't one.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TToken|null
|
||||
*/
|
||||
protected function getStashedToken()
|
||||
{
|
||||
$stashedToken = $this->stashedToken;
|
||||
$this->stashedToken = null;
|
||||
|
||||
return $stashedToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns deferred token or null if there isn't one.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TToken|null
|
||||
*/
|
||||
protected function getDeferredToken()
|
||||
{
|
||||
if (!$this->deferredObjectsCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
--$this->deferredObjectsCount;
|
||||
|
||||
return array_shift($this->deferredObjects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token from input.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @phpstan-return TToken
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @phpstan-param TTokenType $type Expected token type
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TStringValueToken|null
|
||||
*/
|
||||
protected function scanInput(string $regex, string $type)
|
||||
{
|
||||
if (!preg_match($regex, $this->line, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
assert($matches[1] !== '');
|
||||
|
||||
$token = $this->takeToken($type, $matches[1]);
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans for token with specified keywords.
|
||||
*
|
||||
* @param string $keywords Keywords (separated by "|")
|
||||
*
|
||||
* @phpstan-param TTitleKeyword $type Expected token type
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TTitleToken|null
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
protected function scanInputForKeywords(string $keywords, string $type)
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
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 and feature detection
|
||||
if ($type === 'Feature') {
|
||||
$this->allowFeature = false;
|
||||
$this->allowLanguageTag = false;
|
||||
}
|
||||
|
||||
// turn off PyString and Table searching
|
||||
if ($type === 'Feature' || $type === 'Scenario' || $type === 'Outline') {
|
||||
$this->allowMultilineArguments = false;
|
||||
} elseif ($type === 'Examples') {
|
||||
$this->allowMultilineArguments = true;
|
||||
}
|
||||
|
||||
// turn on steps searching
|
||||
if ($type === 'Scenario' || $type === 'Background' || $type === 'Outline') {
|
||||
$this->allowSteps = true;
|
||||
}
|
||||
|
||||
return $token;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $keywords
|
||||
*
|
||||
* @phpstan-param TTitleKeyword $type
|
||||
*
|
||||
* @phpstan-return TTitleToken|null
|
||||
*/
|
||||
private function scanTitleLine(array $keywords, string $type): ?array
|
||||
{
|
||||
$trimmedLine = $this->getTrimmedLine();
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
if (str_starts_with($trimmedLine, $keyword . ':')) {
|
||||
$title = trim(mb_substr($trimmedLine, mb_strlen($keyword) + 1));
|
||||
|
||||
$token = $this->takeToken($type, $title);
|
||||
$token['keyword'] = $keyword;
|
||||
$token['indent'] = mb_strlen($this->line, 'utf8') - mb_strlen(ltrim($this->line), 'utf8');
|
||||
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans EOS from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TNullValueToken|null
|
||||
*/
|
||||
protected function scanEOS()
|
||||
{
|
||||
if (!$this->eos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->takeToken('EOS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a regex matching the keywords for the provided type.
|
||||
*
|
||||
* @phpstan-param 'Step'|TTitleKeyword|TStepKeyword $type Keyword type
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
protected function getKeywords(string $type)
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
$keywords = match ($type) {
|
||||
'Feature' => $this->currentDialect->getFeatureKeywords(),
|
||||
'Background' => $this->currentDialect->getBackgroundKeywords(),
|
||||
'Scenario' => $this->currentDialect->getScenarioKeywords(),
|
||||
'Outline' => $this->currentDialect->getScenarioOutlineKeywords(),
|
||||
'Examples' => $this->currentDialect->getExamplesKeywords(),
|
||||
'Step' => $this->currentDialect->getStepKeywords(),
|
||||
'Given' => $this->currentDialect->getGivenKeywords(),
|
||||
'When' => $this->currentDialect->getWhenKeywords(),
|
||||
'Then' => $this->currentDialect->getThenKeywords(),
|
||||
'And' => $this->currentDialect->getAndKeywords(),
|
||||
'But' => $this->currentDialect->getButKeywords(),
|
||||
default => throw new \InvalidArgumentException(sprintf('Unknown keyword type "%s"', $type)),
|
||||
};
|
||||
|
||||
$keywordsRegex = implode('|', array_map(fn ($keyword) => preg_quote($keyword, '/'), $keywords));
|
||||
|
||||
if ($type === 'Step') {
|
||||
$keywordsRegex = '(?:' . $keywordsRegex . ')\s*';
|
||||
}
|
||||
|
||||
return $keywordsRegex;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Feature from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TTitleToken|null
|
||||
*/
|
||||
protected function scanFeature()
|
||||
{
|
||||
if (!$this->allowFeature) {
|
||||
// The Feature: tag is only allowed once in a file, later in the file it may be part of a description node
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->scanTitleLine($this->currentDialect->getFeatureKeywords(), 'Feature');
|
||||
|
||||
if ($token === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->allowFeature = false;
|
||||
$this->allowLanguageTag = false;
|
||||
$this->allowMultilineArguments = false;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Background from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TTitleToken|null
|
||||
*/
|
||||
protected function scanBackground()
|
||||
{
|
||||
$token = $this->scanTitleLine($this->currentDialect->getBackgroundKeywords(), 'Background');
|
||||
|
||||
if ($token === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->allowSteps = true;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Scenario from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TTitleToken|null
|
||||
*/
|
||||
protected function scanScenario()
|
||||
{
|
||||
$token = $this->scanTitleLine($this->currentDialect->getScenarioKeywords(), 'Scenario');
|
||||
|
||||
if ($token === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->allowMultilineArguments = false;
|
||||
$this->allowSteps = true;
|
||||
$this->allowExamples = true;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Scenario Outline from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TTitleToken|null
|
||||
*/
|
||||
protected function scanOutline()
|
||||
{
|
||||
$token = $this->scanTitleLine($this->currentDialect->getScenarioOutlineKeywords(), 'Outline');
|
||||
|
||||
if ($token === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->allowMultilineArguments = false;
|
||||
$this->allowSteps = true;
|
||||
$this->allowExamples = true;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Scenario Outline Examples from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TTitleToken|null
|
||||
*/
|
||||
protected function scanExamples()
|
||||
{
|
||||
if (!$this->allowExamples) {
|
||||
return null;
|
||||
}
|
||||
$token = $this->scanTitleLine($this->currentDialect->getExamplesKeywords(), 'Examples');
|
||||
|
||||
if ($token === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->allowMultilineArguments = true;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Step from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TStepToken|null
|
||||
*/
|
||||
protected function scanStep()
|
||||
{
|
||||
if (!$this->allowSteps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$trimmedLine = $this->getTrimmedLine();
|
||||
$matchedKeyword = null;
|
||||
|
||||
foreach ($this->currentDialect->getStepKeywords() as $keyword) {
|
||||
if (str_starts_with($trimmedLine, $keyword)) {
|
||||
$matchedKeyword = $keyword;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedKeyword === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = ltrim(mb_substr($trimmedLine, mb_strlen($matchedKeyword)));
|
||||
|
||||
$nodeKeyword = $this->compatibilityMode->shouldRemoveStepKeywordSpace() ? trim($matchedKeyword) : $matchedKeyword;
|
||||
assert($nodeKeyword !== '');
|
||||
|
||||
$token = $this->takeToken('Step', $nodeKeyword);
|
||||
$token['keyword_type'] = $this->getStepKeywordType($matchedKeyword);
|
||||
$token['text'] = $text;
|
||||
|
||||
$this->consumeLine();
|
||||
$this->allowMultilineArguments = true;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans PyString from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TNullValueToken|null
|
||||
*/
|
||||
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 array|null
|
||||
*
|
||||
* @phpstan-return TStringValueToken|null
|
||||
*/
|
||||
protected function scanPyStringContent()
|
||||
{
|
||||
if (!$this->inPyString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->scanText();
|
||||
// swallow trailing spaces
|
||||
$value = (string) preg_replace('/^\s{0,' . $this->pyStringSwallow . '}/u', '', $token['value'] ?? '');
|
||||
|
||||
if ($this->compatibilityMode->shouldUnespaceDocStringDelimiters()) {
|
||||
\assert($this->pyStringDelimiter !== null);
|
||||
$escapedDelimiter = match ($this->pyStringDelimiter) {
|
||||
'"""' => '\\"\\"\\"',
|
||||
'```' => '\\`\\`\\`',
|
||||
};
|
||||
$value = str_replace($escapedDelimiter, $this->pyStringDelimiter, $value);
|
||||
}
|
||||
|
||||
$token['value'] = $value;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Table Row from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TTableRowToken|null
|
||||
*/
|
||||
protected function scanTableRow()
|
||||
{
|
||||
if (!$this->allowMultilineArguments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$line = $this->getTrimmedLine();
|
||||
if (!str_starts_with($line, '|')) {
|
||||
// Strictly speaking, a table row only has to begin with a pipe - content to the right
|
||||
// of the final pipe will be ignored after we split the cells.
|
||||
return null;
|
||||
}
|
||||
|
||||
$rawColumns = preg_split(self::CELL_PATTERN, $line);
|
||||
assert($rawColumns !== false);
|
||||
|
||||
// Safely remove elements before the first and last separators
|
||||
array_shift($rawColumns);
|
||||
array_pop($rawColumns);
|
||||
|
||||
$token = $this->takeToken('TableRow');
|
||||
if ($this->compatibilityMode->shouldUseNewTableCellParsing()) {
|
||||
$columns = array_map($this->parseTableCell(...), $rawColumns);
|
||||
} else {
|
||||
$columns = array_map(static fn ($column) => trim(str_replace(['\\|', '\\\\'], ['|', '\\'], $column)), $rawColumns);
|
||||
}
|
||||
$token['columns'] = $columns;
|
||||
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
private function parseTableCell(string $cell): string
|
||||
{
|
||||
$trimmedCell = preg_replace('/^[ \\t\\n\\x0B\\f\\r\\x85\\xA0]++|[ \\t\\n\\x0B\\f\\r\\x85\\xA0]++$/u', '', $cell);
|
||||
\assert($trimmedCell !== null);
|
||||
|
||||
$value = preg_replace_callback('/\\\\./', function (array $matches) {
|
||||
return match ($matches[0]) {
|
||||
'\\n' => "\n",
|
||||
'\\\\' => '\\',
|
||||
'\\|' => '|',
|
||||
default => $matches[0],
|
||||
};
|
||||
}, $trimmedCell);
|
||||
|
||||
assert($value !== null);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Tags from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TTagToken|null
|
||||
*/
|
||||
protected function scanTags()
|
||||
{
|
||||
$line = $this->getTrimmedLine();
|
||||
|
||||
if ($line === '' || !str_starts_with($line, '@')) {
|
||||
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');
|
||||
|
||||
if ($this->compatibilityMode->shouldRemoveTagPrefixChar()) {
|
||||
// Legacy behaviour
|
||||
$tags = explode('@', mb_substr($line, 1, mb_strlen($line, 'utf8') - 1, 'utf8'));
|
||||
$tags = array_map(trim(...), $tags);
|
||||
$token['tags'] = $tags;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
$tags = preg_split('/(?=@)/u', $line);
|
||||
assert($tags !== false);
|
||||
// Remove the empty content before the first tag prefix
|
||||
array_shift($tags);
|
||||
|
||||
// Note: checking for whitespace in tags is done in the Parser to fit with existing logic
|
||||
$token['tags'] = array_map(trim(...), $tags);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Language specifier from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TStringValueToken|null
|
||||
*/
|
||||
protected function scanLanguage()
|
||||
{
|
||||
if (!$this->allowLanguageTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->inPyString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!str_starts_with(ltrim($this->line), '#')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pattern = $this->compatibilityMode->allowWhitespaceInLanguageTag()
|
||||
? '/^\s*#\s*language\s*:\s*([\w_\-]+)\s*$/u'
|
||||
: '/^\s*#\s*language:\s*([\w_\-]+)\s*$/';
|
||||
|
||||
$token = $this->scanInput($pattern, 'Language');
|
||||
|
||||
if ($token) {
|
||||
\assert(\is_string($token['value']));
|
||||
\assert($token['value'] !== ''); // the regex can only match a non-empty value.
|
||||
$this->allowLanguageTag = false;
|
||||
$this->setLanguage($token['value']);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Comment from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TStringValueToken|null
|
||||
*/
|
||||
protected function scanComment()
|
||||
{
|
||||
if ($this->inPyString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$line = $this->getTrimmedLine();
|
||||
if (!str_starts_with($line, '#')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->takeToken('Comment', $line);
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans Newline from input & returns it if found.
|
||||
*
|
||||
* @return array|null
|
||||
*
|
||||
* @phpstan-return TNullValueToken|null
|
||||
*/
|
||||
protected function scanNewline()
|
||||
{
|
||||
if ($this->getTrimmedLine() !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->takeToken('Newline');
|
||||
$this->consumeLine();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans text from input & returns it if found.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @phpstan-return TStringValueToken|TNullValueToken
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @phpstan-return TStepKeyword
|
||||
*/
|
||||
private function getStepKeywordType(string $native): string
|
||||
{
|
||||
if ($this->stepKeywordTypesCache === null) {
|
||||
$this->stepKeywordTypesCache = [];
|
||||
$this->addStepKeywordTypes($this->currentDialect->getGivenKeywords(), 'Given');
|
||||
$this->addStepKeywordTypes($this->currentDialect->getWhenKeywords(), 'When');
|
||||
$this->addStepKeywordTypes($this->currentDialect->getThenKeywords(), 'Then');
|
||||
$this->addStepKeywordTypes($this->currentDialect->getAndKeywords(), 'And');
|
||||
$this->addStepKeywordTypes($this->currentDialect->getButKeywords(), 'But');
|
||||
}
|
||||
|
||||
if (!isset($this->stepKeywordTypesCache[$native])) { // should not happen when the native keyword belongs to the dialect
|
||||
return 'Given'; // cucumber/gherkin has an UNKNOWN type, but we don't have it.
|
||||
}
|
||||
|
||||
if (\count($this->stepKeywordTypesCache[$native]) === 1) {
|
||||
return $this->stepKeywordTypesCache[$native][0];
|
||||
}
|
||||
|
||||
// Consider ambiguous keywords as AND keywords so that they are normalized to the previous step type.
|
||||
// This happens in English for the `* ` keyword for instance.
|
||||
// cucumber/gherkin returns that as an UNKNOWN type, but we don't have it.
|
||||
return 'And';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $keywords
|
||||
*
|
||||
* @phpstan-param TStepKeyword $type
|
||||
*/
|
||||
private function addStepKeywordTypes(array $keywords, string $type): void
|
||||
{
|
||||
foreach ($keywords as $keyword) {
|
||||
$this->stepKeywordTypesCache[$keyword][] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
95
vendor/behat/gherkin/src/Loader/AbstractFileLoader.php
vendored
Normal file
95
vendor/behat/gherkin/src/Loader/AbstractFileLoader.php
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Filesystem;
|
||||
|
||||
/**
|
||||
* Abstract filesystem loader.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* @template TResourceType
|
||||
*
|
||||
* @extends AbstractLoader<TResourceType>
|
||||
*
|
||||
* @implements FileLoaderInterface<TResourceType>
|
||||
*/
|
||||
abstract class AbstractFileLoader extends AbstractLoader implements FileLoaderInterface
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $basePath;
|
||||
|
||||
/**
|
||||
* Sets base features path.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setBasePath(string $path)
|
||||
{
|
||||
$this->basePath = Filesystem::getRealPath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds relative path for provided absolute (relative to base features path).
|
||||
*
|
||||
* @param string $path Absolute path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function findRelativePath(string $path)
|
||||
{
|
||||
if ($this->basePath !== null) {
|
||||
return strtr($path, [$this->basePath . DIRECTORY_SEPARATOR => '']);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds absolute path for provided relative (relative to base features path).
|
||||
*
|
||||
* @param string $path Relative path
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
protected function findAbsolutePath(string $path)
|
||||
{
|
||||
if (file_exists($path)) {
|
||||
return realpath($path);
|
||||
}
|
||||
|
||||
if ($this->basePath === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_exists($this->basePath . DIRECTORY_SEPARATOR . $path)) {
|
||||
return realpath($this->basePath . DIRECTORY_SEPARATOR . $path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
final protected function getAbsolutePath(string $path): string
|
||||
{
|
||||
$resolvedPath = $this->findAbsolutePath($path);
|
||||
if ($resolvedPath === false) {
|
||||
throw new \RuntimeException("Unable to locate absolute path of \"$path\"");
|
||||
}
|
||||
|
||||
return $resolvedPath;
|
||||
}
|
||||
}
|
||||
42
vendor/behat/gherkin/src/Loader/AbstractLoader.php
vendored
Normal file
42
vendor/behat/gherkin/src/Loader/AbstractLoader.php
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
|
||||
/**
|
||||
* @template TResourceType
|
||||
*
|
||||
* @implements LoaderInterface<TResourceType>
|
||||
*/
|
||||
abstract class AbstractLoader implements LoaderInterface
|
||||
{
|
||||
public function load(mixed $resource)
|
||||
{
|
||||
if (!$this->supports($resource)) {
|
||||
throw new \LogicException(sprintf(
|
||||
'%s::%s() was called with unsupported resource `%s`.',
|
||||
static::class,
|
||||
__FUNCTION__,
|
||||
json_encode($resource)
|
||||
));
|
||||
}
|
||||
|
||||
return $this->doLoad($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TResourceType $resource
|
||||
*
|
||||
* @return list<FeatureNode>
|
||||
*/
|
||||
abstract protected function doLoad(mixed $resource): array;
|
||||
}
|
||||
299
vendor/behat/gherkin/src/Loader/ArrayLoader.php
vendored
Normal file
299
vendor/behat/gherkin/src/Loader/ArrayLoader.php
vendored
Normal file
@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @phpstan-type TFeatureHash array{title?: string|null, description?: string|null, tags?: list<string>, keyword?: string, language?: string, line?: int, background?: TBackgroundHash|null, scenarios?: array<int, TScenarioHash|TOutlineHash>}
|
||||
* @phpstan-type TBackgroundHash array{title?: string|null, keyword?: string, line?: int, steps?: array<int, TStepHash>}
|
||||
* @phpstan-type TScenarioHash array{title?: string|null, tags?: list<string>, keyword?: string, line?: int, steps?: array<int, TStepHash>}
|
||||
* @phpstan-type TOutlineHash array{type: 'outline', title?: string|null, tags?: list<string>, keyword?: string, line?: int, steps?: array<int, TStepHash>, examples?: TExampleTableHash|array<array-key, TExampleHash>}
|
||||
* @phpstan-type TExampleHash array{table: TExampleTableHash, tags?: list<string>}|TExampleTableHash
|
||||
* @phpstan-type TExampleTableHash array<int<1, max>, list<string>>
|
||||
* @phpstan-type TStepHash array{keyword_type?: string, type?: string, text: string, keyword?: string, line?: int, arguments?: array<array-key, TArgumentHash>}
|
||||
* @phpstan-type TArgumentHash array{type: 'table', rows: TTableHash}|TPyStringHash
|
||||
* @phpstan-type TTableHash array<int, list<string>>
|
||||
* @phpstan-type TPyStringHash array{type: 'pystring', line?: int, text: string}
|
||||
* @phpstan-type TArrayResource array{feature: TFeatureHash}|array{features: array<int, TFeatureHash>}
|
||||
*
|
||||
* @phpstan-extends AbstractLoader<TArrayResource>
|
||||
*/
|
||||
class ArrayLoader extends AbstractLoader
|
||||
{
|
||||
public function supports(mixed $resource)
|
||||
{
|
||||
return is_array($resource) && (isset($resource['features']) || isset($resource['feature']));
|
||||
}
|
||||
|
||||
protected function doLoad(mixed $resource): array
|
||||
{
|
||||
$features = [];
|
||||
|
||||
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.
|
||||
*
|
||||
* @phpstan-param TFeatureHash $hash
|
||||
*
|
||||
* @return FeatureNode
|
||||
*/
|
||||
protected function loadFeatureHash(array $hash, int $line = 0)
|
||||
{
|
||||
$hash = array_merge(
|
||||
[
|
||||
'title' => null,
|
||||
'description' => null,
|
||||
'tags' => [],
|
||||
'keyword' => 'Feature',
|
||||
'language' => 'en',
|
||||
'line' => $line,
|
||||
'scenarios' => [],
|
||||
],
|
||||
$hash
|
||||
);
|
||||
$background = isset($hash['background']) ? $this->loadBackgroundHash($hash['background']) : null;
|
||||
|
||||
$scenarios = [];
|
||||
foreach ((array) $hash['scenarios'] as $scenarioIterator => $scenarioHash) {
|
||||
if (isset($scenarioHash['type']) && $scenarioHash['type'] === 'outline') {
|
||||
$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.
|
||||
*
|
||||
* @phpstan-param TBackgroundHash $hash
|
||||
*
|
||||
* @return BackgroundNode
|
||||
*/
|
||||
protected function loadBackgroundHash(array $hash)
|
||||
{
|
||||
$hash = array_merge(
|
||||
[
|
||||
'title' => null,
|
||||
'keyword' => 'Background',
|
||||
'line' => 0,
|
||||
'steps' => [],
|
||||
],
|
||||
$hash
|
||||
);
|
||||
|
||||
$steps = $this->loadStepsHash($hash['steps']);
|
||||
|
||||
return new BackgroundNode($hash['title'], $steps, $hash['keyword'], $hash['line']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads scenario from provided scenario hash.
|
||||
*
|
||||
* @phpstan-param TScenarioHash $hash
|
||||
*
|
||||
* @return ScenarioNode
|
||||
*/
|
||||
protected function loadScenarioHash(array $hash, int $line = 0)
|
||||
{
|
||||
$hash = array_merge(
|
||||
[
|
||||
'title' => null,
|
||||
'tags' => [],
|
||||
'keyword' => 'Scenario',
|
||||
'line' => $line,
|
||||
'steps' => [],
|
||||
],
|
||||
$hash
|
||||
);
|
||||
|
||||
$steps = $this->loadStepsHash($hash['steps']);
|
||||
|
||||
return new ScenarioNode($hash['title'], $hash['tags'], $steps, $hash['keyword'], $hash['line']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads outline from provided outline hash.
|
||||
*
|
||||
* @phpstan-param TOutlineHash $hash
|
||||
*
|
||||
* @return OutlineNode
|
||||
*/
|
||||
protected function loadOutlineHash(array $hash, int $line = 0)
|
||||
{
|
||||
$hash = array_merge(
|
||||
[
|
||||
'title' => null,
|
||||
'tags' => [],
|
||||
'keyword' => 'Scenario Outline',
|
||||
'line' => $line,
|
||||
'steps' => [],
|
||||
'examples' => [],
|
||||
],
|
||||
$hash
|
||||
);
|
||||
|
||||
$steps = $this->loadStepsHash($hash['steps']);
|
||||
|
||||
if (isset($hash['examples']['keyword'])) {
|
||||
$examplesKeyword = $hash['examples']['keyword'];
|
||||
assert(is_string($examplesKeyword));
|
||||
unset($hash['examples']['keyword']);
|
||||
} else {
|
||||
$examplesKeyword = 'Examples';
|
||||
}
|
||||
|
||||
$examples = $this->loadExamplesHash($hash['examples'], $examplesKeyword);
|
||||
|
||||
return new OutlineNode($hash['title'], $hash['tags'], $steps, $examples, $hash['keyword'], $hash['line']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads steps from provided hash.
|
||||
*
|
||||
* @phpstan-param array<int, TStepHash> $hash
|
||||
*
|
||||
* @return list<StepNode>
|
||||
*/
|
||||
private function loadStepsHash(array $hash)
|
||||
{
|
||||
$steps = [];
|
||||
foreach ($hash as $stepIterator => $stepHash) {
|
||||
$steps[] = $this->loadStepHash($stepHash, $stepIterator);
|
||||
}
|
||||
|
||||
return $steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads step from provided hash.
|
||||
*
|
||||
* @phpstan-param TStepHash $hash
|
||||
*
|
||||
* @return StepNode
|
||||
*/
|
||||
protected function loadStepHash(array $hash, int $line = 0)
|
||||
{
|
||||
$hash = array_merge(
|
||||
[
|
||||
'keyword_type' => 'Given',
|
||||
'type' => 'Given',
|
||||
'text' => null,
|
||||
'keyword' => 'Scenario',
|
||||
'line' => $line,
|
||||
'arguments' => [],
|
||||
],
|
||||
$hash
|
||||
);
|
||||
|
||||
$arguments = [];
|
||||
foreach ($hash['arguments'] as $argumentHash) {
|
||||
if ($argumentHash['type'] === 'table') {
|
||||
$arguments[] = $this->loadTableHash($argumentHash['rows']);
|
||||
} elseif ($argumentHash['type'] === 'pystring') {
|
||||
$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.
|
||||
*
|
||||
* @phpstan-param TTableHash $hash
|
||||
*
|
||||
* @return TableNode
|
||||
*/
|
||||
protected function loadTableHash(array $hash)
|
||||
{
|
||||
return new TableNode($hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PyString from provided hash.
|
||||
*
|
||||
* @phpstan-param TPyStringHash $hash
|
||||
*
|
||||
* @return PyStringNode
|
||||
*/
|
||||
protected function loadPyStringHash(array $hash, int $line = 0)
|
||||
{
|
||||
$line = $hash['line'] ?? $line;
|
||||
|
||||
$strings = [];
|
||||
foreach (explode("\n", $hash['text']) as $string) {
|
||||
$strings[] = $string;
|
||||
}
|
||||
|
||||
return new PyStringNode($strings, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes cases when examples are in the form of array of arrays
|
||||
* OR in the form of array of objects.
|
||||
*
|
||||
* @phpstan-param TExampleHash|array<array-key, TExampleHash> $examplesHash
|
||||
*
|
||||
* @return list<ExampleTableNode>
|
||||
*/
|
||||
private function loadExamplesHash(array $examplesHash, string $examplesKeyword): array
|
||||
{
|
||||
if (!isset($examplesHash[0])) {
|
||||
// examples as a single table - create a list with the one element
|
||||
// @phpstan-ignore argument.type
|
||||
return [new ExampleTableNode($examplesHash, $examplesKeyword)];
|
||||
}
|
||||
|
||||
$examples = [];
|
||||
|
||||
foreach ($examplesHash as $exampleHash) {
|
||||
if (isset($exampleHash['table'])) {
|
||||
// we have examples as objects, hence there could be tags
|
||||
$exHashTags = $exampleHash['tags'] ?? [];
|
||||
// @phpstan-ignore argument.type,argument.type
|
||||
$examples[] = new ExampleTableNode($exampleHash['table'], $examplesKeyword, $exHashTags);
|
||||
} else {
|
||||
// we have examples as arrays
|
||||
// @phpstan-ignore argument.type
|
||||
$examples[] = new ExampleTableNode($exampleHash, $examplesKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
return $examples;
|
||||
}
|
||||
}
|
||||
265
vendor/behat/gherkin/src/Loader/CucumberNDJsonAstLoader.php
vendored
Normal file
265
vendor/behat/gherkin/src/Loader/CucumberNDJsonAstLoader.php
vendored
Normal file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Exception\NodeException;
|
||||
use Behat\Gherkin\Node\ArgumentInterface;
|
||||
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;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Loads a feature from cucumber's messages JSON format.
|
||||
*
|
||||
* Lines in the ndjson file are expected to match the Cucumber Messages JSON schema defined at https://github.com/cucumber/messages/tree/main/jsonschema
|
||||
*
|
||||
* @deprecated This loader is deprecated and will be removed in 5.0
|
||||
*
|
||||
* @phpstan-type TLocation array{line: int, column?: int}
|
||||
* @phpstan-type TBackground array{location: TLocation, keyword: string, name: string, description: string, steps: list<TStep>, id: string}
|
||||
* @phpstan-type TComment array{location: TLocation, text: string}
|
||||
* @phpstan-type TDataTable array{location: TLocation, rows: list<TTableRow>}
|
||||
* @phpstan-type TDocString array{location: TLocation, content: string, delimiter: string, mediaType?: string}
|
||||
* @phpstan-type TExamples array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, tableHeader?: TTableRow, tableBody: list<TTableRow>, id: string}
|
||||
* @phpstan-type TFeature array{location: TLocation, tags: list<TTag>, language: string, keyword: string, name: string, description: string, children: list<TFeatureChild>}
|
||||
* @phpstan-type TFeatureChild array{background?: TBackground, scenario?: TScenario, rule?: TRule}
|
||||
* @phpstan-type TRule array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, children: list<TRuleChild>, id: string}
|
||||
* @phpstan-type TRuleChild array{background?: TBackground, scenario?: TScenario}
|
||||
* @phpstan-type TScenario array{location: TLocation, tags: list<TTag>, keyword: string, name: string, description: string, steps: list<TStep>, examples: list<TExamples>, id: string}
|
||||
* @phpstan-type TStep array{location: TLocation, keyword: string, keywordType?: 'Unknown'|'Context'|'Action'|'Outcome'|'Conjunction', text: string, docString?: TDocString, dataTable?: TDataTable, id: string}
|
||||
* @phpstan-type TTableCell array{location: TLocation, value: string}
|
||||
* @phpstan-type TTableRow array{location: TLocation, cells: list<TTableCell>, id: string}
|
||||
* @phpstan-type TTag array{location: TLocation, name: string, id: string}
|
||||
* @phpstan-type TGherkinDocument array{uri?: string, feature?: TFeature, comments: list<TComment>}
|
||||
* // We only care about the gherkinDocument messages for our use case, so this does not describe the envelope fully
|
||||
* @phpstan-type TEnvelope array{gherkinDocument?: TGherkinDocument, ...}
|
||||
*
|
||||
* @extends AbstractLoader<string>
|
||||
*/
|
||||
class CucumberNDJsonAstLoader extends AbstractLoader
|
||||
{
|
||||
public function supports(mixed $resource)
|
||||
{
|
||||
return is_string($resource);
|
||||
}
|
||||
|
||||
protected function doLoad(mixed $resource): array
|
||||
{
|
||||
return array_values(
|
||||
array_filter(
|
||||
array_map(
|
||||
static function ($line) use ($resource) {
|
||||
// As we load data from the official Cucumber project, we assume the data matches the JSON schema.
|
||||
// @phpstan-ignore argument.type
|
||||
return self::getFeature(json_decode($line, true, 512, \JSON_THROW_ON_ERROR), $resource);
|
||||
},
|
||||
file($resource)
|
||||
?: throw new RuntimeException("Could not load Cucumber json file: $resource."),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TEnvelope $json
|
||||
*/
|
||||
private static function getFeature(array $json, string $filePath): ?FeatureNode
|
||||
{
|
||||
if (!isset($json['gherkinDocument']['feature'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$featureJson = $json['gherkinDocument']['feature'];
|
||||
|
||||
return new FeatureNode(
|
||||
$featureJson['name'],
|
||||
$featureJson['description'],
|
||||
self::getTags($featureJson),
|
||||
self::getBackground($featureJson),
|
||||
self::getScenarios($featureJson),
|
||||
$featureJson['keyword'],
|
||||
$featureJson['language'],
|
||||
preg_replace('/(?<=\\.feature).*$/', '', $filePath),
|
||||
$featureJson['location']['line']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param array{tags: list<TTag>, ...} $json
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function getTags(array $json): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (array $tag) => preg_replace('/^@/', '', $tag['name']) ?? $tag['name'],
|
||||
$json['tags']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TFeature $json
|
||||
*
|
||||
* @return list<ScenarioInterface>
|
||||
*/
|
||||
private static function getScenarios(array $json): array
|
||||
{
|
||||
return array_values(
|
||||
array_map(
|
||||
static function ($child) {
|
||||
$tables = self::getTables($child['scenario']['examples']);
|
||||
|
||||
if ($tables) {
|
||||
return new OutlineNode(
|
||||
$child['scenario']['name'],
|
||||
self::getTags($child['scenario']),
|
||||
self::getSteps($child['scenario']['steps']),
|
||||
$tables,
|
||||
$child['scenario']['keyword'],
|
||||
$child['scenario']['location']['line']
|
||||
);
|
||||
}
|
||||
|
||||
return new ScenarioNode(
|
||||
$child['scenario']['name'],
|
||||
self::getTags($child['scenario']),
|
||||
self::getSteps($child['scenario']['steps']),
|
||||
$child['scenario']['keyword'],
|
||||
$child['scenario']['location']['line']
|
||||
);
|
||||
},
|
||||
array_filter(
|
||||
$json['children'],
|
||||
static function ($child) {
|
||||
return isset($child['scenario']);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TFeature $json
|
||||
*/
|
||||
private static function getBackground(array $json): ?BackgroundNode
|
||||
{
|
||||
$backgrounds = array_filter(
|
||||
$json['children'],
|
||||
static fn ($child) => isset($child['background']),
|
||||
);
|
||||
|
||||
if (count($backgrounds) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$background = array_shift($backgrounds);
|
||||
|
||||
return new BackgroundNode(
|
||||
$background['background']['name'],
|
||||
self::getSteps($background['background']['steps']),
|
||||
$background['background']['keyword'],
|
||||
$background['background']['location']['line']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param list<TStep> $items
|
||||
*
|
||||
* @return list<StepNode>
|
||||
*/
|
||||
private static function getSteps(array $items): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (array $item) => new StepNode(
|
||||
trim($item['keyword']),
|
||||
$item['text'],
|
||||
self::getStepArguments($item),
|
||||
$item['location']['line'],
|
||||
trim($item['keyword'])
|
||||
),
|
||||
$items
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TStep $step
|
||||
*
|
||||
* @return list<ArgumentInterface>
|
||||
*/
|
||||
private static function getStepArguments(array $step): array
|
||||
{
|
||||
$args = [];
|
||||
|
||||
if (isset($step['docString'])) {
|
||||
$args[] = new PyStringNode(
|
||||
explode("\n", $step['docString']['content']),
|
||||
$step['docString']['location']['line'],
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($step['dataTable'])) {
|
||||
$table = [];
|
||||
foreach ($step['dataTable']['rows'] as $row) {
|
||||
$table[$row['location']['line']] = array_column($row['cells'], 'value');
|
||||
}
|
||||
$args[] = new TableNode($table);
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param list<TExamples> $items
|
||||
*
|
||||
* @return list<ExampleTableNode>
|
||||
*/
|
||||
private static function getTables(array $items): array
|
||||
{
|
||||
return array_map(
|
||||
static function ($tableJson): ExampleTableNode {
|
||||
$headerRow = $tableJson['tableHeader'] ?? null;
|
||||
$tableBody = $tableJson['tableBody'];
|
||||
|
||||
if ($headerRow === null && ($tableBody !== [])) {
|
||||
throw new NodeException(
|
||||
sprintf(
|
||||
'Table header is required when a table body is provided for the example on line %s.',
|
||||
$tableJson['location']['line'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$table = [];
|
||||
if ($headerRow !== null) {
|
||||
$table[$headerRow['location']['line']] = array_column($headerRow['cells'], 'value');
|
||||
}
|
||||
|
||||
foreach ($tableBody as $bodyRow) {
|
||||
$table[$bodyRow['location']['line']] = array_column($bodyRow['cells'], 'value');
|
||||
}
|
||||
|
||||
return new ExampleTableNode(
|
||||
$table,
|
||||
$tableJson['keyword'],
|
||||
self::getTags($tableJson)
|
||||
);
|
||||
},
|
||||
$items
|
||||
);
|
||||
}
|
||||
}
|
||||
71
vendor/behat/gherkin/src/Loader/DirectoryLoader.php
vendored
Normal file
71
vendor/behat/gherkin/src/Loader/DirectoryLoader.php
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Directory contents loader.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* @extends AbstractFileLoader<string>
|
||||
*/
|
||||
class DirectoryLoader extends AbstractFileLoader
|
||||
{
|
||||
/**
|
||||
* @var Gherkin
|
||||
*/
|
||||
protected $gherkin;
|
||||
|
||||
/**
|
||||
* Initializes loader.
|
||||
*/
|
||||
public function __construct(Gherkin $gherkin)
|
||||
{
|
||||
$this->gherkin = $gherkin;
|
||||
}
|
||||
|
||||
public function supports(mixed $resource)
|
||||
{
|
||||
return is_string($resource)
|
||||
&& ($path = $this->findAbsolutePath($resource)) !== false
|
||||
&& is_dir($path);
|
||||
}
|
||||
|
||||
protected function doLoad(mixed $resource): array
|
||||
{
|
||||
$path = $this->getAbsolutePath($resource);
|
||||
/** @var Traversable<SplFileInfo> $iterator */
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
$paths = array_map(strval(...), iterator_to_array($iterator));
|
||||
uasort($paths, strnatcasecmp(...));
|
||||
|
||||
$features = [];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$path = (string) $path;
|
||||
$loader = $this->gherkin->resolveLoader($path);
|
||||
|
||||
if ($loader !== null) {
|
||||
array_push($features, ...$loader->load($path));
|
||||
}
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
}
|
||||
30
vendor/behat/gherkin/src/Loader/FileLoaderInterface.php
vendored
Normal file
30
vendor/behat/gherkin/src/Loader/FileLoaderInterface.php
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @template TResourceType
|
||||
*
|
||||
* @extends LoaderInterface<TResourceType>
|
||||
*/
|
||||
interface FileLoaderInterface extends LoaderInterface
|
||||
{
|
||||
/**
|
||||
* Sets base features path.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setBasePath(string $path);
|
||||
}
|
||||
87
vendor/behat/gherkin/src/Loader/GherkinFileLoader.php
vendored
Normal file
87
vendor/behat/gherkin/src/Loader/GherkinFileLoader.php
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\Filesystem;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
use Behat\Gherkin\ParserInterface;
|
||||
|
||||
/**
|
||||
* Gherkin *.feature files loader.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* @extends AbstractFileLoader<string>
|
||||
*/
|
||||
class GherkinFileLoader extends AbstractFileLoader
|
||||
{
|
||||
/**
|
||||
* @var ParserInterface
|
||||
*/
|
||||
protected $parser;
|
||||
/**
|
||||
* @var CacheInterface|null
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
public function __construct(ParserInterface $parser, ?CacheInterface $cache = null)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets cache layer.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setCache(CacheInterface $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
public function supports(mixed $resource)
|
||||
{
|
||||
return is_string($resource)
|
||||
&& ($path = $this->findAbsolutePath($resource)) !== false
|
||||
&& is_file($path)
|
||||
&& pathinfo($path, PATHINFO_EXTENSION) === 'feature';
|
||||
}
|
||||
|
||||
protected function doLoad(mixed $resource): array
|
||||
{
|
||||
$path = $this->getAbsolutePath($resource);
|
||||
if ($this->cache) {
|
||||
if ($this->cache->isFresh($path, Filesystem::getLastModified($path))) {
|
||||
$feature = $this->cache->read($path);
|
||||
} elseif (null !== $feature = $this->parseFeature($path)) {
|
||||
$this->cache->write($path, $feature);
|
||||
}
|
||||
} else {
|
||||
$feature = $this->parseFeature($path);
|
||||
}
|
||||
|
||||
return $feature !== null ? [$feature] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses feature at provided absolute path.
|
||||
*
|
||||
* @param string $path Feature path
|
||||
*
|
||||
* @return FeatureNode|null
|
||||
*/
|
||||
protected function parseFeature(string $path)
|
||||
{
|
||||
return $this->parser->parseFile($path);
|
||||
}
|
||||
}
|
||||
45
vendor/behat/gherkin/src/Loader/LoaderInterface.php
vendored
Normal file
45
vendor/behat/gherkin/src/Loader/LoaderInterface.php
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @template TResourceType
|
||||
*/
|
||||
interface LoaderInterface
|
||||
{
|
||||
/**
|
||||
* Checks if current loader supports provided resource.
|
||||
*
|
||||
* @template TSupportedResourceType
|
||||
*
|
||||
* @param TSupportedResourceType $resource Resource to load
|
||||
*
|
||||
* @phpstan-assert-if-true =LoaderInterface<TSupportedResourceType> $this
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function supports(mixed $resource);
|
||||
|
||||
/**
|
||||
* Loads features from provided resource.
|
||||
*
|
||||
* @param TResourceType $resource Resource to load
|
||||
*
|
||||
* @return list<FeatureNode>
|
||||
*/
|
||||
public function load(mixed $resource);
|
||||
}
|
||||
66
vendor/behat/gherkin/src/Loader/YamlFileLoader.php
vendored
Normal file
66
vendor/behat/gherkin/src/Loader/YamlFileLoader.php
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @extends AbstractFileLoader<string>
|
||||
*
|
||||
* @phpstan-import-type TArrayResource from ArrayLoader
|
||||
*/
|
||||
class YamlFileLoader extends AbstractFileLoader
|
||||
{
|
||||
/**
|
||||
* @phpstan-param LoaderInterface<TArrayResource> $loader
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly LoaderInterface $loader = new ArrayLoader(),
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(mixed $resource)
|
||||
{
|
||||
return is_string($resource)
|
||||
&& ($path = $this->findAbsolutePath($resource)) !== false
|
||||
&& is_file($path)
|
||||
&& pathinfo($path, PATHINFO_EXTENSION) === 'yml';
|
||||
}
|
||||
|
||||
protected function doLoad(mixed $resource): array
|
||||
{
|
||||
$path = $this->getAbsolutePath($resource);
|
||||
$hash = Yaml::parseFile($path);
|
||||
|
||||
// @phpstan-ignore argument.type
|
||||
$features = $this->loader->load($hash);
|
||||
|
||||
return array_map(
|
||||
static fn (FeatureNode $feature) => new FeatureNode(
|
||||
$feature->getTitle(),
|
||||
$feature->getDescription(),
|
||||
$feature->getTags(),
|
||||
$feature->getBackground(),
|
||||
$feature->getScenarios(),
|
||||
$feature->getKeyword(),
|
||||
$feature->getLanguage(),
|
||||
$path,
|
||||
$feature->getLine()
|
||||
),
|
||||
$features
|
||||
);
|
||||
}
|
||||
}
|
||||
20
vendor/behat/gherkin/src/Node/ArgumentInterface.php
vendored
Normal file
20
vendor/behat/gherkin/src/Node/ArgumentInterface.php
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
}
|
||||
98
vendor/behat/gherkin/src/Node/BackgroundNode.php
vendored
Normal file
98
vendor/behat/gherkin/src/Node/BackgroundNode.php
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*/
|
||||
class BackgroundNode implements ScenarioLikeInterface, DescribableNodeInterface
|
||||
{
|
||||
/**
|
||||
* @param StepNode[] $steps
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?string $title,
|
||||
private readonly array $steps,
|
||||
private readonly string $keyword,
|
||||
private readonly int $line,
|
||||
private readonly ?string $description = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Background';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns background title.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if background has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps()
|
||||
{
|
||||
return (bool) 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 int
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
}
|
||||
19
vendor/behat/gherkin/src/Node/DescribableNodeInterface.php
vendored
Normal file
19
vendor/behat/gherkin/src/Node/DescribableNodeInterface.php
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
|
||||
interface DescribableNodeInterface
|
||||
{
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getDescription();
|
||||
}
|
||||
248
vendor/behat/gherkin/src/Node/ExampleNode.php
vendored
Normal file
248
vendor/behat/gherkin/src/Node/ExampleNode.php
vendored
Normal file
@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*/
|
||||
class ExampleNode implements ScenarioInterface, NamedScenarioInterface
|
||||
{
|
||||
use TaggedNodeTrait;
|
||||
|
||||
/**
|
||||
* @var list<StepNode>|null
|
||||
*/
|
||||
private ?array $steps = null;
|
||||
|
||||
/**
|
||||
* @param string $text The entire row as a string, e.g. "| 1 | 2 | 3 |"
|
||||
* @param list<string> $tags
|
||||
* @param array<array-key, StepNode> $outlineSteps
|
||||
* @param array<string, string> $tokens
|
||||
* @param int $line line number within the feature file
|
||||
* @param string|null $outlineTitle original title of the scenario outline
|
||||
* @param int|null $index the 1-based index of the row/example within the scenario outline
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $text,
|
||||
private readonly array $tags,
|
||||
private readonly array $outlineSteps,
|
||||
private readonly array $tokens,
|
||||
private readonly int $line,
|
||||
private readonly ?string $outlineTitle = null,
|
||||
private readonly ?int $index = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Example';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->getNodeType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the example row as a single string.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @deprecated you should normally not depend on the original row text, but if you really do, please switch
|
||||
* to {@see self::getExampleText()} as this method will be removed in the next major version
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps()
|
||||
{
|
||||
return count($this->outlineSteps) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline steps.
|
||||
*
|
||||
* @return list<StepNode>
|
||||
*/
|
||||
public function getSteps()
|
||||
{
|
||||
return $this->steps ??= $this->createExampleSteps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns example tokens.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTokens()
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline declaration line number.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline title.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOutlineTitle()
|
||||
{
|
||||
return $this->outlineTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Return type should become `string` in 5.0 when the class is actually `final`
|
||||
*
|
||||
* @phpstan-ignore return.unusedType
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return "{$this->replaceTextTokens($this->outlineTitle ?? '')} #{$this->index}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the example row as a single string.
|
||||
*
|
||||
* You should normally not need this, since it is an implementation detail.
|
||||
* If you need the individual example values, use {@see self::getTokens()}.
|
||||
* To get the fully-normalised/expanded title, use {@see self::getName()}.
|
||||
*/
|
||||
public function getExampleText(): string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates steps for this example from abstract outline steps.
|
||||
*
|
||||
* @return list<StepNode>
|
||||
*/
|
||||
protected function createExampleSteps()
|
||||
{
|
||||
$steps = [];
|
||||
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 array<array-key, ArgumentInterface> $arguments
|
||||
*
|
||||
* @return array<array-key, 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.
|
||||
*
|
||||
* @return TableNode
|
||||
*/
|
||||
protected function replaceTableArgumentTokens(TableNode $argument)
|
||||
{
|
||||
$replacedTable = [];
|
||||
foreach ($argument->getTable() as $line => $row) {
|
||||
$replacedRow = [];
|
||||
foreach ($row as $value) {
|
||||
$replacedRow[] = $this->replaceTextTokens($value);
|
||||
}
|
||||
$replacedTable[$line] = $replacedRow;
|
||||
}
|
||||
|
||||
return new TableNode($replacedTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces tokens in PyString with row values.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function replaceTextTokens(string $text)
|
||||
{
|
||||
foreach ($this->tokens as $key => $val) {
|
||||
$text = str_replace('<' . $key . '>', $val, $text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
86
vendor/behat/gherkin/src/Node/ExampleTableNode.php
vendored
Normal file
86
vendor/behat/gherkin/src/Node/ExampleTableNode.php
vendored
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*/
|
||||
class ExampleTableNode extends TableNode implements TaggedNodeInterface, DescribableNodeInterface
|
||||
{
|
||||
use TaggedNodeTrait;
|
||||
|
||||
/**
|
||||
* @param array<int, list<string>> $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
|
||||
* @param list<string> $tags
|
||||
*/
|
||||
public function __construct(
|
||||
array $table,
|
||||
private readonly string $keyword,
|
||||
private readonly array $tags = [],
|
||||
private readonly ?string $name = null,
|
||||
private readonly ?string $description = null,
|
||||
) {
|
||||
parent::__construct($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'ExampleTable';
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns example table keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, list<string>> $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
|
||||
*/
|
||||
public function withTable(array $table): self
|
||||
{
|
||||
return new self(
|
||||
$table,
|
||||
$this->keyword,
|
||||
$this->tags,
|
||||
$this->name,
|
||||
$this->description,
|
||||
);
|
||||
}
|
||||
}
|
||||
217
vendor/behat/gherkin/src/Node/FeatureNode.php
vendored
Normal file
217
vendor/behat/gherkin/src/Node/FeatureNode.php
vendored
Normal file
@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 InvalidArgumentException;
|
||||
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Feature.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*/
|
||||
class FeatureNode implements KeywordNodeInterface, TaggedNodeInterface, DescribableNodeInterface
|
||||
{
|
||||
use TaggedNodeTrait;
|
||||
|
||||
/**
|
||||
* @param list<string> $tags
|
||||
* @param ScenarioInterface[] $scenarios
|
||||
* @param string|null $file the absolute path to the feature file
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?string $title,
|
||||
private readonly ?string $description,
|
||||
private readonly array $tags,
|
||||
private readonly ?BackgroundNode $background,
|
||||
private readonly array $scenarios,
|
||||
private readonly string $keyword,
|
||||
private readonly string $language,
|
||||
private readonly ?string $file,
|
||||
private readonly int $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.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Feature';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature title.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
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 string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if feature has background.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasBackground()
|
||||
{
|
||||
return $this->background !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature background.
|
||||
*
|
||||
* @return BackgroundNode|null
|
||||
*/
|
||||
public function getBackground()
|
||||
{
|
||||
return $this->background;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if feature has scenarios.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasScenarios()
|
||||
{
|
||||
return count($this->scenarios) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 string|null
|
||||
*/
|
||||
public function getFile()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature declaration line number.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this feature, but with a different set of scenarios.
|
||||
*
|
||||
* @param array<array-key, ScenarioInterface> $scenarios
|
||||
*/
|
||||
public function withScenarios(array $scenarios): self
|
||||
{
|
||||
return new self(
|
||||
$this->title,
|
||||
$this->description,
|
||||
$this->tags,
|
||||
$this->background,
|
||||
array_values($scenarios),
|
||||
$this->keyword,
|
||||
$this->language,
|
||||
$this->file,
|
||||
$this->line,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the file path is an absolute path.
|
||||
*
|
||||
* @param string|null $file A file path
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @see https://github.com/symfony/filesystem/blob/master/Filesystem.php
|
||||
*/
|
||||
protected function isAbsolutePath(?string $file)
|
||||
{
|
||||
if ($file === null) {
|
||||
throw new InvalidArgumentException('The provided file path must not be null.');
|
||||
}
|
||||
|
||||
return strspn($file, '/\\', 0, 1)
|
||||
|| (strlen($file) > 3 && ctype_alpha($file[0])
|
||||
&& $file[1] === ':'
|
||||
&& strspn($file, '/\\', 2, 1)
|
||||
)
|
||||
|| parse_url($file, PHP_URL_SCHEME) !== null;
|
||||
}
|
||||
}
|
||||
33
vendor/behat/gherkin/src/Node/KeywordNodeInterface.php
vendored
Normal file
33
vendor/behat/gherkin/src/Node/KeywordNodeInterface.php
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 string|null
|
||||
*/
|
||||
public function getTitle();
|
||||
}
|
||||
19
vendor/behat/gherkin/src/Node/NamedScenarioInterface.php
vendored
Normal file
19
vendor/behat/gherkin/src/Node/NamedScenarioInterface.php
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
|
||||
interface NamedScenarioInterface
|
||||
{
|
||||
/**
|
||||
* Returns the human-readable name of the scenario.
|
||||
*/
|
||||
public function getName(): ?string;
|
||||
}
|
||||
33
vendor/behat/gherkin/src/Node/NodeInterface.php
vendored
Normal file
33
vendor/behat/gherkin/src/Node/NodeInterface.php
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 int
|
||||
*/
|
||||
public function getLine();
|
||||
}
|
||||
219
vendor/behat/gherkin/src/Node/OutlineNode.php
vendored
Normal file
219
vendor/behat/gherkin/src/Node/OutlineNode.php
vendored
Normal file
@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*/
|
||||
class OutlineNode implements ScenarioInterface, DescribableNodeInterface
|
||||
{
|
||||
use TaggedNodeTrait;
|
||||
|
||||
/**
|
||||
* @var array<array-key, ExampleTableNode>
|
||||
*/
|
||||
private array $tables;
|
||||
/**
|
||||
* @var ExampleNode[]
|
||||
*/
|
||||
private array $examples;
|
||||
|
||||
/**
|
||||
* @param list<string> $tags
|
||||
* @param StepNode[] $steps
|
||||
* @param ExampleTableNode|array<array-key, ExampleTableNode> $tables
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?string $title,
|
||||
private readonly array $tags,
|
||||
private readonly array $steps,
|
||||
array|ExampleTableNode $tables,
|
||||
private readonly string $keyword,
|
||||
private readonly int $line,
|
||||
private readonly ?string $description = null,
|
||||
) {
|
||||
$this->tables = is_array($tables) ? $tables : [$tables];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Outline';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline title.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps()
|
||||
{
|
||||
return count($this->steps) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline steps.
|
||||
*
|
||||
* @return StepNode[]
|
||||
*/
|
||||
public function getSteps()
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if outline has examples.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasExamples()
|
||||
{
|
||||
return count($this->tables) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns examples table for the outline.
|
||||
*
|
||||
* WARNING: it returns a merged table with tags, names & descriptions lost.
|
||||
*
|
||||
* @return ExampleTableNode
|
||||
*
|
||||
* @deprecated use getExampleTables instead
|
||||
*/
|
||||
public function getExampleTable()
|
||||
{
|
||||
$table = [];
|
||||
foreach ($this->tables[0]->getTable() as $k => $v) {
|
||||
$table[$k] = $v;
|
||||
}
|
||||
|
||||
/** @var ExampleTableNode $exampleTableNode */
|
||||
$exampleTableNode = new ExampleTableNode($table, $this->tables[0]->getKeyword());
|
||||
$tableCount = count($this->tables);
|
||||
for ($i = 1; $i < $tableCount; ++$i) {
|
||||
$exampleTableNode->mergeRowsFromTable($this->tables[$i]);
|
||||
}
|
||||
|
||||
return $exampleTableNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of examples for the outline.
|
||||
*
|
||||
* @return ExampleNode[]
|
||||
*/
|
||||
public function getExamples()
|
||||
{
|
||||
return $this->examples ??= $this->createExamples();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns examples tables array for the outline.
|
||||
*
|
||||
* @return array<array-key, ExampleTableNode>
|
||||
*/
|
||||
public function getExampleTables()
|
||||
{
|
||||
return $this->tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline keyword.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyword()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns outline declaration line number.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this outline, but with a different table.
|
||||
*
|
||||
* @param array<array-key, ExampleTableNode> $exampleTables
|
||||
*/
|
||||
public function withTables(array $exampleTables): self
|
||||
{
|
||||
return new self(
|
||||
$this->title,
|
||||
$this->tags,
|
||||
$this->steps,
|
||||
$exampleTables,
|
||||
$this->keyword,
|
||||
$this->line,
|
||||
$this->description,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates examples for this outline using examples table.
|
||||
*
|
||||
* @return ExampleNode[]
|
||||
*/
|
||||
protected function createExamples()
|
||||
{
|
||||
$examples = [];
|
||||
|
||||
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(),
|
||||
$rowNum + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $examples;
|
||||
}
|
||||
}
|
||||
83
vendor/behat/gherkin/src/Node/PyStringNode.php
vendored
Normal file
83
vendor/behat/gherkin/src/Node/PyStringNode.php
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 Stringable;
|
||||
|
||||
/**
|
||||
* Represents Gherkin PyString argument.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*/
|
||||
class PyStringNode implements Stringable, ArgumentInterface
|
||||
{
|
||||
/**
|
||||
* @param list<string> $strings String in form of [$stringLine]
|
||||
* @param int $line Line number where string been started
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $strings,
|
||||
private readonly int $line,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'PyString';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entire PyString lines set.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
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 int
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
}
|
||||
20
vendor/behat/gherkin/src/Node/ScenarioInterface.php
vendored
Normal file
20
vendor/behat/gherkin/src/Node/ScenarioInterface.php
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
}
|
||||
20
vendor/behat/gherkin/src/Node/ScenarioLikeInterface.php
vendored
Normal file
20
vendor/behat/gherkin/src/Node/ScenarioLikeInterface.php
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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
|
||||
{
|
||||
}
|
||||
115
vendor/behat/gherkin/src/Node/ScenarioNode.php
vendored
Normal file
115
vendor/behat/gherkin/src/Node/ScenarioNode.php
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*/
|
||||
class ScenarioNode implements ScenarioInterface, NamedScenarioInterface, DescribableNodeInterface
|
||||
{
|
||||
use TaggedNodeTrait;
|
||||
|
||||
/**
|
||||
* @param StepNode[] $steps
|
||||
* @param list<string> $tags
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?string $title,
|
||||
private readonly array $tags,
|
||||
private readonly array $steps,
|
||||
private readonly string $keyword,
|
||||
private readonly int $line,
|
||||
private readonly ?string $description = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Scenario';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns scenario title.
|
||||
*
|
||||
* @return string|null
|
||||
*
|
||||
* @deprecated you should use {@see self::getName()} instead as this method will be removed in the next
|
||||
* major version
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if scenario has steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSteps()
|
||||
{
|
||||
return count($this->steps) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 int
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
}
|
||||
33
vendor/behat/gherkin/src/Node/StepContainerInterface.php
vendored
Normal file
33
vendor/behat/gherkin/src/Node/StepContainerInterface.php
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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 list<StepNode>
|
||||
*/
|
||||
public function getSteps();
|
||||
}
|
||||
129
vendor/behat/gherkin/src/Node/StepNode.php
vendored
Normal file
129
vendor/behat/gherkin/src/Node/StepNode.php
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*/
|
||||
class StepNode implements NodeInterface
|
||||
{
|
||||
private readonly string $keywordType;
|
||||
|
||||
/**
|
||||
* @param ArgumentInterface[] $arguments
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $keyword,
|
||||
private readonly string $text,
|
||||
private readonly array $arguments,
|
||||
private readonly int $line,
|
||||
?string $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->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 (bool) count($this->arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step arguments.
|
||||
*
|
||||
* @return ArgumentInterface[]
|
||||
*/
|
||||
public function getArguments()
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns step declaration line number.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLine()
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
}
|
||||
378
vendor/behat/gherkin/src/Node/TableNode.php
vendored
Normal file
378
vendor/behat/gherkin/src/Node/TableNode.php
vendored
Normal file
@ -0,0 +1,378 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Represents Gherkin Table argument.
|
||||
*
|
||||
* @author Konstantin Kudryashov <ever.zet@gmail.com>
|
||||
*
|
||||
* @template-implements IteratorAggregate<int, array<string, string>>
|
||||
*/
|
||||
class TableNode implements Stringable, ArgumentInterface, IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var array<array-key, int>
|
||||
*/
|
||||
private array $maxLineLength = [];
|
||||
|
||||
/**
|
||||
* Initializes table.
|
||||
*
|
||||
* @param array<int, list<string>> $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
|
||||
*
|
||||
* @throws NodeException If the given table is invalid
|
||||
*/
|
||||
public function __construct(
|
||||
private array $table,
|
||||
) {
|
||||
$columnCount = null;
|
||||
|
||||
foreach ($this->getRows() as $rowIndex => $row) {
|
||||
if (!is_array($row)) {
|
||||
throw new NodeException(sprintf(
|
||||
"Table row '%s' is expected to be array, got %s",
|
||||
$rowIndex,
|
||||
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",
|
||||
$rowIndex,
|
||||
$columnCount,
|
||||
count($row)
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($row as $columnIndex => $cellValue) {
|
||||
if (!isset($this->maxLineLength[$columnIndex])) {
|
||||
$this->maxLineLength[$columnIndex] = 0;
|
||||
}
|
||||
|
||||
if (!is_scalar($cellValue)) {
|
||||
throw new NodeException(sprintf(
|
||||
"Table cell at row '%s', column '%s' is expected to be scalar, got %s",
|
||||
$rowIndex,
|
||||
$columnIndex,
|
||||
get_debug_type($cellValue)
|
||||
));
|
||||
}
|
||||
|
||||
$this->maxLineLength[$columnIndex] = max($this->maxLineLength[$columnIndex], mb_strlen($cellValue, 'utf8'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table from a given list.
|
||||
*
|
||||
* @param array<int, string> $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.');
|
||||
}
|
||||
|
||||
$table = array_map(fn ($item) => [$item], $list);
|
||||
|
||||
return new self($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns node type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNodeType()
|
||||
{
|
||||
return 'Table';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table hash, formed by columns (ColumnsHash).
|
||||
*
|
||||
* @return list<array<string, string>>
|
||||
*/
|
||||
public function getHash()
|
||||
{
|
||||
return $this->getColumnsHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table hash, formed by columns.
|
||||
*
|
||||
* @return list<array<string, string>>
|
||||
*/
|
||||
public function getColumnsHash()
|
||||
{
|
||||
$rows = $this->getRows();
|
||||
$keys = array_shift($rows);
|
||||
|
||||
$hash = [];
|
||||
foreach ($rows as $row) {
|
||||
assert($keys !== null); // If there is no first row due to an empty table, we won't enter this loop either.
|
||||
$hash[] = array_combine($keys, $row);
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table hash, formed by rows.
|
||||
*
|
||||
* @return array<string, string|list<string>>
|
||||
*/
|
||||
public function getRowsHash()
|
||||
{
|
||||
$hash = [];
|
||||
|
||||
foreach ($this->getRows() as $row) {
|
||||
$hash[array_shift($row)] = count($row) === 1 ? $row[0] : $row;
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns numerated table lines.
|
||||
* Line numbers are keys, lines are values.
|
||||
*
|
||||
* @return array<int, list<string>>
|
||||
*/
|
||||
public function getTable()
|
||||
{
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table rows.
|
||||
*
|
||||
* @return list<list<string>>
|
||||
*/
|
||||
public function getRows()
|
||||
{
|
||||
return array_values($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns table definition lines.
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
public function getLines()
|
||||
{
|
||||
return array_keys($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns specific row in a table.
|
||||
*
|
||||
* @param int $index Row number
|
||||
*
|
||||
* @return list<string>
|
||||
*
|
||||
* @throws NodeException If row with specified index does not exist
|
||||
*/
|
||||
public function getRow(int $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 int $index Column number
|
||||
*
|
||||
* @return list<string>
|
||||
*
|
||||
* @throws NodeException If column with specified index does not exist
|
||||
*/
|
||||
public function getColumn(int $index)
|
||||
{
|
||||
if ($index >= count($this->getRow(0))) {
|
||||
throw new NodeException(sprintf('Column #%d does not exist in table.', $index));
|
||||
}
|
||||
|
||||
$rows = $this->getRows();
|
||||
$column = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$column[] = $row[$index];
|
||||
}
|
||||
|
||||
return $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line number at which specific row was defined.
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @throws NodeException If row with specified index does not exist
|
||||
*/
|
||||
public function getRowLine(int $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 int $rowNum Row number
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRowAsString(int $rowNum)
|
||||
{
|
||||
$values = [];
|
||||
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 int $rowNum Row number
|
||||
* @param callable(string, int): string $wrapper Wrapper function
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRowAsStringWithWrappedValues(int $rowNum, callable $wrapper)
|
||||
{
|
||||
$values = [];
|
||||
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 = [];
|
||||
$rowCount = count($this->getRows());
|
||||
for ($i = 0; $i < $rowCount; ++$i) {
|
||||
$lines[] = $this->getRowAsString($i);
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line number at which table was started.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function padRight(string $text, int $length)
|
||||
{
|
||||
while ($length > mb_strlen($text, 'utf8')) {
|
||||
$text .= ' ';
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
40
vendor/behat/gherkin/src/Node/TaggedNodeInterface.php
vendored
Normal file
40
vendor/behat/gherkin/src/Node/TaggedNodeInterface.php
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTag(string $tag);
|
||||
|
||||
/**
|
||||
* Checks if node has tags (including any inherited tags e.g. from feature).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTags();
|
||||
|
||||
/**
|
||||
* Returns node tags (including any inherited tags e.g. from feature).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getTags();
|
||||
}
|
||||
40
vendor/behat/gherkin/src/Node/TaggedNodeTrait.php
vendored
Normal file
40
vendor/behat/gherkin/src/Node/TaggedNodeTrait.php
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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;
|
||||
|
||||
/**
|
||||
* This trait partially implements {@see TaggedNodeInterface}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait TaggedNodeTrait
|
||||
{
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
abstract public function getTags();
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTag(string $tag)
|
||||
{
|
||||
return in_array($tag, $this->getTags(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTags()
|
||||
{
|
||||
return $this->getTags() !== [];
|
||||
}
|
||||
}
|
||||
760
vendor/behat/gherkin/src/Parser.php
vendored
Normal file
760
vendor/behat/gherkin/src/Parser.php
vendored
Normal file
@ -0,0 +1,760 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\FilesystemException;
|
||||
use Behat\Gherkin\Exception\InvalidTagContentException;
|
||||
use Behat\Gherkin\Exception\LexerException;
|
||||
use Behat\Gherkin\Exception\NodeException;
|
||||
use Behat\Gherkin\Exception\ParserException;
|
||||
use Behat\Gherkin\Exception\UnexpectedParserNodeException;
|
||||
use Behat\Gherkin\Exception\UnexpectedTaggedNodeException;
|
||||
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;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
* @final since 4.15.0
|
||||
*
|
||||
* @phpstan-import-type TTokenType from Lexer
|
||||
* @phpstan-import-type TToken from Lexer
|
||||
* @phpstan-import-type TNullValueToken from Lexer
|
||||
* @phpstan-import-type TStringValueToken from Lexer
|
||||
* @phpstan-import-type TTagToken from Lexer
|
||||
* @phpstan-import-type TStepToken from Lexer
|
||||
* @phpstan-import-type TTitleToken from Lexer
|
||||
* @phpstan-import-type TTableRowToken from Lexer
|
||||
* @phpstan-import-type TTitleKeyword from Lexer
|
||||
*
|
||||
* @phpstan-type TParsedExpressionResult FeatureNode|BackgroundNode|ScenarioNode|OutlineNode|ExampleTableNode|TableNode|PyStringNode|StepNode|string
|
||||
*/
|
||||
class Parser implements ParserInterface
|
||||
{
|
||||
private string $input;
|
||||
private ?string $file = null;
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private array $tags = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly Lexer $lexer,
|
||||
private GherkinCompatibilityMode $compatibilityMode = GherkinCompatibilityMode::LEGACY,
|
||||
) {
|
||||
}
|
||||
|
||||
public function setGherkinCompatibilityMode(GherkinCompatibilityMode $mode): void
|
||||
{
|
||||
$this->compatibilityMode = $mode;
|
||||
}
|
||||
|
||||
public function parse(string $input, ?string $file = null)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->file = $file;
|
||||
$this->tags = [];
|
||||
$this->lexer->setCompatibilityMode($this->compatibilityMode);
|
||||
|
||||
try {
|
||||
$this->lexer->analyse($this->input);
|
||||
} catch (LexerException $e) {
|
||||
throw new ParserException(
|
||||
sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
$feature = null;
|
||||
while ($this->predictTokenType() !== 'EOS') {
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node === "\n" || $node === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$feature && $node instanceof FeatureNode) {
|
||||
$feature = $node;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new UnexpectedParserNodeException('Feature', $node, $this->file);
|
||||
}
|
||||
|
||||
return $feature;
|
||||
}
|
||||
|
||||
public function parseFile(string $file): ?FeatureNode
|
||||
{
|
||||
try {
|
||||
return $this->parse(Filesystem::readFile($file), $file);
|
||||
} catch (FilesystemException $ex) {
|
||||
throw new ParserException("Cannot parse file: {$ex->getMessage()}", previous: $ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token if it's type equals to expected.
|
||||
*
|
||||
* @phpstan-param TTokenType $type
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @phpstan-return (
|
||||
* $type is 'TableRow'
|
||||
* ? TTableRowToken
|
||||
* : ($type is 'Tag'
|
||||
* ? TTagToken
|
||||
* : ($type is 'Step'
|
||||
* ? TStepToken
|
||||
* : ($type is 'Text'
|
||||
* ? TStringValueToken
|
||||
* : ($type is TTitleKeyword
|
||||
* ? TTitleToken
|
||||
* : TNullValueToken|TStringValueToken
|
||||
* )))))
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function expectTokenType(string $type)
|
||||
{
|
||||
if ($this->predictTokenType() === $type) {
|
||||
return $this->lexer->getAdvancedToken();
|
||||
}
|
||||
|
||||
$token = $this->lexer->predictToken();
|
||||
|
||||
throw new ParserException(sprintf(
|
||||
'Expected %s token, but got %s on line: %d%s',
|
||||
$type,
|
||||
$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 array|null
|
||||
*
|
||||
* @phpstan-return TToken|null
|
||||
*/
|
||||
protected function acceptTokenType(string $type)
|
||||
{
|
||||
if ($type !== $this->predictTokenType()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->lexer->getAdvancedToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token type without real input reading (prediction).
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @phpstan-return TTokenType
|
||||
*/
|
||||
protected function predictTokenType()
|
||||
{
|
||||
$token = $this->lexer->predictToken();
|
||||
|
||||
return $token['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses current expression & returns Node.
|
||||
*
|
||||
* @phpstan-return TParsedExpressionResult
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseExpression()
|
||||
{
|
||||
$type = $this->predictTokenType();
|
||||
|
||||
while ($type === 'Comment') {
|
||||
$this->expectTokenType('Comment');
|
||||
|
||||
$type = $this->predictTokenType();
|
||||
}
|
||||
|
||||
return match ($type) {
|
||||
'Feature' => $this->parseFeature(),
|
||||
'Background' => $this->parseBackground(),
|
||||
'Scenario' => $this->parseScenario(),
|
||||
'Outline' => $this->parseOutline(),
|
||||
'Examples' => $this->parseExamples(),
|
||||
'TableRow' => $this->parseTable(),
|
||||
'PyStringOp' => $this->parsePyString(),
|
||||
'Step' => $this->parseStep(),
|
||||
'Text' => $this->parseText(),
|
||||
'Newline' => $this->parseNewline(),
|
||||
'Tag' => $this->parseTags(),
|
||||
'Language' => $this->parseLanguage(),
|
||||
'EOS' => '',
|
||||
default => 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' => $title, 'description' => $description] = $this->parseTitleAndDescription($token);
|
||||
$tags = $this->popTags();
|
||||
$background = null;
|
||||
$scenarios = [];
|
||||
$keyword = $token['keyword'];
|
||||
$language = $this->lexer->getLanguage();
|
||||
$file = $this->file;
|
||||
$line = $token['line'];
|
||||
|
||||
// Parse description, background, scenarios & outlines
|
||||
while ($this->predictTokenType() !== 'EOS') {
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node === "\n") {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isBackgroundAllowed = ($background === null && $scenarios === []);
|
||||
|
||||
if ($isBackgroundAllowed && $node instanceof BackgroundNode) {
|
||||
$background = $node;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node instanceof ScenarioInterface) {
|
||||
$scenarios[] = $node;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new UnexpectedParserNodeException(
|
||||
match ($isBackgroundAllowed) {
|
||||
true => 'Background, Scenario or Outline',
|
||||
false => 'Scenario or Outline',
|
||||
},
|
||||
$node,
|
||||
$this->file,
|
||||
);
|
||||
}
|
||||
|
||||
return new FeatureNode(
|
||||
$title,
|
||||
$description,
|
||||
$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');
|
||||
|
||||
$keyword = $token['keyword'];
|
||||
$line = $token['line'];
|
||||
['title' => $title, 'description' => $description] = $this->parseTitleAndDescription($token);
|
||||
|
||||
if (count($this->popTags()) !== 0) {
|
||||
// Should not be possible to happen, parseTags should have already picked this up.
|
||||
throw new UnexpectedTaggedNodeException($token, $this->file);
|
||||
}
|
||||
|
||||
// Parse description and steps
|
||||
$steps = [];
|
||||
$allowedTokenTypes = ['Step', 'Newline', 'Text', 'Comment'];
|
||||
while (in_array($this->predictTokenType(), $allowedTokenTypes)) {
|
||||
// NB: Technically, we do not support `Text` inside this loop. However, there is no situation where `Text`
|
||||
// can be a direct child or immediately following a Scenario. Therefore, we consume it here as the most
|
||||
// logical context for throwing an UnexpectedParserNodeException.
|
||||
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node instanceof StepNode) {
|
||||
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node === "\n") {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new UnexpectedParserNodeException('Step', $node, $this->file);
|
||||
}
|
||||
|
||||
return new BackgroundNode($title, $steps, $keyword, $line, $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses scenario token & returns it's node.
|
||||
*
|
||||
* @return OutlineNode|ScenarioNode
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseScenario()
|
||||
{
|
||||
return $this->parseScenarioOrOutlineBody($this->expectTokenType('Scenario'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses scenario outline token & returns it's node.
|
||||
*
|
||||
* @return OutlineNode|ScenarioNode
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
protected function parseOutline()
|
||||
{
|
||||
return $this->parseScenarioOrOutlineBody($this->expectTokenType('Outline'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param TTitleToken $token
|
||||
*/
|
||||
private function parseScenarioOrOutlineBody(array $token): OutlineNode|ScenarioNode
|
||||
{
|
||||
['title' => $title, 'description' => $description] = $this->parseTitleAndDescription($token);
|
||||
$tags = $this->popTags();
|
||||
$keyword = $token['keyword'];
|
||||
|
||||
/** @var list<ExampleTableNode> $examples */
|
||||
$examples = [];
|
||||
$line = $token['line'];
|
||||
$steps = [];
|
||||
|
||||
while (in_array($nextTokenType = $this->predictTokenType(), ['Step', 'Examples', 'Newline', 'Text', 'Comment', 'Tag'])) {
|
||||
// NB: Technically, we do not support `Text` inside this loop. However, there is no situation where `Text`
|
||||
// can be a direct child or immediately following a Scenario. Therefore, we consume it here as the most
|
||||
// logical context for throwing an UnexpectedParserNodeException.
|
||||
|
||||
if ($nextTokenType === 'Comment') {
|
||||
$this->lexer->skipPredictedToken();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($nextTokenType === 'Tag') {
|
||||
// The only thing inside a Scenario / Scenario Outline that can be tagged is an Examples table
|
||||
// Scan on to see what the tags are attached to - if it's not Examples then we must have reached the
|
||||
// end of this scenario and be about to start a new one.
|
||||
if ($this->validateAndGetNextTaggedNodeType() !== 'Examples') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node === "\n") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($examples === [] && $node instanceof StepNode) {
|
||||
// Steps are only allowed before the first Examples table (if any)
|
||||
$steps[] = $this->normalizeStepNodeKeywordType($node, $steps);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node instanceof ExampleTableNode) {
|
||||
// NB: It is valid to have a Scenario with Examples: but no Steps
|
||||
// It is also valid to have an Examples: with no table rows (this produces no actual examples)
|
||||
$examples[] = $node;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new UnexpectedParserNodeException(
|
||||
match ($examples) {
|
||||
[] => 'Step, Examples table, or end of Scenario',
|
||||
default => 'Examples table or end of Scenario',
|
||||
},
|
||||
$node,
|
||||
$this->file,
|
||||
);
|
||||
}
|
||||
|
||||
if ($examples !== []) {
|
||||
return new OutlineNode($title, $tags, $steps, $examples, $keyword, $line, $description);
|
||||
}
|
||||
|
||||
return new ScenarioNode($title, $tags, $steps, $keyword, $line, $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek ahead to find the node that the current tags belong to.
|
||||
*
|
||||
* @throws UnexpectedTaggedNodeException if there is not a taggable node
|
||||
*/
|
||||
private function validateAndGetNextTaggedNodeType(): string
|
||||
{
|
||||
$deferred = [];
|
||||
try {
|
||||
while (true) {
|
||||
$deferred[] = $next = $this->lexer->getAdvancedToken();
|
||||
$nextType = $next['type'];
|
||||
|
||||
if (in_array($nextType, ['Tag', 'Comment', 'Newline'], true)) {
|
||||
// These are the only node types allowed between tag node(s) and the node they are tagging
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($nextType, ['Feature', 'Examples', 'Scenario', 'Outline'], true)) {
|
||||
// These are the only taggable node types
|
||||
return $nextType;
|
||||
}
|
||||
|
||||
throw new UnexpectedTaggedNodeException($next, $this->file);
|
||||
}
|
||||
} finally {
|
||||
// Rewind the lexer back to where it was when we started scanning ahead
|
||||
foreach ($deferred as $token) {
|
||||
$this->lexer->deferToken($token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses step token & returns it's node.
|
||||
*
|
||||
* @return StepNode
|
||||
*/
|
||||
protected function parseStep()
|
||||
{
|
||||
$token = $this->expectTokenType('Step');
|
||||
|
||||
$arguments = [];
|
||||
while (in_array($predicted = $this->predictTokenType(), ['PyStringOp', 'TableRow', 'Newline', 'Comment'])) {
|
||||
if ($predicted === 'Comment' || $predicted === 'Newline') {
|
||||
$this->acceptTokenType($predicted);
|
||||
continue;
|
||||
}
|
||||
|
||||
$node = $this->parseExpression();
|
||||
|
||||
if ($node instanceof PyStringNode || $node instanceof TableNode) {
|
||||
$arguments[] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
return new StepNode($token['value'], trim($token['text']), $arguments, $token['line'], $token['keyword_type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses examples table node.
|
||||
*
|
||||
* @return ExampleTableNode
|
||||
*/
|
||||
protected function parseExamples()
|
||||
{
|
||||
$token = $this->expectTokenType('Examples');
|
||||
$keyword = $token['keyword'];
|
||||
$tags = empty($this->tags) ? [] : $this->popTags();
|
||||
['title' => $title, 'description' => $description] = $this->parseTitleAndDescription($token);
|
||||
$table = $this->parseTableRows();
|
||||
|
||||
try {
|
||||
return new ExampleTableNode($table, $keyword, $tags, $title, $description);
|
||||
} 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 = [];
|
||||
while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && $predicted === 'Text') {
|
||||
$strings[] = $this->expectTokenType('Text')['value'];
|
||||
}
|
||||
|
||||
$this->expectTokenType('PyStringOp');
|
||||
|
||||
return new PyStringNode($strings, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses tags.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function parseTags()
|
||||
{
|
||||
$token = $this->expectTokenType('Tag');
|
||||
|
||||
// Validate that the tags are followed by a node that can be tagged
|
||||
$this->validateAndGetNextTaggedNodeType();
|
||||
|
||||
$this->guardTags($token['tags']);
|
||||
|
||||
$this->tags = array_merge($this->tags, $token['tags']);
|
||||
|
||||
return "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current set of tags and clears tag buffer.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function popTags()
|
||||
{
|
||||
$tags = $this->tags;
|
||||
$this->tags = [];
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the tags fit the required format.
|
||||
*
|
||||
* @param array<array-key, string> $tags
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function guardTags(array $tags)
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (preg_match('/\s/', $tag)) {
|
||||
if ($this->compatibilityMode->shouldThrowOnWhitespaceInTag()) {
|
||||
throw new InvalidTagContentException($tag, $this->file);
|
||||
}
|
||||
|
||||
trigger_error(
|
||||
sprintf('Whitespace in tags is deprecated, found "%s" in %s', $tag, $this->file ?? 'unknown file'),
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next text line & returns it.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function parseText()
|
||||
{
|
||||
$token = $this->expectTokenType('Text');
|
||||
\assert(\is_string($token['value']));
|
||||
|
||||
return $token['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next newline & returns \n.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function parseNewline()
|
||||
{
|
||||
$this->expectTokenType('Newline');
|
||||
|
||||
return "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips over language tags (they are handled inside the Lexer).
|
||||
*
|
||||
* @phpstan-return TParsedExpressionResult
|
||||
*
|
||||
* @throws ParserException
|
||||
*
|
||||
* @deprecated language tags are handled inside the Lexer, they skipped over (like any other comment) in the Parser
|
||||
*/
|
||||
protected function parseLanguage()
|
||||
{
|
||||
$this->expectTokenType('Language');
|
||||
|
||||
return $this->parseExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the rows of a table.
|
||||
*
|
||||
* @return array<int, list<string>>
|
||||
*/
|
||||
private function parseTableRows(): array
|
||||
{
|
||||
$table = [];
|
||||
while (in_array($predicted = $this->predictTokenType(), ['TableRow', 'Newline', 'Comment'])) {
|
||||
if ($predicted === 'Comment' || $predicted === 'Newline') {
|
||||
$this->acceptTokenType($predicted);
|
||||
continue;
|
||||
}
|
||||
|
||||
$token = $this->expectTokenType('TableRow');
|
||||
|
||||
$table[$token['line']] = $token['columns'];
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TTitleToken $keywordToken
|
||||
*
|
||||
* @return array{title:string|null, description:string|null}
|
||||
*/
|
||||
private function parseTitleAndDescription(array $keywordToken): array
|
||||
{
|
||||
$originalTitle = trim($keywordToken['value'] ?? '');
|
||||
$textLines = [];
|
||||
|
||||
while (in_array($predicted = $this->predictTokenType(), ['Newline', 'Text', 'Comment'])) {
|
||||
$token = $this->expectTokenType($predicted);
|
||||
|
||||
if ($token['type'] === 'Comment') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$text = match ($token['type']) {
|
||||
'Newline' => '',
|
||||
'Text' => $token['value'],
|
||||
default => throw new LogicException('Unexpected token type: ' . $token['type']),
|
||||
};
|
||||
|
||||
// The only time we use $token['value'] is if we got a `Text` token.
|
||||
// ->expectTokenType('Text') is tagged as returning a `TStringValueToken`, where 'value' cannot be null
|
||||
// However PHPStan cannot follow the chain through predictTokenType -> expectTokenType -> $token['type']
|
||||
assert($text !== null, 'Text token value should not be null');
|
||||
|
||||
if ($this->compatibilityMode->shouldRemoveDescriptionPadding()) {
|
||||
$text = preg_replace('/^\s{0,' . ($keywordToken['indent'] + 2) . '}|\s*$/', '', $text);
|
||||
}
|
||||
|
||||
$textLines[] = $text;
|
||||
}
|
||||
|
||||
if ($this->compatibilityMode->allowAllNodeDescriptions()) {
|
||||
// Gherkin-compatible format - title is the original keyword value, description is all following lines.
|
||||
// Blank lines between title and description are removed, as are any after the description.
|
||||
return [
|
||||
'title' => $originalTitle ?: null,
|
||||
'description' => trim(implode("\n", $textLines), "\n") ?: null,
|
||||
];
|
||||
}
|
||||
|
||||
if ($keywordToken['type'] === 'Feature') {
|
||||
// Legacy format always supported a title & description for a Feature
|
||||
// But kept blank lines between title and description as the start of the description.
|
||||
return [
|
||||
'title' => $originalTitle ?: null,
|
||||
'description' => rtrim(implode("\n", $textLines), "\n") ?: null,
|
||||
];
|
||||
}
|
||||
|
||||
// Legacy format for nodes without description support - the full text block (title & description) is parsed
|
||||
// as the title.
|
||||
array_unshift($textLines, $originalTitle);
|
||||
|
||||
return [
|
||||
'title' => rtrim(implode("\n", $textLines)) ?: null,
|
||||
'description' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes step node type for types But, And to type of previous step if it exists else sets to Given.
|
||||
*
|
||||
* @param StepNode[] $steps
|
||||
*/
|
||||
private function normalizeStepNodeKeywordType(StepNode $node, array $steps = []): StepNode
|
||||
{
|
||||
if (!in_array($node->getKeywordType(), ['And', 'But'])) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if ($prev = end($steps)) {
|
||||
$keywordType = $prev->getKeywordType();
|
||||
} else {
|
||||
$keywordType = 'Given';
|
||||
}
|
||||
|
||||
return new StepNode(
|
||||
$node->getKeyword(),
|
||||
$node->getText(),
|
||||
$node->getArguments(),
|
||||
$node->getLine(),
|
||||
$keywordType
|
||||
);
|
||||
}
|
||||
|
||||
private function rethrowNodeException(NodeException $e): never
|
||||
{
|
||||
throw new ParserException(
|
||||
$e->getMessage() . ($this->file ? ' in file ' . $this->file : ''),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
36
vendor/behat/gherkin/src/ParserInterface.php
vendored
Normal file
36
vendor/behat/gherkin/src/ParserInterface.php
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Behat Gherkin Parser.
|
||||
* (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\ParserException;
|
||||
use Behat\Gherkin\Node\FeatureNode;
|
||||
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* Parses a Gherkin document string and returns feature (or null when none found).
|
||||
*
|
||||
* @param string $input Gherkin string document
|
||||
* @param string|null $file File name
|
||||
*
|
||||
* @return FeatureNode|null
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
public function parse(string $input, ?string $file = null);
|
||||
|
||||
/**
|
||||
* Parses a Gherkin file and returns feature (or null when none found).
|
||||
*
|
||||
* @throws ParserException
|
||||
*/
|
||||
public function parseFile(string $file): ?FeatureNode;
|
||||
}
|
||||
Reference in New Issue
Block a user