From fab89e56d48573b21fa53e5a5f238ef99814d3ac Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 20 Oct 2026 10:01:31 +0200 Subject: [PATCH 1/2] Support Psalm 7 and psalm/psalm-plugin-api 0.4 TCallable no longer takes a value, and PHP-Parser 5 only attaches the phpdoc of "$var = apply_filters(...)" to the expression statement, so documented hooks got mixed types. Psalm 7 reports the docblock issues of the stubs plugins add: the wp_parse_url stub no longer uses a PHP_URL_* wildcard, which Psalm doesn't support for global constants, and the tests suppress those of the third-party stubs. The tests use Psalm's test case, which now needs PHPUnit 10+, static data providers and dg/bypass-finals. Claude-Session: https://claude.ai/code/session_01WNSb29tptiTQoc44Tk7XZW --- .github/workflows/test.yml | 2 -- Plugin.php | 10 +++++++--- composer.json | 8 +++++--- phpunit.xml.dist | 5 +---- stubs/overrides.php | 2 +- test.php | 2 ++ tests/bootstrap.php | 6 ++++++ tests/unit/BaseTestCase.php | 7 +++++++ tests/unit/FiltersTest.php | 4 ++-- 9 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 tests/bootstrap.php diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8af26e..765f4b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,6 @@ jobs: strategy: matrix: php-version: - - "7.4" - - "8.0" - "8.1" - "8.2" - "8.3" diff --git a/Plugin.php b/Plugin.php index 9ca390b..1ae6e5e 100644 --- a/Plugin.php +++ b/Plugin.php @@ -9,11 +9,13 @@ use PhpParser; use PhpParser\Node\Arg; use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Echo_; +use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Return_; @@ -723,6 +725,8 @@ public static function afterEveryFunctionCallAnalysis( AfterEveryFunctionCallAna /** * @return non-empty-list + * + * @psalm-pure */ public static function getFunctionIds() : array { return [ @@ -1062,7 +1066,6 @@ public static function getFunctionParams( FunctionParamsProviderEvent $event ) : new FunctionLikeParameter( 'hook_name', false, Type::getNonEmptyString(), null, null, null, false ), new FunctionLikeParameter( 'callback', false, new Union( [ new TCallable( - 'callable', $hook_params, $return_type ), @@ -1431,9 +1434,10 @@ public function enterNode( PhpParser\Node $node ) { // "$var = apply_filters" directly after a function declaration // "echo apply_filters" // cannot do this for all cases, as often it will assign completely wrong stuff otherwise - if ( $node->getDocComment() && ( $node instanceof FuncCall || $node instanceof Return_ || $node instanceof Variable || $node instanceof Echo_ ) ) { + // PHP-Parser 5 attaches the phpdoc of "$var = apply_filters" to the expression statement only + if ( $node->getDocComment() && ( $node instanceof FuncCall || $node instanceof Return_ || $node instanceof Variable || $node instanceof Echo_ || $node instanceof Expression ) ) { $this->last_doc = $node->getDocComment(); - } elseif ( isset( $this->last_doc ) && ! $node instanceof FuncCall ) { + } elseif ( isset( $this->last_doc ) && ! $node instanceof FuncCall && ! $node instanceof Assign && ! $node instanceof Variable ) { // if it's set already and this is not a FuncCall, reset it to null, since there's something else and it would be used incorrectly $this->last_doc = null; } diff --git a/composer.json b/composer.json index b05fe45..cdb19d7 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,14 @@ "php-stubs/wordpress-stubs": "^6.0", "php-stubs/wordpress-globals": "^0.2.0", "php-stubs/wp-cli-stubs": "^2.7", - "vimeo/psalm": "^5 || ^6" + "psalm/psalm-plugin-api": "^0.4", + "vimeo/psalm": "^7|dev-master" }, "require-dev": { + "dg/bypass-finals": "^1.5", "humanmade/coding-standards": "^1.2", - "phpunit/phpunit": "^9.0", - "psalm/plugin-phpunit": "^0.18.4" + "phpunit/phpunit": "^10.5 || ^11.5", + "psalm/plugin-phpunit": "^0.21" }, "extra": { "psalm" : { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f0a9376..4d36e08 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,15 +2,12 @@ diff --git a/stubs/overrides.php b/stubs/overrides.php index a024ebe..8a13b41 100644 --- a/stubs/overrides.php +++ b/stubs/overrides.php @@ -91,7 +91,7 @@ function apply_filters( string $hook_name, $value, ...$args ) {} * | PHP_URL_QUERY | 6 | * | PHP_URL_FRAGMENT | 7 | * - * @template TComponent of (-1|PHP_URL_*) + * @template TComponent of (-1|0|1|2|3|4|5|6|7) * @param string $url * @param TComponent $component * @return ( diff --git a/test.php b/test.php index 88c6c34..d35418c 100644 --- a/test.php +++ b/test.php @@ -7,6 +7,8 @@ /** * @param array{basedir: string, baseurl: string, error: false|string, path: string, subdir: string, url: string} $dir * @return array{basedir: string, baseurl: string, error: false|string, path: string, subdir: string, url: string} + * + * @psalm-pure */ function filter_upload_dir( array $dir ) : array { return $dir; diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..76fabd4 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,6 @@ +setAdvancedErrorLevel( 'InvalidDocblock', [ [ 'type' => 'suppress', 'directory' => [ [ 'name' => 'vendor' ] ] ] ], 'error' ); + } + protected function getContents() : string { return ' diff --git a/tests/unit/FiltersTest.php b/tests/unit/FiltersTest.php index 34f4c7e..c3dfb8b 100644 --- a/tests/unit/FiltersTest.php +++ b/tests/unit/FiltersTest.php @@ -12,7 +12,7 @@ class FiltersTest extends BaseTestCase { /** * @return iterable,error_levels?:string[]}> */ - public function providerValidCodeParse() : iterable { + public static function providerValidCodeParse() : iterable { return [ 'add_filter with docblock' => [ <<<'EOD' @@ -67,7 +67,7 @@ public function providerValidCodeParse() : iterable { /** * @return iterable */ - public function providerInvalidCodeParse() : iterable { + public static function providerInvalidCodeParse() : iterable { return [ 'add_filter fails wrong types' => [ <<<'EOD' From 7bf53284a1fc1031ea828083c6b2c17236d9a244 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 20 Oct 2026 10:02:03 +0200 Subject: [PATCH 2/2] Install the coding standards despite their security advisory in CI Composer refuses to install wp-coding-standards/wpcs 2.3.0, which humanmade/coding-standards requires, because of a security advisory. Claude-Session: https://claude.ai/code/session_01WNSb29tptiTQoc44Tk7XZW --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 765f4b0..cb769dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,9 @@ jobs: - name: Install dependencies run: composer install --ansi --no-interaction --no-progress + env: + # humanmade/coding-standards requires wp-coding-standards/wpcs 2.3.0, which has a security advisory + COMPOSER_NO_SECURITY_BLOCKING: 1 - name: Run Tests run: vendor/bin/phpunit