diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9e34877 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['8.2', '8.3', '8.4'] + name: PHP ${{ matrix.php-version }} + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, readline + coverage: none + - name: Validate composer.json and composer.lock + run: composer validate --strict + - name: Unset local path repositories + run: | + composer config --unset repositories + rm composer.lock + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php-version }}- + - name: Install dependencies + run: composer update --prefer-dist --no-progress + - name: Run PHPUnit tests + run: ./vendor/bin/phpunit --testdox + - name: Run Behat tests (version-aware) + run: ./bin/run-behat-tests.sh + + lint: + name: PHP ${{ matrix.php-version }} Code Style (PHP-CS-Fixer) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, readline + coverage: none + tools: php-cs-fixer + - name: Run PHP-CS-Fixer + run: php-cs-fixer fix --dry-run --diff --verbose --allow-risky=yes + + static-analysis: + name: PHP ${{ matrix.php-version }} Static Analysis (PHPStan) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v4 + - name: Unset local path repositories + run: | + composer config --unset repositories + rm composer.lock + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, readline + coverage: none + - name: Install dependencies + run: composer update --prefer-dist --no-progress + - name: Run PHPStan + run: vendor/bin/phpstan analyse src diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index f4baf10..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Tests - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -jobs: - test: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - php-version: ['8.2', '8.3', '8.4'] - - name: PHP ${{ matrix.php-version }} - - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: mbstring, readline - coverage: none - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v3 - with: - path: vendor - key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php-${{ matrix.php-version }}- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHPUnit tests - run: ./vendor/bin/phpunit --testdox - - - name: Run Behat tests (version-aware) - run: ./bin/run-behat-tests.sh diff --git a/.gitignore b/.gitignore index ba27929..febeea2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ vendor/ .phpunit.cache phpunit.xml .claude -.specs \ No newline at end of file +.specs +.php-cs-fixer.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..5860e0e --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,30 @@ +setParallelConfig(ParallelConfigFactory::detect()) // @TODO 4.0 no need to call this manually + ->setRiskyAllowed(false) + ->setRules([ + '@auto' => true + ]) + // 💡 by default, Fixer looks for `*.php` files excluding `./vendor/` - here, you can groom this config + ->setFinder( + (new Finder()) + // 💡 root folder to check + ->in(__DIR__) + // 💡 additional files, eg bin entry file + // ->append([__DIR__.'/bin-entry-file']) + // 💡 folders to exclude, if any + // ->exclude([/* ... */]) + // 💡 path patterns to exclude, if any + // ->notPath([/* ... */]) + // 💡 extra configs + // ->ignoreDotFiles(false) // true by default in v3, false in v4 or future mode + // ->ignoreVCSIgnored(true) // true by default + ) +; diff --git a/README.md b/README.md index 14c6b7f..a158797 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Phunkie Console -[![Tests](https://github.com/phunkie/console/workflows/Tests/badge.svg)](https://github.com/phunkie/console/actions) +[![CI](https://github.com/phunkie/console/actions/workflows/ci.yml/badge.svg)](https://github.com/phunkie/console/actions) [![PHP Version](https://img.shields.io/packagist/php-v/phunkie/console?color=8892BF)](https://packagist.org/packages/phunkie/console) [![Latest Stable Version](https://img.shields.io/packagist/v/phunkie/console)](https://packagist.org/packages/phunkie/console) [![Total Downloads](https://img.shields.io/packagist/dt/phunkie/console)](https://packagist.org/packages/phunkie/console) diff --git a/THOUGHTS.md b/THOUGHTS.md new file mode 100644 index 0000000..59a6559 --- /dev/null +++ b/THOUGHTS.md @@ -0,0 +1,187 @@ +# Stream Reading Improvements for CI Testing + +## Problem Statement +Tests fail on CI (GitHub Actions) because `stream_select` behavior differs from local environment. The REPL process outputs to stdout, but tests only capture the banner and miss command output. This is likely due to: +- GitHub Actions TTY limitations +- Stderr writes that unblock select +- Different buffering behavior in CI + +## Core Considerations + +### 1. ✅ Fix the Loop Logic (DONE) +**Problem**: Current loop exits on first timeout without a prompt, even with longer timeouts. +**Solution**: Continue polling on timeout instead of breaking. Use overall timeout guard. +**Status**: Implemented in `ReplOutputReader::readOutput()` +- Changed `break` to `continue` on timeout without prompt +- Added overall timeout tracking + +### 2. ✅ Environment Configuration (DONE) +**Solution**: Use .env for local, .env.test for CI with configurable timeouts +**Status**: Implemented +- `.env` with 1.5s timeout (local, gitignored) +- `.env.test` with 5.0s timeout (CI, tracked) +- ReplOutputReader reads from environment + +### 3. ✅ Read from Both stdout AND stderr (DONE) +**Problem**: GitHub Actions might write to stderr, causing stream_select to unblock, but we only read stdout. +**Solution**: +- Pass both `$pipes[1]` (stdout) and `$pipes[2]` (stderr) to stream_select +- Read from whichever is ready +- Only append stdout to output (discard stderr noise, or log it) +**Status**: IMPLEMENTED in `ReplOutputReader::readOutput()` +- Modified signature to accept `$stderrStream` parameter +- Loop reads from both streams +- stderr output logged but not included in return value +- Updated `ReplSteps::readOutput()` to pass stderr + +### 4. ✅ Debug Logging for CI (DONE) +**Problem**: Can't see what's happening in CI without visibility, but don't want noise on passing tests +**Solution**: Add conditional debug logging to ReplOutputReader +**Status**: IMPLEMENTED +- Buffers log messages instead of immediately outputting +- Only outputs logs when: + - `REPL_DEBUG=true` in .env (always log) + - Timeout occurs with no output (indicates failure) + - stream_select error occurs +- Keeps local test output clean (.env has `REPL_DEBUG=false`) +- CI gets full logs (.env.test has `REPL_DEBUG=true`) +- Added to `ReplProcessManager::sendInput()` as well + +### 5. ✅ Process Management Review (DONE) +**Current**: Using `proc_open` with pipes, streams set to non-blocking +**Considerations**: +- After writing to stdin (`$pipes[0]`), explicitly `fflush($pipes[0])` +- Consider `fclose($pipes[0])` after all input to signal EOF (may not be appropriate for REPL) +- Ensure both stdout and stderr are non-blocking +**Status**: REVIEWED - Already doing `fflush()` after writes +- Added logging to `sendInput()` to verify bytes written + +### 6. 🔮 Sentinel/Test Mode Approach (FUTURE) +**Alternative**: Instead of guessing prompts, have REPL print a sentinel in test mode +```php +// In test mode, REPL prints: +echo "COMMAND_DONE_MARKER\n"; +``` +Then look for sentinel instead of prompt patterns. +**Status**: NOT IMPLEMENTED - Requires REPL changes +**Priority**: LOW - Nice to have, but invasive + +### 7. 🔮 Phunkie Streams Solution (FUTURE) +**Idea**: Extract this into a reusable Phunkie Streams component +- `Stream\Process\asyncRead($stream, $predicate, $timeout)` +- Handles non-blocking reads, multiple streams, sentinel detection +**Status**: NOT IMPLEMENTED +**Priority**: LOW - After we solve the immediate problem + +## Action Items (Prioritized) + +### Immediate (Baby Steps) +1. ✅ Fix loop to continue polling instead of breaking on timeout +2. ✅ Add .env configuration for timeouts +3. ✅ Modify `ReplOutputReader::readOutput()` to accept stderr stream +4. ✅ Update `ReplSteps` to pass stderr to `readOutput()` +5. ✅ Read from both streams in the loop, only append stdout to output +6. ✅ Add debug logging (error_log) to see what's happening in CI +7. ✅ Review `ReplProcessManager` for proper fflush() usage +8. ⏳ **NEXT**: Test locally to verify changes don't break existing tests +9. ⏳ **NEXT**: Test on CI with full test suite + +### Long Term +10. Review CI logs to see if stderr reading solves the issue +11. Consider reducing timeouts if tests are passing consistently +12. Consider sentinel-based approach for more reliable testing +13. Extract pattern into Phunkie Streams if successful + +## Notes +- **Don't just increase timeouts** - that makes CI slow and doesn't address root cause +- **Stderr reading is likely the key** - GitHub Actions might be writing to stderr +- **Keep local tests fast** - use .env for short timeouts locally +- **Baby steps** - implement one thing at a time and test + +## Current Status +- Loop logic fixed ✅ +- Environment config added ✅ +- Stderr reading implemented ✅ +- Debug logging implemented with buffering ✅ +- **SENTINEL APPROACH IMPLEMENTED** ✅ +- **LOCAL TESTS**: All 418 scenarios, 2056 steps PASSING in ~18s ✅ +- **NEXT**: Test on CI to verify sentinel approach works in GitHub Actions + +## Test Results +- **Local (PHP 8.4)**: 418 scenarios, 2056 steps - ALL PASSED ✅ +- **CI**: Pending - need to push and test + +## Sentinel Approach + Blocking Read Strategy + +### The Problem +GitHub Actions has different TTY/buffering behavior than local environments. The original non-blocking, short-timeout polling approach with `stream_select` would break too aggressively on timeouts, exiting the read loop before all data arrived. This caused tests to only capture the REPL banner, missing actual command output. + +### The Solution: Two-Part Fix + +#### Part 1: Sentinel Marker (Explicit Ready Signal) +Instead of guessing when the REPL is ready by detecting prompt patterns, the REPL explicitly prints `__PHUNKIE_READY__` when ready for input. + +**REPL side** (`src/Repl/ReplLoop.php`): +- Added `isTestMode()` to check `REPL_TEST_MODE` environment variable +- Added `printTestSentinel()` that prints `__PHUNKIE_READY__\n` in test mode +- Called before each prompt in `replLoopTrampoline()` + +**Test side**: +- `ReplProcessManager` loads `.env` and passes `REPL_TEST_MODE` to child process +- `ReplOutputReader` detects sentinel instead of prompt patterns in test mode +- Sentinel is stripped from output before returning to tests +- `ReplSteps.waitForPrompt()` uses `ReplOutputReader` for consistent detection + +#### Part 2: Blocking Read Strategy (Patience Over Speed) +Changed from aggressive timeout-based polling to blocking reads that wait naturally for data. + +**Key changes in `ReplOutputReader::readOutput()`**: +1. **Longer `stream_select` timeout**: Up to 1 second (vs 50ms) to let data arrive naturally +2. **No early exits on timeout**: Only breaks when: + - Prompt/sentinel found ✅ + - Overall timeout hit ⏱️ + - EOF/error encountered ❌ +3. **Calculated remaining timeout**: Uses `min(1.0, remainingTimeout)` for smart waiting +4. **Smaller read chunks**: 4096 bytes for more incremental reading +5. **Proper error handling**: Detects EOF and read errors explicitly + +**Before (aggressive)**: +```php +// 50ms timeout on stream_select +$result = @stream_select($read, $write, $except, 0, 50000); +if ($result === 0) { + break; // ❌ Exits too early! +} +``` + +**After (patient)**: +```php +// Up to 1 second timeout, calculated from remaining time +$selectTimeout = min(1.0, $remainingTimeout); +$result = @stream_select($read, $write, $except, $selectTimeoutSec, $selectTimeoutUsec); +if ($result === 0) { + if (self::endsWithPrompt($output)) { + break; // ✅ Only exit if we have complete response + } + continue; // ✅ Keep waiting for data +} +``` + +### Benefits +- ✅ **Reliable in CI**: Doesn't break early when I/O is slow +- ✅ **Explicit ready signal**: Sentinel removes guesswork +- ✅ **Fast locally**: ~18 seconds for 418 scenarios +- ✅ **Handles slow environments**: Waits patiently up to overall timeout +- ✅ **Clean output**: Sentinel stripped automatically +- ✅ **Proper error handling**: Detects EOF and errors gracefully + +### Configuration +- `.env` (local): `REPL_TEST_MODE=true`, `REPL_OUTPUT_TIMEOUT=1.5` +- `.env.test` (CI): `REPL_TEST_MODE=true`, `REPL_OUTPUT_TIMEOUT=5.0` + +### Files Modified +- `src/Repl/ReplLoop.php` - Sentinel printing +- `tests/Acceptance/Support/ReplProcessManager.php` - Environment passing +- `tests/Acceptance/Support/ReplOutputReader.php` - **Blocking read strategy + sentinel detection** +- `tests/Acceptance/ReplSteps.php` - Use ReplOutputReader in test mode +- `.env` and `.env.test` - Configuration diff --git a/behat.php b/behat.php index e415167..b9ac55a 100644 --- a/behat.php +++ b/behat.php @@ -8,8 +8,9 @@ return (new Config()) ->withProfile( (new Profile('default')) - ->withSuite((new Suite('default')) + ->withSuite( + (new Suite('default')) ->withContexts(ReplSteps::class) - ) + ) ) ; diff --git a/bin/phpstan b/bin/phpstan new file mode 120000 index 0000000..c0be5e0 --- /dev/null +++ b/bin/phpstan @@ -0,0 +1 @@ +/Users/md/code/phunkie/console/vendor/bin/phpstan \ No newline at end of file diff --git a/bin/phunkie b/bin/phunkie index 8b98384..b1e0b9a 100755 --- a/bin/phunkie +++ b/bin/phunkie @@ -18,9 +18,10 @@ use function Phunkie\Console\Functions\{setColors, printBanner, loadHistory, sav (function (){ $autoloadPaths = [ - __DIR__ . '/../vendor/autoload.php', - __DIR__ . '/../../../autoload.php', - __DIR__ . '/../../../../autoload.php' + __DIR__ . '/../vendor/autoload.php', // Local development (console project) + dirname(__DIR__, 3) . '/autoload.php', // Installed as dependency (vendor/phunkie/console/bin/phunkie -> vendor/autoload.php) + dirname(__DIR__, 4) . '/autoload.php', // Global install sometimes puts bin deeper? + getcwd() . '/vendor/autoload.php' // Running from project root ]; $autoloaded = false; @@ -79,9 +80,21 @@ use function Phunkie\Console\Functions\{setColors, printBanner, loadHistory, sav saveHistory()->unsafeRun(); }); - $app = new PhunkieConsole(); + $args = $_SERVER['argv']; + $app = null; - $app->run($_SERVER['argv'])->unsafeRun(); + if (isset($args[1]) && !str_starts_with($args[1], '-')) { + $className = $args[1]; + if (class_exists($className) && is_subclass_of($className, IOApp::class)) { + $app = new $className(); + } + } + + if ($app === null) { + $app = new PhunkieConsole(); + } + + $app->run($args)->unsafeRun(); })(); diff --git a/composer.json b/composer.json index 11c4506..37ef6c2 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "phunkie/console", "description": "A console for Phunkie development", - "license":"MIT", + "license": "MIT", "authors": [ { "name": "Marcello Duarte", @@ -10,15 +10,16 @@ ], "require": { "php": "^8.2 || ^8.3 || ^8.4", - "phunkie/phunkie": "^0.11.7", - "phunkie/effect": "^0.4", + "phunkie/phunkie": "^1.0", + "phunkie/effect": "^1.0", "nikic/php-parser": "^5.6" }, "require-dev": { "phpunit/phpunit": "^11", "behat/behat": "^3.22", "phpstan/phpstan": "^2.1", - "friendsofphp/php-cs-fixer": "^3.75" + "friendsofphp/php-cs-fixer": "^3.90", + "phunkie/phpstan": "^1.0" }, "autoload": { "psr-4": { @@ -41,5 +42,20 @@ }, "bin": [ "bin/phunkie" - ] + ], + "scripts": { + "test": "vendor/bin/phpunit", + "cs-fix": "vendor/bin/php-cs-fixer fix --allow-risky=yes", + "cs-check": "vendor/bin/php-cs-fixer fix --dry-run --diff --allow-risky=yes", + "phpstan": "phpstan analyse --memory-limit=512M", + "lint": [ + "@cs-check", + "@phpstan" + ], + "test-all": "scripts/test-all-versions.sh", + "check": [ + "@lint", + "@test" + ] + } } diff --git a/composer.lock b/composer.lock index da0445d..1673cda 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b35ef1b89cfdedac896c9fb3ebdf1368", + "content-hash": "25cb59ccc0129361f933679948084f73", "packages": [ { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -60,33 +60,33 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phunkie/effect", - "version": "v0.4.2", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/phunkie/effect.git", - "reference": "d5a3fb83fe4a31bbfe84dd46c85e8fc6754d5700" + "reference": "fd90513682eb3a5a76a4e9f43ec89d1a9785458e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phunkie/effect/zipball/d5a3fb83fe4a31bbfe84dd46c85e8fc6754d5700", - "reference": "d5a3fb83fe4a31bbfe84dd46c85e8fc6754d5700", + "url": "https://api.github.com/repos/phunkie/effect/zipball/fd90513682eb3a5a76a4e9f43ec89d1a9785458e", + "reference": "fd90513682eb3a5a76a4e9f43ec89d1a9785458e", "shasum": "" }, "require": { - "php": ">=8.2", - "phunkie/phunkie": "^0.11" + "php": "^8.2 || ^8.3 || ^8.4", + "phunkie/phunkie": "^1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.75", + "friendsofphp/php-cs-fixer": "^3.90", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.5", - "phunkie/phunkie-console": "dev-master" + "phunkie/phpstan": "^1.0" }, "suggest": { "ext-parallel": "Required for parallel execution using threads. PHP must be compiled with ZTS support." @@ -113,32 +113,34 @@ "description": "A functional effects library for PHP inspired by Scala's cats-effect", "support": { "issues": "https://github.com/phunkie/effect/issues", - "source": "https://github.com/phunkie/effect/tree/v0.4.2" + "source": "https://github.com/phunkie/effect/tree/1.0.0" }, - "time": "2025-10-15T20:35:28+00:00" + "time": "2025-12-08T19:02:35+00:00" }, { "name": "phunkie/phunkie", - "version": "0.11.7", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/phunkie/phunkie.git", - "reference": "1d7620e41062c86e3f1d3dbf22f7035b4675667e" + "reference": "edfe0c5e3b382d8827bdaef5c0ef027840eceaf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phunkie/phunkie/zipball/1d7620e41062c86e3f1d3dbf22f7035b4675667e", - "reference": "1d7620e41062c86e3f1d3dbf22f7035b4675667e", + "url": "https://api.github.com/repos/phunkie/phunkie/zipball/edfe0c5e3b382d8827bdaef5c0ef027840eceaf5", + "reference": "edfe0c5e3b382d8827bdaef5c0ef027840eceaf5", "shasum": "" }, "require": { - "php": ">=8.1" + "php": "^8.2 || ^8.3 || ^8.4" }, "require-dev": { "ergebnis/composer-normalize": "^2", - "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/php-cs-fixer": "^3.90", "giorgiosironi/eris": "^0", - "phpunit/phpunit": "^9" + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9", + "phunkie/phpstan": "@dev" }, "type": "library", "autoload": { @@ -164,24 +166,24 @@ "description": "Functional structures library for PHP", "support": { "issues": "https://github.com/phunkie/phunkie/issues", - "source": "https://github.com/phunkie/phunkie/tree/0.11.7" + "source": "https://github.com/phunkie/phunkie/tree/1.0.0" }, - "time": "2025-02-22T21:55:21+00:00" + "time": "2025-12-08T18:48:37+00:00" } ], "packages-dev": [ { "name": "behat/behat", - "version": "v3.25.0", + "version": "v3.27.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "bc7f149dde1cd0da82616e6b280e1c9be2ee53e1" + "reference": "3282ad774358e4eaf533855e9a1f48559894d1b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/bc7f149dde1cd0da82616e6b280e1c9be2ee53e1", - "reference": "bc7f149dde1cd0da82616e6b280e1c9be2ee53e1", + "url": "https://api.github.com/repos/Behat/Behat/zipball/3282ad774358e4eaf533855e9a1f48559894d1b5", + "reference": "3282ad774358e4eaf533855e9a1f48559894d1b5", "shasum": "" }, "require": { @@ -190,7 +192,7 @@ "composer/xdebug-handler": "^1.4 || ^2.0 || ^3.0", "ext-mbstring": "*", "nikic/php-parser": "^4.19.2 || ^5.2", - "php": "8.1.* || 8.2.* || 8.3.* || 8.4.* ", + "php": ">=8.1 <8.6", "psr/container": "^1.0 || ^2.0", "symfony/config": "^5.4 || ^6.4 || ^7.0", "symfony/console": "^5.4 || ^6.4 || ^7.0", @@ -201,6 +203,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.68", + "opis/json-schema": "^2.5", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^9.6", "rector/rector": "2.1.7", @@ -260,31 +263,31 @@ ], "support": { "issues": "https://github.com/Behat/Behat/issues", - "source": "https://github.com/Behat/Behat/tree/v3.25.0" + "source": "https://github.com/Behat/Behat/tree/v3.27.0" }, - "time": "2025-10-03T20:14:49+00:00" + "time": "2025-11-23T12:12:41+00:00" }, { "name": "behat/gherkin", - "version": "v4.14.0", + "version": "v4.16.1", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4" + "reference": "e26037937dfd48528746764dd870bc5d0836665f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", - "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/e26037937dfd48528746764dd870bc5d0836665f", + "reference": "e26037937dfd48528746764dd870bc5d0836665f", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", - "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" + "php": ">=8.1 <8.6" }, "require-dev": { - "cucumber/gherkin-monorepo": "dev-gherkin-v32.1.1", - "friendsofphp/php-cs-fixer": "^3.65", + "cucumber/gherkin-monorepo": "dev-gherkin-v37.0.0", + "friendsofphp/php-cs-fixer": "^3.77", "mikey179/vfsstream": "^1.6", "phpstan/extension-installer": "^1", "phpstan/phpstan": "^2", @@ -329,9 +332,23 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.14.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.16.1" }, - "time": "2025-05-23T15:06:40+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2025-12-08T16:12:58+00:00" }, { "name": "clue/ndjson-react", @@ -729,58 +746,57 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.75.0", + "version": "v3.91.3", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c" + "reference": "9f10aa6390cea91da175ea608880e942d7c0226e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/399a128ff2fdaf4281e4e79b755693286cdf325c", - "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9f10aa6390cea91da175ea608880e942d7c0226e", + "reference": "9f10aa6390cea91da175ea608880e942d7c0226e", "shasum": "" }, "require": { - "clue/ndjson-react": "^1.0", + "clue/ndjson-react": "^1.3", "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", + "composer/xdebug-handler": "^3.0.5", "ext-filter": "*", "ext-hash": "*", "ext-json": "*", "ext-tokenizer": "*", - "fidry/cpu-core-counter": "^1.2", + "fidry/cpu-core-counter": "^1.3", "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", - "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", - "react/socket": "^1.0", - "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0", - "symfony/console": "^5.4 || ^6.4 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", - "symfony/finder": "^5.4 || ^6.4 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", - "symfony/polyfill-mbstring": "^1.31", - "symfony/polyfill-php80": "^1.31", - "symfony/polyfill-php81": "^1.31", - "symfony/process": "^5.4 || ^6.4 || ^7.2", - "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.33", + "symfony/polyfill-php80": "^1.33", + "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.6", - "infection/infection": "^0.29.14", - "justinrainbow/json-schema": "^5.3 || ^6.2", - "keradus/cli-executor": "^2.1", + "facile-it/paraunit": "^1.3.1 || ^2.7", + "infection/infection": "^0.31.0", + "justinrainbow/json-schema": "^6.5", + "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.7", - "php-cs-fixer/accessible-object": "^1.1", + "php-coveralls/php-coveralls": "^2.9", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12", - "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3", - "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3" + "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", + "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2 || ^8.0", + "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2 || ^8.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -795,7 +811,7 @@ "PhpCsFixer\\": "src/" }, "exclude-from-classmap": [ - "src/Fixer/Internal/*" + "src/**/Internal/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -821,7 +837,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.75.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.91.3" }, "funding": [ { @@ -829,7 +845,7 @@ "type": "github" } ], - "time": "2025-03-31T18:40:42+00:00" + "time": "2025-12-05T19:45:37+00:00" }, { "name": "myclabs/deep-copy", @@ -1011,11 +1027,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.31", + "version": "2.1.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", - "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", "shasum": "" }, "require": { @@ -1060,7 +1076,7 @@ "type": "github" } ], - "time": "2025-10-10T14:14:11+00:00" + "time": "2025-12-05T10:24:31+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1399,16 +1415,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.42", + "version": "11.5.46", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c" + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c", - "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/75dfe79a2aa30085b7132bb84377c24062193f33", + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33", "shasum": "" }, "require": { @@ -1480,7 +1496,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.42" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.46" }, "funding": [ { @@ -1504,7 +1520,60 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:09:13+00:00" + "time": "2025-12-06T08:01:15+00:00" + }, + { + "name": "phunkie/phpstan", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/phunkie/phpstan.git", + "reference": "91dd9931edc1fb81d5a5d5eb61179a87e77f8bc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phunkie/phpstan/zipball/91dd9931edc1fb81d5a5d5eb61179a87e77f8bc6", + "reference": "91dd9931edc1fb81d5a5d5eb61179a87e77f8bc6", + "shasum": "" + }, + "require": { + "php": "^8.2 || ^8.3 || ^8.4", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^11", + "phunkie/effect": "dev-developing-1.0.0 as 1.0.0", + "phunkie/phunkie": "dev-developing-1.0.0 as 1.0.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Phunkie\\PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "PHPStan extensions for Phunkie functional programming library", + "support": { + "issues": "https://github.com/phunkie/phpstan/issues", + "source": "https://github.com/phunkie/phpstan/tree/1.0.0" + }, + "time": "2025-12-08T18:01:33+00:00" }, { "name": "psr/container", @@ -1808,16 +1877,16 @@ }, { "name": "react/dns", - "version": "v1.13.0", + "version": "v1.14.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", "shasum": "" }, "require": { @@ -1872,7 +1941,7 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.13.0" + "source": "https://github.com/reactphp/dns/tree/v1.14.0" }, "funding": [ { @@ -1880,20 +1949,20 @@ "type": "open_collective" } ], - "time": "2024-06-13T14:18:03+00:00" + "time": "2025-11-18T19:34:28+00:00" }, { "name": "react/event-loop", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", "shasum": "" }, "require": { @@ -1944,7 +2013,7 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" }, "funding": [ { @@ -1952,7 +2021,7 @@ "type": "open_collective" } ], - "time": "2023-11-13T13:48:05+00:00" + "time": "2025-11-17T20:46:25+00:00" }, { "name": "react/promise", @@ -2029,16 +2098,16 @@ }, { "name": "react/socket", - "version": "v1.16.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", "shasum": "" }, "require": { @@ -2097,7 +2166,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" + "source": "https://github.com/reactphp/socket/tree/v1.17.0" }, "funding": [ { @@ -2105,7 +2174,7 @@ "type": "open_collective" } ], - "time": "2024-07-26T10:38:09+00:00" + "time": "2025-11-19T20:47:34+00:00" }, { "name": "react/stream", @@ -3225,22 +3294,22 @@ }, { "name": "symfony/config", - "version": "v7.3.4", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "8a09223170046d2cfda3d2e11af01df2c641e961" + "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/8a09223170046d2cfda3d2e11af01df2c641e961", - "reference": "8a09223170046d2cfda3d2e11af01df2c641e961", + "url": "https://api.github.com/repos/symfony/config/zipball/2c323304c354a43a48b61c5fa760fc4ed60ce495", + "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^7.1", + "symfony/filesystem": "^7.1|^8.0", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -3248,11 +3317,11 @@ "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3280,7 +3349,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.3.4" + "source": "https://github.com/symfony/config/tree/v7.4.1" }, "funding": [ { @@ -3300,20 +3369,20 @@ "type": "tidelift" } ], - "time": "2025-09-22T12:46:16+00:00" + "time": "2025-12-05T07:52:08+00:00" }, { "name": "symfony/console", - "version": "v7.3.4", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", "shasum": "" }, "require": { @@ -3321,7 +3390,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -3335,16 +3404,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3378,7 +3447,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.4" + "source": "https://github.com/symfony/console/tree/v7.4.1" }, "funding": [ { @@ -3398,28 +3467,28 @@ "type": "tidelift" } ], - "time": "2025-09-22T15:31:00+00:00" + "time": "2025-12-05T15:23:39+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.3.4", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "82119812ab0bf3425c1234d413efd1b19bb92ae4" + "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/82119812ab0bf3425c1234d413efd1b19bb92ae4", - "reference": "82119812ab0bf3425c1234d413efd1b19bb92ae4", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", + "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", "shasum": "" }, "require": { "php": ">=8.2", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^3.5", - "symfony/var-exporter": "^6.4.20|^7.2.5" + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -3432,9 +3501,9 @@ "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3462,7 +3531,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.3.4" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.2" }, "funding": [ { @@ -3482,7 +3551,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-12-08T06:57:04+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3553,16 +3622,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", "shasum": "" }, "require": { @@ -3579,13 +3648,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3613,7 +3683,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0" }, "funding": [ { @@ -3633,7 +3703,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-28T09:38:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -3713,25 +3783,25 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.2", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + "reference": "d937d400b980523dc9ee946bb69972b5e619058d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3759,7 +3829,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + "source": "https://github.com/symfony/filesystem/tree/v8.0.1" }, "funding": [ { @@ -3779,27 +3849,27 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:47+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3827,7 +3897,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.2" + "source": "https://github.com/symfony/finder/tree/v8.0.0" }, "funding": [ { @@ -3847,24 +3917,24 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -3898,7 +3968,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -3918,7 +3988,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4172,19 +4242,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -4232,7 +4303,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -4243,12 +4314,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", @@ -4414,22 +4489,102 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149", + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -4457,7 +4612,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v8.0.0" }, "funding": [ { @@ -4477,20 +4632,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T16:25:44+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -4544,7 +4699,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -4555,29 +4710,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/stopwatch", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -4606,7 +4765,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" + "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" }, "funding": [ { @@ -4617,43 +4776,47 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-24T10:49:57+00:00" + "time": "2025-08-04T07:36:47+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4692,7 +4855,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.1" }, "funding": [ { @@ -4712,27 +4875,27 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", @@ -4751,17 +4914,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4792,7 +4955,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v7.4.0" }, "funding": [ { @@ -4812,20 +4975,20 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -4874,7 +5037,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -4885,35 +5048,38 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.4" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4951,7 +5117,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" }, "funding": [ { @@ -4971,32 +5137,32 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-05T18:53:00+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.3", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -5027,7 +5193,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.3" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -5047,20 +5213,20 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:34:33+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -5089,7 +5255,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -5097,7 +5263,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], diff --git a/features/execution/run_app.feature b/features/execution/run_app.feature new file mode 100644 index 0000000..14e7284 --- /dev/null +++ b/features/execution/run_app.feature @@ -0,0 +1,49 @@ +Feature: Run IOApp + As a developer + I want to run a Phunkie IOApp using the console binary + So that I can execute side effects + + Scenario: Run a custom IOApp + Given I have a file "tests/Acceptance/Fixtures/MyApp.php" with content: + """ + /dev/null && \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer > /dev/null && \ + composer install --no-interaction --prefer-dist --quiet && \ + echo '--- Running lint (cs-check + phpstan) ---' && \ + composer lint && \ + echo '--- Running tests ---' && \ + composer test" + + echo "✅ PHP $version passed (lint + tests)" +done + +echo "" +echo "==========================================" +echo "✅ All PHP versions passed!" +echo "==========================================" + diff --git a/src/Functions/display.php b/src/Functions/display.php index 723a01a..449f870 100644 --- a/src/Functions/display.php +++ b/src/Functions/display.php @@ -14,6 +14,7 @@ use Phunkie\Console\Types\ReplSession; use Phunkie\Effect\IO\IO; use Phunkie\Types\Unit; + use function Phunkie\Effect\Functions\console\printLn; /** @@ -25,21 +26,21 @@ function printHelp(): IO { $help = << Load a .phunkie or .php file (functions & classes become available) + :help Show this help message + :exit Exit the REPL (also :quit, Ctrl-C, Ctrl-D) + :vars List all defined variables + :history Show command history + :reset Reset the REPL state (clear all variables and history) + :load Load a .phunkie or .php file (functions & classes become available) -Evaluate any PHP expression or Phunkie data structure: - Some(42) - ImmList(1, 2, 3) - \$var0->map(fn(\$x) => \$x + 1) + Evaluate any PHP expression or Phunkie data structure: + Some(42) + ImmList(1, 2, 3) + \$var0->map(fn(\$x) => \$x + 1) -HELP; + HELP; return printLn($help); } diff --git a/src/Functions/evaluation.php b/src/Functions/evaluation.php index e2b37c5..1b5efae 100644 --- a/src/Functions/evaluation.php +++ b/src/Functions/evaluation.php @@ -20,6 +20,7 @@ use Phunkie\Console\Types\ReplError; use Phunkie\Console\Types\ReplSession; use Phunkie\Validation\Validation; + use function Success; use function Failure; @@ -32,8 +33,9 @@ */ function evaluateExpression(string $input, ReplSession $session): Validation { - return \Phunkie\Console\Functions\parseInput($input) - ->flatMap(fn($ast) => evaluateAst($ast, $session)); + /** @var Validation $parsed */ + $parsed = \Phunkie\Console\Functions\parseInput($input); + return $parsed->flatMap(fn(array $ast) => evaluateAst($ast, $session)); } /** @@ -67,10 +69,10 @@ function evaluateAst(array $ast, ReplSession $session): Validation // If this is an assignment, we need to track the variable name // Skip variable variables ($$var) as they're already handled in evaluateAssignment - if ($stmt->expr instanceof Expr\Assign && - $stmt->expr->var instanceof Expr\Variable && - is_string($stmt->expr->var->name)) { - return $result->map(function($evalResult) use ($stmt) { + if ($stmt->expr instanceof Expr\Assign + && $stmt->expr->var instanceof Expr\Variable + && is_string($stmt->expr->var->name)) { + return $result->map(function ($evalResult) use ($stmt) { $varName = '$' . $stmt->expr->var->name; // Create a new result with assignment metadata return new EvaluationResult( @@ -178,116 +180,116 @@ function evaluateAst(array $ast, ReplSession $session): Validation function evaluateNode(Node $node, ReplSession $session): Validation { return match (true) { - $node instanceof Scalar\Int_ => - Success(EvaluationResult::of($node->value, 'Int')), + $node instanceof Scalar\Int_ + => Success(EvaluationResult::of($node->value, 'Int')), - $node instanceof Scalar\Float_ => - Success(EvaluationResult::of($node->value, 'Float')), + $node instanceof Scalar\Float_ + => Success(EvaluationResult::of($node->value, 'Float')), - $node instanceof Scalar\String_ => - Success(EvaluationResult::of($node->value, 'String')), + $node instanceof Scalar\String_ + => Success(EvaluationResult::of($node->value, 'String')), - $node instanceof Scalar\InterpolatedString => - evaluateInterpolatedString($node, $session), + $node instanceof Scalar\InterpolatedString + => evaluateInterpolatedString($node, $session), - $node instanceof Expr\ConstFetch && $node->name->toString() === 'true' => - Success(EvaluationResult::of(true, 'Bool')), + $node instanceof Expr\ConstFetch && $node->name->toString() === 'true' + => Success(EvaluationResult::of(true, 'Bool')), - $node instanceof Expr\ConstFetch && $node->name->toString() === 'false' => - Success(EvaluationResult::of(false, 'Bool')), + $node instanceof Expr\ConstFetch && $node->name->toString() === 'false' + => Success(EvaluationResult::of(false, 'Bool')), - $node instanceof Expr\ConstFetch && $node->name->toString() === 'null' => - Success(EvaluationResult::of(null, 'Null')), + $node instanceof Expr\ConstFetch && $node->name->toString() === 'null' + => Success(EvaluationResult::of(null, 'Null')), - $node instanceof Expr\ConstFetch && $node->name->toString() === 'None' => - Success(EvaluationResult::of(\None(), getType(\None()))), + $node instanceof Expr\ConstFetch && $node->name->toString() === 'None' + => Success(EvaluationResult::of(\None(), getType(\None()))), - $node instanceof Expr\ConstFetch => - evaluateConstant($node), + $node instanceof Expr\ConstFetch + => evaluateConstant($node), - $node instanceof Expr\ClassConstFetch => - evaluateClassConstFetch($node, $session), + $node instanceof Expr\ClassConstFetch + => evaluateClassConstFetch($node, $session), - $node instanceof Expr\Variable => - evaluateVariableNode($node, $session), + $node instanceof Expr\Variable + => evaluateVariableNode($node, $session), - $node instanceof Expr\Assign => - evaluateAssignment($node, $session), + $node instanceof Expr\Assign + => evaluateAssignment($node, $session), - $node instanceof Expr\BinaryOp => - evaluateBinaryOp($node, $session), + $node instanceof Expr\BinaryOp + => evaluateBinaryOp($node, $session), - $node instanceof Expr\BooleanNot || - $node instanceof Expr\UnaryPlus || - $node instanceof Expr\UnaryMinus || - $node instanceof Expr\BitwiseNot => - evaluateUnaryOp($node, $session), + $node instanceof Expr\BooleanNot + || $node instanceof Expr\UnaryPlus + || $node instanceof Expr\UnaryMinus + || $node instanceof Expr\BitwiseNot + => evaluateUnaryOp($node, $session), - $node instanceof Expr\StaticCall => - evaluateStaticCall($node, $session), + $node instanceof Expr\StaticCall + => evaluateStaticCall($node, $session), - $node instanceof Expr\MethodCall => - evaluateMethodCall($node, $session), + $node instanceof Expr\MethodCall + => evaluateMethodCall($node, $session), - $node instanceof Expr\NullsafeMethodCall => - evaluateNullsafeMethodCall($node, $session), + $node instanceof Expr\NullsafeMethodCall + => evaluateNullsafeMethodCall($node, $session), - $node instanceof Expr\PropertyFetch => - evaluatePropertyFetch($node, $session), + $node instanceof Expr\PropertyFetch + => evaluatePropertyFetch($node, $session), - $node instanceof Expr\NullsafePropertyFetch => - evaluateNullsafePropertyFetch($node, $session), + $node instanceof Expr\NullsafePropertyFetch + => evaluateNullsafePropertyFetch($node, $session), - $node instanceof Expr\FuncCall => - evaluateFunctionCall($node, $session), + $node instanceof Expr\FuncCall + => evaluateFunctionCall($node, $session), - $node instanceof Expr\Array_ => - evaluateArray($node, $session), + $node instanceof Expr\Array_ + => evaluateArray($node, $session), - $node instanceof Expr\ArrayDimFetch => - evaluateArrayAccess($node, $session), + $node instanceof Expr\ArrayDimFetch + => evaluateArrayAccess($node, $session), - $node instanceof Expr\ArrowFunction => - evaluateArrowFunction($node, $session), + $node instanceof Expr\ArrowFunction + => evaluateArrowFunction($node, $session), - $node instanceof Expr\Closure => - evaluateClosure($node, $session), + $node instanceof Expr\Closure + => evaluateClosure($node, $session), - $node instanceof Expr\Ternary => - evaluateTernary($node, $session), + $node instanceof Expr\Ternary + => evaluateTernary($node, $session), - $node instanceof Expr\Match_ => - evaluateMatch($node, $session), + $node instanceof Expr\Match_ + => evaluateMatch($node, $session), - $node instanceof Expr\Yield_ => - evaluateYield($node, $session), + $node instanceof Expr\Yield_ + => evaluateYield($node, $session), - $node instanceof Expr\New_ => - evaluateNew($node, $session), + $node instanceof Expr\New_ + => evaluateNew($node, $session), - $node instanceof Expr\Print_ => - evaluatePrint($node, $session), + $node instanceof Expr\Print_ + => evaluatePrint($node, $session), - $node instanceof Expr\Throw_ => - evaluateThrow($node, $session), + $node instanceof Expr\Throw_ + => evaluateThrow($node, $session), - $node instanceof Expr\Instanceof_ => - evaluateInstanceof($node, $session), + $node instanceof Expr\Instanceof_ + => evaluateInstanceof($node, $session), - $node instanceof Expr\Clone_ => - evaluateClone($node, $session), + $node instanceof Expr\Clone_ + => evaluateClone($node, $session), - $node instanceof Expr\ErrorSuppress => - evaluateErrorSuppress($node, $session), + $node instanceof Expr\ErrorSuppress + => evaluateErrorSuppress($node, $session), - $node instanceof Expr\PreInc || - $node instanceof Expr\PreDec || - $node instanceof Expr\PostInc || - $node instanceof Expr\PostDec => - evaluateIncDec($node, $session), + $node instanceof Expr\PreInc + || $node instanceof Expr\PreDec + || $node instanceof Expr\PostInc + || $node instanceof Expr\PostDec + => evaluateIncDec($node, $session), - $node instanceof Node\Scalar\MagicConst => - evaluateMagicConstant($node, $session), + $node instanceof Node\Scalar\MagicConst + => evaluateMagicConstant($node, $session), default => Failure(new EvaluationError( get_class($node), @@ -308,23 +310,26 @@ function evaluateVariableNode(Expr\Variable $node, ReplSession $session): Valida // Check if this is a variable-variable ($$var) if ($node->name instanceof Node) { // Evaluate the inner expression to get the variable name - return evaluateNode($node->name, $session)->flatMap(function($result) use ($session) { - $varName = $result->value; + return evaluateNode($node->name, $session)->flatMap( + /** @param EvaluationResult $result */ + function ($result) use ($session) { + $varName = $result->value; - if (!is_string($varName)) { - return Failure(new EvaluationError('$$var', 'Variable variable name must be a string')); - } + if (!is_string($varName)) { + return Failure(new EvaluationError('$$var', 'Variable variable name must be a string')); + } - return evaluateVariable($varName, $session); - }); + return evaluateVariable($varName, $session); + } + ); } // Simple variable - name is a string - if (is_string($node->name)) { - return evaluateVariable($node->name, $session); - } - - return Failure(new EvaluationError('Variable', 'Invalid variable name')); + // At this point, if $node->name was a Node it would have been handled above, + // so it must be a string + /** @var string $varName */ + $varName = $node->name; + return evaluateVariable($varName, $session); } /** @@ -371,11 +376,11 @@ function evaluateInterpolatedString(Scalar\InterpolatedString $node, ReplSession // Convert value to string for interpolation $result .= match (true) { is_string($value) => $value, - is_numeric($value) => (string)$value, + is_numeric($value) => (string) $value, is_bool($value) => $value ? '1' : '', is_null($value) => '', is_array($value) => 'Array', - is_object($value) && method_exists($value, '__toString') => (string)$value, + is_object($value) && method_exists($value, '__toString') => (string) $value, is_object($value) => get_class($value), default => '' }; @@ -389,11 +394,11 @@ function evaluateInterpolatedString(Scalar\InterpolatedString $node, ReplSession // Convert value to string for interpolation $result .= match (true) { is_string($value) => $value, - is_numeric($value) => (string)$value, + is_numeric($value) => (string) $value, is_bool($value) => $value ? '1' : '', is_null($value) => '', is_array($value) => 'Array', - is_object($value) && method_exists($value, '__toString') => (string)$value, + is_object($value) && method_exists($value, '__toString') => (string) $value, is_object($value) => get_class($value), default => '' }; @@ -686,6 +691,14 @@ function evaluateMethodCall(Expr\MethodCall $node, ReplSession $session): Valida } } + // Check if method is callable + if (!is_callable([$obj, $methodName])) { + return Failure(new EvaluationError( + get_class($node), + sprintf('Uncaught Error: Call to undefined method %s::%s()', is_object($obj) ? get_class($obj) : gettype($obj), $methodName) + )); + } + // Call the method $value = call_user_func_array([$obj, $methodName], $args); @@ -910,7 +923,7 @@ function evaluateFunctionCall(Expr\FuncCall $node, ReplSession $session): Valida // Check if this is an expression-based function call (e.g., $funcs[0]()) // This handles cases where the function name is not a simple Name node - if (!($node->name instanceof Node\Name) && !($node->name instanceof Expr\Variable)) { + if (!($node->name instanceof Node\Name)) { // Evaluate the expression to get the callable $callableResult = evaluateNode($node->name, $session); if ($callableResult->isLeft()) { @@ -1174,8 +1187,8 @@ function evaluateFunctionCall(Expr\FuncCall $node, ReplSession $session): Valida // Check if this is an output function that shouldn't get auto-assigned $outputFunctions = ['var_dump', 'print_r', 'var_export', 'debug_zval_dump', 'debug_print_backtrace']; - $isOutputFunction = in_array(strtolower($funcName), $outputFunctions) || - in_array(strtolower($resolvedFuncName), $outputFunctions); + $isOutputFunction = in_array(strtolower($funcName), $outputFunctions) + || in_array(strtolower($resolvedFuncName), $outputFunctions); return Success(EvaluationResult::of($value, getType($value), null, [], $isOutputFunction)); } catch (\TypeError $e) { @@ -1210,7 +1223,7 @@ function getType(mixed $value): string is_float($value) => 'Float', is_string($value) => 'String', is_array($value) => 'Array', - is_callable($value) && is_object($value) && get_class($value) === 'Closure' => 'Callable', + $value instanceof \Closure => 'Callable', $value instanceof \Generator => 'Generator', is_object($value) => getObjectType($value), default => 'Unknown' @@ -1383,7 +1396,7 @@ function evaluateArrowFunction(Expr\ArrowFunction $node, ReplSession $session): { try { // Capture the arrow function AST and session for later execution - $arrowFn = function(...$args) use ($node, $session) { + $arrowFn = function (...$args) use ($node, $session) { // Create a new session with the function parameters bound $newVars = $session->variables; foreach ($node->params as $i => $param) { @@ -1440,7 +1453,7 @@ function evaluateClosure(Expr\Closure $node, ReplSession $session): Validation } // Create the closure - $closure = function(...$args) use ($node, $session, $useVars) { + $closure = function (...$args) use ($node, $session, $useVars) { // Create a new session with the function parameters bound $newVars = $session->variables; @@ -1786,7 +1799,8 @@ function evaluateWhileLoop(Node\Stmt\While_ $stmt, ReplSession $session): Valida // Execute loop body and capture variable updates $blockResult = evaluateStmtBlockWithSession($stmt->stmts, $loopSession); if ($blockResult->isLeft()) { - return $blockResult; + /** @var \Phunkie\Validation\Failure $blockResult */ + return Failure($blockResult->fold(fn($e) => $e)(fn($s) => $s)); } // Update loop session with any variable changes from the body @@ -1821,7 +1835,8 @@ function evaluateDoWhileLoop(Node\Stmt\Do_ $stmt, ReplSession $session): Validat // Execute loop body and capture variable updates $blockResult = evaluateStmtBlockWithSession($stmt->stmts, $loopSession); if ($blockResult->isLeft()) { - return $blockResult; + /** @var \Phunkie\Validation\Failure $blockResult */ + return Failure($blockResult->fold(fn($e) => $e)(fn($s) => $s)); } // Update loop session with any variable changes from the body @@ -1926,7 +1941,7 @@ function evaluateEnumDefinition(Node\Stmt\Enum_ $stmt, ReplSession $session): Va // Set up error handler to catch fatal errors from eval() $errorMessage = null; - set_error_handler(function($severity, $message, $file, $line) use (&$errorMessage) { + set_error_handler(function ($severity, $message, $file, $line) use (&$errorMessage) { $errorMessage = $message; return true; // Don't execute PHP's internal error handler }); @@ -1990,12 +2005,12 @@ function evaluatePrint(Expr\Print_ $node, ReplSession $session): Validation } elseif (is_null($value)) { // null outputs nothing } elseif (is_scalar($value)) { - $output = (string)$value; + $output = (string) $value; } elseif (is_array($value)) { $output = 'Array'; } elseif (is_object($value)) { if (method_exists($value, '__toString')) { - $output = (string)$value; + $output = (string) $value; } else { $output = 'Object'; } @@ -2168,7 +2183,7 @@ function evaluateErrorSuppress(Expr\ErrorSuppress $node, ReplSession $session): { try { // Set up error handler to suppress errors - $oldHandler = set_error_handler(function() { + $oldHandler = set_error_handler(function () { // Suppress all errors and warnings return true; }); @@ -2237,16 +2252,14 @@ function evaluateIncDec(Expr $node, ReplSession $session): Validation } // Calculate new value based on operation - $newValue = match (true) { - $node instanceof Expr\PreInc || $node instanceof Expr\PostInc => $currentValue + 1, - $node instanceof Expr\PreDec || $node instanceof Expr\PostDec => $currentValue - 1, - }; + $newValue = ($node instanceof Expr\PreInc || $node instanceof Expr\PostInc) + ? $currentValue + 1 + : $currentValue - 1; // Determine return value (pre-inc/dec returns new value, post-inc/dec returns old value) - $returnValue = match (true) { - $node instanceof Expr\PreInc || $node instanceof Expr\PreDec => $newValue, - $node instanceof Expr\PostInc || $node instanceof Expr\PostDec => $currentValue, - }; + $returnValue = ($node instanceof Expr\PreInc || $node instanceof Expr\PreDec) + ? $newValue + : $currentValue; // Update the variable in the session // For inc/dec, we return the value but update the variable via additional assignments @@ -2413,12 +2426,12 @@ function evaluateEchoStatement(Node\Stmt\Echo_ $stmt, ReplSession $session): Val } elseif (is_null($value)) { // null outputs nothing } elseif (is_scalar($value)) { - $output .= (string)$value; + $output .= (string) $value; } elseif (is_array($value)) { $output .= 'Array'; } elseif (is_object($value)) { if (method_exists($value, '__toString')) { - $output .= (string)$value; + $output .= (string) $value; } else { $output .= 'Object'; } @@ -2481,13 +2494,14 @@ function evaluateStmtBlockWithSession(array $stmts, ReplSession $session): Valid } $currentSession = $session; - $lastResult = null; + $lastResult = Success(EvaluationResult::of(null, 'Null')); foreach ($stmts as $stmt) { if ($stmt instanceof Node\Stmt\Expression) { $result = evaluateNode($stmt->expr, $currentSession); if ($result->isLeft()) { - return $result; + /** @var \Phunkie\Validation\Failure $result */ + return Failure($result->fold(fn($e) => $e)(fn($s) => $s)); } $lastResult = $result; @@ -2525,7 +2539,8 @@ function evaluateStmtBlockWithSession(array $stmts, ReplSession $session): Valid } elseif ($stmt instanceof Node\Stmt\Echo_) { $result = evaluateEchoStatement($stmt, $currentSession); if ($result->isLeft()) { - return $result; + /** @var \Phunkie\Validation\Failure $result */ + return Failure($result->fold(fn($e) => $e)(fn($s) => $s)); } $lastResult = $result; } else { @@ -2533,14 +2548,15 @@ function evaluateStmtBlockWithSession(array $stmts, ReplSession $session): Valid // (this handles if statements, nested loops, returns, etc.) $result = evaluateStmtBlock([$stmt], $currentSession); if ($result->isLeft()) { - return $result; + /** @var \Phunkie\Validation\Failure $result */ + return Failure($result->fold(fn($e) => $e)(fn($s) => $s)); } $lastResult = $result; } } return Success(new StmtBlockResult( - $lastResult ?? Success(EvaluationResult::of(null, 'Null')), + $lastResult, $currentSession )); } @@ -2560,7 +2576,7 @@ function evaluateStmtBlock(array $stmts, ReplSession $session): Validation return Success(EvaluationResult::of(null, 'Null')); } - $lastResult = null; + $lastResult = Success(EvaluationResult::of(null, 'Null')); foreach ($stmts as $stmt) { if ($stmt instanceof Node\Stmt\Return_) { @@ -2630,7 +2646,7 @@ function evaluateStmtBlock(array $stmts, ReplSession $session): Validation } } - return $lastResult ?? Success(EvaluationResult::of(null, 'Null')); + return $lastResult; } /** @@ -2715,8 +2731,7 @@ function evaluateBinaryOp(Expr\BinaryOp $node, ReplSession $session): Validation $node instanceof Expr\BinaryOp\ShiftLeft => $left << $right, $node instanceof Expr\BinaryOp\ShiftRight => $left >> $right, - // Null coalescing - $node instanceof Expr\BinaryOp\Coalesce => $left ?? $right, + // Note: Coalesce is handled above with short-circuit evaluation default => throw new \RuntimeException('Unsupported binary operation: ' . get_class($node)) }; @@ -2733,11 +2748,11 @@ function evaluateBinaryOp(Expr\BinaryOp $node, ReplSession $session): Validation /** * Evaluates a unary operation. * - * @param Node $node + * @param Expr\UnaryPlus|Expr\UnaryMinus|Expr\BooleanNot|Expr\BitwiseNot $node * @param ReplSession $session * @return Validation */ -function evaluateUnaryOp(Node $node, ReplSession $session): Validation +function evaluateUnaryOp(Expr $node, ReplSession $session): Validation { // Evaluate the operand $exprResult = evaluateNode($node->expr, $session); @@ -2747,11 +2762,11 @@ function evaluateUnaryOp(Node $node, ReplSession $session): Validation $value = $exprResult->getOrElse(null)->value; try { - $result = match (true) { - $node instanceof Expr\UnaryPlus => +$value, - $node instanceof Expr\UnaryMinus => -$value, - $node instanceof Expr\BooleanNot => !$value, - $node instanceof Expr\BitwiseNot => ~$value, + $result = match (get_class($node)) { + Expr\UnaryPlus::class => +$value, + Expr\UnaryMinus::class => -$value, + Expr\BooleanNot::class => !$value, + Expr\BitwiseNot::class => ~$value, default => throw new \RuntimeException('Unsupported unary operation: ' . get_class($node)) }; @@ -2778,10 +2793,8 @@ function evaluateArray(Expr\Array_ $node, ReplSession $session): Validation $array = []; foreach ($node->items as $item) { - if ($item === null) { - continue; - } - + // PHPStan says this is always ArrayItem + $key = $item->key; // Evaluate the value $valueResult = evaluateNode($item->value, $session); if ($valueResult->isLeft()) { @@ -2926,19 +2939,25 @@ function evaluateAssignment(Expr\Assign $node, ReplSession $session): Validation // Handle variable variables ($$var = value) if ($node->var->name instanceof Expr\Variable || $node->var->name instanceof Node) { // Evaluate the variable name - return evaluateNode($node->var->name, $session)->flatMap(function($nameResult) use ($node, $session) { - $varName = $nameResult->value; + return evaluateNode($node->var->name, $session)->flatMap( + /** @param EvaluationResult $nameResult */ + function ($nameResult) use ($node, $session) { + $varName = $nameResult->value; - if (!is_string($varName)) { - return Failure(new EvaluationError('$$var', 'Variable variable name must be a string')); - } + if (!is_string($varName)) { + return Failure(new EvaluationError('$$var', 'Variable variable name must be a string')); + } - // Evaluate the value to assign - return evaluateNode($node->expr, $session)->map(function($valueResult) use ($varName) { - $value = $valueResult->value; - return EvaluationResult::of($value, getType($value), '$' . $varName); - }); - }); + // Evaluate the value to assign + return evaluateNode($node->expr, $session)->map( + /** @param EvaluationResult $valueResult */ + function ($valueResult) use ($varName) { + $value = $valueResult->value; + return EvaluationResult::of($value, getType($value), '$' . $varName); + } + ); + } + ); } $varName = '$' . $node->var->name; @@ -2973,6 +2992,12 @@ function evaluateAssignment(Expr\Assign $node, ReplSession $session): Validation function evaluateArrayElementAssignment(Expr\Assign $node, ReplSession $session): Validation { try { + if (!($node->var instanceof Expr\ArrayDimFetch)) { + return Failure(new EvaluationError( + 'ArrayAssignment', + 'Expected array dimension fetch, got ' . get_class($node->var) + )); + } $arrayDimFetch = $node->var; // Get the base variable name @@ -3043,6 +3068,12 @@ function evaluateArrayElementAssignment(Expr\Assign $node, ReplSession $session) function evaluatePropertyAssignment(Expr\Assign $node, ReplSession $session): Validation { try { + if (!($node->var instanceof Expr\PropertyFetch)) { + return Failure(new EvaluationError( + 'PropertyAssignment', + 'Expected property fetch, got ' . get_class($node->var) + )); + } $propertyFetch = $node->var; // Navigate to the target object by evaluating the left side except the final property @@ -3147,7 +3178,14 @@ function evaluateNamespace(Node\Stmt\Namespace_ $stmt, ReplSession $session): Va function evaluateListAssignment(Expr\Assign $node, ReplSession $session): Validation { try { - $list = $node->var; + if ($node->var instanceof Expr\List_ || $node->var instanceof Expr\Array_) { + $list = $node->var; + } else { + return Failure(new EvaluationError( + 'ListAssignment', + 'Expected list() or [...] on left-hand side, got ' . get_class($node->var) + )); + } // Evaluate the right-hand side (should be an array) $rhsResult = evaluateNode($node->expr, $session); @@ -3239,12 +3277,10 @@ function isTypeValid(mixed $value, Node\Identifier|Node\Name $type): bool 'null' => is_null($value), default => false }; - } elseif ($type instanceof Node\Name) { - // Class/interface type - return is_object($value) && is_a($value, $typeName); } - return false; + // Class/interface type (this is a Name) + return is_object($value) && is_a($value, $typeName); } /** @@ -3256,19 +3292,15 @@ function isTypeValid(mixed $value, Node\Identifier|Node\Name $type): bool */ function isUnionTypeValid(mixed $value, Node\UnionType $unionType): bool { + // UnionType->types contains: Identifier | Name | IntersectionType foreach ($unionType->types as $type) { - if ($type instanceof Node\UnionType) { - // Nested union types - if (isUnionTypeValid($value, $type)) { - return true; - } - } elseif ($type instanceof Node\IntersectionType) { - // Union containing intersection + if ($type instanceof Node\IntersectionType) { + // Union containing intersection (e.g., (A&B)|C) if (isIntersectionTypeValid($value, $type)) { return true; } } else { - // Regular type + // Regular type (Identifier or Name) if (isTypeValid($value, $type)) { return true; } @@ -3286,22 +3318,11 @@ function isUnionTypeValid(mixed $value, Node\UnionType $unionType): bool */ function isIntersectionTypeValid(mixed $value, Node\IntersectionType $intersectionType): bool { + // IntersectionType->types contains: Identifier | Name only foreach ($intersectionType->types as $type) { - if ($type instanceof Node\IntersectionType) { - // Nested intersection types - if (!isIntersectionTypeValid($value, $type)) { - return false; - } - } elseif ($type instanceof Node\UnionType) { - // Intersection containing union (rare but possible) - if (!isUnionTypeValid($value, $type)) { - return false; - } - } else { - // Regular type - if (!isTypeValid($value, $type)) { - return false; - } + // All types in intersection must be Identifier or Name + if (!isTypeValid($value, $type)) { + return false; } } return true; @@ -3472,7 +3493,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Create the function if ($hasYield) { // For generator functions, we need to create a function that returns a Generator - $func = function(...$args) use ($node, $session, $funcName) { + $func = function (...$args) use ($node, $session, $funcName) { // Validate argument count and types before executing foreach ($node->params as $i => $param) { // Check if argument is missing for required parameter @@ -3565,19 +3586,20 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess } else { yield $value; } - } else { - // Handle other statements in the function body + } elseif ($stmt instanceof Node\Stmt\Expression) { + // Handle expression statements in the function body $result = evaluateNode($stmt->expr, $newSession); if ($result->isLeft()) { $error = $result->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Statement evaluation failed: ' . $error->reason); } } + // Other statement types (Return, etc.) can be ignored here } }; } else { // For regular functions - $func = function(...$args) use ($node, $session, $funcName) { + $func = function (...$args) use ($node, $session, $funcName) { // Validate argument count and types before executing foreach ($node->params as $i => $param) { // Check if argument is missing for required parameter @@ -3929,7 +3951,7 @@ function evaluateAnonymousClass(Expr\New_ $node, ReplSession $session): Validati // Set up error handler $errorMessage = null; - set_error_handler(function($severity, $message, $file, $line) use (&$errorMessage) { + set_error_handler(function ($severity, $message, $file, $line) use (&$errorMessage) { $errorMessage = $message; return true; }); @@ -3985,12 +4007,12 @@ function evaluateInterfaceDefinition(Node\Stmt\Interface_ $interfaceNode, ReplSe if ($interfaceName === null) { return Failure(new EvaluationError( 'Interface', - 'Interface must have a name' + 'Interface name must be a string' )); } // Check if interface already exists - if (interface_exists($interfaceName, false)) { + if ($session->isEntityDefined($interfaceName, 'interface')) { return Failure(new EvaluationError( $interfaceName, "Interface $interfaceName already exists" @@ -4015,14 +4037,14 @@ function evaluateInterfaceDefinition(Node\Stmt\Interface_ $interfaceNode, ReplSe $code = $printer->prettyPrint([$interfaceNode]); // Add namespace context if needed - if (isset($session->namespace) && !$session->namespace->isEmpty()) { - $namespace = $session->namespace->get(); + if ($session->currentNamespace !== null) { + $namespace = $session->currentNamespace; $code = "namespace $namespace;\n" . $code; } // Set up error handler $errorMessage = null; - set_error_handler(function($severity, $message, $file, $line) use (&$errorMessage) { + set_error_handler(function ($severity, $message, $file, $line) use (&$errorMessage) { $errorMessage = $message; return true; }); @@ -4041,7 +4063,7 @@ function evaluateInterfaceDefinition(Node\Stmt\Interface_ $interfaceNode, ReplSe } // Verify interface was created - if (!interface_exists($interfaceName, false)) { + if (!$session->isEntityDefined($interfaceName, 'interface', 1)) { return Failure(new EvaluationError( $interfaceName, 'Failed to define interface' @@ -4096,14 +4118,14 @@ function evaluateTraitDefinition(Node\Stmt\Trait_ $traitNode, ReplSession $sessi $code = $printer->prettyPrint([$traitNode]); // Add namespace context if needed - if (isset($session->namespace) && !$session->namespace->isEmpty()) { - $namespace = $session->namespace->get(); + if ($session->currentNamespace !== null) { + $namespace = $session->currentNamespace; $code = "namespace $namespace;\n" . $code; } // Set up error handler $errorMessage = null; - set_error_handler(function($severity, $message, $file, $line) use (&$errorMessage) { + set_error_handler(function ($severity, $message, $file, $line) use (&$errorMessage) { $errorMessage = $message; return true; }); @@ -4122,7 +4144,7 @@ function evaluateTraitDefinition(Node\Stmt\Trait_ $traitNode, ReplSession $sessi } // Verify trait was created - if (!trait_exists($traitName, false)) { + if (!$session->isEntityDefined($traitName, 'trait', 1)) { return Failure(new EvaluationError( $traitName, 'Failed to define trait' @@ -4210,7 +4232,7 @@ function evaluateClassDefinition(Node\Stmt\Class_ $classNode, ReplSession $sessi // Set up error handler to catch warnings and notices from eval() $errorMessage = null; - set_error_handler(function($severity, $message, $file, $line) use (&$errorMessage) { + set_error_handler(function ($severity, $message, $file, $line) use (&$errorMessage) { $errorMessage = $message; return true; // Don't execute PHP's internal error handler }); @@ -4231,7 +4253,7 @@ function evaluateClassDefinition(Node\Stmt\Class_ $classNode, ReplSession $sessi } // Verify the class was defined - if (!class_exists($className, false)) { + if (!$session->isEntityDefined($className, 'class', 1)) { return Failure(new EvaluationError( $className, "Failed to define class '$className'" @@ -4285,3 +4307,13 @@ function cleanErrorMessage(string $message): string return trim($message); } + +/** + * Checks if a class, interface, or trait exists at runtime. + * This helper isolates dynamic existence checks to prevent PHPStan from + * inferring "always false" based on static codebase analysis. + * + * @param string $name + * @param string $kind 'class', 'interface', or 'trait' + * @return bool + */ diff --git a/src/Functions/parsing.php b/src/Functions/parsing.php index c3570cc..5364b91 100644 --- a/src/Functions/parsing.php +++ b/src/Functions/parsing.php @@ -15,6 +15,7 @@ use PhpParser\ParserFactory; use Phunkie\Console\Types\ParseError; use Phunkie\Validation\Validation; + use function Success; use function Failure; @@ -27,7 +28,7 @@ function parseInput(string $input): Validation { try { - $parser = (new ParserFactory)->createForNewestSupportedVersion(); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); // Preprocess: Don't add extra semicolons - PHP-Parser handles this // The original regex was causing issues with empty blocks and method definitions diff --git a/src/Functions/session.php b/src/Functions/session.php index 220572f..dffd62f 100644 --- a/src/Functions/session.php +++ b/src/Functions/session.php @@ -52,8 +52,9 @@ function modifySession(callable $f): State */ function addToHistory(string $expression): State { - return modifySession(fn(ReplSession $s) => - new ReplSession( + return modifySession( + fn(ReplSession $s) + => new ReplSession( $s->history->append($expression), $s->variables, $s->colorEnabled, @@ -70,12 +71,13 @@ function addToHistory(string $expression): State * * @param string $name * @param mixed $value - * @return State + * @return State */ function setVariable(string $name, mixed $value): State { - return modifySession(fn(ReplSession $s) => - new ReplSession( + return modifySession( + fn(ReplSession $s) + => new ReplSession( $s->history, $s->variables->plus($name, $value), $s->colorEnabled, @@ -91,7 +93,7 @@ function setVariable(string $name, mixed $value): State * Gets a variable from the session. * * @param string $name - * @return State + * @return State */ function getVariable(string $name): State { @@ -105,7 +107,7 @@ function getVariable(string $name): State */ function nextVariable(): State { - return new State(function(ReplSession $s): Pair { + return new State(function (ReplSession $s): Pair { $varName = '$var' . $s->variableCounter; $newSession = new ReplSession( $s->history, @@ -148,8 +150,9 @@ function getHistory(): State */ function setColors(bool $enabled): State { - return modifySession(fn(ReplSession $s) => - new ReplSession( + return modifySession( + fn(ReplSession $s) + => new ReplSession( $s->history, $s->variables, $enabled, @@ -178,8 +181,9 @@ function isColorEnabled(): State */ function resetSession(): State { - return modifySession(fn(ReplSession $s) => - new ReplSession( + return modifySession( + fn(ReplSession $s) + => new ReplSession( ImmList(), ImmMap(), $s->colorEnabled, @@ -199,8 +203,9 @@ function resetSession(): State */ function setNamespace(?string $namespace): State { - return modifySession(fn(ReplSession $s) => - new ReplSession( + return modifySession( + fn(ReplSession $s) + => new ReplSession( $s->history, $s->variables, $s->colorEnabled, @@ -231,8 +236,9 @@ function getCurrentNamespace(): State */ function addUseStatement(string $alias, string $fullName): State { - return modifySession(fn(ReplSession $s) => - new ReplSession( + return modifySession( + fn(ReplSession $s) + => new ReplSession( $s->history, $s->variables, $s->colorEnabled, diff --git a/src/Repl/ReplLoop.php b/src/Repl/ReplLoop.php index e191afc..1f31785 100644 --- a/src/Repl/ReplLoop.php +++ b/src/Repl/ReplLoop.php @@ -15,8 +15,11 @@ use Phunkie\Console\Types\EvaluationResult; use Phunkie\Console\Types\ContinueRepl; use Phunkie\Console\Types\ExitRepl; +use Phunkie\Console\Types\ReplResult; +use Phunkie\Console\Types\ReplError; use Phunkie\Effect\IO\IO; use Phunkie\Utils\Trampoline\Trampoline; + use function Phunkie\Effect\Functions\console\printLn; use function Phunkie\Console\Functions\{evaluateExpression, addToHistory, setVariable, nextVariable, isColorEnabled, printHelp, printVariables, printHistory, printBanner, readLineFiltered, resetSession, setNamespace, addUseStatement}; use function Phunkie\Functions\trampoline\{More, Done}; @@ -295,18 +298,18 @@ function processCommand(string $command, ReplSession $session): IO } // Check for :kind command with expression argument - if (preg_match('/^:kind\s+(.+)$/', $command, $matches)) { + if (preg_match('/^:(?:kind|k)\s+(.+)$/', $command, $matches)) { return showKindCommand(trim($matches[1]), $session); } return match ($command) { ':exit', ':quit' => new IO(fn() => new ExitRepl()), - ':help' => printHelp()->map(fn() => new ContinueRepl($session)), - ':vars' => printVariables($session)->map(fn() => new ContinueRepl($session)), - ':history' => printHistory($session)->map(fn() => new ContinueRepl($session)), + ':help' => printHelp()->as(new ContinueRepl($session)), + ':vars' => printVariables($session)->as(new ContinueRepl($session)), + ':history' => printHistory($session)->as(new ContinueRepl($session)), ':reset' => resetReplState($session), default => printLn("Unknown command: $command") - ->map(fn() => new ContinueRepl($session)) + ->as(new ContinueRepl($session)) }; } @@ -334,7 +337,7 @@ function resetReplState(ReplSession $session): IO */ function loadFile(string $filepath, ReplSession $session): IO { - return new IO(function() use ($filepath, $session) { + return new IO(function () use ($filepath, $session) { // Check if file exists if (!file_exists($filepath)) { printLn("Error: File not found: $filepath")->unsafeRun(); @@ -434,7 +437,7 @@ function findModuleFile(string $package, string $packagePath, string $module): ? */ function importFunction(string $import, ReplSession $session): IO { - return new IO(function() use ($import, $session) { + return new IO(function () use ($import, $session) { // Parse package::module/function or module/function pattern $package = 'phunkie'; // Default to core phunkie $packagePath = 'Phunkie/Functions'; @@ -490,10 +493,10 @@ function importFunction(string $import, ReplSession $session): IO $availableFunctions = $functionMatches[1]; // Filter out internal functions (those starting with assert or format) - $exportedFunctions = array_filter($availableFunctions, function($name) { + $exportedFunctions = array_filter($availableFunctions, function ($name) { return !in_array($name, ['assertListOrString', 'formatError', 'ImmList', 'Nil', 'Cons', - 'ImmSet', 'ImmMap', 'Pair', 'Some', 'None', 'Success', 'Failure', - 'Unit', 'Tuple', 'Function1']); + 'ImmSet', 'ImmMap', 'Pair', 'Some', 'None', 'Success', 'Failure', + 'Unit', 'Tuple', 'Function1']); }); // Determine which functions to import @@ -557,19 +560,19 @@ function $function(...\$args) { */ function showTypeCommand(string $expression, ReplSession $session): IO { - return new IO(function() use ($expression, $session) { + return new IO(function () use ($expression, $session) { // Evaluate the expression to get the value $result = evaluateExpression($expression, $session); // Use fold to handle both success and failure cases $result->fold( // Failure case: error is passed to this function - function($error) use ($session) { + function ($error) use ($session) { printLn(formatError($error, $session))->unsafeRun(); } )( // Success case: result is passed to this function - function($evalResult) { + function ($evalResult) { // Use Phunkie's showType function to get the type $type = \Phunkie\Functions\show\showType($evalResult->value); printLn($type)->unsafeRun(); @@ -591,53 +594,50 @@ function($evalResult) { */ function showKindCommand(string $expression, ReplSession $session): IO { - return new IO(function() use ($expression, $session) { + return new IO(function () use ($expression, $session) { // Evaluate the expression to get the value $result = evaluateExpression($expression, $session); // Use fold to handle both success and failure cases $result->fold( // Failure case: error is passed to this function - function($error) use ($session) { + function ($error) use ($session) { printLn(formatError($error, $session))->unsafeRun(); } )( // Success case: result is passed to this function - function($evalResult) { + function ($evalResult) { // Get the type of the value $type = \Phunkie\Functions\show\showType($evalResult->value); - // Get the actual class name for more accurate kind lookup - $value = $evalResult->value; - $className = is_object($value) ? get_class($value) : null; - - // Extract base type name for showKind + // For :kind command on a value, we usually want the kind of the type constructor + // e.g. Some(1) -> Option -> we want kind of "Option" (* -> *) + // If we passed "Option" to showKind, we'd get "*" $baseType = $type; - // Handle special cases - if ($className === 'Phunkie\Types\None') { + // Handle None -> Option + if ($type === 'None') { $baseType = 'Option'; - } elseif ($className === 'Phunkie\Types\Pair') { + } + + // Handle Tuple syntax (Int, String) -> Pair + elseif (str_starts_with($type, '(') && str_contains($type, ',')) { $baseType = 'Pair'; } elseif (preg_match('/^([^<(]+)/', $type, $matches)) { - // Remove type parameters: "Option" -> "Option", "List" -> "List" $baseType = $matches[1]; } - // Use Phunkie's showKind function to get the kind - $kindOption = \Phunkie\Functions\show\showKind($baseType); - - if ($kindOption->isDefined()) { + $kind = \Phunkie\Functions\show\showKind($baseType); + if ($kind->isDefined()) { // Extract just the kind signature (e.g., "* -> *") - $kindInfo = $kindOption->get(); - // Parse the kind info to extract just the signature + $kindInfo = $kind->get(); if (preg_match('/:: (.+)$/', $kindInfo, $matches)) { printLn($matches[1])->unsafeRun(); } else { printLn($kindInfo)->unsafeRun(); } } else { - printLn("Error: Could not determine kind for type: $baseType")->unsafeRun(); + printLn("Error: Could not calculate kind for: $type ($baseType)")->unsafeRun(); } } ); @@ -646,37 +646,25 @@ function($evalResult) { }); } + + /** - * Formats an error message with optional color support. + * Formats an error message for display. * - * @param \Phunkie\Console\Types\ReplError $error + * @param ReplError $error * @param ReplSession $session * @return string */ -function formatError($error, ReplSession $session): string +function formatError(ReplError $error, ReplSession $session): string { - // Extract error type from class name (e.g., "EvaluationError" -> "Error") - $className = get_class($error); - $parts = explode('\\', $className); - $errorType = end($parts); - - // Map error types to display format: - // - EvaluationError -> "Error" - // - ParseError -> "Parse error" - // - TypeError -> "TypeError" - if ($errorType === 'EvaluationError') { - $errorType = 'Error'; - } elseif ($errorType === 'ParseError') { - $errorType = 'Parse error'; - } - // Otherwise keep as-is (e.g., "TypeError") + $message = $error->message(); if ($session->colorEnabled) { - // Red error type, normal color for the rest - return "\033[31m{$errorType}:\033[0m {$error->reason}"; + // Red error prefix + return "\033[31mError:\033[0m {$message}"; } - return "{$errorType}: {$error->reason}"; + return "Error: {$message}"; } /** @@ -694,7 +682,7 @@ function evaluateAndDisplay(string $expression, ReplSession $session): IO return $evalResult->fold( // Failure case: error is passed to this function fn($error) => printLn(formatError($error, $session)) - ->map(fn() => new ContinueRepl($session)) + ->as(new ContinueRepl($session)) )( // Success case: result is passed to this function fn($result) => displayResult($result, $session, $expression) diff --git a/src/Types/EvaluationResult.php b/src/Types/EvaluationResult.php index f50ee72..ce2edc1 100644 --- a/src/Types/EvaluationResult.php +++ b/src/Types/EvaluationResult.php @@ -46,14 +46,14 @@ private function formatValue(mixed $value): string return match (true) { is_null($value) => 'null', is_bool($value) => $value ? 'true' : 'false', - is_int($value) => (string)$value, - is_float($value) => (string)$value, + is_int($value) => (string) $value, + is_float($value) => (string) $value, is_string($value) => '"' . addslashes($value) . '"', is_array($value) => $this->formatArray($value), is_callable($value) => '', is_object($value) && method_exists($value, 'show') => $value->show(), is_object($value) && method_exists($value, 'toString') => $value->toString(), - is_object($value) && method_exists($value, '__toString') => (string)$value, + is_object($value) && method_exists($value, '__toString') => (string) $value, is_object($value) => $this->formatObject($value), default => var_export($value, true) }; diff --git a/src/Types/ReplSession.php b/src/Types/ReplSession.php index 2aab3d1..a20bdfb 100644 --- a/src/Types/ReplSession.php +++ b/src/Types/ReplSession.php @@ -22,8 +22,18 @@ */ final readonly class ReplSession { + /** @var ImmMap */ public ImmMap $useStatements; + /** + * @param ImmList $history + * @param ImmMap $variables + * @param bool $colorEnabled + * @param int $variableCounter + * @param string $incompleteInput + * @param string|null $currentNamespace + * @param ImmMap|null $useStatements + */ public function __construct( public ImmList $history, public ImmMap $variables, @@ -49,4 +59,17 @@ public static function empty(): ReplSession ImmMap() ); } + /** + * Checks if a class, interface, or trait exists in the runtime environment. + * This encapsulates global state access, serving as a boundary for static analysis. + */ + public function isEntityDefined(string $name, string $kind = 'class', int $attempt = 0): bool + { + return match ($kind) { + 'class' => class_exists($name, false), + 'interface' => interface_exists($name, false), + 'trait' => trait_exists($name, false), + default => false + }; + } } diff --git a/tests/Acceptance/Fixtures/.gitkeep b/tests/Acceptance/Fixtures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/Acceptance/ReplSteps.php b/tests/Acceptance/ReplSteps.php index 18fdd6f..7a3db2d 100644 --- a/tests/Acceptance/ReplSteps.php +++ b/tests/Acceptance/ReplSteps.php @@ -56,6 +56,14 @@ private function startRepl(string $command = 'php bin/phunkie'): void $newOutput = ReplOutputReader::readOutput($stdout); $this->output .= $newOutput; } + // Also capture stderr for debugging + $stderr = $this->processManager->getStderr(); + if ($stderr !== null) { + $errorOutput = ReplOutputReader::readOutput($stderr); + if ($errorOutput !== '') { + $this->output .= "\n[STDERR]: " . $errorOutput; + } + } } else { $colorEnabled = str_contains($command, '-c'); $this->directManager->start($colorEnabled); @@ -203,7 +211,19 @@ private function shouldUseProcessManager(): bool #[Given('I run :command')] public function iRun(string $command): void { - $this->cleanup(); + // Don't call cleanup() here as it deletes files created in Given steps + if ($this->useProcessManager) { + $this->processManager->terminate(); + } else { + $this->directManager->reset(); + } + + $this->output = ''; + $this->inputs = []; + $this->sentInputs = []; + $this->variableCount = 0; + $this->hasExited = false; + $this->useProcessManager = true; // Always use process manager for "I run" scenarios $this->startRepl($command); } @@ -233,8 +253,8 @@ public function iShouldSeeOutputContaining(string $expected): void if (!str_contains($this->output, $expected)) { throw new \Exception( - "Expected output to contain '$expected'\n" . - "Actual output:\n" . $this->output + "Expected output to contain '$expected'\n" + . "Actual output:\n" . $this->output ); } } @@ -253,8 +273,8 @@ public function theReplShouldSupportColors(): void // When -c flag is used, the prompt should have color codes if (!str_contains($this->output, "\033[")) { throw new \Exception( - "Expected output to contain ANSI color codes\n" . - "Actual output:\n" . $this->output + "Expected output to contain ANSI color codes\n" + . "Actual output:\n" . $this->output ); } } @@ -275,8 +295,8 @@ public function theSessionShouldHaveVariables(int $count): void $varName = '$var' . $i; if (!str_contains($this->output, $varName)) { throw new \Exception( - "Expected session to have variable $varName\n" . - "Actual output:\n" . $this->output + "Expected session to have variable $varName\n" + . "Actual output:\n" . $this->output ); } } @@ -348,8 +368,8 @@ public function iShouldSeeOutputContainingInVariable(string $expected, string $v $pattern = preg_quote($variable, '/') . '.*' . preg_quote($expected, '/'); if (!preg_match('/' . $pattern . '/s', $this->output)) { throw new \Exception( - "Expected output to contain '$expected' in variable '$variable'\n" . - "Actual output:\n" . $this->output + "Expected output to contain '$expected' in variable '$variable'\n" + . "Actual output:\n" . $this->output ); } } @@ -359,8 +379,8 @@ public function iShouldNotSee(string $unexpected): void { if (str_contains($this->output, $unexpected)) { throw new \Exception( - "Expected output NOT to contain '$unexpected'\n" . - "Actual output:\n" . $this->output + "Expected output NOT to contain '$unexpected'\n" + . "Actual output:\n" . $this->output ); } } @@ -371,15 +391,15 @@ public function iShouldSeeErrorContaining(string $expected): void { if (!str_contains($this->output, 'Error') && !str_contains($this->output, 'error')) { throw new \Exception( - "Expected output to contain an error\n" . - "Actual output:\n" . $this->output + "Expected output to contain an error\n" + . "Actual output:\n" . $this->output ); } if (!str_contains($this->output, $expected)) { throw new \Exception( - "Expected error to contain '$expected'\n" . - "Actual output:\n" . $this->output + "Expected error to contain '$expected'\n" + . "Actual output:\n" . $this->output ); } } diff --git a/tests/Acceptance/Support/DirectReplManager.php b/tests/Acceptance/Support/DirectReplManager.php index 3e4a289..4b189cb 100644 --- a/tests/Acceptance/Support/DirectReplManager.php +++ b/tests/Acceptance/Support/DirectReplManager.php @@ -1,4 +1,5 @@