Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.2']
php: ['8.3']
composer-mode: ['low-deps', 'high-deps']

steps:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
- (i18n) Remove duplicate scenario keyword from "sr-Cyrl" ([#264](https://github.com/cucumber/gherkin/pull/264))
- Intermittent failure of cpp test jobs in CI ([#217](https://github.com/cucumber/gherkin/issues/217))

### Changed
- [PHP] Require PHP 8.2+ in CI and in composer.json.

## [28.0.0] - 2024-02-15
### Added
- [Python] Added release workflow for releasing to Pypi ([#213](https://github.com/cucumber/gherkin/pull/213))
Expand Down
9 changes: 7 additions & 2 deletions php/bin/gherkin
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ assert(is_array($argv), "Script must be run from the command line");
$options = ['predictable-ids', 'no-source', 'no-ast', 'no-pickles'];

$selectedOptions = getopt('', $options, $restIndex);
assert(is_array($selectedOptions), "Could not get options");

$paths = array_slice($argv, $restIndex);

Expand All @@ -20,7 +21,9 @@ $sources = (
/** @param list<string> $paths */
function (array $paths) {
foreach ($paths as $path) {
yield new Source(uri: $path, data: file_get_contents($path));
$contents = file_get_contents($path);
assert(is_string($contents), "Could not read " . $path);
yield new Source(uri: $path, data: $contents);
}
}
)($paths);
Expand All @@ -33,6 +36,8 @@ $parser = new GherkinParser(
);
$envelopes = $parser->parse($sources);

$output = fopen('php://stdout', 'w');
$file = 'php://stdout';
$output = fopen($file, 'w');
assert(is_resource($output), "Could not open " . $file);
$writer = NdJsonStreamWriter::fromFileHandle($output);
$writer->writeEnvelopes($envelopes);
4 changes: 3 additions & 1 deletion php/bin/gherkin-generate-tokens
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ $parser = new Parser(new TokenFormatterBuilder());
array_shift($argv);

foreach($argv as $fileName) {
$contents = file_get_contents($fileName);
assert(is_string($contents), "Could not read " . $fileName);
$result = $parser->parse(
$fileName,
new StringTokenScanner(file_get_contents($fileName)),
new StringTokenScanner($contents),
new TokenMatcher(),
);
echo $result;
Expand Down
59 changes: 24 additions & 35 deletions php/composer.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
{
"name": "cucumber/gherkin",
"description": "Gherkin parser",
"author": "Cucumber Limited <[email protected]>",
"license": "MIT",
"type": "library",
"autoload": {
"psr-4": {
"Cucumber\\Gherkin\\": [
"src/",
"src-generated/"
]
}
},
"require": {
"php": "^8.1",
"ext-mbstring": "*",
"cucumber/messages": ">=19.1.4 <=29"
},
"require-dev": {
"phpunit/phpunit": "^10.5||^11.0",
"vimeo/psalm": "5.26.1",
"friendsofphp/php-cs-fixer": "^3.51",
"psalm/plugin-phpunit": "^0.19.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/cucumber/messages-php",
"options": {
"versions": {
"cucumber/messages": "19.1.4"
}
}
}
]
"name": "cucumber/gherkin",
"description": "Gherkin parser",
"author": "Cucumber Limited <[email protected]>",
"license": "MIT",
"type": "library",
"autoload": {
"psr-4": {
"Cucumber\\Gherkin\\": [
"src/",
"src-generated/"
]
}
},
"require": {
"php": "^8.3",
"ext-mbstring": "*",
"cucumber/messages": ">=19.1.4 <=29"
},
"require-dev": {
"phpunit/phpunit": "11.5.25",
"vimeo/psalm": "^6.5.0",
"friendsofphp/php-cs-fixer": "3.51",
"psalm/plugin-phpunit": "^0.19.3"
}
}
6 changes: 0 additions & 6 deletions php/psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@
<file name="src/AstNode.php"/>
</errorLevel>
</InvalidReturnType>
<InvalidReturnStatement>
<errorLevel type="suppress">
<!-- See https://github.com/vimeo/psalm/issues/8572 -->
<file name="src/AstNode.php"/>
</errorLevel>
</InvalidReturnStatement>
</issueHandlers>
<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
Expand Down
4 changes: 3 additions & 1 deletion php/src/GherkinDialectProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ public function __construct(
private readonly string $defaultDialectName = 'en',
) {
try {
$contents = file_get_contents(self::JSON_PATH);
assert(is_string($contents), "Could not read " . self::JSON_PATH);
/**
* 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, Dialect> $data
*/
$data = json_decode(file_get_contents(self::JSON_PATH), true, flags: JSON_THROW_ON_ERROR);
$data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);
$this->DIALECTS = $data;
} catch (JsonException $e) {
throw new RuntimeException("Unable to parse " . self::JSON_PATH, previous: $e);
Expand Down
7 changes: 6 additions & 1 deletion php/src/GherkinDocumentBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function __construct(
$this->reset($uri);
}

#[\Override]
public function build(Token $token): void
{
if (null === $token->match) {
Expand All @@ -60,11 +61,13 @@ public function build(Token $token): void
}
}

#[\Override]
public function startRule(RuleType $ruleType): void
{
array_push($this->stack, new AstNode($ruleType));
}

#[\Override]
public function endRule(RuleType $ruleType): void
{
$node = array_pop($this->stack);
Expand All @@ -74,6 +77,7 @@ public function endRule(RuleType $ruleType): void
}
}

#[\Override]
public function getResult(): GherkinDocument
{
$document = $this->currentNode()->getSingle(GherkinDocument::class, Ruletype::GherkinDocument);
Expand All @@ -85,6 +89,7 @@ public function getResult(): GherkinDocument
return $document;
}

#[\Override]
public function reset(string $uri): void
{
$this->stack = [new AstNode(RuleType::None)];
Expand Down Expand Up @@ -310,7 +315,7 @@ private function transformDescriptionNode(AstNode $node): string
{
$lineTokens = $node->getTokenMatches(TokenType::Other);

$lineText = preg_replace(
$lineText = (string) preg_replace(
'/(\\n\\s*)*$/u',
'',
$this->joinMatchedTextWithLinebreaks($lineTokens),
Expand Down
12 changes: 6 additions & 6 deletions php/src/GherkinLanguageConstants.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

interface GherkinLanguageConstants
{
public const TAG_PREFIX = "@";
public const COMMENT_PREFIX = "#";
public const TITLE_KEYWORD_SEPARATOR = ":";
public const TABLE_CELL_SEPARATOR = "|";
public const DOCSTRING_SEPARATOR = "\"\"\"";
public const DOCSTRING_ALTERNATIVE_SEPARATOR = "```";
public const string TAG_PREFIX = "@";
public const string COMMENT_PREFIX = "#";
public const string TITLE_KEYWORD_SEPARATOR = ":";
public const string TABLE_CELL_SEPARATOR = "|";
public const string DOCSTRING_SEPARATOR = "\"\"\"";
public const string DOCSTRING_ALTERNATIVE_SEPARATOR = "```";
}
12 changes: 10 additions & 2 deletions php/src/StringGherkinLine.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ public function __construct(
$this->indent = StringUtils::symbolCount($lineText) - StringUtils::symbolCount(StringUtils::ltrim($lineText));
}

#[\Override]
public function indent(): int
{
return $this->indent;
}

#[\Override]
public function getLineText(int $indentToRemove): string
{
if ($indentToRemove < 0 || $indentToRemove > $this->indent) {
Expand All @@ -38,6 +40,7 @@ public function getLineText(int $indentToRemove): string
}

/** @param non-empty-string $keyword */
#[\Override]
public function startsWithTitleKeyword(string $keyword): bool
{
$textLength = StringUtils::symbolCount($keyword);
Expand All @@ -51,22 +54,26 @@ public function startsWithTitleKeyword(string $keyword): bool
) === GherkinLanguageConstants::TITLE_KEYWORD_SEPARATOR;
}

#[\Override]
public function getRestTrimmed(int $length): string
{
return StringUtils::trim(StringUtils::substring($this->trimmedLineText, $length));
}

#[\Override]
public function isEmpty(): bool
{
return StringUtils::symbolCount($this->trimmedLineText) === 0;
}

#[\Override]
public function startsWith(string $string): bool
{
return StringUtils::startsWith($this->trimmedLineText, $string);
}

/** @return list<GherkinLineSpan> */
#[\Override]
public function getTableCells(): array
{
/**
Expand All @@ -91,7 +98,7 @@ function ($match) {

// Match \N and then replace based on what X is
// done this way so that \\n => \n once and isn't then recursively replaced again (or similar)
$unescaped = preg_replace_callback(
$unescaped = (string) preg_replace_callback(
'/(\\\\.)/u',
function ($groups) {
return match ($groups[0]) {
Expand All @@ -111,9 +118,10 @@ function ($groups) {
}

/** @return list<GherkinLineSpan> */
#[\Override]
public function getTags(): array
{
$uncommentedLine = preg_replace('/\s' . preg_quote(GherkinLanguageConstants::COMMENT_PREFIX) . '.*$/u', '', $this->trimmedLineText);
$uncommentedLine = (string) preg_replace('/\s' . preg_quote(GherkinLanguageConstants::COMMENT_PREFIX) . '.*$/u', '', $this->trimmedLineText);

/**
* @var list<array{0:string, 1:int}> $elements guaranteed by PREG_SPLIT_OFFSET_CAPTURE
Expand Down
1 change: 1 addition & 0 deletions php/src/StringTokenScanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public function __construct(
) {
}

#[\Override]
public function read(): Token
{
if (preg_match(self::FIRST_LINE_PATTERN, $this->source, $matches)) {
Expand Down
15 changes: 9 additions & 6 deletions php/src/StringUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,26 @@ public static function substring(string $string, int $start, int $length = null)

public static function rtrim(string $string): string
{
return preg_replace('/' . self::WHITESPACE_PATTERN . '$/u', '', $string);
$pattern = '/' . self::WHITESPACE_PATTERN . '$/u';
return (string) preg_replace($pattern, '', $string);
}

public static function rtrimKeepNewLines(string $string): string
{
return preg_replace('/' . self::WHITESPACE_PATTERN_NO_NEWLINE . '$/u', '', $string);
$pattern = '/' . self::WHITESPACE_PATTERN_NO_NEWLINE . '$/u';
return (string) preg_replace($pattern, '', $string);
}

public static function ltrim(string $string): string
{
return preg_replace('/^'. self::WHITESPACE_PATTERN . '/u', '', $string);
$pattern = '/^' . self::WHITESPACE_PATTERN . '/u';
return (string) preg_replace($pattern, '', $string);
}

public static function ltrimKeepNewLines(string $string): string
{
return preg_replace('/^'. self::WHITESPACE_PATTERN_NO_NEWLINE . '/u', '', $string);
$pattern = '/^' . self::WHITESPACE_PATTERN_NO_NEWLINE . '/u';
return (string) preg_replace($pattern, '', $string);
}

public static function trim(string $string): string
Expand All @@ -60,8 +64,7 @@ public static function trim(string $string): string
public static function replace(string $string, array $replacements): string
{
$patterns = array_map(fn ($p) => '/' . preg_quote($p) . '/u', array_keys($replacements));

return preg_replace($patterns, array_values($replacements), $string);
return (string) preg_replace($patterns, array_values($replacements), $string);
}

/** @return array<non-empty-string> */
Expand Down
5 changes: 5 additions & 0 deletions php/src/TokenFormatterBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,30 @@ public function __construct()
$this->tokenFormatter = new TokenFormatter();
}

#[\Override]
public function build(Token $token): void
{
$this->lines[] = $this->tokenFormatter->formatToken($token);
}

#[\Override]
public function startRule(RuleType $ruleType): void
{
}

#[\Override]
public function endRule(RuleType $ruleType): void
{
}

#[\Override]
public function getResult(): string
{
// implode at the end is more efficient than repeated concat
return implode("\n", [...$this->lines, '']);
}

#[\Override]
public function reset(string $uri): void
{
}
Expand Down
Loading
Loading