From 42805decb5b9ef1fe5a022959620b93fbece4ac5 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sat, 6 Dec 2025 16:02:40 +0000 Subject: [PATCH 01/14] Finalize static analysis, add IOApp runner feature, and prep for 1.0.0 --- .github/workflows/{tests.yml => ci.yml} | 40 +- .php-cs-fixer.cache | 1 + THOUGHTS.md | 187 +++++++++ bin/phunkie | 23 +- composer.json | 16 +- composer.lock | 510 +++++++++++++----------- features/execution/run_app.feature | 49 +++ src/Functions/display.php | 1 + src/Functions/evaluation.php | 70 ++-- src/Functions/parsing.php | 3 +- src/Functions/session.php | 36 +- src/Repl/ReplLoop.php | 104 ++--- src/Types/CommandError.php | 3 +- src/Types/ContinueRepl.php | 4 +- src/Types/EvaluationError.php | 3 +- src/Types/EvaluationResult.php | 5 +- src/Types/ExitRepl.php | 4 +- src/Types/ParseError.php | 3 +- src/Types/ReplResult.php | 4 +- src/Types/TypeError.php | 3 +- src/Types/VariableNotFoundError.php | 3 +- tests/Acceptance/ReplSteps.php | 14 +- 22 files changed, 722 insertions(+), 364 deletions(-) rename .github/workflows/{tests.yml => ci.yml} (56%) create mode 100644 .php-cs-fixer.cache create mode 100644 THOUGHTS.md create mode 100644 features/execution/run_app.feature diff --git a/.github/workflows/tests.yml b/.github/workflows/ci.yml similarity index 56% rename from .github/workflows/tests.yml rename to .github/workflows/ci.yml index f4baf10..15aa7ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/ci.yml @@ -9,27 +9,21 @@ on: 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 @@ -38,12 +32,40 @@ jobs: 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 + + lint: + name: Code Style (PHP-CS-Fixer) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: mbstring, readline + coverage: none + tools: php-cs-fixer + - name: Run PHP-CS-Fixer + run: php-cs-fixer fix --dry-run --diff --verbose + + static-analysis: + name: Static Analysis (PHPStan) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: mbstring, readline + coverage: none + - name: Install dependencies + run: composer install --prefer-dist --no-progress + - name: Run PHPStan + run: vendor/bin/phpstan analyse src diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache new file mode 100644 index 0000000..e01ec41 --- /dev/null +++ b/.php-cs-fixer.cache @@ -0,0 +1 @@ +{"php":"8.2.12","version":"3.75.0:v3.75.0#399a128ff2fdaf4281e4e79b755693286cdf325c","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_parentheses":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"hashes":{"src\/Types\/ReplSession.php":"17546e77b5eac5d19e33c6300077ee58","src\/Types\/CommandError.php":"30977920d96a17285089fe7cab38f079","src\/Types\/TypeError.php":"7e7f08d2bafadf25a9fef554d28235a2","src\/Types\/ContinueRepl.php":"da40e20d051546441c8e0e693e86b781","src\/Types\/ParseError.php":"5c9ae2d644dd0943c71d1fa7d74260ce","src\/Types\/ExitRepl.php":"9d93ca80ff79297dfc9f01943561f990","src\/Types\/EvaluationError.php":"691e557e00f3cbc34273cda5f6f97866","src\/Types\/ReplError.php":"dc627149a374a4f4cff98db2f798b896","src\/Types\/EvaluationResult.php":"1be4b54b4c4087e1534d9bfca1f58307","src\/Types\/ReplResult.php":"2f1e93fe316e51f07c2faa23d6887e78","src\/Types\/VariableNotFoundError.php":"3fc511af94c661c6a7e3c7ec249f1520","src\/Repl\/ReplLoop.php":"aeba47348fa51db95e9e384308e3fd53","src\/Functions\/evaluation.php":"540b217f681737d1ac81167b81ddf115","src\/Functions\/terminal.php":"4c07be5d3bf603c6d5d8ee310138a2d3","src\/Functions\/session.php":"e208fe6ebe59b6225a1451d9965e64e6","src\/Functions\/parsing.php":"7cb9fe7e58d684f2e52d7ba5c5670bef","src\/Functions\/display.php":"e10c2cfaa5996a3d04b3ccc2a1cb164f","src\/Functions\/common.php":"38978a7963d52026b0aeaba4ffc10fe2"}} \ No newline at end of file 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/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..bcd69ad 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,8 +10,8 @@ ], "require": { "php": "^8.2 || ^8.3 || ^8.4", - "phunkie/phunkie": "^0.11.7", - "phunkie/effect": "^0.4", + "phunkie/phunkie": "^1.0.0", + "phunkie/effect": "^1.0.0", "nikic/php-parser": "^5.6" }, "require-dev": { @@ -41,5 +41,11 @@ }, "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": "vendor/bin/phpstan analyse" + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index da0445d..6b5198f 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": "a577f8e70b97917e9d3eb6dd5e581992", "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,47 +60,59 @@ ], "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", - "source": { - "type": "git", - "url": "https://github.com/phunkie/effect.git", - "reference": "d5a3fb83fe4a31bbfe84dd46c85e8fc6754d5700" - }, + "version": "1.0.0", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phunkie/effect/zipball/d5a3fb83fe4a31bbfe84dd46c85e8fc6754d5700", - "reference": "d5a3fb83fe4a31bbfe84dd46c85e8fc6754d5700", - "shasum": "" + "type": "path", + "url": "../effect", + "reference": "dd033e03edb4b02bbe2d36b6c503556d06308fc7" }, "require": { - "php": ">=8.2", - "phunkie/phunkie": "^0.11" + "php": "^8.2 || ^8.3 || ^8.4", + "phunkie/phunkie": "^1.0.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.75", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.5", - "phunkie/phunkie-console": "dev-master" + "phunkie/console": "^1.0.0" }, "suggest": { "ext-parallel": "Required for parallel execution using threads. PHP must be compiled with ZTS support." }, "type": "library", "autoload": { + "psr-4": { + "Phunkie\\Effect\\": "src/" + }, "files": [ "src/Functions/common.php" - ], + ] + }, + "autoload-dev": { "psr-4": { - "Phunkie\\Effect\\": "src/" + "Tests\\Unit\\Phunkie\\Effect\\": "tests/Unit/" } }, - "notification-url": "https://packagist.org/downloads/", + "scripts": { + "test": [ + "phpunit" + ], + "phpstan": [ + "phpstan analyse" + ], + "cs-fix": [ + "php-cs-fixer fix" + ], + "cs-check": [ + "php-cs-fixer fix --dry-run --diff" + ] + }, "license": [ "MIT" ], @@ -111,47 +123,60 @@ } ], "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" - }, - "time": "2025-10-15T20:35:28+00:00" + "transport-options": { + "relative": true + } }, { "name": "phunkie/phunkie", - "version": "0.11.7", - "source": { - "type": "git", - "url": "https://github.com/phunkie/phunkie.git", - "reference": "1d7620e41062c86e3f1d3dbf22f7035b4675667e" - }, + "version": "1.0.0", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phunkie/phunkie/zipball/1d7620e41062c86e3f1d3dbf22f7035b4675667e", - "reference": "1d7620e41062c86e3f1d3dbf22f7035b4675667e", - "shasum": "" + "type": "path", + "url": "../phunkie", + "reference": "898fed43f1ef0eee9a82b0c0440a58a83e9f82d9" }, "require": { - "php": ">=8.1" + "php": "^8.2 || ^8.3 || ^8.4" }, "require-dev": { "ergebnis/composer-normalize": "^2", "friendsofphp/php-cs-fixer": "^3", "giorgiosironi/eris": "^0", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9" }, "type": "library", "autoload": { - "files": [ - "src/Phunkie/Functions/common.php" - ], "psr-0": { "": [ "src/" ] + }, + "files": [ + "src/Phunkie/Functions/common.php" + ] + }, + "autoload-dev": { + "psr-4": { + "\\spec\\": [ + "spec/" + ] } }, - "notification-url": "https://packagist.org/downloads/", + "scripts": { + "cs-fix": [ + "bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --verbose" + ], + "phpstan": [ + "bin/phpstan analyse src" + ], + "test": [ + "bin/phpunit -c phpunit.xml.dist --do-not-cache-result" + ], + "test-debug": [ + "bin/phpunit -c phpunit.xml.dist --debug" + ] + }, "license": [ "MIT" ], @@ -162,26 +187,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" - }, - "time": "2025-02-22T21:55:21+00:00" + "transport-options": { + "relative": true + } } ], "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 +213,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 +224,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 +284,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.15.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4" + "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", - "reference": "34c9b59c59355a7b4c53b9f041c8dbd1c8acc3b4", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", + "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", "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-v36.0.0", + "friendsofphp/php-cs-fixer": "^3.77", "mikey179/vfsstream": "^1.6", "phpstan/extension-installer": "^1", "phpstan/phpstan": "^2", @@ -329,9 +353,9 @@ ], "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.15.0" }, - "time": "2025-05-23T15:06:40+00:00" + "time": "2025-11-05T15:34:04+00:00" }, { "name": "clue/ndjson-react", @@ -1011,11 +1035,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 +1084,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 +1423,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 +1504,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 +1528,7 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:09:13+00:00" + "time": "2025-12-06T08:01:15+00:00" }, { "name": "psr/container", @@ -1808,16 +1832,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 +1896,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 +1904,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 +1968,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 +1976,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 +2053,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 +2121,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 +2129,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 +3249,22 @@ }, { "name": "symfony/config", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "8a09223170046d2cfda3d2e11af01df2c641e961" + "reference": "f76c74e93bce2b9285f2dad7fbd06fa8182a7a41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/8a09223170046d2cfda3d2e11af01df2c641e961", - "reference": "8a09223170046d2cfda3d2e11af01df2c641e961", + "url": "https://api.github.com/repos/symfony/config/zipball/f76c74e93bce2b9285f2dad7fbd06fa8182a7a41", + "reference": "f76c74e93bce2b9285f2dad7fbd06fa8182a7a41", "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 +3272,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 +3304,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.0" }, "funding": [ { @@ -3300,20 +3324,20 @@ "type": "tidelift" } ], - "time": "2025-09-22T12:46:16+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/console", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", "shasum": "" }, "require": { @@ -3321,7 +3345,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 +3359,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 +3402,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.4" + "source": "https://github.com/symfony/console/tree/v7.4.0" }, "funding": [ { @@ -3398,28 +3422,28 @@ "type": "tidelift" } ], - "time": "2025-09-22T15:31:00+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "82119812ab0bf3425c1234d413efd1b19bb92ae4" + "reference": "3972ca7bbd649467b21a54870721b9e9f3652f9b" }, "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/3972ca7bbd649467b21a54870721b9e9f3652f9b", + "reference": "3972ca7bbd649467b21a54870721b9e9f3652f9b", "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 +3456,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 +3486,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.0" }, "funding": [ { @@ -3482,7 +3506,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3553,16 +3577,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 +3603,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 +3638,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 +3658,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,16 +3738,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.2", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", "shasum": "" }, "require": { @@ -3731,7 +3756,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3759,7 +3784,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/v7.4.0" }, "funding": [ { @@ -3779,27 +3804,27 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:47+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3827,7 +3852,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/v7.4.0" }, "funding": [ { @@ -3847,20 +3872,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "b38026df55197f9e39a44f3215788edf83187b80" }, "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/b38026df55197f9e39a44f3215788edf83187b80", + "reference": "b38026df55197f9e39a44f3215788edf83187b80", "shasum": "" }, "require": { @@ -3898,7 +3923,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v7.4.0" }, "funding": [ { @@ -3918,7 +3943,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4416,16 +4441,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { @@ -4457,7 +4482,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/v7.4.0" }, "funding": [ { @@ -4477,20 +4502,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T11:21:06+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 +4569,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,25 +4580,29 @@ "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": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + "reference": "8a24af0a2e8a872fb745047180649b8418303084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084", + "reference": "8a24af0a2e8a872fb745047180649b8418303084", "shasum": "" }, "require": { @@ -4606,7 +4635,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/v7.4.0" }, "funding": [ { @@ -4617,31 +4646,36 @@ "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:05:15+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -4649,11 +4683,11 @@ "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.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4692,7 +4726,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v7.4.0" }, "funding": [ { @@ -4712,27 +4746,27 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-11-27T13:27:24+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 +4785,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 +4826,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 +4846,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 +4908,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,25 +4919,29 @@ "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": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" }, "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/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", "shasum": "" }, "require": { @@ -4911,9 +4949,9 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "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": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4951,7 +4989,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -4971,32 +5009,32 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", "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 +5065,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.0" }, "funding": [ { @@ -5047,20 +5085,20 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:34:33+00:00" + "time": "2025-11-16T10:14:42+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 +5127,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 +5135,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: + """ + flatMap(fn($ast) => evaluateAst($ast, $session)); + ->flatMap(fn ($ast) => evaluateAst($ast, $session)); } /** @@ -70,7 +71,7 @@ function evaluateAst(array $ast, ReplSession $session): Validation 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) { + return $result->map(function ($evalResult) use ($stmt) { $varName = '$' . $stmt->expr->var->name; // Create a new result with assignment metadata return new EvaluationResult( @@ -308,7 +309,7 @@ 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) { + return evaluateNode($node->name, $session)->flatMap(function ($result) use ($session) { $varName = $result->value; if (!is_string($varName)) { @@ -686,6 +687,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); @@ -1383,7 +1392,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) { @@ -1403,7 +1412,7 @@ function evaluateArrowFunction(Expr\ArrowFunction $node, ReplSession $session): if ($result->isLeft()) { // Get the error using fold - $error = $result->fold(fn($e) => $e)(fn($r) => null); + $error = $result->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Arrow function evaluation failed: ' . $error->reason); } @@ -1440,7 +1449,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; @@ -1469,7 +1478,7 @@ function evaluateClosure(Expr\Closure $node, ReplSession $session): Validation if ($stmt->expr !== null) { $result = evaluateNode($stmt->expr, $newSession); if ($result->isLeft()) { - $error = $result->fold(fn($e) => $e)(fn($r) => null); + $error = $result->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Closure evaluation failed: ' . $error->reason); } $returnValue = $result->getOrElse(null)->value; @@ -1478,7 +1487,7 @@ function evaluateClosure(Expr\Closure $node, ReplSession $session): Validation } elseif ($stmt instanceof Node\Stmt\Expression) { $result = evaluateNode($stmt->expr, $newSession); if ($result->isLeft()) { - $error = $result->fold(fn($e) => $e)(fn($r) => null); + $error = $result->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Closure evaluation failed: ' . $error->reason); } // Update session if this is an assignment @@ -1926,7 +1935,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 }); @@ -2168,7 +2177,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; }); @@ -2459,7 +2468,8 @@ class StmtBlockResult public function __construct( public readonly Validation $result, public readonly ReplSession $updatedSession - ) {} + ) { + } } /** @@ -2568,7 +2578,7 @@ function evaluateStmtBlock(array $stmts, ReplSession $session): Validation if ($stmt->expr !== null) { $result = evaluateNode($stmt->expr, $session); if ($result->isLeft()) { - $error = $result->fold(fn($e) => $e)(fn($r) => null); + $error = $result->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Return expression evaluation failed: ' . $error->reason); } throw new FunctionReturnException($result->getOrElse(null)->value); @@ -2926,7 +2936,7 @@ 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) { + return evaluateNode($node->var->name, $session)->flatMap(function ($nameResult) use ($node, $session) { $varName = $nameResult->value; if (!is_string($varName)) { @@ -2934,7 +2944,7 @@ function evaluateAssignment(Expr\Assign $node, ReplSession $session): Validation } // Evaluate the value to assign - return evaluateNode($node->expr, $session)->map(function($valueResult) use ($varName) { + return evaluateNode($node->expr, $session)->map(function ($valueResult) use ($varName) { $value = $valueResult->value; return EvaluationResult::of($value, getType($value), '$' . $varName); }); @@ -3334,10 +3344,10 @@ function isComplexTypeValid(mixed $value, mixed $type): bool function getTypeName(mixed $type): string { if ($type instanceof Node\UnionType) { - $names = array_map(fn($t) => getTypeName($t), $type->types); + $names = array_map(fn ($t) => getTypeName($t), $type->types); return implode('|', $names); } elseif ($type instanceof Node\IntersectionType) { - $names = array_map(fn($t) => getTypeName($t), $type->types); + $names = array_map(fn ($t) => getTypeName($t), $type->types); return implode('&', $names); } else { return $type->toString(); @@ -3472,7 +3482,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 @@ -3480,7 +3490,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Check if this parameter has a default value if ($param->default === null) { // Required parameter is missing - $expectedCount = count(array_filter($node->params, fn($p) => $p->default === null)); + $expectedCount = count(array_filter($node->params, fn ($p) => $p->default === null)); throw new \TypeError( "$funcName() expects at least $expectedCount argument" . ($expectedCount === 1 ? '' : 's') . ", " . count($args) . " given" ); @@ -3525,7 +3535,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Evaluate the default value expression $defaultResult = evaluateNode($param->default, $session); if ($defaultResult->isLeft()) { - $error = $defaultResult->fold(fn($e) => $e)(fn($r) => null); + $error = $defaultResult->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Default parameter evaluation failed: ' . $error->reason); } $defaultValue = $defaultResult->getOrElse(null)->value; @@ -3548,7 +3558,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Evaluate the yield value $yieldResult = evaluateNode($stmt->expr->value, $newSession); if ($yieldResult->isLeft()) { - $error = $yieldResult->fold(fn($e) => $e)(fn($r) => null); + $error = $yieldResult->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Yield evaluation failed: ' . $error->reason); } @@ -3558,7 +3568,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess if ($stmt->expr->key !== null) { $keyResult = evaluateNode($stmt->expr->key, $newSession); if ($keyResult->isLeft()) { - $error = $keyResult->fold(fn($e) => $e)(fn($r) => null); + $error = $keyResult->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Yield key evaluation failed: ' . $error->reason); } yield $keyResult->getOrElse(null)->value => $value; @@ -3569,7 +3579,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Handle other statements in the function body $result = evaluateNode($stmt->expr, $newSession); if ($result->isLeft()) { - $error = $result->fold(fn($e) => $e)(fn($r) => null); + $error = $result->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Statement evaluation failed: ' . $error->reason); } } @@ -3577,7 +3587,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess }; } 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 @@ -3585,7 +3595,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Check if this parameter has a default value if ($param->default === null) { // Required parameter is missing - $expectedCount = count(array_filter($node->params, fn($p) => $p->default === null)); + $expectedCount = count(array_filter($node->params, fn ($p) => $p->default === null)); throw new \TypeError( "$funcName() expects at least $expectedCount argument" . ($expectedCount === 1 ? '' : 's') . ", " . count($args) . " given" ); @@ -3630,7 +3640,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Evaluate the default value expression $defaultResult = evaluateNode($param->default, $session); if ($defaultResult->isLeft()) { - $error = $defaultResult->fold(fn($e) => $e)(fn($r) => null); + $error = $defaultResult->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Default parameter evaluation failed: ' . $error->reason); } $defaultValue = $defaultResult->getOrElse(null)->value; @@ -3654,7 +3664,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess $result = evaluateStmtBlock($stmtsToEvaluate, $newSession); if ($result->isLeft()) { - $error = $result->fold(fn($e) => $e)(fn($r) => null); + $error = $result->fold(fn ($e) => $e)(fn ($r) => null); throw new \RuntimeException('Function body evaluation failed: ' . $error->reason); } @@ -3929,7 +3939,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; }); @@ -4022,7 +4032,7 @@ function evaluateInterfaceDefinition(Node\Stmt\Interface_ $interfaceNode, ReplSe // 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; }); @@ -4103,7 +4113,7 @@ function evaluateTraitDefinition(Node\Stmt\Trait_ $traitNode, ReplSession $sessi // 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; }); @@ -4210,7 +4220,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 }); 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..6aeeb2e 100644 --- a/src/Functions/session.php +++ b/src/Functions/session.php @@ -30,7 +30,7 @@ */ function getSession(): State { - return (new State(fn($s) => Pair($s, $s))); + return (new State(fn ($s) => Pair($s, $s))); } /** @@ -41,7 +41,7 @@ function getSession(): State */ function modifySession(callable $f): State { - return new State(fn(ReplSession $s) => Pair($f($s), null)); + return new State(fn (ReplSession $s) => Pair($f($s), null)); } /** @@ -52,7 +52,8 @@ function modifySession(callable $f): State */ function addToHistory(string $expression): State { - return modifySession(fn(ReplSession $s) => + return modifySession( + fn (ReplSession $s) => new ReplSession( $s->history->append($expression), $s->variables, @@ -74,7 +75,8 @@ function addToHistory(string $expression): State */ function setVariable(string $name, mixed $value): State { - return modifySession(fn(ReplSession $s) => + return modifySession( + fn (ReplSession $s) => new ReplSession( $s->history, $s->variables->plus($name, $value), @@ -95,7 +97,7 @@ function setVariable(string $name, mixed $value): State */ function getVariable(string $name): State { - return new State(fn(ReplSession $s) => Pair($s, $s->variables->get($name))); + return new State(fn (ReplSession $s) => Pair($s, $s->variables->get($name))); } /** @@ -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, @@ -127,7 +129,7 @@ function nextVariable(): State */ function getVariables(): State { - return new State(fn(ReplSession $s) => Pair($s, $s->variables)); + return new State(fn (ReplSession $s) => Pair($s, $s->variables)); } /** @@ -137,7 +139,7 @@ function getVariables(): State */ function getHistory(): State { - return new State(fn(ReplSession $s) => Pair($s, $s->history)); + return new State(fn (ReplSession $s) => Pair($s, $s->history)); } /** @@ -148,7 +150,8 @@ function getHistory(): State */ function setColors(bool $enabled): State { - return modifySession(fn(ReplSession $s) => + return modifySession( + fn (ReplSession $s) => new ReplSession( $s->history, $s->variables, @@ -168,7 +171,7 @@ function setColors(bool $enabled): State */ function isColorEnabled(): State { - return new State(fn(ReplSession $s) => Pair($s, $s->colorEnabled)); + return new State(fn (ReplSession $s) => Pair($s, $s->colorEnabled)); } /** @@ -178,7 +181,8 @@ function isColorEnabled(): State */ function resetSession(): State { - return modifySession(fn(ReplSession $s) => + return modifySession( + fn (ReplSession $s) => new ReplSession( ImmList(), ImmMap(), @@ -199,7 +203,8 @@ function resetSession(): State */ function setNamespace(?string $namespace): State { - return modifySession(fn(ReplSession $s) => + return modifySession( + fn (ReplSession $s) => new ReplSession( $s->history, $s->variables, @@ -219,7 +224,7 @@ function setNamespace(?string $namespace): State */ function getCurrentNamespace(): State { - return new State(fn(ReplSession $s) => Pair($s, $s->currentNamespace)); + return new State(fn (ReplSession $s) => Pair($s, $s->currentNamespace)); } /** @@ -231,7 +236,8 @@ function getCurrentNamespace(): State */ function addUseStatement(string $alias, string $fullName): State { - return modifySession(fn(ReplSession $s) => + return modifySession( + fn (ReplSession $s) => new ReplSession( $s->history, $s->variables, @@ -251,5 +257,5 @@ function addUseStatement(string $alias, string $fullName): State */ function getUseStatements(): State { - return new State(fn(ReplSession $s) => Pair($s, $s->useStatements)); + return new State(fn (ReplSession $s) => Pair($s, $s->useStatements)); } diff --git a/src/Repl/ReplLoop.php b/src/Repl/ReplLoop.php index e191afc..e4adea1 100644 --- a/src/Repl/ReplLoop.php +++ b/src/Repl/ReplLoop.php @@ -17,6 +17,7 @@ use Phunkie\Console\Types\ExitRepl; 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}; @@ -33,7 +34,7 @@ function replLoop(ReplSession $session): IO { // Run the trampolined loop - return new IO(fn() => replLoopTrampoline($session)->run()); + return new IO(fn () => replLoopTrampoline($session)->run()); } /** @@ -66,7 +67,7 @@ function replLoopTrampoline(ReplSession $session): Trampoline if ($result instanceof ContinueRepl) { // Return More to continue the trampoline - return More(fn() => replLoopTrampoline($result->session)); + return More(fn () => replLoopTrampoline($result->session)); } // Shouldn't happen but handle gracefully @@ -85,7 +86,7 @@ function processInput(?string $input, ReplSession $session): IO { // Handle EOF (Control-D) if ($input === null) { - return new IO(fn() => new ExitRepl()); + return new IO(fn () => new ExitRepl()); } // Combine with any incomplete input from previous lines @@ -97,7 +98,7 @@ function processInput(?string $input, ReplSession $session): IO // Handle empty input if ($trimmed === '') { - return new IO(fn() => new ContinueRepl($session)); + return new IO(fn () => new ContinueRepl($session)); } // Handle REPL commands (only if not in multi-line mode) @@ -115,7 +116,7 @@ function processInput(?string $input, ReplSession $session): IO $session->variableCounter, $combinedInput ); - return new IO(fn() => new ContinueRepl($newSession)); + return new IO(fn () => new ContinueRepl($newSession)); } // Clear incomplete input buffer for complete expression @@ -295,18 +296,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)), + ':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)), ':reset' => resetReplState($session), default => printLn("Unknown command: $command") - ->map(fn() => new ContinueRepl($session)) + ->map(fn () => new ContinueRepl($session)) }; } @@ -322,7 +323,7 @@ function resetReplState(ReplSession $session): IO $newSession = $pair->_1; return printLn("REPL state reset") - ->map(fn() => new ContinueRepl($newSession)); + ->map(fn () => new ContinueRepl($newSession)); } /** @@ -334,7 +335,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 +435,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,7 +491,7 @@ 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']); @@ -557,19 +558,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 +592,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,6 +644,8 @@ function($evalResult) { }); } + + /** * Formats an error message with optional color support. * @@ -693,11 +693,11 @@ function evaluateAndDisplay(string $expression, ReplSession $session): IO // Use fold to handle both success and failure cases return $evalResult->fold( // Failure case: error is passed to this function - fn($error) => printLn(formatError($error, $session)) - ->map(fn() => new ContinueRepl($session)) + fn ($error) => printLn(formatError($error, $session)) + ->map(fn () => new ContinueRepl($session)) )( // Success case: result is passed to this function - fn($result) => displayResult($result, $session, $expression) + fn ($result) => displayResult($result, $session, $expression) ); } @@ -728,7 +728,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "Namespace cleared"; return printLn($output) - ->map(fn() => new ContinueRepl($newSession2)); + ->map(fn () => new ContinueRepl($newSession2)); } // Handle use statement @@ -749,7 +749,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e $output = "Imported $importCount " . ($importCount === 1 ? 'class/function' : 'classes/functions'); return printLn($output) - ->map(fn() => new ContinueRepl($newSession)); + ->map(fn () => new ContinueRepl($newSession)); } // Handle silent operations (property assignments, etc.) that shouldn't produce output @@ -782,14 +782,14 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e $newSession = $pair->_1; return printLn($output) - ->map(fn() => new ContinueRepl($newSession)); + ->map(fn () => new ContinueRepl($newSession)); } // Other silent operations - add to history but don't print anything $pair = (addToHistory($expression))->run($session); $newSession = $pair->_1; - return new IO(fn() => new ContinueRepl($newSession)); + return new IO(fn () => new ContinueRepl($newSession)); } // Check if this is an enum definition @@ -804,7 +804,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// enum {$result->value} defined"; return printLn($output) - ->map(fn() => new ContinueRepl($newSession)); + ->map(fn () => new ContinueRepl($newSession)); } // Check if this was an assignment with a specific variable name @@ -834,7 +834,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// function $displayName defined"; return printLn($output) - ->map(fn() => new ContinueRepl($currentSession)); + ->map(fn () => new ContinueRepl($currentSession)); } // Special handling for class definitions @@ -844,7 +844,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// class $varName defined"; return printLn($output) - ->map(fn() => new ContinueRepl($currentSession)); + ->map(fn () => new ContinueRepl($currentSession)); } // Special handling for interface definitions @@ -854,7 +854,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// interface $varName defined"; return printLn($output) - ->map(fn() => new ContinueRepl($currentSession)); + ->map(fn () => new ContinueRepl($currentSession)); } // Special handling for trait definitions @@ -864,7 +864,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// trait $varName defined"; return printLn($output) - ->map(fn() => new ContinueRepl($currentSession)); + ->map(fn () => new ContinueRepl($currentSession)); } // Format output with bold variable name, pink type, and bold value if colors are enabled @@ -873,7 +873,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "$varName: {$result->type} = {$result->format()}"; return printLn($output) - ->map(fn() => new ContinueRepl($currentSession)); + ->map(fn () => new ContinueRepl($currentSession)); } // Check if this is an output statement (echo, print, var_dump, etc.) @@ -886,7 +886,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e // Add a newline after the output to prevent prompt from running into it echo "\n"; - return new IO(fn() => new ContinueRepl($newSession)); + return new IO(fn () => new ContinueRepl($newSession)); } // Generate next variable name for auto-assignment @@ -915,5 +915,5 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "$varName: {$result->type} = {$result->format()}"; return printLn($output) - ->map(fn() => new ContinueRepl($currentSession)); + ->map(fn () => new ContinueRepl($currentSession)); } diff --git a/src/Types/CommandError.php b/src/Types/CommandError.php index 9566bf6..095c83c 100644 --- a/src/Types/CommandError.php +++ b/src/Types/CommandError.php @@ -16,7 +16,8 @@ final class CommandError extends ReplError public function __construct( public readonly string $command, public readonly string $reason - ) {} + ) { + } public function message(): string { diff --git a/src/Types/ContinueRepl.php b/src/Types/ContinueRepl.php index a16589e..d65892e 100644 --- a/src/Types/ContinueRepl.php +++ b/src/Types/ContinueRepl.php @@ -13,5 +13,7 @@ final class ContinueRepl extends ReplResult { - public function __construct(public readonly ReplSession $session) {} + public function __construct(public readonly ReplSession $session) + { + } } diff --git a/src/Types/EvaluationError.php b/src/Types/EvaluationError.php index f99d6c4..863d645 100644 --- a/src/Types/EvaluationError.php +++ b/src/Types/EvaluationError.php @@ -16,7 +16,8 @@ final class EvaluationError extends ReplError public function __construct( public readonly string $expression, public readonly string $reason - ) {} + ) { + } public function message(): string { diff --git a/src/Types/EvaluationResult.php b/src/Types/EvaluationResult.php index f50ee72..d64457a 100644 --- a/src/Types/EvaluationResult.php +++ b/src/Types/EvaluationResult.php @@ -24,7 +24,8 @@ public function __construct( public ?string $assignedVariable = null, public array $additionalAssignments = [], public bool $isOutputStatement = false - ) {} + ) { + } public static function of(mixed $value, string $type, ?string $assignedVariable = null, array $additionalAssignments = [], bool $isOutputStatement = false): EvaluationResult { @@ -61,7 +62,7 @@ private function formatValue(mixed $value): string private function formatArray(array $arr): string { - $items = array_map(fn($v) => $this->formatValue($v), $arr); + $items = array_map(fn ($v) => $this->formatValue($v), $arr); return '[' . implode(', ', $items) . ']'; } diff --git a/src/Types/ExitRepl.php b/src/Types/ExitRepl.php index 5ebb860..421423b 100644 --- a/src/Types/ExitRepl.php +++ b/src/Types/ExitRepl.php @@ -11,4 +11,6 @@ namespace Phunkie\Console\Types; -final class ExitRepl extends ReplResult {} +final class ExitRepl extends ReplResult +{ +} diff --git a/src/Types/ParseError.php b/src/Types/ParseError.php index f8973a9..c4812f3 100644 --- a/src/Types/ParseError.php +++ b/src/Types/ParseError.php @@ -16,7 +16,8 @@ final class ParseError extends ReplError public function __construct( public readonly string $input, public readonly string $reason - ) {} + ) { + } public function message(): string { diff --git a/src/Types/ReplResult.php b/src/Types/ReplResult.php index 2357587..11cc176 100644 --- a/src/Types/ReplResult.php +++ b/src/Types/ReplResult.php @@ -14,4 +14,6 @@ /** * ADT for REPL control flow. */ -abstract class ReplResult {} +abstract class ReplResult +{ +} diff --git a/src/Types/TypeError.php b/src/Types/TypeError.php index 121a101..e42a5af 100644 --- a/src/Types/TypeError.php +++ b/src/Types/TypeError.php @@ -22,7 +22,8 @@ class TypeError extends ReplError public function __construct( public readonly string $subject, public readonly string $reason - ) {} + ) { + } public function message(): string { diff --git a/src/Types/VariableNotFoundError.php b/src/Types/VariableNotFoundError.php index 7b79bce..d25bc55 100644 --- a/src/Types/VariableNotFoundError.php +++ b/src/Types/VariableNotFoundError.php @@ -15,7 +15,8 @@ final class VariableNotFoundError extends ReplError { public function __construct( public readonly string $variableName - ) {} + ) { + } public function message(): string { diff --git a/tests/Acceptance/ReplSteps.php b/tests/Acceptance/ReplSteps.php index 18fdd6f..e29b95d 100644 --- a/tests/Acceptance/ReplSteps.php +++ b/tests/Acceptance/ReplSteps.php @@ -203,7 +203,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); } From 5a00363b525729270486786b4c1ee415aaf6700c Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:19:06 +0000 Subject: [PATCH 02/14] Add lint, test-all, check scripts and pre-commit hook --- composer.json | 11 ++++++++++- scripts/pre-commit | 30 ++++++++++++++++++++++++++++++ scripts/setup.sh | 32 ++++++++++++++++++++++++++++++++ scripts/test-all-versions.sh | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100755 scripts/pre-commit create mode 100755 scripts/setup.sh create mode 100755 scripts/test-all-versions.sh diff --git a/composer.json b/composer.json index bcd69ad..c1d7674 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,15 @@ "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": "vendor/bin/phpstan analyse" + "phpstan": "vendor/bin/phpstan analyse", + "lint": [ + "@cs-check", + "@phpstan" + ], + "test-all": "scripts/test-all-versions.sh", + "check": [ + "@lint", + "@test" + ] } } \ No newline at end of file diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..3f8a882 --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,30 @@ +#!/bin/bash + +# Pre-commit hook for Phunkie projects +# Runs lint and tests before allowing commit + +set -e + +echo "🔍 Running pre-commit checks..." + +# Run lint (cs-check + phpstan) +echo "" +echo "📋 Running lint checks..." +if ! composer lint; then + echo "" + echo "❌ Lint checks failed. Please fix the issues before committing." + echo " Run 'composer cs-fix' to auto-fix code style issues." + exit 1 +fi + +# Run tests +echo "" +echo "🧪 Running tests..." +if ! composer test; then + echo "" + echo "❌ Tests failed. Please fix the issues before committing." + exit 1 +fi + +echo "" +echo "✅ All pre-commit checks passed!" diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..298d99c --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Setup script for development environment +# Installs git hooks and dependencies + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +echo "🔧 Setting up development environment..." + +# Install composer dependencies +echo "" +echo "📦 Installing dependencies..." +composer install + +# Install pre-commit hook +echo "" +echo "🪝 Installing git hooks..." +cp "$SCRIPT_DIR/pre-commit" "$PROJECT_ROOT/.git/hooks/pre-commit" +chmod +x "$PROJECT_ROOT/.git/hooks/pre-commit" + +echo "" +echo "✅ Development environment setup complete!" +echo "" +echo "Available commands:" +echo " composer test - Run tests" +echo " composer lint - Run code style and static analysis" +echo " composer cs-fix - Auto-fix code style issues" +echo " composer check - Run lint + tests" +echo " composer test-all - Run tests on all PHP versions (requires Docker)" diff --git a/scripts/test-all-versions.sh b/scripts/test-all-versions.sh new file mode 100755 index 0000000..eca53c9 --- /dev/null +++ b/scripts/test-all-versions.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Test against all supported PHP versions using Docker +# Requires Docker to be installed and running + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +PHP_VERSIONS=("8.2" "8.3" "8.4") + +echo "==========================================" +echo "Testing against PHP versions: ${PHP_VERSIONS[*]}" +echo "==========================================" + +for version in "${PHP_VERSIONS[@]}"; do + echo "" + echo "==========================================" + echo "Testing PHP $version" + echo "==========================================" + + docker run --rm -v "$PROJECT_ROOT:/app" -w /app \ + "php:${version}-cli" \ + sh -c "apt-get update && apt-get install -y git unzip && \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ + composer install --no-interaction --prefer-dist && \ + composer test" + + echo "✅ PHP $version tests passed" +done + +echo "" +echo "==========================================" +echo "✅ All PHP versions passed!" +echo "==========================================" From 321309e23e54edd23b59eeba196153b53b900d6e Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:28:22 +0000 Subject: [PATCH 03/14] Update test-all to include lint + static analysis --- scripts/test-all-versions.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/test-all-versions.sh b/scripts/test-all-versions.sh index eca53c9..e7faf68 100755 --- a/scripts/test-all-versions.sh +++ b/scripts/test-all-versions.sh @@ -11,26 +11,30 @@ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" PHP_VERSIONS=("8.2" "8.3" "8.4") echo "==========================================" -echo "Testing against PHP versions: ${PHP_VERSIONS[*]}" +echo "Running lint + tests on PHP versions: ${PHP_VERSIONS[*]}" echo "==========================================" for version in "${PHP_VERSIONS[@]}"; do echo "" echo "==========================================" - echo "Testing PHP $version" + echo "PHP $version - Installing dependencies..." echo "==========================================" docker run --rm -v "$PROJECT_ROOT:/app" -w /app \ "php:${version}-cli" \ - sh -c "apt-get update && apt-get install -y git unzip && \ - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ - composer install --no-interaction --prefer-dist && \ + sh -c "apt-get update -qq && apt-get install -y -qq git unzip > /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 tests passed" + echo "✅ PHP $version passed (lint + tests)" done echo "" echo "==========================================" echo "✅ All PHP versions passed!" echo "==========================================" + From a5b99a96642a5621803fdb6c70a4bb23eae24cb0 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:31:31 +0000 Subject: [PATCH 04/14] Increase PHPStan memory limit to 512M --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c1d7674..4f4ccc7 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "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": "vendor/bin/phpstan analyse", + "phpstan": "phpstan analyse --memory-limit=512M", "lint": [ "@cs-check", "@phpstan" From d1d471ab76e061384a6cb5b136b85e7efba982fc Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:42:16 +0000 Subject: [PATCH 05/14] Upgrade php-cs-fixer to ^3.90 for PHP 8.4 support --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4f4ccc7..f207549 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "phpunit/phpunit": "^11", "behat/behat": "^3.22", "phpstan/phpstan": "^2.1", - "friendsofphp/php-cs-fixer": "^3.75" + "friendsofphp/php-cs-fixer": "^3.90" }, "autoload": { "psr-4": { From 0f788eac2d2cd02d5aaa5f6c99d27e1123d3563d Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 11:16:05 +0000 Subject: [PATCH 06/14] Add phunkie/phpstan extension --- .php-cs-fixer.dist.php | 30 ++++ composer.json | 19 ++- composer.lock | 347 +++++++++++++++++++++++++++++++---------- phpstan.neon | 7 + 4 files changed, 316 insertions(+), 87 deletions(-) create mode 100644 .php-cs-fixer.dist.php create mode 100644 phpstan.neon 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/composer.json b/composer.json index f207549..f3fab56 100644 --- a/composer.json +++ b/composer.json @@ -8,17 +8,28 @@ "email": "marcello.duarte@gmail.com" } ], + "repositories": [ + { + "type": "path", + "url": "../phunkie" + }, + { + "type": "path", + "url": "../effect" + } + ], "require": { "php": "^8.2 || ^8.3 || ^8.4", - "phunkie/phunkie": "^1.0.0", - "phunkie/effect": "^1.0.0", + "phunkie/phunkie": "dev-developing-1.0.0 as 1.0.0", + "phunkie/effect": "dev-developing-1.0.0 as 1.0.0", "nikic/php-parser": "^5.6" }, "require-dev": { "phpunit/phpunit": "^11", "behat/behat": "^3.22", "phpstan/phpstan": "^2.1", - "friendsofphp/php-cs-fixer": "^3.90" + "friendsofphp/php-cs-fixer": "^3.90", + "phunkie/phpstan": "@dev" }, "autoload": { "psr-4": { @@ -57,4 +68,4 @@ "@test" ] } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 6b5198f..7134b96 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a577f8e70b97917e9d3eb6dd5e581992", + "content-hash": "39e510a206d77bda1a91571688c1621d", "packages": [ { "name": "nikic/php-parser", @@ -66,21 +66,21 @@ }, { "name": "phunkie/effect", - "version": "1.0.0", + "version": "dev-developing-1.0.0", "dist": { "type": "path", "url": "../effect", - "reference": "dd033e03edb4b02bbe2d36b6c503556d06308fc7" + "reference": "ea55dd81d9af4a202774e628168c698e10841d91" }, "require": { "php": "^8.2 || ^8.3 || ^8.4", - "phunkie/phunkie": "^1.0.0" + "phunkie/phunkie": "dev-developing-1.0.0 as 1.0.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.75", + "friendsofphp/php-cs-fixer": "^3.90", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.5", - "phunkie/console": "^1.0.0" + "phunkie/phpstan": "@dev" }, "suggest": { "ext-parallel": "Required for parallel execution using threads. PHP must be compiled with ZTS support." @@ -104,13 +104,24 @@ "phpunit" ], "phpstan": [ - "phpstan analyse" + "phpstan analyse --memory-limit=512M" ], "cs-fix": [ - "php-cs-fixer fix" + "php-cs-fixer fix --allow-risky=yes" ], "cs-check": [ - "php-cs-fixer fix --dry-run --diff" + "php-cs-fixer fix --dry-run --diff --allow-risky=yes" + ], + "lint": [ + "@cs-check", + "@phpstan" + ], + "test-all": [ + "scripts/test-all-versions.sh" + ], + "check": [ + "@lint", + "@test" ] }, "license": [ @@ -129,21 +140,22 @@ }, { "name": "phunkie/phunkie", - "version": "1.0.0", + "version": "dev-developing-1.0.0", "dist": { "type": "path", "url": "../phunkie", - "reference": "898fed43f1ef0eee9a82b0c0440a58a83e9f82d9" + "reference": "c15ee399a943f038bfa45c18a4d9a9ddc09c1f5b" }, "require": { "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", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9" + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9", + "phunkie/phpstan": "@dev" }, "type": "library", "autoload": { @@ -165,16 +177,30 @@ }, "scripts": { "cs-fix": [ - "bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --verbose" + "bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --verbose --allow-risky=yes" + ], + "cs-check": [ + "bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff --verbose --allow-risky=yes" ], "phpstan": [ - "bin/phpstan analyse src" + "phpstan analyse --memory-limit=512M src" + ], + "lint": [ + "@cs-check", + "@phpstan" ], "test": [ "bin/phpunit -c phpunit.xml.dist --do-not-cache-result" ], "test-debug": [ "bin/phpunit -c phpunit.xml.dist --debug" + ], + "test-all": [ + "scripts/test-all-versions.sh" + ], + "check": [ + "@lint", + "@test" ] }, "license": [ @@ -753,58 +779,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", @@ -819,7 +844,7 @@ "PhpCsFixer\\": "src/" }, "exclude-from-classmap": [ - "src/Fixer/Internal/*" + "src/**/Internal/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -845,7 +870,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": [ { @@ -853,7 +878,7 @@ "type": "github" } ], - "time": "2025-03-31T18:40:42+00:00" + "time": "2025-12-05T19:45:37+00:00" }, { "name": "myclabs/deep-copy", @@ -1530,6 +1555,60 @@ ], "time": "2025-12-06T08:01:15+00:00" }, + { + "name": "phunkie/phpstan", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/phunkie/phpstan.git", + "reference": "9e2e9386bef5fa35adcc306106172160c5e78fb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phunkie/phpstan/zipball/9e2e9386bef5fa35adcc306106172160c5e78fb3", + "reference": "9e2e9386bef5fa35adcc306106172160c5e78fb3", + "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" + }, + "default-branch": true, + "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-08T11:09:04+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -3249,16 +3328,16 @@ }, { "name": "symfony/config", - "version": "v7.4.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "f76c74e93bce2b9285f2dad7fbd06fa8182a7a41" + "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/f76c74e93bce2b9285f2dad7fbd06fa8182a7a41", - "reference": "f76c74e93bce2b9285f2dad7fbd06fa8182a7a41", + "url": "https://api.github.com/repos/symfony/config/zipball/2c323304c354a43a48b61c5fa760fc4ed60ce495", + "reference": "2c323304c354a43a48b61c5fa760fc4ed60ce495", "shasum": "" }, "require": { @@ -3304,7 +3383,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.4.0" + "source": "https://github.com/symfony/config/tree/v7.4.1" }, "funding": [ { @@ -3324,20 +3403,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-05T07:52:08+00:00" }, { "name": "symfony/console", - "version": "v7.4.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", "shasum": "" }, "require": { @@ -3402,7 +3481,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.0" + "source": "https://github.com/symfony/console/tree/v7.4.1" }, "funding": [ { @@ -3422,20 +3501,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-05T15:23:39+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.0", + "version": "v7.4.2", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "3972ca7bbd649467b21a54870721b9e9f3652f9b" + "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/3972ca7bbd649467b21a54870721b9e9f3652f9b", - "reference": "3972ca7bbd649467b21a54870721b9e9f3652f9b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", + "reference": "baf614f7c15b30ba6762d4b1ddabdf83dbf0d29b", "shasum": "" }, "require": { @@ -3486,7 +3565,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.4.0" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.2" }, "funding": [ { @@ -3506,7 +3585,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-08T06:57:04+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4197,19 +4276,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": { @@ -4257,7 +4337,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": [ { @@ -4268,12 +4348,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", @@ -4439,6 +4523,86 @@ ], "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.4.0", @@ -5013,16 +5177,16 @@ }, { "name": "symfony/yaml", - "version": "v7.4.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", - "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { @@ -5065,7 +5229,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.0" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -5085,7 +5249,7 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "theseer/tokenizer", @@ -5138,9 +5302,26 @@ "time": "2025-11-17T20:03:58+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "phunkie/effect", + "version": "dev-developing-1.0.0", + "alias": "1.0.0", + "alias_normalized": "1.0.0.0" + }, + { + "package": "phunkie/phunkie", + "version": "dev-developing-1.0.0", + "alias": "1.0.0", + "alias_normalized": "1.0.0.0" + } + ], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "phunkie/effect": 20, + "phunkie/phpstan": 20, + "phunkie/phunkie": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..fd9ca1a --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - vendor/phunkie/phpstan/extension.neon + +parameters: + level: 5 + paths: + - src From 1e3ff9846721f636d99dde302eab1177367604ad Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 14:06:17 +0000 Subject: [PATCH 07/14] Fix PHPStan errors and update test expectations --- .php-cs-fixer.cache | 2 +- behat.php | 5 +- bin/phpstan | 1 + features/repl/clone_expressions.feature | 6 +- .../repl/union_intersection_types.feature | 4 +- src/Functions/display.php | 24 +- src/Functions/evaluation.php | 424 +++++++++--------- src/Functions/session.php | 44 +- src/Repl/ReplLoop.php | 86 ++-- src/Types/CommandError.php | 3 +- src/Types/ContinueRepl.php | 4 +- src/Types/EvaluationError.php | 3 +- src/Types/EvaluationResult.php | 11 +- src/Types/ExitRepl.php | 4 +- src/Types/ParseError.php | 3 +- src/Types/ReplResult.php | 4 +- src/Types/ReplSession.php | 23 + src/Types/TypeError.php | 3 +- src/Types/VariableNotFoundError.php | 3 +- tests/Acceptance/ReplSteps.php | 28 +- .../Acceptance/Support/DirectReplManager.php | 2 + tests/Unit/Functions/DisplayTest.php | 1 + tests/Unit/Functions/EvaluationTest.php | 1 + tests/Unit/Functions/SessionTest.php | 1 + tests/Unit/Functions/TerminalTest.php | 1 + 25 files changed, 360 insertions(+), 331 deletions(-) create mode 120000 bin/phpstan diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache index e01ec41..3bb1f7b 100644 --- a/.php-cs-fixer.cache +++ b/.php-cs-fixer.cache @@ -1 +1 @@ -{"php":"8.2.12","version":"3.75.0:v3.75.0#399a128ff2fdaf4281e4e79b755693286cdf325c","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_parentheses":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"hashes":{"src\/Types\/ReplSession.php":"17546e77b5eac5d19e33c6300077ee58","src\/Types\/CommandError.php":"30977920d96a17285089fe7cab38f079","src\/Types\/TypeError.php":"7e7f08d2bafadf25a9fef554d28235a2","src\/Types\/ContinueRepl.php":"da40e20d051546441c8e0e693e86b781","src\/Types\/ParseError.php":"5c9ae2d644dd0943c71d1fa7d74260ce","src\/Types\/ExitRepl.php":"9d93ca80ff79297dfc9f01943561f990","src\/Types\/EvaluationError.php":"691e557e00f3cbc34273cda5f6f97866","src\/Types\/ReplError.php":"dc627149a374a4f4cff98db2f798b896","src\/Types\/EvaluationResult.php":"1be4b54b4c4087e1534d9bfca1f58307","src\/Types\/ReplResult.php":"2f1e93fe316e51f07c2faa23d6887e78","src\/Types\/VariableNotFoundError.php":"3fc511af94c661c6a7e3c7ec249f1520","src\/Repl\/ReplLoop.php":"aeba47348fa51db95e9e384308e3fd53","src\/Functions\/evaluation.php":"540b217f681737d1ac81167b81ddf115","src\/Functions\/terminal.php":"4c07be5d3bf603c6d5d8ee310138a2d3","src\/Functions\/session.php":"e208fe6ebe59b6225a1451d9965e64e6","src\/Functions\/parsing.php":"7cb9fe7e58d684f2e52d7ba5c5670bef","src\/Functions\/display.php":"e10c2cfaa5996a3d04b3ccc2a1cb164f","src\/Functions\/common.php":"38978a7963d52026b0aeaba4ffc10fe2"}} \ No newline at end of file +{"php":"8.2.12","version":"3.91.3:v3.91.3#9f10aa6390cea91da175ea608880e942d7c0226e","indent":" ","lineEnding":"\n","rules":{"nullable_type_declaration":true,"operator_linebreak":true,"ordered_types":{"null_adjustment":"always_last","sort_algorithm":"none"},"single_class_element_per_statement":true,"types_spaces":true,"array_indentation":true,"array_syntax":true,"cast_spaces":true,"concat_space":{"spacing":"one"},"function_declaration":{"closure_fn_spacing":"none"},"method_argument_space":{"after_heredoc":true},"new_with_parentheses":{"anonymous_class":false},"single_line_empty_body":true,"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const","const_import","do","else","elseif","enum","final","finally","for","foreach","function","function_import","if","insteadof","interface","match","named_argument","namespace","new","private","protected","public","readonly","static","switch","trait","try","type_colon","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"trailing_comma_in_multiline":{"after_heredoc":true},"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"simple_to_complex_string_variable":true,"octal_notation":true,"clean_namespace":true,"no_unset_cast":true,"assign_null_coalescing_to_coalesce_equal":true,"normalize_index_brace":true,"heredoc_indentation":true,"no_whitespace_before_comma_in_array":{"after_heredoc":true},"list_syntax":true,"ternary_to_null_coalescing":true},"hashes":{"tests\/Acceptance\/Support\/TestFileManager.php":"e403d6015d6686d20286e0c8d8ce1ae4","src\/Types\/ReplSession.php":"afccb8992da455e7a6fa46c09c631f7d","src\/Types\/CommandError.php":"ae5bd8636b3dfecef04aa61ebf6cfabc","src\/Types\/TypeError.php":"6447b3ebeb391cc9fdcd23b608a4cce5","src\/Types\/ContinueRepl.php":"b287863c9f34ee070db17d46181a3a6e","src\/Types\/ParseError.php":"f36893420aa0c611228945ed16f5bac2","src\/Types\/ExitRepl.php":"5797dbd770c8cdeb7bee955f8d62ba92","src\/Types\/EvaluationError.php":"af6bed65b3fd582c57dc7a65ce158a92","src\/Types\/ReplError.php":"dc627149a374a4f4cff98db2f798b896","src\/Types\/EvaluationResult.php":"9277221429e4de114fbccb63171efcc7","behat.php":"691ca2437cbefef7f92d2ab115b8c68f","tests\/Unit\/Functions\/TerminalTest.php":"59e76ee62cd3c3983e017b4b549f0cee","tests\/Unit\/Functions\/DisplayTest.php":"bd5cbb245b403e147405452c697fe111","tests\/Unit\/Functions\/EvaluationTest.php":"1acae3adbc4227d3a62197867771cbb7","tests\/Unit\/Functions\/SessionTest.php":"d5ac3fda4524be2504ace4ccd3d6bd64","tests\/Acceptance\/ReplSteps.php":"cd36ddb1505d4d884ab6fc785e00ee21","tests\/Acceptance\/Support\/ReplOutputReader.php":"8a1256b02f32fd3d6c5e61a4e2a67ea6","tests\/Acceptance\/Support\/ReplProcessManager.php":"bcadd4a7b8878c30fe4df57454ae434d","tests\/Acceptance\/Support\/DirectReplManager.php":"f424d20bb85b4fcf3b54264d7a61bbe2","tests\/Acceptance\/Support\/StringHelper.php":"987552f874f1d313ff9b86dbd3a4cf33","src\/Types\/ReplResult.php":"308f182be73cf8c704b9be18a7f4da60","src\/Types\/VariableNotFoundError.php":"2d2ea8518b49d7c2fd42c06a73ec61e8","src\/Repl\/ReplLoop.php":"46eea0d20b0ee60efc8b4d34cf6cdc55","src\/Functions\/evaluation.php":"5d8344f3450a0204cfdea22a3c9878e1","src\/Functions\/terminal.php":"4c07be5d3bf603c6d5d8ee310138a2d3","src\/Functions\/session.php":"378ec9f40896d7c01db9d88ba61f507d","src\/Functions\/parsing.php":"7cb9fe7e58d684f2e52d7ba5c5670bef","src\/Functions\/display.php":"82f5d30dbe08e4fc29489fead238dd9d","src\/Functions\/common.php":"38978a7963d52026b0aeaba4ffc10fe2"}} \ No newline at end of file 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/features/repl/clone_expressions.feature b/features/repl/clone_expressions.feature index 1263537..17c121f 100644 --- a/features/repl/clone_expressions.feature +++ b/features/repl/clone_expressions.feature @@ -73,7 +73,7 @@ Feature: Clone expressions And I press enter And I type "clone $num" And I press enter - Then I should see "Error: Cannot clone non-object (integer)" + Then I should see "Error: Evaluation error: Cannot clone non-object (integer)" Scenario: Error when trying to clone non-object (string) Given I am running the repl @@ -81,7 +81,7 @@ Feature: Clone expressions And I press enter And I type "clone $str" And I press enter - Then I should see "Error: Cannot clone non-object (string)" + Then I should see "Error: Evaluation error: Cannot clone non-object (string)" Scenario: Error when trying to clone non-object (array) Given I am running the repl @@ -89,7 +89,7 @@ Feature: Clone expressions And I press enter And I type "clone $arr" And I press enter - Then I should see "Error: Cannot clone non-object (array)" + Then I should see "Error: Evaluation error: Cannot clone non-object (array)" Scenario: Clone demonstrates shallow copy behavior Given I am running the repl diff --git a/features/repl/union_intersection_types.feature b/features/repl/union_intersection_types.feature index d1be5e2..78b46c9 100644 --- a/features/repl/union_intersection_types.feature +++ b/features/repl/union_intersection_types.feature @@ -25,7 +25,7 @@ Feature: Union and intersection types And I press enter And I type "foo(true)" And I press enter - Then I should see "TypeError" + Then I should see "Type error" And I should see "must be of type int|string, bool given" Scenario: Function with union type return value @@ -60,7 +60,7 @@ Feature: Union and intersection types And I press enter And I type "baz(new OnlyCountable())" And I press enter - Then I should see "TypeError" + Then I should see "Type error" And I should see "must be of type Countable&Traversable" Scenario: Function with complex union type diff --git a/src/Functions/display.php b/src/Functions/display.php index 1927247..449f870 100644 --- a/src/Functions/display.php +++ b/src/Functions/display.php @@ -26,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 382883e..1b5efae 100644 --- a/src/Functions/evaluation.php +++ b/src/Functions/evaluation.php @@ -33,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)); } /** @@ -68,9 +69,9 @@ 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)) { + 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 @@ -179,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), @@ -309,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); } /** @@ -372,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 => '' }; @@ -390,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 => '' }; @@ -919,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()) { @@ -1183,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) { @@ -1219,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' @@ -1412,7 +1416,7 @@ function evaluateArrowFunction(Expr\ArrowFunction $node, ReplSession $session): if ($result->isLeft()) { // Get the error using fold - $error = $result->fold(fn ($e) => $e)(fn ($r) => null); + $error = $result->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Arrow function evaluation failed: ' . $error->reason); } @@ -1478,7 +1482,7 @@ function evaluateClosure(Expr\Closure $node, ReplSession $session): Validation if ($stmt->expr !== null) { $result = evaluateNode($stmt->expr, $newSession); if ($result->isLeft()) { - $error = $result->fold(fn ($e) => $e)(fn ($r) => null); + $error = $result->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Closure evaluation failed: ' . $error->reason); } $returnValue = $result->getOrElse(null)->value; @@ -1487,7 +1491,7 @@ function evaluateClosure(Expr\Closure $node, ReplSession $session): Validation } elseif ($stmt instanceof Node\Stmt\Expression) { $result = evaluateNode($stmt->expr, $newSession); if ($result->isLeft()) { - $error = $result->fold(fn ($e) => $e)(fn ($r) => null); + $error = $result->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Closure evaluation failed: ' . $error->reason); } // Update session if this is an assignment @@ -1795,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 @@ -1830,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 @@ -1999,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'; } @@ -2246,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 @@ -2422,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'; } @@ -2468,8 +2472,7 @@ class StmtBlockResult public function __construct( public readonly Validation $result, public readonly ReplSession $updatedSession - ) { - } + ) {} } /** @@ -2491,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; @@ -2535,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 { @@ -2543,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 )); } @@ -2570,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_) { @@ -2578,7 +2584,7 @@ function evaluateStmtBlock(array $stmts, ReplSession $session): Validation if ($stmt->expr !== null) { $result = evaluateNode($stmt->expr, $session); if ($result->isLeft()) { - $error = $result->fold(fn ($e) => $e)(fn ($r) => null); + $error = $result->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Return expression evaluation failed: ' . $error->reason); } throw new FunctionReturnException($result->getOrElse(null)->value); @@ -2640,7 +2646,7 @@ function evaluateStmtBlock(array $stmts, ReplSession $session): Validation } } - return $lastResult ?? Success(EvaluationResult::of(null, 'Null')); + return $lastResult; } /** @@ -2725,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)) }; @@ -2743,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); @@ -2757,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)) }; @@ -2788,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()) { @@ -2936,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; @@ -2983,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 @@ -3053,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 @@ -3157,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); @@ -3249,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); } /** @@ -3266,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; } @@ -3296,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; @@ -3344,10 +3355,10 @@ function isComplexTypeValid(mixed $value, mixed $type): bool function getTypeName(mixed $type): string { if ($type instanceof Node\UnionType) { - $names = array_map(fn ($t) => getTypeName($t), $type->types); + $names = array_map(fn($t) => getTypeName($t), $type->types); return implode('|', $names); } elseif ($type instanceof Node\IntersectionType) { - $names = array_map(fn ($t) => getTypeName($t), $type->types); + $names = array_map(fn($t) => getTypeName($t), $type->types); return implode('&', $names); } else { return $type->toString(); @@ -3490,7 +3501,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Check if this parameter has a default value if ($param->default === null) { // Required parameter is missing - $expectedCount = count(array_filter($node->params, fn ($p) => $p->default === null)); + $expectedCount = count(array_filter($node->params, fn($p) => $p->default === null)); throw new \TypeError( "$funcName() expects at least $expectedCount argument" . ($expectedCount === 1 ? '' : 's') . ", " . count($args) . " given" ); @@ -3535,7 +3546,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Evaluate the default value expression $defaultResult = evaluateNode($param->default, $session); if ($defaultResult->isLeft()) { - $error = $defaultResult->fold(fn ($e) => $e)(fn ($r) => null); + $error = $defaultResult->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Default parameter evaluation failed: ' . $error->reason); } $defaultValue = $defaultResult->getOrElse(null)->value; @@ -3558,7 +3569,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Evaluate the yield value $yieldResult = evaluateNode($stmt->expr->value, $newSession); if ($yieldResult->isLeft()) { - $error = $yieldResult->fold(fn ($e) => $e)(fn ($r) => null); + $error = $yieldResult->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Yield evaluation failed: ' . $error->reason); } @@ -3568,21 +3579,22 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess if ($stmt->expr->key !== null) { $keyResult = evaluateNode($stmt->expr->key, $newSession); if ($keyResult->isLeft()) { - $error = $keyResult->fold(fn ($e) => $e)(fn ($r) => null); + $error = $keyResult->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Yield key evaluation failed: ' . $error->reason); } yield $keyResult->getOrElse(null)->value => $value; } 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); + $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 { @@ -3595,7 +3607,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Check if this parameter has a default value if ($param->default === null) { // Required parameter is missing - $expectedCount = count(array_filter($node->params, fn ($p) => $p->default === null)); + $expectedCount = count(array_filter($node->params, fn($p) => $p->default === null)); throw new \TypeError( "$funcName() expects at least $expectedCount argument" . ($expectedCount === 1 ? '' : 's') . ", " . count($args) . " given" ); @@ -3640,7 +3652,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess // Evaluate the default value expression $defaultResult = evaluateNode($param->default, $session); if ($defaultResult->isLeft()) { - $error = $defaultResult->fold(fn ($e) => $e)(fn ($r) => null); + $error = $defaultResult->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Default parameter evaluation failed: ' . $error->reason); } $defaultValue = $defaultResult->getOrElse(null)->value; @@ -3664,7 +3676,7 @@ function evaluateFunctionDefinition(Node\Stmt\Function_ $node, ReplSession $sess $result = evaluateStmtBlock($stmtsToEvaluate, $newSession); if ($result->isLeft()) { - $error = $result->fold(fn ($e) => $e)(fn ($r) => null); + $error = $result->fold(fn($e) => $e)(fn($r) => null); throw new \RuntimeException('Function body evaluation failed: ' . $error->reason); } @@ -3995,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" @@ -4025,8 +4037,8 @@ 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; } @@ -4051,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' @@ -4106,8 +4118,8 @@ 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; } @@ -4132,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' @@ -4241,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'" @@ -4295,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/session.php b/src/Functions/session.php index 6aeeb2e..dffd62f 100644 --- a/src/Functions/session.php +++ b/src/Functions/session.php @@ -30,7 +30,7 @@ */ function getSession(): State { - return (new State(fn ($s) => Pair($s, $s))); + return (new State(fn($s) => Pair($s, $s))); } /** @@ -41,7 +41,7 @@ function getSession(): State */ function modifySession(callable $f): State { - return new State(fn (ReplSession $s) => Pair($f($s), null)); + return new State(fn(ReplSession $s) => Pair($f($s), null)); } /** @@ -53,8 +53,8 @@ function modifySession(callable $f): State function addToHistory(string $expression): State { return modifySession( - fn (ReplSession $s) => - new ReplSession( + fn(ReplSession $s) + => new ReplSession( $s->history->append($expression), $s->variables, $s->colorEnabled, @@ -71,13 +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( + fn(ReplSession $s) + => new ReplSession( $s->history, $s->variables->plus($name, $value), $s->colorEnabled, @@ -93,11 +93,11 @@ 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 { - return new State(fn (ReplSession $s) => Pair($s, $s->variables->get($name))); + return new State(fn(ReplSession $s) => Pair($s, $s->variables->get($name))); } /** @@ -129,7 +129,7 @@ function nextVariable(): State */ function getVariables(): State { - return new State(fn (ReplSession $s) => Pair($s, $s->variables)); + return new State(fn(ReplSession $s) => Pair($s, $s->variables)); } /** @@ -139,7 +139,7 @@ function getVariables(): State */ function getHistory(): State { - return new State(fn (ReplSession $s) => Pair($s, $s->history)); + return new State(fn(ReplSession $s) => Pair($s, $s->history)); } /** @@ -151,8 +151,8 @@ function getHistory(): State function setColors(bool $enabled): State { return modifySession( - fn (ReplSession $s) => - new ReplSession( + fn(ReplSession $s) + => new ReplSession( $s->history, $s->variables, $enabled, @@ -171,7 +171,7 @@ function setColors(bool $enabled): State */ function isColorEnabled(): State { - return new State(fn (ReplSession $s) => Pair($s, $s->colorEnabled)); + return new State(fn(ReplSession $s) => Pair($s, $s->colorEnabled)); } /** @@ -182,8 +182,8 @@ function isColorEnabled(): State function resetSession(): State { return modifySession( - fn (ReplSession $s) => - new ReplSession( + fn(ReplSession $s) + => new ReplSession( ImmList(), ImmMap(), $s->colorEnabled, @@ -204,8 +204,8 @@ function resetSession(): State function setNamespace(?string $namespace): State { return modifySession( - fn (ReplSession $s) => - new ReplSession( + fn(ReplSession $s) + => new ReplSession( $s->history, $s->variables, $s->colorEnabled, @@ -224,7 +224,7 @@ function setNamespace(?string $namespace): State */ function getCurrentNamespace(): State { - return new State(fn (ReplSession $s) => Pair($s, $s->currentNamespace)); + return new State(fn(ReplSession $s) => Pair($s, $s->currentNamespace)); } /** @@ -237,8 +237,8 @@ function getCurrentNamespace(): State function addUseStatement(string $alias, string $fullName): State { return modifySession( - fn (ReplSession $s) => - new ReplSession( + fn(ReplSession $s) + => new ReplSession( $s->history, $s->variables, $s->colorEnabled, @@ -257,5 +257,5 @@ function addUseStatement(string $alias, string $fullName): State */ function getUseStatements(): State { - return new State(fn (ReplSession $s) => Pair($s, $s->useStatements)); + return new State(fn(ReplSession $s) => Pair($s, $s->useStatements)); } diff --git a/src/Repl/ReplLoop.php b/src/Repl/ReplLoop.php index e4adea1..1f31785 100644 --- a/src/Repl/ReplLoop.php +++ b/src/Repl/ReplLoop.php @@ -15,6 +15,8 @@ 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; @@ -34,7 +36,7 @@ function replLoop(ReplSession $session): IO { // Run the trampolined loop - return new IO(fn () => replLoopTrampoline($session)->run()); + return new IO(fn() => replLoopTrampoline($session)->run()); } /** @@ -67,7 +69,7 @@ function replLoopTrampoline(ReplSession $session): Trampoline if ($result instanceof ContinueRepl) { // Return More to continue the trampoline - return More(fn () => replLoopTrampoline($result->session)); + return More(fn() => replLoopTrampoline($result->session)); } // Shouldn't happen but handle gracefully @@ -86,7 +88,7 @@ function processInput(?string $input, ReplSession $session): IO { // Handle EOF (Control-D) if ($input === null) { - return new IO(fn () => new ExitRepl()); + return new IO(fn() => new ExitRepl()); } // Combine with any incomplete input from previous lines @@ -98,7 +100,7 @@ function processInput(?string $input, ReplSession $session): IO // Handle empty input if ($trimmed === '') { - return new IO(fn () => new ContinueRepl($session)); + return new IO(fn() => new ContinueRepl($session)); } // Handle REPL commands (only if not in multi-line mode) @@ -116,7 +118,7 @@ function processInput(?string $input, ReplSession $session): IO $session->variableCounter, $combinedInput ); - return new IO(fn () => new ContinueRepl($newSession)); + return new IO(fn() => new ContinueRepl($newSession)); } // Clear incomplete input buffer for complete expression @@ -301,13 +303,13 @@ function processCommand(string $command, ReplSession $session): IO } 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)), + ':exit', ':quit' => new IO(fn() => new ExitRepl()), + ':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)) }; } @@ -323,7 +325,7 @@ function resetReplState(ReplSession $session): IO $newSession = $pair->_1; return printLn("REPL state reset") - ->map(fn () => new ContinueRepl($newSession)); + ->map(fn() => new ContinueRepl($newSession)); } /** @@ -493,8 +495,8 @@ function importFunction(string $import, ReplSession $session): IO // Filter out internal functions (those starting with assert or format) $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 @@ -647,36 +649,22 @@ 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}"; } /** @@ -693,11 +681,11 @@ function evaluateAndDisplay(string $expression, ReplSession $session): IO // Use fold to handle both success and failure cases return $evalResult->fold( // Failure case: error is passed to this function - fn ($error) => printLn(formatError($error, $session)) - ->map(fn () => new ContinueRepl($session)) + fn($error) => printLn(formatError($error, $session)) + ->as(new ContinueRepl($session)) )( // Success case: result is passed to this function - fn ($result) => displayResult($result, $session, $expression) + fn($result) => displayResult($result, $session, $expression) ); } @@ -728,7 +716,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "Namespace cleared"; return printLn($output) - ->map(fn () => new ContinueRepl($newSession2)); + ->map(fn() => new ContinueRepl($newSession2)); } // Handle use statement @@ -749,7 +737,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e $output = "Imported $importCount " . ($importCount === 1 ? 'class/function' : 'classes/functions'); return printLn($output) - ->map(fn () => new ContinueRepl($newSession)); + ->map(fn() => new ContinueRepl($newSession)); } // Handle silent operations (property assignments, etc.) that shouldn't produce output @@ -782,14 +770,14 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e $newSession = $pair->_1; return printLn($output) - ->map(fn () => new ContinueRepl($newSession)); + ->map(fn() => new ContinueRepl($newSession)); } // Other silent operations - add to history but don't print anything $pair = (addToHistory($expression))->run($session); $newSession = $pair->_1; - return new IO(fn () => new ContinueRepl($newSession)); + return new IO(fn() => new ContinueRepl($newSession)); } // Check if this is an enum definition @@ -804,7 +792,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// enum {$result->value} defined"; return printLn($output) - ->map(fn () => new ContinueRepl($newSession)); + ->map(fn() => new ContinueRepl($newSession)); } // Check if this was an assignment with a specific variable name @@ -834,7 +822,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// function $displayName defined"; return printLn($output) - ->map(fn () => new ContinueRepl($currentSession)); + ->map(fn() => new ContinueRepl($currentSession)); } // Special handling for class definitions @@ -844,7 +832,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// class $varName defined"; return printLn($output) - ->map(fn () => new ContinueRepl($currentSession)); + ->map(fn() => new ContinueRepl($currentSession)); } // Special handling for interface definitions @@ -854,7 +842,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// interface $varName defined"; return printLn($output) - ->map(fn () => new ContinueRepl($currentSession)); + ->map(fn() => new ContinueRepl($currentSession)); } // Special handling for trait definitions @@ -864,7 +852,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "// trait $varName defined"; return printLn($output) - ->map(fn () => new ContinueRepl($currentSession)); + ->map(fn() => new ContinueRepl($currentSession)); } // Format output with bold variable name, pink type, and bold value if colors are enabled @@ -873,7 +861,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "$varName: {$result->type} = {$result->format()}"; return printLn($output) - ->map(fn () => new ContinueRepl($currentSession)); + ->map(fn() => new ContinueRepl($currentSession)); } // Check if this is an output statement (echo, print, var_dump, etc.) @@ -886,7 +874,7 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e // Add a newline after the output to prevent prompt from running into it echo "\n"; - return new IO(fn () => new ContinueRepl($newSession)); + return new IO(fn() => new ContinueRepl($newSession)); } // Generate next variable name for auto-assignment @@ -915,5 +903,5 @@ function displayResult(EvaluationResult $result, ReplSession $session, string $e : "$varName: {$result->type} = {$result->format()}"; return printLn($output) - ->map(fn () => new ContinueRepl($currentSession)); + ->map(fn() => new ContinueRepl($currentSession)); } diff --git a/src/Types/CommandError.php b/src/Types/CommandError.php index 095c83c..9566bf6 100644 --- a/src/Types/CommandError.php +++ b/src/Types/CommandError.php @@ -16,8 +16,7 @@ final class CommandError extends ReplError public function __construct( public readonly string $command, public readonly string $reason - ) { - } + ) {} public function message(): string { diff --git a/src/Types/ContinueRepl.php b/src/Types/ContinueRepl.php index d65892e..a16589e 100644 --- a/src/Types/ContinueRepl.php +++ b/src/Types/ContinueRepl.php @@ -13,7 +13,5 @@ final class ContinueRepl extends ReplResult { - public function __construct(public readonly ReplSession $session) - { - } + public function __construct(public readonly ReplSession $session) {} } diff --git a/src/Types/EvaluationError.php b/src/Types/EvaluationError.php index 863d645..f99d6c4 100644 --- a/src/Types/EvaluationError.php +++ b/src/Types/EvaluationError.php @@ -16,8 +16,7 @@ final class EvaluationError extends ReplError public function __construct( public readonly string $expression, public readonly string $reason - ) { - } + ) {} public function message(): string { diff --git a/src/Types/EvaluationResult.php b/src/Types/EvaluationResult.php index d64457a..ce2edc1 100644 --- a/src/Types/EvaluationResult.php +++ b/src/Types/EvaluationResult.php @@ -24,8 +24,7 @@ public function __construct( public ?string $assignedVariable = null, public array $additionalAssignments = [], public bool $isOutputStatement = false - ) { - } + ) {} public static function of(mixed $value, string $type, ?string $assignedVariable = null, array $additionalAssignments = [], bool $isOutputStatement = false): EvaluationResult { @@ -47,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) }; @@ -62,7 +61,7 @@ private function formatValue(mixed $value): string private function formatArray(array $arr): string { - $items = array_map(fn ($v) => $this->formatValue($v), $arr); + $items = array_map(fn($v) => $this->formatValue($v), $arr); return '[' . implode(', ', $items) . ']'; } diff --git a/src/Types/ExitRepl.php b/src/Types/ExitRepl.php index 421423b..5ebb860 100644 --- a/src/Types/ExitRepl.php +++ b/src/Types/ExitRepl.php @@ -11,6 +11,4 @@ namespace Phunkie\Console\Types; -final class ExitRepl extends ReplResult -{ -} +final class ExitRepl extends ReplResult {} diff --git a/src/Types/ParseError.php b/src/Types/ParseError.php index c4812f3..f8973a9 100644 --- a/src/Types/ParseError.php +++ b/src/Types/ParseError.php @@ -16,8 +16,7 @@ final class ParseError extends ReplError public function __construct( public readonly string $input, public readonly string $reason - ) { - } + ) {} public function message(): string { diff --git a/src/Types/ReplResult.php b/src/Types/ReplResult.php index 11cc176..2357587 100644 --- a/src/Types/ReplResult.php +++ b/src/Types/ReplResult.php @@ -14,6 +14,4 @@ /** * ADT for REPL control flow. */ -abstract class ReplResult -{ -} +abstract class ReplResult {} 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/src/Types/TypeError.php b/src/Types/TypeError.php index e42a5af..121a101 100644 --- a/src/Types/TypeError.php +++ b/src/Types/TypeError.php @@ -22,8 +22,7 @@ class TypeError extends ReplError public function __construct( public readonly string $subject, public readonly string $reason - ) { - } + ) {} public function message(): string { diff --git a/src/Types/VariableNotFoundError.php b/src/Types/VariableNotFoundError.php index d25bc55..7b79bce 100644 --- a/src/Types/VariableNotFoundError.php +++ b/src/Types/VariableNotFoundError.php @@ -15,8 +15,7 @@ final class VariableNotFoundError extends ReplError { public function __construct( public readonly string $variableName - ) { - } + ) {} public function message(): string { diff --git a/tests/Acceptance/ReplSteps.php b/tests/Acceptance/ReplSteps.php index e29b95d..2edb70d 100644 --- a/tests/Acceptance/ReplSteps.php +++ b/tests/Acceptance/ReplSteps.php @@ -245,8 +245,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 ); } } @@ -265,8 +265,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 ); } } @@ -287,8 +287,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 ); } } @@ -360,8 +360,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 ); } } @@ -371,8 +371,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 ); } } @@ -383,15 +383,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 @@ Date: Mon, 8 Dec 2025 17:25:51 +0000 Subject: [PATCH 08/14] Fix CI --- .github/workflows/ci.yml | 22 +++++++++++++++++----- .gitignore | 3 ++- README.md | 2 +- phpstan.neon | 2 ++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15aa7ae..68962ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,8 @@ jobs: name: PHP ${{ matrix.php-version }} steps: - uses: actions/checkout@v4 + - name: Unset local path repositories + run: composer config --unset repositories - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -40,29 +42,39 @@ jobs: run: ./bin/run-behat-tests.sh lint: - name: Code Style (PHP-CS-Fixer) + 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: 8.3 + 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 + run: php-cs-fixer fix --dry-run --diff --verbose --allow-risky=yes static-analysis: - name: Static Analysis (PHPStan) + 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 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: ${{ matrix.php-version }} extensions: mbstring, readline coverage: none - name: Install dependencies 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/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/phpstan.neon b/phpstan.neon index fd9ca1a..73c7995 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,5 +3,7 @@ includes: parameters: level: 5 + parallel: + maximumNumberOfProcesses: 1 paths: - src From 53da05768a20280967c27894fe26a11c56666dc1 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 17:32:11 +0000 Subject: [PATCH 09/14] Fix CI --- .github/workflows/ci.yml | 15 ++++++++++----- .php-cs-fixer.cache | 1 - 2 files changed, 10 insertions(+), 6 deletions(-) delete mode 100644 .php-cs-fixer.cache diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68962ec..9e34877 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,7 @@ jobs: name: PHP ${{ matrix.php-version }} steps: - uses: actions/checkout@v4 - - name: Unset local path repositories - run: composer config --unset repositories + - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -26,6 +25,10 @@ jobs: 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 @@ -35,7 +38,7 @@ jobs: restore-keys: | ${{ runner.os }}-php-${{ matrix.php-version }}- - name: Install dependencies - run: composer install --prefer-dist --no-progress + run: composer update --prefer-dist --no-progress - name: Run PHPUnit tests run: ./vendor/bin/phpunit --testdox - name: Run Behat tests (version-aware) @@ -70,7 +73,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Unset local path repositories - run: composer config --unset repositories + run: | + composer config --unset repositories + rm composer.lock - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -78,6 +83,6 @@ jobs: extensions: mbstring, readline coverage: none - name: Install dependencies - run: composer install --prefer-dist --no-progress + run: composer update --prefer-dist --no-progress - name: Run PHPStan run: vendor/bin/phpstan analyse src diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache deleted file mode 100644 index 3bb1f7b..0000000 --- a/.php-cs-fixer.cache +++ /dev/null @@ -1 +0,0 @@ -{"php":"8.2.12","version":"3.91.3:v3.91.3#9f10aa6390cea91da175ea608880e942d7c0226e","indent":" ","lineEnding":"\n","rules":{"nullable_type_declaration":true,"operator_linebreak":true,"ordered_types":{"null_adjustment":"always_last","sort_algorithm":"none"},"single_class_element_per_statement":true,"types_spaces":true,"array_indentation":true,"array_syntax":true,"cast_spaces":true,"concat_space":{"spacing":"one"},"function_declaration":{"closure_fn_spacing":"none"},"method_argument_space":{"after_heredoc":true},"new_with_parentheses":{"anonymous_class":false},"single_line_empty_body":true,"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const","const_import","do","else","elseif","enum","final","finally","for","foreach","function","function_import","if","insteadof","interface","match","named_argument","namespace","new","private","protected","public","readonly","static","switch","trait","try","type_colon","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"trailing_comma_in_multiline":{"after_heredoc":true},"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"simple_to_complex_string_variable":true,"octal_notation":true,"clean_namespace":true,"no_unset_cast":true,"assign_null_coalescing_to_coalesce_equal":true,"normalize_index_brace":true,"heredoc_indentation":true,"no_whitespace_before_comma_in_array":{"after_heredoc":true},"list_syntax":true,"ternary_to_null_coalescing":true},"hashes":{"tests\/Acceptance\/Support\/TestFileManager.php":"e403d6015d6686d20286e0c8d8ce1ae4","src\/Types\/ReplSession.php":"afccb8992da455e7a6fa46c09c631f7d","src\/Types\/CommandError.php":"ae5bd8636b3dfecef04aa61ebf6cfabc","src\/Types\/TypeError.php":"6447b3ebeb391cc9fdcd23b608a4cce5","src\/Types\/ContinueRepl.php":"b287863c9f34ee070db17d46181a3a6e","src\/Types\/ParseError.php":"f36893420aa0c611228945ed16f5bac2","src\/Types\/ExitRepl.php":"5797dbd770c8cdeb7bee955f8d62ba92","src\/Types\/EvaluationError.php":"af6bed65b3fd582c57dc7a65ce158a92","src\/Types\/ReplError.php":"dc627149a374a4f4cff98db2f798b896","src\/Types\/EvaluationResult.php":"9277221429e4de114fbccb63171efcc7","behat.php":"691ca2437cbefef7f92d2ab115b8c68f","tests\/Unit\/Functions\/TerminalTest.php":"59e76ee62cd3c3983e017b4b549f0cee","tests\/Unit\/Functions\/DisplayTest.php":"bd5cbb245b403e147405452c697fe111","tests\/Unit\/Functions\/EvaluationTest.php":"1acae3adbc4227d3a62197867771cbb7","tests\/Unit\/Functions\/SessionTest.php":"d5ac3fda4524be2504ace4ccd3d6bd64","tests\/Acceptance\/ReplSteps.php":"cd36ddb1505d4d884ab6fc785e00ee21","tests\/Acceptance\/Support\/ReplOutputReader.php":"8a1256b02f32fd3d6c5e61a4e2a67ea6","tests\/Acceptance\/Support\/ReplProcessManager.php":"bcadd4a7b8878c30fe4df57454ae434d","tests\/Acceptance\/Support\/DirectReplManager.php":"f424d20bb85b4fcf3b54264d7a61bbe2","tests\/Acceptance\/Support\/StringHelper.php":"987552f874f1d313ff9b86dbd3a4cf33","src\/Types\/ReplResult.php":"308f182be73cf8c704b9be18a7f4da60","src\/Types\/VariableNotFoundError.php":"2d2ea8518b49d7c2fd42c06a73ec61e8","src\/Repl\/ReplLoop.php":"46eea0d20b0ee60efc8b4d34cf6cdc55","src\/Functions\/evaluation.php":"5d8344f3450a0204cfdea22a3c9878e1","src\/Functions\/terminal.php":"4c07be5d3bf603c6d5d8ee310138a2d3","src\/Functions\/session.php":"378ec9f40896d7c01db9d88ba61f507d","src\/Functions\/parsing.php":"7cb9fe7e58d684f2e52d7ba5c5670bef","src\/Functions\/display.php":"82f5d30dbe08e4fc29489fead238dd9d","src\/Functions\/common.php":"38978a7963d52026b0aeaba4ffc10fe2"}} \ No newline at end of file From 4953a97a16218d8b8e1d2fd9f5ca2e205c2eef2e Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 17:44:25 +0000 Subject: [PATCH 10/14] Update composer lock --- composer.lock | 4 ++-- phpstan.neon | 4 ++-- scripts/phpstan-bootstrap.php | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 scripts/phpstan-bootstrap.php diff --git a/composer.lock b/composer.lock index 7134b96..e67e311 100644 --- a/composer.lock +++ b/composer.lock @@ -70,7 +70,7 @@ "dist": { "type": "path", "url": "../effect", - "reference": "ea55dd81d9af4a202774e628168c698e10841d91" + "reference": "aeb112f585c7f75ddf4c585a7fd53c8486cdb953" }, "require": { "php": "^8.2 || ^8.3 || ^8.4", @@ -144,7 +144,7 @@ "dist": { "type": "path", "url": "../phunkie", - "reference": "c15ee399a943f038bfa45c18a4d9a9ddc09c1f5b" + "reference": "9ddcbe0fa2dc433f88007a2223d2162ad2c3f303" }, "require": { "php": "^8.2 || ^8.3 || ^8.4" diff --git a/phpstan.neon b/phpstan.neon index 73c7995..e294110 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,7 +3,7 @@ includes: parameters: level: 5 - parallel: - maximumNumberOfProcesses: 1 + bootstrapFiles: + - scripts/phpstan-bootstrap.php paths: - src diff --git a/scripts/phpstan-bootstrap.php b/scripts/phpstan-bootstrap.php new file mode 100644 index 0000000..a777dda --- /dev/null +++ b/scripts/phpstan-bootstrap.php @@ -0,0 +1,3 @@ + Date: Mon, 8 Dec 2025 18:03:26 +0000 Subject: [PATCH 11/14] Fix composer --- composer.json | 2 +- composer.lock | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index f3fab56..0c6366e 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "behat/behat": "^3.22", "phpstan/phpstan": "^2.1", "friendsofphp/php-cs-fixer": "^3.90", - "phunkie/phpstan": "@dev" + "phunkie/phpstan": "1.0.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index e67e311..f1d6574 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "39e510a206d77bda1a91571688c1621d", + "content-hash": "7f67a46ed1d213095b7a3965191f1d36", "packages": [ { "name": "nikic/php-parser", @@ -1557,16 +1557,16 @@ }, { "name": "phunkie/phpstan", - "version": "dev-main", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/phunkie/phpstan.git", - "reference": "9e2e9386bef5fa35adcc306106172160c5e78fb3" + "reference": "91dd9931edc1fb81d5a5d5eb61179a87e77f8bc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phunkie/phpstan/zipball/9e2e9386bef5fa35adcc306106172160c5e78fb3", - "reference": "9e2e9386bef5fa35adcc306106172160c5e78fb3", + "url": "https://api.github.com/repos/phunkie/phpstan/zipball/91dd9931edc1fb81d5a5d5eb61179a87e77f8bc6", + "reference": "91dd9931edc1fb81d5a5d5eb61179a87e77f8bc6", "shasum": "" }, "require": { @@ -1578,7 +1578,6 @@ "phunkie/effect": "dev-developing-1.0.0 as 1.0.0", "phunkie/phunkie": "dev-developing-1.0.0 as 1.0.0" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -1607,7 +1606,7 @@ "issues": "https://github.com/phunkie/phpstan/issues", "source": "https://github.com/phunkie/phpstan/tree/1.0.0" }, - "time": "2025-12-08T11:09:04+00:00" + "time": "2025-12-08T18:01:33+00:00" }, { "name": "psr/container", @@ -5319,7 +5318,6 @@ "minimum-stability": "stable", "stability-flags": { "phunkie/effect": 20, - "phunkie/phpstan": 20, "phunkie/phunkie": 20 }, "prefer-stable": false, From 6ed9d135e6a6eff08e89a1b6f3f80be54a2154eb Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 18:32:19 +0000 Subject: [PATCH 12/14] Fix CI --- composer.lock | 4 ++-- tests/Acceptance/Fixtures/.gitkeep | 0 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 tests/Acceptance/Fixtures/.gitkeep diff --git a/composer.lock b/composer.lock index f1d6574..8771873 100644 --- a/composer.lock +++ b/composer.lock @@ -70,7 +70,7 @@ "dist": { "type": "path", "url": "../effect", - "reference": "aeb112f585c7f75ddf4c585a7fd53c8486cdb953" + "reference": "690ef14faad984740ef8d6bc2a8f1bded54dbd12" }, "require": { "php": "^8.2 || ^8.3 || ^8.4", @@ -144,7 +144,7 @@ "dist": { "type": "path", "url": "../phunkie", - "reference": "9ddcbe0fa2dc433f88007a2223d2162ad2c3f303" + "reference": "d961f2fc1ba80ab20b2106e95ae7d29d439c0d68" }, "require": { "php": "^8.2 || ^8.3 || ^8.4" diff --git a/tests/Acceptance/Fixtures/.gitkeep b/tests/Acceptance/Fixtures/.gitkeep new file mode 100644 index 0000000..e69de29 From bf2396730000f6bbcd9018b998db6de6e2493cb4 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Tue, 9 Dec 2025 17:45:40 +0000 Subject: [PATCH 13/14] Preparing for release --- composer.json | 16 +-- composer.lock | 301 +++++++++++++++++++++----------------------------- 2 files changed, 128 insertions(+), 189 deletions(-) diff --git a/composer.json b/composer.json index 0c6366e..37ef6c2 100644 --- a/composer.json +++ b/composer.json @@ -8,20 +8,10 @@ "email": "marcello.duarte@gmail.com" } ], - "repositories": [ - { - "type": "path", - "url": "../phunkie" - }, - { - "type": "path", - "url": "../effect" - } - ], "require": { "php": "^8.2 || ^8.3 || ^8.4", - "phunkie/phunkie": "dev-developing-1.0.0 as 1.0.0", - "phunkie/effect": "dev-developing-1.0.0 as 1.0.0", + "phunkie/phunkie": "^1.0", + "phunkie/effect": "^1.0", "nikic/php-parser": "^5.6" }, "require-dev": { @@ -29,7 +19,7 @@ "behat/behat": "^3.22", "phpstan/phpstan": "^2.1", "friendsofphp/php-cs-fixer": "^3.90", - "phunkie/phpstan": "1.0.0" + "phunkie/phpstan": "^1.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 8771873..1673cda 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7f67a46ed1d213095b7a3965191f1d36", + "content-hash": "25cb59ccc0129361f933679948084f73", "packages": [ { "name": "nikic/php-parser", @@ -66,64 +66,41 @@ }, { "name": "phunkie/effect", - "version": "dev-developing-1.0.0", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/phunkie/effect.git", + "reference": "fd90513682eb3a5a76a4e9f43ec89d1a9785458e" + }, "dist": { - "type": "path", - "url": "../effect", - "reference": "690ef14faad984740ef8d6bc2a8f1bded54dbd12" + "type": "zip", + "url": "https://api.github.com/repos/phunkie/effect/zipball/fd90513682eb3a5a76a4e9f43ec89d1a9785458e", + "reference": "fd90513682eb3a5a76a4e9f43ec89d1a9785458e", + "shasum": "" }, "require": { "php": "^8.2 || ^8.3 || ^8.4", - "phunkie/phunkie": "dev-developing-1.0.0 as 1.0.0" + "phunkie/phunkie": "^1.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.90", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.5", - "phunkie/phpstan": "@dev" + "phunkie/phpstan": "^1.0" }, "suggest": { "ext-parallel": "Required for parallel execution using threads. PHP must be compiled with ZTS support." }, "type": "library", "autoload": { - "psr-4": { - "Phunkie\\Effect\\": "src/" - }, "files": [ "src/Functions/common.php" - ] - }, - "autoload-dev": { + ], "psr-4": { - "Tests\\Unit\\Phunkie\\Effect\\": "tests/Unit/" + "Phunkie\\Effect\\": "src/" } }, - "scripts": { - "test": [ - "phpunit" - ], - "phpstan": [ - "phpstan analyse --memory-limit=512M" - ], - "cs-fix": [ - "php-cs-fixer fix --allow-risky=yes" - ], - "cs-check": [ - "php-cs-fixer fix --dry-run --diff --allow-risky=yes" - ], - "lint": [ - "@cs-check", - "@phpstan" - ], - "test-all": [ - "scripts/test-all-versions.sh" - ], - "check": [ - "@lint", - "@test" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -134,17 +111,25 @@ } ], "description": "A functional effects library for PHP inspired by Scala's cats-effect", - "transport-options": { - "relative": true - } + "support": { + "issues": "https://github.com/phunkie/effect/issues", + "source": "https://github.com/phunkie/effect/tree/1.0.0" + }, + "time": "2025-12-08T19:02:35+00:00" }, { "name": "phunkie/phunkie", - "version": "dev-developing-1.0.0", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/phunkie/phunkie.git", + "reference": "edfe0c5e3b382d8827bdaef5c0ef027840eceaf5" + }, "dist": { - "type": "path", - "url": "../phunkie", - "reference": "d961f2fc1ba80ab20b2106e95ae7d29d439c0d68" + "type": "zip", + "url": "https://api.github.com/repos/phunkie/phunkie/zipball/edfe0c5e3b382d8827bdaef5c0ef027840eceaf5", + "reference": "edfe0c5e3b382d8827bdaef5c0ef027840eceaf5", + "shasum": "" }, "require": { "php": "^8.2 || ^8.3 || ^8.4" @@ -159,50 +144,16 @@ }, "type": "library", "autoload": { + "files": [ + "src/Phunkie/Functions/common.php" + ], "psr-0": { "": [ "src/" ] - }, - "files": [ - "src/Phunkie/Functions/common.php" - ] - }, - "autoload-dev": { - "psr-4": { - "\\spec\\": [ - "spec/" - ] } }, - "scripts": { - "cs-fix": [ - "bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --verbose --allow-risky=yes" - ], - "cs-check": [ - "bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff --verbose --allow-risky=yes" - ], - "phpstan": [ - "phpstan analyse --memory-limit=512M src" - ], - "lint": [ - "@cs-check", - "@phpstan" - ], - "test": [ - "bin/phpunit -c phpunit.xml.dist --do-not-cache-result" - ], - "test-debug": [ - "bin/phpunit -c phpunit.xml.dist --debug" - ], - "test-all": [ - "scripts/test-all-versions.sh" - ], - "check": [ - "@lint", - "@test" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -213,9 +164,11 @@ } ], "description": "Functional structures library for PHP", - "transport-options": { - "relative": true - } + "support": { + "issues": "https://github.com/phunkie/phunkie/issues", + "source": "https://github.com/phunkie/phunkie/tree/1.0.0" + }, + "time": "2025-12-08T18:48:37+00:00" } ], "packages-dev": [ @@ -316,16 +269,16 @@ }, { "name": "behat/gherkin", - "version": "v4.15.0", + "version": "v4.16.1", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b" + "reference": "e26037937dfd48528746764dd870bc5d0836665f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", - "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/e26037937dfd48528746764dd870bc5d0836665f", + "reference": "e26037937dfd48528746764dd870bc5d0836665f", "shasum": "" }, "require": { @@ -333,7 +286,7 @@ "php": ">=8.1 <8.6" }, "require-dev": { - "cucumber/gherkin-monorepo": "dev-gherkin-v36.0.0", + "cucumber/gherkin-monorepo": "dev-gherkin-v37.0.0", "friendsofphp/php-cs-fixer": "^3.77", "mikey179/vfsstream": "^1.6", "phpstan/extension-installer": "^1", @@ -379,9 +332,23 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.15.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.16.1" }, - "time": "2025-11-05T15:34:04+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", @@ -3816,25 +3783,25 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.0", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + "reference": "d937d400b980523dc9ee946bb69972b5e619058d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "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|^8.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3862,7 +3829,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + "source": "https://github.com/symfony/filesystem/tree/v8.0.1" }, "funding": [ { @@ -3882,27 +3849,27 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/finder", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", + "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|^8.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3930,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.4.0" + "source": "https://github.com/symfony/finder/tree/v8.0.0" }, "funding": [ { @@ -3950,24 +3917,24 @@ "type": "tidelift" } ], - "time": "2025-11-05T05:42:40+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "b38026df55197f9e39a44f3215788edf83187b80" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80", - "reference": "b38026df55197f9e39a44f3215788edf83187b80", + "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", @@ -4001,7 +3968,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.4.0" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -4021,7 +3988,7 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:39:26+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4604,20 +4571,20 @@ }, { "name": "symfony/process", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149", + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -4645,7 +4612,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.0" + "source": "https://github.com/symfony/process/tree/v8.0.0" }, "funding": [ { @@ -4665,7 +4632,7 @@ "type": "tidelift" } ], - "time": "2025-10-16T11:21:06+00:00" + "time": "2025-10-16T16:25:44+00:00" }, { "name": "symfony/service-contracts", @@ -4756,20 +4723,20 @@ }, { "name": "symfony/stopwatch", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "8a24af0a2e8a872fb745047180649b8418303084" + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084", - "reference": "8a24af0a2e8a872fb745047180649b8418303084", + "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", @@ -4798,7 +4765,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.4.0" + "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" }, "funding": [ { @@ -4818,39 +4785,38 @@ "type": "tidelift" } ], - "time": "2025-08-04T07:05:15+00:00" + "time": "2025-08-04T07:36:47+00:00" }, { "name": "symfony/string", - "version": "v7.4.0", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", - "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", + "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.33", - "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|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/intl": "^6.4|^7.0|^8.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|^8.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -4889,7 +4855,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.0" + "source": "https://github.com/symfony/string/tree/v8.0.1" }, "funding": [ { @@ -4909,7 +4875,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/translation", @@ -5095,26 +5061,25 @@ }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "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|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5152,7 +5117,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" }, "funding": [ { @@ -5172,7 +5137,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2025-11-05T18:53:00+00:00" }, { "name": "symfony/yaml", @@ -5301,25 +5266,9 @@ "time": "2025-11-17T20:03:58+00:00" } ], - "aliases": [ - { - "package": "phunkie/effect", - "version": "dev-developing-1.0.0", - "alias": "1.0.0", - "alias_normalized": "1.0.0.0" - }, - { - "package": "phunkie/phunkie", - "version": "dev-developing-1.0.0", - "alias": "1.0.0", - "alias_normalized": "1.0.0.0" - } - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "phunkie/effect": 20, - "phunkie/phunkie": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { From 13b8432acce0ca8da83d03da552c8f5526c82b8e Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Wed, 10 Dec 2025 18:10:35 +0000 Subject: [PATCH 14/14] Debug: capture stderr in tests to diagnose PHP 8.4 CI failure --- tests/Acceptance/ReplSteps.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Acceptance/ReplSteps.php b/tests/Acceptance/ReplSteps.php index 2edb70d..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);