From f59a54369613ab202225d40678269205a459ff87 Mon Sep 17 00:00:00 2001 From: Robert van Steen Date: Wed, 10 Jun 2026 17:39:06 +0200 Subject: [PATCH 1/3] Pass allowUnknownFields positionally to Type\shape for Psl cross-major support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package supports php-standard-library ^4.0 || ^5.0 || ^6.0, but Psl renamed Type\shape()'s second parameter between majors: `allow_unknown_fields` in 4.x and `allowUnknownFields` in 5.x/6.x. Calling it by name therefore only ever works on a subset of supported majors — under Psl 4.x the named call throws "Unknown named parameter $allowUnknownFields" at rule-registration, which aborts PHPStan for every downstream consumer pinned to 4.x. Pass the argument positionally instead; the parameter's position is stable across all supported majors, so the rule works regardless of the Psl version resolved by the consumer. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Rules/RestrictImplicitDependencyUsage.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Rules/RestrictImplicitDependencyUsage.php b/src/Rules/RestrictImplicitDependencyUsage.php index 11fe50a..cd1fc23 100644 --- a/src/Rules/RestrictImplicitDependencyUsage.php +++ b/src/Rules/RestrictImplicitDependencyUsage.php @@ -233,10 +233,10 @@ private function getInstalledJson(): array 'name' => Type\string(), 'autoload' => Type\optional(Type\shape([ 'psr-4' => Type\optional(Type\dict(Type\string(), Type\union(Type\string(), Type\vec(Type\string())))), - ], allowUnknownFields: true)), + ], true)), 'replace' => Type\optional(Type\dict(Type\string(), Type\string())), - ], allowUnknownFields: true)), - ], allowUnknownFields: true)->assert(decode(read(basepath() . '/vendor/composer/installed.json'))); + ], true)), + ], true)->assert(decode(read(basepath() . '/vendor/composer/installed.json'))); } /** @@ -249,7 +249,7 @@ private function getComposerJson(): array 'require-dev' => Type\optional(Type\dict(Type\string(), Type\string())), 'autoload' => Type\optional(Type\shape([ 'psr-4' => Type\optional(Type\dict(Type\string(), Type\union(Type\string(), Type\vec(Type\string())))), - ], allowUnknownFields: true)), - ], allowUnknownFields: true)->assert(decode(read('composer.json'))); + ], true)), + ], true)->assert(decode(read('composer.json'))); } } From 141dc13e900bae1feaa8c41a061165bd11ba01e8 Mon Sep 17 00:00:00 2001 From: Robert van Steen Date: Wed, 10 Jun 2026 18:30:52 +0200 Subject: [PATCH 2/3] Fix CI: green PHPStan and the prefer-lowest / prefer-stable test matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-existing failures unrelated to the Psl arg fix, surfaced now that the suite runs to completion: - StrictComparisonOfObjectsRule::processNode() must return list; add ->identifier() so the builder yields one. - basepath(): realpath() is string|false. Guard both calls so preg_match receives a string and the function never returns false. - RestrictImplicitDependencyUsage test used Psl\Collection\Vector as the "undefined package" example, but php-standard-library is now a declared dependency (the rule itself uses Psl\Type), so Psl\ is an allowed namespace. Use SebastianBergmann\Diff\Differ — installed transitively via phpunit but not declared — which is genuinely an implicit dependency. - Raise phpstan/phpstan floor to ^2.1.13: the rule implements RestrictedClassNameUsageExtension, added in 2.1.13, so prefer-lowest previously resolved 2.1.0 (missing the interface) and fatal-errored. Co-Authored-By: Claude Opus 4.8 (1M context) --- composer.json | 2 +- src/Rules/StrictComparisonOfObjectsRule.php | 1 + src/basepath.php | 6 +++++- .../RestrictImplicitDependencyUsageTest.php | 6 ++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 8aaf315..2d72fed 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ }, "require": { "php": "^8.3", - "phpstan/phpstan": "^2.1", + "phpstan/phpstan": "^2.1.13", "php-standard-library/php-standard-library": "^4.0 || ^5.0 || ^6.0" }, "require-dev": { diff --git a/src/Rules/StrictComparisonOfObjectsRule.php b/src/Rules/StrictComparisonOfObjectsRule.php index 97ee567..87cb6ed 100644 --- a/src/Rules/StrictComparisonOfObjectsRule.php +++ b/src/Rules/StrictComparisonOfObjectsRule.php @@ -54,6 +54,7 @@ public function processNode(Node $node, Scope $scope): array $rightTypeDescription, $expectedType->describe(VerbosityLevel::typeOnly()) )) + ->identifier('superscript.strictComparisonOfObjects') ->build(), ]; } diff --git a/src/basepath.php b/src/basepath.php index 45e2806..5c55ba1 100644 --- a/src/basepath.php +++ b/src/basepath.php @@ -7,6 +7,10 @@ function basepath(): ?string { $real_path = realpath(__DIR__); + if ($real_path === false) { + return null; + } + preg_match( "/.+\b(vendor\/.+)/", $real_path, @@ -30,7 +34,7 @@ function basepath(): ?string $project_path = realpath(__DIR__ . DIRECTORY_SEPARATOR . $out); - if ( !file_exists($project_path . DIRECTORY_SEPARATOR . "composer.json") ) { + if ( $project_path === false || !file_exists($project_path . DIRECTORY_SEPARATOR . "composer.json") ) { return null; } diff --git a/tests/Rules/RestrictImplicitDepndencyUsage/RestrictImplicitDependencyUsageTest.php b/tests/Rules/RestrictImplicitDepndencyUsage/RestrictImplicitDependencyUsageTest.php index 041dd00..d0c3795 100644 --- a/tests/Rules/RestrictImplicitDepndencyUsage/RestrictImplicitDependencyUsageTest.php +++ b/tests/Rules/RestrictImplicitDepndencyUsage/RestrictImplicitDependencyUsageTest.php @@ -16,7 +16,6 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use Psl\Collection\Vector; use Superscript\PHPStanRules\Rules\RestrictImplicitDependencyUsage; final class RestrictImplicitDependencyUsageTest extends PHPStanTestCase @@ -65,6 +64,9 @@ public function it_can_be_restricted(string $class): void public static function restrictedCases(): \Generator { - yield 'class from undefined package' => [\Psl\Collection\Vector::class]; + // sebastian/diff is installed transitively (via phpunit) but not + // declared in this package's composer.json, so a class from its + // namespace must be flagged as an implicit-dependency usage. + yield 'class from undefined package' => [\SebastianBergmann\Diff\Differ::class]; } } From 4838950480e658b048c5af0e2ecdeb9c9eda0006 Mon Sep 17 00:00:00 2001 From: Robert van Steen Date: Wed, 10 Jun 2026 18:33:59 +0200 Subject: [PATCH 3/3] ci: enable ext-sodium so Windows can install php-standard-library 4.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On windows-latest + PHP 8.3 + prefer-stable, php-standard-library 5.x/6.x require PHP ~8.4, so composer must resolve to 4.x — which requires ext-sodium. sodium is bundled on the Ubuntu images but not enabled on Windows unless requested, so the install step failed there. Add it to the setup-php extensions list. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5a2d32c..b08d6e6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -34,7 +34,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, sodium coverage: pcov - name: Setup problem matchers