diff --git a/.docker/php85.Dockerfile b/.docker/php85.Dockerfile new file mode 100644 index 0000000..766b38b --- /dev/null +++ b/.docker/php85.Dockerfile @@ -0,0 +1,12 @@ +FROM php:8.5-cli-alpine + +# Dedicated runtime for the `@group php85` tests -- syntax (e.g. the 8.5 +# pipe operator `|>`) that the host-version parser can only tokenize on +# PHP 8.5. These run with coverage disabled, so no xdebug/pcov here: +# just composer plus the tools composer + `make test/unit/php85` need. +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +RUN apk add --no-cache \ + git \ + unzip \ + make diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml index 94e6fde..5115e79 100644 --- a/.github/workflows/ci-core.yml +++ b/.github/workflows/ci-core.yml @@ -15,15 +15,27 @@ concurrency: jobs: phpunit: - name: PHPUnit + # 8.4 is the supported/default runtime and runs the full suite minus the + # `php85` group; a dedicated 8.5 container runs only that group, which + # covers syntax (e.g. the pipe operator `|>`) that the host-version + # parser can only tokenize on PHP 8.5. + name: PHPUnit (PHP ${{ matrix.php }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - php: '8.4' + target: test/unit + - php: '8.5' + target: test/unit/php85 steps: - uses: actions/checkout@v4 - - name: Setup PHP 8.4 + - name: Setup PHP ${{ matrix.php }} uses: shivammathur/setup-php@v2 with: - php-version: '8.4' + php-version: ${{ matrix.php }} extensions: dom, json, mbstring, tokenizer coverage: none tools: composer:v2 @@ -32,7 +44,7 @@ jobs: uses: ramsey/composer-install@v3 - name: Run PHPUnit - run: make test/unit + run: make ${{ matrix.target }} phpstan: name: PHPStan diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ca9e17..84b9757 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,10 +3,22 @@ ## Test ```bash -make test/unit # PHPUnit -make test/mutation # Infection, MSI under a 95 % gate +make test/unit # PHPUnit on the default PHP 8.4 runtime +make test/unit/php85 # only tests tagged `@group php85`; needs a PHP 8.5 runtime +make test/mutation # Infection, MSI under a 95 % gate ``` -CI gates every PR on both targets. `infection.json5` carries a curated set +The supported runtime is PHP `^8.4`. Tests that exercise newer-PHP syntax +(e.g. the 8.5 pipe operator) are tagged `@group php85`; `make test/unit` +excludes them and they self-skip via `#[RequiresPhp]` off an 8.5 host. CI +runs them in a dedicated PHP 8.5 job. + +No PHP 8.5 locally? Run them in the bundled 8.5 container: + +```bash +docker compose run --rm php85 make test/unit/php85 +``` + +CI gates every PR on these targets. `infection.json5` carries a curated set of per-mutator `ignore` rules for genuinely-equivalent / defensive mutations so the report only surfaces real test gaps. \ No newline at end of file diff --git a/Makefile b/Makefile index e323a9a..3288b02 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,18 @@ # targets here. .PHONY: test/unit +# Default runtime is PHP 8.4 (composer requires ^8.4). Tests exercising +# newer-PHP syntax are tagged `@group php85` and excluded here; they run +# on an 8.5 runtime via `make test/unit/php85`. test/unit: - php vendor/bin/phpunit + php vendor/bin/phpunit --exclude-group php85 + +.PHONY: test/unit/php85 +# Runs only the PHP 8.5-specific syntax tests (e.g. the pipe operator). +# Requires a PHP 8.5 runtime -- CI uses a dedicated 8.5 container; on 8.4 +# these self-skip via #[RequiresPhp]. +test/unit/php85: + php vendor/bin/phpunit --group php85 .PHONY: lint/phpstan # Static analysis at level 7. Memory limit lifted because deep generic diff --git a/README.md b/README.md index f318ffe..1d4ee90 100644 --- a/README.md +++ b/README.md @@ -117,14 +117,18 @@ Write a generic class and use it: namespace App; class Collection { - public function __construct(public T ...$items) {} + private array $items = []; + + public function add(T $item): void { $this->items[] = $item; } public function first(): ?T { return $this->items[0] ?? null; } } // src/Use.xphp namespace App; -$users = new Collection::(new User('Alice'), new User('Bob')); +$users = new Collection::(); +$users->add(new User('Alice')); +$users->add(new User('Bob')); echo $users->first()->name; ``` diff --git a/bin/xphp b/bin/xphp index cdf9b11..a1c75ee 100755 --- a/bin/xphp +++ b/bin/xphp @@ -3,7 +3,31 @@ declare(strict_types=1); -require dirname(__DIR__) . '/vendor/autoload.php'; +(static function (): void { + $candidates = [ + // Set by Composer's generated bin proxy (vendor/bin/xphp). + $GLOBALS['_composer_autoload_path'] ?? null, + // Installed as a dependency: vendor/xphp-lang/xphp/bin -> /vendor/autoload.php + dirname(__DIR__, 3) . '/autoload.php', + // Standalone checkout AND the PHAR (phar://alias.phar/vendor/autoload.php) rely on + // this one -- keep it present and last. + dirname(__DIR__) . '/vendor/autoload.php', + ]; + + foreach ($candidates as $file) { + if (is_string($file) && is_file($file)) { + require $file; + + return; + } + } + + fwrite( + STDERR, + 'xphp: could not locate Composer\'s autoloader. Run `composer install`.' . PHP_EOL, + ); + exit(1); +})(); use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; diff --git a/docker-compose.yml b/docker-compose.yml index ab29a5d..31851af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,15 @@ services: environment: XDEBUG_MODE: coverage,develop + # PHP 8.5 runtime for the `@group php85` tests. Run them with: + # docker compose run --rm php85 make test/unit/php85 + php85: + build: + dockerfile: .docker/php85.Dockerfile + working_dir: /opt/app + volumes: + - ./:/opt/app + xphp: extends: service: php diff --git a/docs/caveats.md b/docs/caveats.md index ce37261..4799252 100644 --- a/docs/caveats.md +++ b/docs/caveats.md @@ -493,3 +493,35 @@ class TempContainer { } $x = new TempContainer::(); ``` + +## Newer PHP syntax needs a matching host runtime + +### ❌ What doesn't work + +Running the transpiler on PHP 8.4 over a source file that uses PHP 8.5 +syntax — for example the pipe operator: + +```php +$slug = $title |> trim(...) |> strtolower(...); +``` + +``` +Syntax error, unexpected '>' +``` + +### Why + +xphp owns only the generic syntax; everything else is plain PHP. It +parses your source with `nikic/php-parser` configured for the **host** +PHP version (the one running the transpiler). On PHP 8.4 the lexer +can't tokenize 8.5-only syntax like `|>`, so the parse fails before +specialization even begins. There's nothing generic about the line — +it just never reaches the host parser's grammar. + +### ✅ Workaround + +Run the transpiler on a PHP version that can parse your syntax — e.g. +PHP 8.5 for the pipe operator. Plain (non-generic) code, including +newer-PHP syntax, passes straight through untouched. Note the emitted +PHP still requires a runtime that supports those features to *execute*; +the supported floor for xphp itself is PHP 8.4 (`composer.json`). diff --git a/docs/guides/how-it-works.md b/docs/guides/how-it-works.md index d9ad767..3452aee 100644 --- a/docs/guides/how-it-works.md +++ b/docs/guides/how-it-works.md @@ -22,8 +22,9 @@ defaults to `dist` and `[cache-dir]` defaults to `.xphp-cache`. The data flows t turn into AST, the AST populates a Registry and a TypeHierarchy, a fixed-point loop expands every concrete instantiation into a specialized class file, and finally the rewritten user code lands in the target -directory while specialized classes land in a cache directory under -`XPHP\Generated\