From 053a9f84d2a78685b3213e97566269ab335773fe Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 21 Oct 2026 23:21:08 +0530 Subject: [PATCH 1/4] feat(phpstan): run shared baseline at level 8 --- composer-packages/phpstan/CHANGELOG.md | 9 ++++++ composer-packages/phpstan/README.md | 30 +++++++++++++------ composer-packages/phpstan/composer.json | 2 +- composer-packages/phpstan/phpstan.neon.dist | 8 +++-- .../phpstan/tests/ConfigTest.php | 8 +++++ .../phpstan/tests/fixtures/nullable.php | 9 ++++++ composer.json | 2 +- composer.lock | 21 +++++++++---- monorepo-builder.php | 2 +- 9 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 composer-packages/phpstan/tests/fixtures/nullable.php diff --git a/composer-packages/phpstan/CHANGELOG.md b/composer-packages/phpstan/CHANGELOG.md index 63a6019..5a913dd 100644 --- a/composer-packages/phpstan/CHANGELOG.md +++ b/composer-packages/phpstan/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added + +- `phpVersion` range `80200`–`80500`, so code is analysed against every PHP version from the 8.2 floor up to 8.5. + +### Changed + +- **Breaking:** the baseline runs at level 8 (was 5). Projects now get missing-type errors for untyped parameters, returns and properties and for arrays with no value type (level 6), partially wrong union types (level 7), and nullable access (level 8). To migrate, record existing errors with `phpstan analyse --generate-baseline`, or set `level` lower in your own neon. +- `phpstan/phpstan` minimum raised to `^2.3`. 2.3 rewrote the analysis engine and bundles the optional Turbo extension, which loads automatically on PHP 8.3+: per its release notes, analysing WordPress core went from 60 s on 2.1.33 to under 8 s. 2.3 also reports some errors older releases missed, mostly `function.resultUnused`. + ## [1.0.1] - 2026-10-01 ### Added diff --git a/composer-packages/phpstan/README.md b/composer-packages/phpstan/README.md index d19f6fe..4f25d3b 100644 --- a/composer-packages/phpstan/README.md +++ b/composer-packages/phpstan/README.md @@ -3,7 +3,7 @@

License: GPL-2.0-or-later PHP 8.2+ - PHPStan 2.x + PHPStan 2.3+

@@ -16,7 +16,7 @@ > The [`rtCamp/wp-phpstan`](https://github.com/rtCamp/wp-phpstan) repository is a read-only mirror, split from > there on each release. Please open issues and pull requests in wp-tooling. -This package ships a single `phpstan.neon.dist` of rtCamp defaults (level 5, WordPress-aware +This package ships a single `phpstan.neon.dist` of rtCamp defaults (level 8, PHP 8.2–8.5, WordPress-aware rules) and pulls in [`szepeviktor/phpstan-wordpress`](https://github.com/szepeviktor/phpstan-wordpress), which loads the WordPress function stubs and dynamic return-type extensions PHPStan needs to understand core. @@ -40,14 +40,20 @@ includes: - vendor/rtcamp/wp-phpstan/phpstan.neon.dist parameters: + # Constants define()d in the main plugin file are only known if it is loaded. + bootstrapFiles: + - my-plugin.php paths: - - src - - includes + - my-plugin.php + - inc/ excludePaths: - - vendor - - node_modules - - build - - tests + # Not reported on, but still scanned so their classes and functions resolve. + analyse: + - tests/ (?) + - vendor/ + # Neither reported on nor scanned. (?) stops PHPStan failing when the path is absent. + analyseAndScan: + - node_modules (?) # Project-specific overrides and suppressions go here. ``` @@ -57,6 +63,10 @@ That single include is all you need. Run the analysis with: vendor/bin/phpstan analyse ``` +On PHP 8.3 and newer, PHPStan loads its bundled Turbo extension automatically (`vendor/bin/phpstan diagnose` shows its status); PHP 8.2 runs without it. For fast CI runs, set `tmpDir` to a path inside the project and cache it between runs: PHPStan only throws the whole result cache away when PHPStan itself or your config changes. + +If the project registers WP-CLI commands, also `composer require --dev php-stubs/wp-cli-stubs` and add `vendor/php-stubs/wp-cli-stubs/wp-cli-stubs.php` under `scanFiles`. The baseline doesn't do this for you because `wp-cli-stubs` 2.12 caps `php-stubs/wordpress-stubs` at `^6.0`, which would hold every consumer back from the WordPress 7 stubs. + > **The WordPress extension must load exactly once.** This baseline already > includes `szepeviktor/phpstan-wordpress`, and loading it twice fails PHPStan. > So: don't include its `extension.neon` in your own neon, and if you use @@ -73,9 +83,11 @@ vendor/bin/phpstan analyse ## Overriding Anything you set under `parameters` *after* the includes wins, so a project can -raise the level, add `paths`, or add `ignoreErrors` without forking this baseline. +change the level, add `paths`, or add `ignoreErrors` without forking this baseline. Per-project suppressions belong in your own neon — keep this shared file clean. +An existing project moving up to level 8 can record its current errors with `vendor/bin/phpstan analyse --generate-baseline` and include the generated `phpstan-baseline.neon`, so only new code has to pass, or set `level` lower in its own neon until it catches up. + ## License GPL-2.0-or-later. See [LICENSE](./LICENSE). diff --git a/composer-packages/phpstan/composer.json b/composer-packages/phpstan/composer.json index b6851ca..d2dc381 100644 --- a/composer-packages/phpstan/composer.json +++ b/composer-packages/phpstan/composer.json @@ -22,7 +22,7 @@ }, "require": { "php": ">=8.2", - "phpstan/phpstan": "^2.0", + "phpstan/phpstan": "^2.3", "szepeviktor/phpstan-wordpress": "^2.0" }, "require-dev": { diff --git a/composer-packages/phpstan/phpstan.neon.dist b/composer-packages/phpstan/phpstan.neon.dist index 332db52..036fd03 100644 --- a/composer-packages/phpstan/phpstan.neon.dist +++ b/composer-packages/phpstan/phpstan.neon.dist @@ -7,14 +7,18 @@ includes: - ../../szepeviktor/phpstan-wordpress/extension.neon parameters: - level: 5 + level: 8 + # Analyse against the whole supported PHP range: the 8.2 floor up to 8.5. + phpVersion: + min: 80200 + max: 80500 # WordPress doc-blocks are unreliable; don't treat their types as certain. treatPhpDocTypesAsCertain: false reportUnmatchedIgnoredErrors: true # Wrong-case function/method calls and provably wrong @var tags. checkFunctionNameCase: true reportWrongPhpDocTypeInVarTag: true - # Low-noise correctness checks — stricter than bare level 5, not a level bump. + # Low-noise correctness checks on top of what the level enables. inferPrivatePropertyTypeFromConstructor: true checkInternalClassCaseSensitivity: true checkExplicitMixedMissingReturn: true diff --git a/composer-packages/phpstan/tests/ConfigTest.php b/composer-packages/phpstan/tests/ConfigTest.php index c29d27b..3d08010 100644 --- a/composer-packages/phpstan/tests/ConfigTest.php +++ b/composer-packages/phpstan/tests/ConfigTest.php @@ -61,6 +61,14 @@ public function test_baseline_catches_a_type_error(): void self::assertStringContainsString('should return int but returns string', $output); } + public function test_baseline_runs_at_level_8(): void + { + [$exitCode, $output] = $this->analyse(__DIR__ . '/fixtures/nullable.php'); + + self::assertNotSame(0, $exitCode, "Expected the nullable fixture to fail analysis. PHPStan output:\n" . $output); + self::assertStringContainsString('Cannot access property $post_title on WP_Post|null', $output); + } + private function makeConsumerProject(): string { $project = tempnam(sys_get_temp_dir(), 'wp-phpstan-consumer-'); diff --git a/composer-packages/phpstan/tests/fixtures/nullable.php b/composer-packages/phpstan/tests/fixtures/nullable.php new file mode 100644 index 0000000..ace4d8d --- /dev/null +++ b/composer-packages/phpstan/tests/fixtures/nullable.php @@ -0,0 +1,9 @@ +post_title; +} diff --git a/composer.json b/composer.json index ff02a77..e27af31 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ } }, "require-dev": { - "phpstan/phpstan": "^2.0", + "phpstan/phpstan": "^2.3", "phpunit/phpunit": "^12.0", "squizlabs/php_codesniffer": "^3.10", "symplify/monorepo-builder": "^12.7" diff --git a/composer.lock b/composer.lock index 0a08206..2486326 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": "e1a41d52d893ae0332190833c26fddf5", + "content-hash": "0b5181d7b721b6f19994229ca8baf81c", "packages": [ { "name": "automattic/vipwpcs", @@ -1188,11 +1188,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.56", + "version": "2.3.1", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/93a603c9fc3be8c3c93bbc8d22170ad766685537", - "reference": "93a603c9fc3be8c3c93bbc8d22170ad766685537", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6155239b488e95d26d1ebd95c6b4bb294d7f09db", + "reference": "6155239b488e95d26d1ebd95c6b4bb294d7f09db", "shasum": "" }, "require": { @@ -1215,6 +1215,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -1237,7 +1248,7 @@ "type": "github" } ], - "time": "2026-05-26T17:04:57+00:00" + "time": "2026-10-08T21:09:11+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/monorepo-builder.php b/monorepo-builder.php index f5d7caf..9d25d6b 100644 --- a/monorepo-builder.php +++ b/monorepo-builder.php @@ -43,7 +43,7 @@ $mbConfig->dataToAppend([ 'require-dev' => [ 'phpunit/phpunit' => '^12.0', - 'phpstan/phpstan' => '^2.0', + 'phpstan/phpstan' => '^2.3', 'szepeviktor/phpstan-wordpress' => '^2.0', 'squizlabs/php_codesniffer' => '^3.10', ], From 3a9c7c604eb1594956fa2698683c7d88dd1e785c Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 21 Oct 2026 23:46:33 +0530 Subject: [PATCH 2/4] test(phpstan): pin the 8.2 phpVersion floor --- .../phpstan/tests/ConfigTest.php | 30 +++++++++++++++++-- .../phpstan/tests/fixtures/typed-constant.php | 9 ++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 composer-packages/phpstan/tests/fixtures/typed-constant.php diff --git a/composer-packages/phpstan/tests/ConfigTest.php b/composer-packages/phpstan/tests/ConfigTest.php index 3d08010..73c3d5a 100644 --- a/composer-packages/phpstan/tests/ConfigTest.php +++ b/composer-packages/phpstan/tests/ConfigTest.php @@ -69,6 +69,14 @@ public function test_baseline_runs_at_level_8(): void self::assertStringContainsString('Cannot access property $post_title on WP_Post|null', $output); } + public function test_baseline_analyses_for_the_php_8_2_floor(): void + { + [$exitCode, $output] = $this->analyse(__DIR__ . '/fixtures/typed-constant.php'); + + self::assertNotSame(0, $exitCode, "Expected the typed-constant fixture to fail analysis. PHPStan output:\n" . $output); + self::assertStringContainsString('Class constants with native types are supported only on PHP 8.3 and later', $output); + } + private function makeConsumerProject(): string { $project = tempnam(sys_get_temp_dir(), 'wp-phpstan-consumer-'); @@ -87,6 +95,20 @@ private function makeConsumerProject(): string 'Could not copy phpstan.neon.dist into the temp consumer project.' ); + // Without a phpVersion, PHPStan infers one from require.php in the + // composer.json of its working directory, but only when a + // vendor/autoload.php sits beside it; otherwise it reads the package's + // own composer.json (>=8.2). Both files below are needed so the baseline + // is the only source of the PHP range. + self::assertNotFalse( + file_put_contents($project . '/composer.json', "{}\n"), + 'Could not write composer.json into the temp consumer project.' + ); + self::assertNotFalse( + file_put_contents($project . '/vendor/autoload.php', "realPath(self::PACKAGE_ROOT . '/vendor/' . $dep), $project . '/vendor/' . $dep); if (! $linked) { @@ -136,10 +158,12 @@ private function analyse(string $targetFile): array ) ); - // WordPress stubs are large; raise the limit above PHP's 128M default so - // analysis completes instead of crashing the worker. + // Run from the project root, as a consumer does. WordPress stubs are + // large; raise the limit above PHP's 128M default so analysis completes + // instead of crashing the worker. $command = sprintf( - '%s %s analyse --no-progress --no-ansi --error-format=raw --memory-limit=1G --configuration=%s 2>&1', + 'cd %s && %s %s analyse --no-progress --no-ansi --error-format=raw --memory-limit=1G --configuration=%s 2>&1', + escapeshellarg($this->project), escapeshellarg(PHP_BINARY), escapeshellarg($this->phpstanBinary()), escapeshellarg($neon) diff --git a/composer-packages/phpstan/tests/fixtures/typed-constant.php b/composer-packages/phpstan/tests/fixtures/typed-constant.php new file mode 100644 index 0000000..10d2533 --- /dev/null +++ b/composer-packages/phpstan/tests/fixtures/typed-constant.php @@ -0,0 +1,9 @@ + Date: Thu, 22 Oct 2026 01:06:33 +0530 Subject: [PATCH 3/4] docs(phpstan): drop consumer how-tos from README --- composer-packages/phpstan/README.md | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/composer-packages/phpstan/README.md b/composer-packages/phpstan/README.md index 4f25d3b..a03a068 100644 --- a/composer-packages/phpstan/README.md +++ b/composer-packages/phpstan/README.md @@ -40,20 +40,11 @@ includes: - vendor/rtcamp/wp-phpstan/phpstan.neon.dist parameters: - # Constants define()d in the main plugin file are only known if it is loaded. - bootstrapFiles: - - my-plugin.php + # Every file and directory holding your own PHP. Dependencies resolve + # through Composer's autoloader, so vendor/ needs no entry. paths: - my-plugin.php - inc/ - excludePaths: - # Not reported on, but still scanned so their classes and functions resolve. - analyse: - - tests/ (?) - - vendor/ - # Neither reported on nor scanned. (?) stops PHPStan failing when the path is absent. - analyseAndScan: - - node_modules (?) # Project-specific overrides and suppressions go here. ``` @@ -63,10 +54,6 @@ That single include is all you need. Run the analysis with: vendor/bin/phpstan analyse ``` -On PHP 8.3 and newer, PHPStan loads its bundled Turbo extension automatically (`vendor/bin/phpstan diagnose` shows its status); PHP 8.2 runs without it. For fast CI runs, set `tmpDir` to a path inside the project and cache it between runs: PHPStan only throws the whole result cache away when PHPStan itself or your config changes. - -If the project registers WP-CLI commands, also `composer require --dev php-stubs/wp-cli-stubs` and add `vendor/php-stubs/wp-cli-stubs/wp-cli-stubs.php` under `scanFiles`. The baseline doesn't do this for you because `wp-cli-stubs` 2.12 caps `php-stubs/wordpress-stubs` at `^6.0`, which would hold every consumer back from the WordPress 7 stubs. - > **The WordPress extension must load exactly once.** This baseline already > includes `szepeviktor/phpstan-wordpress`, and loading it twice fails PHPStan. > So: don't include its `extension.neon` in your own neon, and if you use @@ -82,12 +69,10 @@ If the project registers WP-CLI commands, also `composer require --dev php-stubs ## Overriding -Anything you set under `parameters` *after* the includes wins, so a project can -change the level, add `paths`, or add `ignoreErrors` without forking this baseline. +Anything you set under `parameters` in your own neon overrides this baseline, so a +project can change the level, add `paths`, or add `ignoreErrors` without forking it. Per-project suppressions belong in your own neon — keep this shared file clean. -An existing project moving up to level 8 can record its current errors with `vendor/bin/phpstan analyse --generate-baseline` and include the generated `phpstan-baseline.neon`, so only new code has to pass, or set `level` lower in its own neon until it catches up. - ## License GPL-2.0-or-later. See [LICENSE](./LICENSE). From d6584325ddfac60798a5b86ab4f24ce33ef59329 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 22 Oct 2026 13:01:23 +0530 Subject: [PATCH 4/4] fix(phpstan): update phpVersion max range to 80599 for better compatibility --- composer-packages/phpstan/CHANGELOG.md | 2 +- composer-packages/phpstan/phpstan.neon.dist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer-packages/phpstan/CHANGELOG.md b/composer-packages/phpstan/CHANGELOG.md index 5a913dd..7e0fad2 100644 --- a/composer-packages/phpstan/CHANGELOG.md +++ b/composer-packages/phpstan/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added -- `phpVersion` range `80200`–`80500`, so code is analysed against every PHP version from the 8.2 floor up to 8.5. +- `phpVersion` range `80200`–`80599`, so code is analysed against every PHP version from the 8.2 floor up to 8.5. ### Changed diff --git a/composer-packages/phpstan/phpstan.neon.dist b/composer-packages/phpstan/phpstan.neon.dist index 036fd03..24e8ae9 100644 --- a/composer-packages/phpstan/phpstan.neon.dist +++ b/composer-packages/phpstan/phpstan.neon.dist @@ -11,7 +11,7 @@ parameters: # Analyse against the whole supported PHP range: the 8.2 floor up to 8.5. phpVersion: min: 80200 - max: 80500 + max: 80599 # WordPress doc-blocks are unreliable; don't treat their types as certain. treatPhpDocTypesAsCertain: false reportUnmatchedIgnoredErrors: true