Skip to content

Tests / Build Scripts: Configure PHPStan level 0#10419

Open
justlevine wants to merge 13 commits intoWordPress:trunkfrom
justlevine:tests/phpstan/level-0
Open

Tests / Build Scripts: Configure PHPStan level 0#10419
justlevine wants to merge 13 commits intoWordPress:trunkfrom
justlevine:tests/phpstan/level-0

Conversation

@justlevine
Copy link

Trac ticket: https://core.trac.wordpress.org/ticket/61175

This PR adds a PHPStan configuration for PHPStan level 0, along with tests and docs.

Based from #7619 - which remains in use to explore adopting future levels (alongside parallel remediation branches).

Proposal: https://make.wordpress.org/core/2025/07/11/proposal-phpstan-in-the-wordpress-core-development-workflow/


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

github-actions bot commented Oct 25, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props justlevine, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@justlevine justlevine force-pushed the tests/phpstan/level-0 branch from 86c9442 to 0308f9e Compare November 7, 2025 18:44
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces PHPStan level 0 static analysis configuration to WordPress Core, establishing a foundation for catching code errors without execution. The implementation includes PHPStan configuration files, bootstrap scripts, documentation, GitHub workflows for CI integration, and inline code annotations to suppress legitimate PHPStan warnings.

Changes:

  • Added PHPStan level 0 configuration with baseline support for legacy code
  • Added PHPStan annotations to source files to document legitimate suppressions
  • Integrated PHPStan into CI/CD via GitHub workflows and npm/composer scripts

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
phpstan.neon.dist Main PHPStan configuration with level 0 rules and baseline inclusion
tests/phpstan/base.neon Base configuration defining paths, exclusions, and codebase-specific settings
tests/phpstan/baseline.php Empty baseline file for future tech debt tracking
tests/phpstan/bootstrap.php Defines WordPress constants for PHPStan discovery
tests/phpstan/README.md Documentation for running and configuring PHPStan
src/wp-includes/template.php Added PHPStan annotations and removed obsolete comment
src/wp-includes/style-engine/class-wp-style-engine-css-rules-store.php Added @phpstan-consistent-constructor annotation
src/wp-includes/media.php Added annotation for PHP8+ GdImage class
src/wp-includes/functions.php Added enhanced return type documentation for wp_die()
src/wp-includes/customize/*.php Added @return documentation for overridden update() methods
src/wp-includes/class-wp-theme-json.php Added null return value to match documented return type
src/wp-includes/class-wp-scripts.php Removed obsolete PHPStan suppression comment
src/wp-admin/press-this.php Added PHPStan annotations for plugin file includes
src/wp-admin/includes/class-wp-filesystem-ssh2.php Added phpstan-ignore-next-line for unimplemented method
composer.json Added phpstan/phpstan dependency and analyse script
package.json Added test:php:stan npm script
phpcs.xml.dist Excluded PHPStan files from coding standards checks
.gitignore Added phpstan.neon to ignore list for local overrides
.github/workflows/php-static-analysis.yml Main workflow for running PHPStan on pushes and PRs
.github/workflows/reusable-php-static-analysis.yml Reusable workflow for PHPStan execution

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

composer run analyse

compose run analyse -- --memory-limit=4G
compose run analyse -- tests/phpstan/src/wp-includes/template.php
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect path in example. The path should be "src/wp-includes/template.php" not "tests/phpstan/src/wp-includes/template.php" since PHPStan analyses files in the src directory, not in tests/phpstan/src

Copilot uses AI. Check for mistakes.
"squizlabs/php_codesniffer": "3.13.5",
"wp-coding-standards/wpcs": "~3.3.0",
"phpcompatibility/phpcompatibility-wp": "~2.1.3",
"phpstan/phpstan": "~2.1.33",
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version constraint "~2.1.33" is very specific and may cause issues. The tilde operator (~) for a three-part version like 2.1.33 means ">=2.1.33 <2.2.0". This constraint locks to a specific patch version which may not exist or may prevent receiving important bug fixes. Consider using "^2.1" (which means ">=2.1.0 <3.0.0") or "~2.1.0" (which means ">=2.1.0 <2.2.0") instead to allow flexibility for patch updates while staying within the same minor version

Suggested change
"phpstan/phpstan": "~2.1.33",
"phpstan/phpstan": "~2.1.0",

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@westonruter what are your thoughts on this one?

I definitely think we should pin at the version we commit, but I wouldn't want to Semver because contextually "nonbreaking enhancements" are breaking from an implementation POV if they create a new quality gate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense to me. So switching to ~2.1.0 would keep it at 2.1.x. This seems necessary because there is no composer.lock, which actually is curious since we package-lock.json. If we had a composer.lock then we'd be free to use ^2.1. But I suppose can't use it because of the different versions of PHP which may end up getting used when doing composer install.

So yeah, I guess go with ~2.1.0 and not ^2.1. When PHPStan 2.2 comes out, we'll have to manually upgrade.

name: PHPStan Static Analysis

on:
# PHPStan testing was introduced in @todo.
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains a TODO placeholder "@todo" that should be replaced with the actual version number when PHPStan testing is introduced

Suggested change
# PHPStan testing was introduced in @todo.
# Run PHPStan testing on pushes to the following branches and tags.

Copilot uses AI. Check for mistakes.
npm run test:php:stan -- --memory-limit=4G

# to analyse only a specific file:
npm run test:php:stan -- tests/phpstan/src/wp-includes/template.php
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect path in example. The path should be "src/wp-includes/template.php" not "tests/phpstan/src/wp-includes/template.php" since PHPStan analyses files in the src directory, not in tests/phpstan/src

Copilot uses AI. Check for mistakes.
justlevine and others added 4 commits February 12, 2026 23:53
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* @since 3.4.0
*
* @param mixed $value The value to update. Not used.
* @return bool|void Nothing is returned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return bool|void Nothing is returned.
* @return bool Always returns true.

* @return bool|void Nothing is returned.
*/
public function update( $value ) {
remove_theme_mod( 'background_image_thumb' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
remove_theme_mod( 'background_image_thumb' );
remove_theme_mod( 'background_image_thumb' );
return true;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though I agree this is the real fix, I'm nervous about making any src code changes in a tooling PR. Am I overthinking?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can commit these fixes separately if you like.

* @since 3.4.0
*
* @param mixed $value The value to update.
* @return bool|void Nothing is returned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return bool|void Nothing is returned.
* @return bool Always returns true.

* @param mixed $value The value to update.
* @return bool|void Nothing is returned.
*/
public function update( $value ) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function update( $value ) {}
public function update( $value ) {
return true;
}

* @global Custom_Image_Header $custom_image_header
*
* @param mixed $value The value to update.
* @return bool|void Nothing is returned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return bool|void Nothing is returned.
* @return bool Always returns true.

} else {
$custom_image_header->set_header_image( $value );
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return true;
}

"lock": false
},
"scripts": {
"analyse": "@php ./vendor/bin/phpstan analyse --memory-limit=2G",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😛

Suggested change
"analyse": "@php ./vendor/bin/phpstan analyse --memory-limit=2G",
"analyze": "@php ./vendor/bin/phpstan analyse --memory-limit=2G",

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I wonder, why not just:

Suggested change
"analyse": "@php ./vendor/bin/phpstan analyse --memory-limit=2G",
"phpstan": "@php ./vendor/bin/phpstan analyse --memory-limit=2G",

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be closer to test:php:stan.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm torn. I prefer the US spelling, but the name of the PHPStan command is analyse and the muscle memory of folks who use PHPStan. 2 years ago this would have been a nit, but it also affects LLMs since the inconsistency can get in the way of their inference

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about just phpstan then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a compat script which isn't a verb already.

"env:logs": "node ./tools/local-env/scripts/docker.js logs",
"env:pull": "node ./tools/local-env/scripts/docker.js pull",
"test:performance": "wp-scripts test-playwright --config tests/performance/playwright.config.js",
"test:php:stan": "node ./tools/local-env/scripts/docker.js run --rm php ./vendor/bin/phpstan analyse --memory-limit=2G",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder about this one. There isn't an npm script for PHPCS. Is that an oversight? It's in a grunt task instead: npx grunt lint:php. It seems like there should be something exposed for PHPCS if something is added for PHPStan.

That said, I'm not sure about test:php:stan. It's not actually running tests. It's analyzing. And "stan" isn't a thing, as I understand it is "STatic+ANalysis". I'd feel better about something like static-analysis:phpstan.

Relatedly: I want to add TypeScript tsc here as well, so that could be static-analysis:js which just runs tsc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with whatever here 🤷.

I prefixed it with test because that's matches the files and to be pedantic we're testing type safety, also linting is also a type of "static analysis". FWIW downstream I usually do npm run lint:php:stan (or even npm run lint:php:types to match a lint:js:types ), which sits nicely next to npm run lint:php and npm run lint:js, and works well with autocomplete and progressive disclosure.

And "stan" isn't a thing,

Agreed, the motivation here is just that {lint|test}:php:phpstan is redundantly ugly.

@westonruter
Copy link
Member

westonruter commented Feb 12, 2026

In Gruntfile.js, there is a precommit task, which includes precommit:php as well as format:php. Curiously, it doesn't to lint:php (probably because there are still so many violations), but it should run PHPStan since it can run on the entire codebase without errors.

@westonruter
Copy link
Member

I tried running composer analyse and it seemed to finish but then it hung at 99%. Finally it errored:

image

@justlevine
Copy link
Author

justlevine commented Feb 12, 2026

I tried running composer analyse and it seemed to finish but then it hung at 99%. Finally it errored:

image

@westonruter can you run it with -- --vvv? I believe that should show us which file it's timing out on.

(Also confirming that you're using the vanilla phpstan.neon.dist and the CLI is just inaccurate - if not, please share any modifications there too)

@westonruter
Copy link
Member

Actually, I was using a local phpstan.neon which was overriding the one provided in this PR. When I try using the one in the PR, the result is actually worse. It never starts analyzing. This seems to be due to the paths config:

paths:
- ../../src

If I change it to:

	paths:
		- ../../src/wp-admin
		- ../../src/wp-includes

Then it works, aside from two errors being found:

 ------ ---------------------------------------------------------------------- 
  Line   wp-admin/load-scripts.php                                             
 ------ ---------------------------------------------------------------------- 
  68     Function get_file not found.                                          
         🪪  function.notFound                                                 
         💡  Learn more at https://phpstan.org/user-guide/discovering-symbols  
         at src/wp-admin/load-scripts.php:68                                   
 ------ ---------------------------------------------------------------------- 

 ------ ---------------------------------------------------------------------- 
  Line   wp-admin/load-styles.php                                              
 ------ ---------------------------------------------------------------------- 
  83     Function get_file not found.                                          
         🪪  function.notFound                                                 
         💡  Learn more at https://phpstan.org/user-guide/discovering-symbols  
         at src/wp-admin/load-styles.php:83                                    
 ------ ---------------------------------------------------------------------- 

What appears to be the issue is that I use my wordpress-develop clone for core and plugin development together. So my src/wp-content is very large. I know other contributors do this as well, so this is something which should be accounted for.

Nevertheless, the change I made is not ideal because then it doesn't check the PHP files in the root of src, like wp-login.php. I'm confused because from looking at the excludePaths config, it would seem that src/wp-content is supposed to be excluded, and yet PHPStan still seems to be scanning all the 16GB worth of files in the plugins dir.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants