Skip to content

Commit 8a82e67

Browse files
committed
Build/Test Tools: Integrate PHPStan into the core development workflow.
This change introduces PHPStan static analysis configured at [https://phpstan.org/user-guide/rule-levels rule level 0], which includes: "basic checks, unknown classes, unknown functions, unknown methods called on `$this`, wrong number of arguments passed to those methods and functions, always undefined variables". Contributors may elect for a higher PHPStan rule level by creating a `phpstan.neon` which overrides `phpstan.neon.dist`. * Fix various PHPStan level 0 errors by adding `@phpstan-ignore` comments, updating PHPDoc types, and adding missing return values. * Remove existing `@phpstan-ignore` comments that are now obsolete or inapplicable for level 0. * Add a new GitHub Actions workflow for PHPStan Static Analysis. Reports are currently provided as warnings with inline annotations in pull requests and do not fail the build. * Add a `phpstan` Grunt task and include it in the `precommit:php` task to run before `phpunit`. * Introduce a `typecheck:php` npm script and a `composer phpstan` script to run analysis in local development environments. * Add documentation for PHPStan usage in `tests/phpstan/README.md`. Developed in #10419 Props justlevine, westonruter, johnbillion, desrosj, SirLouen, dmsnell, oglekler, joehoyle, jorbin. See #64238, #63268, #52217, #51423. Fixes #61175. git-svn-id: https://develop.svn.wordpress.org/trunk@61699 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 9022ab1 commit 8a82e67

21 files changed

+601
-7
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: PHPStan Static Analysis
2+
3+
on:
4+
# PHPStan testing was introduced in 7.0.0.
5+
push:
6+
branches:
7+
- trunk
8+
- '[7-9].[0-9]'
9+
tags:
10+
- '[7-9].[0-9]'
11+
- '[7-9]+.[0-9].[0-9]+'
12+
pull_request:
13+
branches:
14+
- trunk
15+
- '[7-9].[0-9]'
16+
paths:
17+
# This workflow only scans PHP files.
18+
- '**.php'
19+
# These files configure Composer. Changes could affect the outcome.
20+
- 'composer.*'
21+
# These files configure PHPStan. Changes could affect the outcome.
22+
- 'phpstan.neon.dist'
23+
- 'tests/phpstan/base.neon'
24+
- 'tests/phpstan/baseline.php'
25+
# Confirm any changes to relevant workflow files.
26+
- '.github/workflows/phpstan-static-analysis.yml'
27+
- '.github/workflows/reusable-phpstan-static-analysis.yml'
28+
workflow_dispatch:
29+
30+
# Cancels all previous workflow runs for pull requests that have not completed.
31+
concurrency:
32+
# The concurrency group contains the workflow name and the branch name for pull requests
33+
# or the commit hash for any other events.
34+
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
35+
cancel-in-progress: true
36+
37+
# Disable permissions for all available scopes by default.
38+
# Any needed permissions should be configured at the job level.
39+
permissions: {}
40+
41+
jobs:
42+
# Runs PHPStan Static Analysis.
43+
phpstan:
44+
name: PHP static analysis
45+
uses: ./.github/workflows/reusable-phpstan-static-analysis.yml
46+
permissions:
47+
contents: read
48+
if: ${{ github.repository == 'WordPress/wordpress-develop' || ( github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' ) }}
49+
50+
slack-notifications:
51+
name: Slack Notifications
52+
uses: ./.github/workflows/slack-notifications.yml
53+
permissions:
54+
actions: read
55+
contents: read
56+
needs: [ phpstan ]
57+
if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }}
58+
with:
59+
calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }}
60+
secrets:
61+
SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }}
62+
SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }}
63+
SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }}
64+
SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }}
65+
66+
failed-workflow:
67+
name: Failed workflow tasks
68+
runs-on: ubuntu-24.04
69+
permissions:
70+
actions: write
71+
needs: [ slack-notifications ]
72+
if: |
73+
always() &&
74+
github.repository == 'WordPress/wordpress-develop' &&
75+
github.event_name != 'pull_request' &&
76+
github.run_attempt < 2 &&
77+
(
78+
contains( needs.*.result, 'cancelled' ) ||
79+
contains( needs.*.result, 'failure' )
80+
)
81+
82+
steps:
83+
- name: Dispatch workflow run
84+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
85+
with:
86+
retries: 2
87+
retry-exempt-status-codes: 418
88+
script: |
89+
github.rest.actions.createWorkflowDispatch({
90+
owner: context.repo.owner,
91+
repo: context.repo.repo,
92+
workflow_id: 'failed-workflow.yml',
93+
ref: 'trunk',
94+
inputs: {
95+
run_id: `${context.runId}`,
96+
}
97+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
##
2+
# A reusable workflow that runs PHP Static Analysis tests.
3+
##
4+
name: PHP Static Analysis
5+
6+
on:
7+
workflow_call:
8+
inputs:
9+
php-version:
10+
description: 'The PHP version to use.'
11+
required: false
12+
type: 'string'
13+
default: 'latest'
14+
15+
# Disable permissions for all available scopes by default.
16+
# Any needed permissions should be configured at the job level.
17+
permissions: {}
18+
19+
jobs:
20+
# Runs PHP static analysis tests.
21+
#
22+
# Violations are reported inline with annotations.
23+
#
24+
# Performs the following steps:
25+
# - Checks out the repository.
26+
# - Sets up PHP.
27+
# - Logs debug information.
28+
# - Installs Composer dependencies.
29+
# - Configures caching for PHP static analysis scans.
30+
# - Make Composer packages available globally.
31+
# - Runs PHPStan static analysis (with Pull Request annotations).
32+
# - Saves the PHPStan result cache.
33+
# - Ensures version-controlled files are not modified or deleted.
34+
phpstan:
35+
name: Run PHP static analysis
36+
runs-on: ubuntu-24.04
37+
permissions:
38+
contents: read
39+
timeout-minutes: 20
40+
41+
steps:
42+
- name: Checkout repository
43+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
44+
with:
45+
show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
46+
persist-credentials: false
47+
48+
- name: Set up Node.js
49+
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
50+
with:
51+
node-version-file: '.nvmrc'
52+
cache: npm
53+
54+
- name: Set up PHP
55+
uses: shivammathur/setup-php@20529878ed81ef8e78ddf08b480401e6101a850f # v2.35.3
56+
with:
57+
php-version: ${{ inputs.php-version }}
58+
coverage: none
59+
tools: cs2pr
60+
61+
# This date is used to ensure that the Composer cache is cleared at least once every week.
62+
# http://man7.org/linux/man-pages/man1/date.1.html
63+
- name: "Get last Monday's date"
64+
id: get-date
65+
run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT"
66+
67+
- name: General debug information
68+
run: |
69+
npm --version
70+
node --version
71+
composer --version
72+
73+
# Since Composer dependencies are installed using `composer update` and no lock file is in version control,
74+
# passing a custom cache suffix ensures that the cache is flushed at least once per week.
75+
- name: Install Composer dependencies
76+
uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # v3.1.1
77+
with:
78+
custom-cache-suffix: ${{ steps.get-date.outputs.date }}
79+
80+
- name: Make Composer packages available globally
81+
run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH"
82+
83+
- name: Install npm dependencies
84+
run: npm ci --ignore-scripts
85+
86+
- name: Build WordPress
87+
run: npm run build:dev
88+
89+
- name: Cache PHP Static Analysis scan cache
90+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
91+
with:
92+
path: .cache # This is defined in the base.neon file.
93+
key: "phpstan-result-cache-${{ github.run_id }}"
94+
restore-keys: |
95+
phpstan-result-cache-
96+
97+
- name: Run PHP static analysis tests
98+
id: phpstan
99+
run: composer run phpstan -- -vvv --error-format=checkstyle | cs2pr --errors-as-warnings --graceful-warnings
100+
101+
- name: "Save result cache"
102+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
103+
if: ${{ !cancelled() }}
104+
with:
105+
path: .cache
106+
key: "phpstan-result-cache-${{ github.run_id }}"
107+
108+
- name: Ensure version-controlled files are not modified or deleted
109+
run: git diff --exit-code

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ wp-tests-config.php
2323
/gutenberg
2424
/tests/phpunit/build
2525
/wp-cli.local.yml
26+
/phpstan.neon
2627
/jsdoc
2728
/composer.lock
2829
/vendor

Gruntfile.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,7 @@ module.exports = function(grunt) {
15601560
] );
15611561

15621562
grunt.registerTask( 'precommit:php', [
1563+
'phpstan',
15631564
'phpunit'
15641565
] );
15651566

@@ -2001,6 +2002,18 @@ module.exports = function(grunt) {
20012002

20022003
grunt.registerTask( 'test', 'Runs all QUnit and PHPUnit tasks.', ['qunit:compiled', 'phpunit'] );
20032004

2005+
grunt.registerTask( 'phpstan', 'Runs PHPStan on the entire codebase.', function() {
2006+
var done = this.async();
2007+
2008+
grunt.util.spawn( {
2009+
cmd: 'composer',
2010+
args: [ 'phpstan' ],
2011+
opts: { stdio: 'inherit' }
2012+
}, function( error ) {
2013+
done( ! error );
2014+
} );
2015+
} );
2016+
20042017
grunt.registerTask( 'format:php', 'Runs the code formatter on changed files.', function() {
20052018
var done = this.async();
20062019
var flags = this.flags;

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"squizlabs/php_codesniffer": "3.13.5",
2424
"wp-coding-standards/wpcs": "~3.3.0",
2525
"phpcompatibility/phpcompatibility-wp": "~2.1.3",
26+
"phpstan/phpstan": "2.1.39",
2627
"yoast/phpunit-polyfills": "^1.1.0"
2728
},
2829
"config": {
@@ -32,6 +33,7 @@
3233
"lock": false
3334
},
3435
"scripts": {
36+
"phpstan": "@php ./vendor/bin/phpstan analyse --memory-limit=2G",
3537
"compat": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcompat.xml.dist --report=summary,source",
3638
"format": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf --report=summary,source",
3739
"lint": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --report=summary,source",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
"test:coverage": "npm run test:php -- --coverage-html ./coverage/html/ --coverage-php ./coverage/php/report.php --coverage-text=./coverage/text/report.txt",
131131
"test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js",
132132
"test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js",
133+
"typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan",
133134
"gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js",
134135
"gutenberg:build": "node tools/gutenberg/build-gutenberg.js",
135136
"gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js",

phpcs.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@
8181
<exclude-pattern>/tests/phpunit/build*</exclude-pattern>
8282
<exclude-pattern>/tests/phpunit/data/*</exclude-pattern>
8383

84+
<!-- PHPStan bootstrap, stubs, and baseline. -->
85+
<exclude-pattern>/tests/phpstan/*</exclude-pattern>
86+
8487
<exclude-pattern>/tools/*</exclude-pattern>
8588

8689
<!-- Drop-in plugins. -->

phpstan.neon.dist

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# PHPStan configuration for WordPress Core.
2+
#
3+
# To overload this configuration, copy this file to phpstan.neon and adjust as needed.
4+
#
5+
# https://phpstan.org/config-reference
6+
7+
includes:
8+
# The base configuration file for using PHPStan with the WordPress core codebase.
9+
- tests/phpstan/base.neon
10+
11+
# The baseline file includes preexisting errors in the codebase that should be ignored.
12+
# https://phpstan.org/user-guide/baseline
13+
- tests/phpstan/baseline.php
14+
15+
parameters:
16+
# https://phpstan.org/user-guide/rule-levels
17+
level: 0
18+
reportUnmatchedIgnoredErrors: true
19+
20+
ignoreErrors:
21+
# Level 0:
22+
- # Inner functions aren't supported by PHPStan.
23+
message: '#Function wxr_[a-z_]+ not found#'
24+
path: src/wp-admin/includes/export.php
25+
-
26+
identifier: function.inner
27+
path: src/wp-admin/includes/export.php
28+
count: 13
29+
-
30+
identifier: function.inner
31+
path: src/wp-admin/includes/file.php
32+
count: 1
33+
-
34+
identifier: function.inner
35+
path: src/wp-includes/canonical.php
36+
count: 1

src/wp-admin/includes/class-wp-filesystem-ssh2.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,9 +670,11 @@ public function size( $file ) {
670670
* Default 0.
671671
* @param int $atime Optional. Access time to set for file.
672672
* Default 0.
673+
* @return false Always returns false because not implemented.
673674
*/
674675
public function touch( $file, $time = 0, $atime = 0 ) {
675676
// Not implemented.
677+
return false;
676678
}
677679

678680
/**

src/wp-admin/press-this.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ function wp_load_press_this() {
2222
403
2323
);
2424
} elseif ( is_plugin_active( $plugin_file ) ) {
25-
include WP_PLUGIN_DIR . '/press-this/class-wp-press-this-plugin.php';
26-
$wp_press_this = new WP_Press_This_Plugin();
25+
include WP_PLUGIN_DIR . '/press-this/class-wp-press-this-plugin.php'; // @phpstan-ignore include.fileNotFound
26+
$wp_press_this = new WP_Press_This_Plugin(); // @phpstan-ignore class.notFound
2727
$wp_press_this->html();
2828
} elseif ( current_user_can( 'activate_plugins' ) ) {
2929
if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin_file ) ) {

0 commit comments

Comments
 (0)