diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml index a5f0b25e6..f2c6ddf4a 100644 --- a/.github/workflows/test-php.yml +++ b/.github/workflows/test-php.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.2'] + php: ['8.3'] composer-mode: ['low-deps', 'high-deps'] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c657f36e..d97121a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/php/bin/gherkin b/php/bin/gherkin index f156ada32..25044c9ae 100755 --- a/php/bin/gherkin +++ b/php/bin/gherkin @@ -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); @@ -20,7 +21,9 @@ $sources = ( /** @param list $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); @@ -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); diff --git a/php/bin/gherkin-generate-tokens b/php/bin/gherkin-generate-tokens index 61a3a1897..89318716c 100755 --- a/php/bin/gherkin-generate-tokens +++ b/php/bin/gherkin-generate-tokens @@ -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; diff --git a/php/composer.json b/php/composer.json index 7c04cb74a..9fce7ecc3 100644 --- a/php/composer.json +++ b/php/composer.json @@ -1,37 +1,26 @@ { - "name": "cucumber/gherkin", - "description": "Gherkin parser", - "author": "Cucumber Limited ", - "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 ", + "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" + } } diff --git a/php/psalm.xml b/php/psalm.xml index 47ad1a129..f94934800 100644 --- a/php/psalm.xml +++ b/php/psalm.xml @@ -31,12 +31,6 @@ - - - - - - diff --git a/php/src/GherkinDialectProvider.php b/php/src/GherkinDialectProvider.php index 52d16889b..ade7887f7 100644 --- a/php/src/GherkinDialectProvider.php +++ b/php/src/GherkinDialectProvider.php @@ -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 $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); diff --git a/php/src/GherkinDocumentBuilder.php b/php/src/GherkinDocumentBuilder.php index 9633aa375..4e9ef1085 100644 --- a/php/src/GherkinDocumentBuilder.php +++ b/php/src/GherkinDocumentBuilder.php @@ -45,6 +45,7 @@ public function __construct( $this->reset($uri); } + #[\Override] public function build(Token $token): void { if (null === $token->match) { @@ -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); @@ -74,6 +77,7 @@ public function endRule(RuleType $ruleType): void } } + #[\Override] public function getResult(): GherkinDocument { $document = $this->currentNode()->getSingle(GherkinDocument::class, Ruletype::GherkinDocument); @@ -85,6 +89,7 @@ public function getResult(): GherkinDocument return $document; } + #[\Override] public function reset(string $uri): void { $this->stack = [new AstNode(RuleType::None)]; @@ -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), diff --git a/php/src/GherkinLanguageConstants.php b/php/src/GherkinLanguageConstants.php index 3269de428..e7a3c360e 100644 --- a/php/src/GherkinLanguageConstants.php +++ b/php/src/GherkinLanguageConstants.php @@ -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 = "```"; } diff --git a/php/src/StringGherkinLine.php b/php/src/StringGherkinLine.php index 6b0cb4a82..1cad6fd7e 100644 --- a/php/src/StringGherkinLine.php +++ b/php/src/StringGherkinLine.php @@ -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) { @@ -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); @@ -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 */ + #[\Override] public function getTableCells(): array { /** @@ -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]) { @@ -111,9 +118,10 @@ function ($groups) { } /** @return list */ + #[\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 $elements guaranteed by PREG_SPLIT_OFFSET_CAPTURE diff --git a/php/src/StringTokenScanner.php b/php/src/StringTokenScanner.php index d7c079511..0e7801efe 100644 --- a/php/src/StringTokenScanner.php +++ b/php/src/StringTokenScanner.php @@ -24,6 +24,7 @@ public function __construct( ) { } + #[\Override] public function read(): Token { if (preg_match(self::FIRST_LINE_PATTERN, $this->source, $matches)) { diff --git a/php/src/StringUtils.php b/php/src/StringUtils.php index 0e5052676..a2c5b26e9 100644 --- a/php/src/StringUtils.php +++ b/php/src/StringUtils.php @@ -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 @@ -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 */ diff --git a/php/src/TokenFormatterBuilder.php b/php/src/TokenFormatterBuilder.php index a96b4f978..443be3ca8 100644 --- a/php/src/TokenFormatterBuilder.php +++ b/php/src/TokenFormatterBuilder.php @@ -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 { } diff --git a/php/src/TokenMatcher.php b/php/src/TokenMatcher.php index 02bd6d79c..ac9061f21 100644 --- a/php/src/TokenMatcher.php +++ b/php/src/TokenMatcher.php @@ -23,6 +23,7 @@ public function __construct( $this->reset(); } + #[\Override] public function reset(): void { $this->currentDialect = $this->dialectProvider->getDefaultDialect(); @@ -54,6 +55,7 @@ private function setTokenMatched( ); } + #[\Override] public function match_EOF(Token $token): bool { if ($token->isEof()) { @@ -65,27 +67,32 @@ public function match_EOF(Token $token): bool return false; } + #[\Override] public function match_FeatureLine(Token $token): bool { return $this->matchTitleLine($token, TokenType::FeatureLine, $this->currentDialect->getFeatureKeywords()); } + #[\Override] public function match_BackgroundLine(Token $token): bool { return $this->matchTitleLine($token, TokenType::BackgroundLine, $this->currentDialect->getBackgroundKeywords()); } + #[\Override] public function match_ScenarioLine(Token $token): bool { return $this->matchTitleLine($token, TokenType::ScenarioLine, $this->currentDialect->getScenarioKeywords()) || $this->matchTitleLine($token, TokenType::ScenarioLine, $this->currentDialect->getScenarioOutlineKeywords()); } + #[\Override] public function match_RuleLine(Token $token): bool { return $this->matchTitleLine($token, TokenType::RuleLine, $this->currentDialect->getRuleKeywords()); } + #[\Override] public function match_ExamplesLine(Token $token): bool { return $this->matchTitleLine($token, TokenType::ExamplesLine, $this->currentDialect->getExamplesKeywords()); @@ -108,6 +115,7 @@ private function matchTitleLine(Token $token, TokenType $tokenType, array $keywo return false; } + #[\Override] public function match_Other(Token $token): bool { //take the entire line, except removing DocString indents @@ -117,6 +125,7 @@ public function match_Other(Token $token): bool return true; } + #[\Override] public function match_Empty(Token $token): bool { if ($token->line?->isEmpty()) { @@ -128,6 +137,7 @@ public function match_Empty(Token $token): bool return false; } + #[\Override] public function match_StepLine(Token $token): bool { $keywords = $this->currentDialect->getStepKeywords(); @@ -146,6 +156,7 @@ public function match_StepLine(Token $token): bool return false; } + #[\Override] public function match_TableRow(Token $token): bool { if ($token->line?->startsWith(GherkinLanguageConstants::TABLE_CELL_SEPARATOR)) { @@ -158,6 +169,7 @@ public function match_TableRow(Token $token): bool return false; } + #[\Override] public function match_Comment(Token $token): bool { if ($token->line?->startsWith(GherkinLanguageConstants::COMMENT_PREFIX)) { @@ -170,6 +182,7 @@ public function match_Comment(Token $token): bool return false; } + #[\Override] public function match_DocStringSeparator(Token $token): bool { return $this->activeDocStringSeparator === null @@ -200,6 +213,7 @@ private function _match_DocStringSeparator(Token $token, string $separator, bool return false; } + #[\Override] public function match_TagLine(Token $token): bool { if ($token->line?->startsWith(GherkinLanguageConstants::TAG_PREFIX)) { @@ -211,6 +225,7 @@ public function match_TagLine(Token $token): bool return false; } + #[\Override] public function match_Language(Token $token): bool { if ($token->line && preg_match(self::LANGUAGE_PATTERN, $token->line->getLineText(0), $matches)) { diff --git a/php/tests/AstNodeTest.php b/php/tests/AstNodeTest.php index bdaa19400..abb8ff636 100644 --- a/php/tests/AstNodeTest.php +++ b/php/tests/AstNodeTest.php @@ -14,6 +14,7 @@ final class AstNodeTest extends TestCase { private AstNode $astNode; + #[\Override] public function setUp(): void { $this->astNode = new AstNode(RuleType::None); diff --git a/php/tests/GherkinDialectProviderTest.php b/php/tests/GherkinDialectProviderTest.php index 7706b196a..5dab9858d 100644 --- a/php/tests/GherkinDialectProviderTest.php +++ b/php/tests/GherkinDialectProviderTest.php @@ -10,6 +10,7 @@ final class GherkinDialectProviderTest extends TestCase { private GherkinDialectProvider $dialectProvider; + #[\Override] public function setUp(): void { $this->dialectProvider = new GherkinDialectProvider(); diff --git a/php/tests/GherkinDialectTest.php b/php/tests/GherkinDialectTest.php index 0aa4a455b..bdf4958db 100644 --- a/php/tests/GherkinDialectTest.php +++ b/php/tests/GherkinDialectTest.php @@ -10,6 +10,7 @@ final class GherkinDialectTest extends TestCase { private GherkinDialect $dialect; + #[\Override] public function setUp(): void { $data = [ diff --git a/php/tests/TokenFormatterBuilderTest.php b/php/tests/TokenFormatterBuilderTest.php index 961ac084f..c3b0e93c3 100644 --- a/php/tests/TokenFormatterBuilderTest.php +++ b/php/tests/TokenFormatterBuilderTest.php @@ -10,6 +10,7 @@ final class TokenFormatterBuilderTest extends TestCase { private TokenFormatterBuilder $tokenBuilder; + #[\Override] public function setUp(): void { $this->tokenBuilder = new TokenFormatterBuilder(); diff --git a/php/tests/TokenMatcherTest.php b/php/tests/TokenMatcherTest.php index 95494b299..358114b19 100644 --- a/php/tests/TokenMatcherTest.php +++ b/php/tests/TokenMatcherTest.php @@ -14,6 +14,7 @@ final class TokenMatcherTest extends TestCase { private TokenMatcherInterface $tokenMatcher; + #[\Override] public function setUp(): void { $this->tokenMatcher = new TokenMatcher();