Make WordPress Core


Ignore:
Timestamp:
08/05/2026 07:16:57 AM (2 months ago)
Author:
westonruter
Message:

Build/Test Tools: Raise the PHPStan rule level to 1.

Level 1 adds detection of possibly undefined variables, and of unknown magic methods and properties on classes with __call and __get. The 494 errors this surfaces in existing code are recorded in baselines rather than being fixed here, so that new code is held to level 1 straight away while the existing reports are worked through separately. No files under src are changed.

The tests/phpstan/baseline.php file is replaced by one baseline per error identifier under tests/phpstan/baselines, so that the remaining work on each kind of error is visible as a single file that should shrink to nothing and then be deleted. Every entry is scoped to the file which the error occurs in and carries an exact occurrence count, so that a new occurrence of an already baselined error is reported rather than absorbed. The consequence is that fixing a baselined error means regenerating its baseline in the same change, because the count no longer matches.

PHPStan's own --generate-baseline captures every error a run reports, with no way to restrict it to one identifier, so tests/phpstan/generate-baselines.php is added to write the files instead, exposed as composer phpstan:baselines and as npm run typecheck:php:baselines. A run also deletes any baseline whose identifier no longer reports anything, and rewrites the list of baselines in phpstan.neon.dist. The ignoreErrors in that file now has a comment explaining how it is distinct from a baseline: an entry there is a decision that the code is right as written, whereas a baseline entry is work still to be done. The constants that add_theme_support() defines are declared in the configuration so that the errors around them are resolved rather than recorded, and tests/phpstan/README.md is updated throughout.

Three problems in the static analysis GHA workflow are fixed as well. Fixing a baselined error makes PHPStan report an unmatched ignore, which surfaced only as an annotation reading like a complaint about a correct fix; the job now detects any ignore.* report and fails with an explanation of what to run. An analysis that did not finish passed as a green run, because the status of the pipeline was that of cs2pr rather than of PHPStan; that status is now recovered and a run that did not finish fails. The path filter deciding whether the workflow runs named only the old baseline file, so a pull request that merely regenerated the baselines would not have run the analysis that checks them.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/11151.
Follow-up to r61699.

Props westonruter, sabernhardt, apermo, johnjamesjacoby, adamsilverstein, justlevine.
See #61175.
Fixes #64680.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/.github/workflows/reusable-phpstan-static-analysis-v1.yml

    r62742 r63019  
    3333  # - Configures caching for PHPStan static analysis scans.
    3434  # - Runs PHPStan static analysis (with Pull Request annotations).
     35  # - Checks whether the baselines need regenerating.
    3536  # - Saves the PHPStan result cache.
    3637  # - Ensures version-controlled files are not modified or deleted.
    … …  
    9495      - name: Run PHP static analysis tests
    9596        id: phpstan
    96         run: composer run phpstan -- -vvv --error-format=checkstyle | cs2pr --errors-as-warnings --graceful-warnings
     97        run: |
     98          # The report is written to a file as well as piped to cs2pr, so that the step below
     99          # can look at it.
     100          #
     101          # cs2pr exits successfully so that reported errors annotate the pull request without
     102          # failing the run. A pipeline reports only the status of its last command, so that
     103          # also discards the status of the analysis itself. Recover it from PIPESTATUS.
     104          composer run phpstan -- -vvv --error-format=checkstyle | tee "${RUNNER_TEMP}/phpstan-report.xml" | cs2pr --errors-as-warnings --graceful-warnings
     105          status="${PIPESTATUS[0]}"
     106
     107          # PHPStan exits 1 when it has errors to report, which is the expected case here and
     108          # is what the annotations are for. Anything higher means it did not finish at all,
     109          # which would otherwise pass silently, since the discarded status was the only sign.
     110          if [ "${status}" -gt 1 ]; then
     111            echo "::error title=PHPStan did not complete::The analysis exited with status ${status}, so the code was not fully checked. This is a failure of the run itself rather than a problem found in the code."
     112            exit "${status}"
     113          fi
     114
     115      # An ignored error that no longer occurs, or occurs a different number of times, is
     116      # reported under an `ignore.*` identifier. That is not something to fix in the code: the
     117      # usual cause is that the error *was* fixed, leaving a baseline describing a state that
     118      # no longer exists. PHPStan does not allow those reports to be ignored or baselined.
     119      #
     120      # The analysis above is reported as warnings, so this would otherwise surface as a
     121      # passing run carrying an annotation that reads like a complaint about a fix. Call it
     122      # out on its own, and say what to do about it.
     123      #
     124      # Detection is on the identifier rather than the message, which is prose and may be
     125      # reworded in any release. The checkstyle format carries it in the `source` attribute.
     126      - name: Check whether the baselines need regenerating
     127        if: ${{ !cancelled() }}
     128        env:
     129          BASELINES_URL: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.sha }}/tests/phpstan/baselines
     130          README_URL: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/tests/phpstan/README.md
     131        run: |
     132          # This step runs even when the analysis above it failed, in which case the report may
     133          # never have been written. That failure is reported there, so there is nothing to add
     134          # here beyond staying quiet about a file that was never going to exist.
     135          if [ ! -f "${RUNNER_TEMP}/phpstan-report.xml" ]; then
     136            exit 0
     137          fi
     138
     139          # Leave the run alone unless PHPStan reported an ignore error, because everything
     140          # below concerns an ignore configuration that no longer describes the code, and
     141          # nothing else. Those errors are `ignore.unmatched`, where a pattern matched nothing
     142          # at all, and `ignore.count`, where it matched a different number of times than the
     143          # entry records. The `ignore.` prefix is matched rather than those two names so that
     144          # any later addition to the group is caught as well.
     145          if ! grep -q 'source="ignore\.' "${RUNNER_TEMP}/phpstan-report.xml"; then
     146            exit 0
     147          fi
     148
     149          # The summary is Markdown, and its code spans and fences are written literally, so the
     150          # heredoc is quoted to keep the backticks out of the shell's hands. The links are
     151          # written in reference style for the same reason: the URLs are the only part needing
     152          # a variable, so defining them afterwards keeps the whole of the prose in here.
     153          #
     154          # A newline renders as a line break rather than a space, so each paragraph is one
     155          # line however long that makes it, and the rendered summary wraps to its own width.
     156          cat >> "${GITHUB_STEP_SUMMARY}" <<'SUMMARY'
     157          ## PHPStan baselines are out of date
     158
     159          An ignored error no longer occurs, or occurs a different number of times, so PHPStan reported it under an `ignore.unmatched` or `ignore.count` identifier.
     160
     161          **If you fixed the error, this is expected.** Each baseline entry records an exact count for a specific file, so that a new occurrence of an already baselined error is reported rather than absorbed. That same exactness means fixing one leaves the baseline describing a state that no longer exists. There is nothing to fix in the code; the baselines just need to catch up.
     162
     163          Regenerate them and commit the result:
     164
     165          ```bash
     166          npm run typecheck:php:baselines
     167          ```
     168
     169          or, outside the Docker environment:
     170
     171          ```bash
     172          composer phpstan:baselines
     173          ```
     174
     175          That rewrites the files under [`tests/phpstan/baselines`][baselines], deletes any whose errors are now all fixed, and updates the list of them in `phpstan.neon.dist`.
     176
     177          Where the report names an `@phpstan-ignore` annotation in the code rather than a baseline entry, remove that annotation instead; regenerating will not clear it.
     178
     179          See [`tests/phpstan/README.md`][readme] for details.
     180
     181          SUMMARY
     182
     183          {
     184            echo "[baselines]: ${BASELINES_URL}"
     185            echo "[readme]: ${README_URL}"
     186          } >> "${GITHUB_STEP_SUMMARY}"
     187
     188          echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:baselines\` or \`composer phpstan:baselines\` and commit the updated baselines. See ${README_URL}"
     189          exit 1
    97190
    98191      - name: "Save result cache"
Note: See TracChangeset for help on using the changeset viewer.