Make WordPress Core

Changeset 63462


Ignore:
Timestamp:
09/04/2026 06:23:52 AM (less than one hour ago)
Author:
westonruter
Message:

Build/Test Tools: Run the PHPStan node visitors on every parse.

A visitor tagged phpstan.parser.richParserNodeVisitor runs in PHPStan's RichParser, and PathRoutingParser sends a file there only when it is one of the files being analyzed. A file merely read so that something it declares can be reflected goes to the simple parser instead, where the visitors never run. That routing is safe for PHPStan's own parsers, which agree about everything reflection exposes, but the HashNotationVisitor added in r63420 rewrites @param, @return and @var docblocks, which are exactly what reflection exposes. Analyzing a subset of the tree therefore reports against types the visitor would have replaced wherever one crosses a file boundary, and writes that reading into the shared cache for the next full run to restore.

Have the simple parser wrap the rich one, so every parse rewrites the same docblocks, the cleaning still happens on top of it, and the reflection cache is keyed on contents correctly again. This redefines PHPStan's own currentPhpVersionSimpleParser service, which is not a documented extension point; the definition records what to check should a future release rename it. Full runs report exactly what they did before, and every baseline still matches. A run over a subset now does the parsing it previously skipped, so a cold cache pays a few percent for it, while a warm one is unchanged.

Developed in https://github.com/WordPress/wordpress-develop/pull/13396.
Follow-up to r63420, r63460.

Props westonruter, swissspidy.
See #65817.

Location:
trunk/tests/phpstan
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpstan/README.md

    r63460 r63462  
    212212The results cache alone is not the problem. PHPStan invalidates that itself when the configuration changes, and [`HookDocsResultCacheMetaExtension`](HookDocsResultCacheMetaExtension.php) already folds every file in this directory into its key, so it is discarded when one of them is edited. What survives either is the per-file reflection, which is keyed by the source file's own contents and has no way to know that reading it now yields something else. CI keys its cache on these files for the same reason; see [`.github/workflows/reusable-phpstan-static-analysis-v1.yml`](../../.github/workflows/reusable-phpstan-static-analysis-v1.yml).
    213213
    214 The same cache is worth clearing after analysing a subset of the tree. A file named on the command line is analysed, but a file merely *read* on its behalf is parsed without these extensions, and the reflection stored for it carries no derived shape. A later full run reads that back and reports against a type that is no longer what the docblock says, which is the same silence as above arriving from the other direction.
     214Analyzing a subset of the tree used to call for the same treatment, the same silence arriving from the other direction. A file named on the command line is analyzed, but a file merely *read* on its behalf went to PHPStan's simple parser, where these visitors do not run, so the reflection stored for it carried no derived shape — wrong for that run, and there in the cache for the next full run to restore. [`base.neon`](base.neon) now has the simple parser wrap the rich one, so every parse rewrites the same docblocks and a run over a subset derives what a full run derives. It does so by redefining one of PHPStan's own parser services, which is not a documented extension point; if a hash notation type ever looks wrong after a PHPStan upgrade, check that definition first.
    215215
    216216Baseline generation is exempt from that last hazard. [`generate-baselines.php`](generate-baselines.php) points its analysis at `.cache/baselines` instead, a directory only it writes to and only ever from a full run, so a baseline can never record a message derived from reflection some earlier narrowed run left behind. Clearing the cache after editing anything in this directory still applies to it, and the `rm -rf .cache` above covers both.
  • trunk/tests/phpstan/base.neon

    r63451 r63462  
    6262                tags:
    6363                        - phpstan.resultCacheMetaExtension
     64
     65        # Runs the visitors above over every file PHPStan parses, not only the ones it
     66        # analyzes.
     67        #
     68        # `phpstan.parser.richParserNodeVisitor` is honored by PHPStan's RichParser, and
     69        # PathRoutingParser sends a file there only when it is one of the files being
     70        # analyzed. Everything else — a file merely read so that a function or class it
     71        # declares can be reflected — goes to the simple parser, which applies only the
     72        # name resolver.
     73        #
     74        # PHPStan can do that because its own two parsers agree about everything
     75        # reflection exposes: the simple one is wrapped in a CleaningParser, whose
     76        # CleaningVisitor empties function, method and closure bodies and touches nothing
     77        # else. Docblocks and signatures come out of either parser identically, so what is
     78        # reflected out of a file does not depend on whether that file was analyzed, and
     79        # the per-file reflection cache can be keyed on the file's contents alone.
     80        #
     81        # HashNotationVisitor breaks that agreement. It rewrites `@param`, `@return` and
     82        # `@var` docblocks, which are exactly what reflection exposes, so a file read
     83        # through the simple parser reflects the documented `array` where the same file
     84        # analyzed reflects the shape its hash notation describes. Analyzing a subset of
     85        # the tree then reports against types no longer derived, and writes that reading
     86        # into the shared cache for the next full run to restore.
     87        #
     88        # Wrapping the rich parser rather than the plain one restores the agreement: every
     89        # parse rewrites the same docblocks, the cleaning still happens on top, and the
     90        # reflection cache is keyed on contents correctly again.
     91        #
     92        # `currentPhpVersionSimpleParser` and `currentPhpVersionRichParser` are PHPStan's
     93        # own service names, from its conf/parsers.neon, and are not a documented extension
     94        # point. Should a future release rename either, this definition stops being wired
     95        # into anything and the behavior above returns; the errors it prevents are all of
     96        # the quiet sort, so check this first when a hash notation type looks wrong.
     97        #
     98        # `arguments!` replaces the inherited arguments rather than merging with them.
     99        # Without the `!`, the original `wrappedParser` argument survives and the container
     100        # fails to build with "Unknown named parameter $wrappedParser".
     101        currentPhpVersionSimpleParser:
     102                class: PHPStan\Parser\CleaningParser
     103                arguments!:
     104                        wrappedParser: @currentPhpVersionRichParser
     105                autowired: false
    64106
    65107parameters:
Note: See TracChangeset for help on using the changeset viewer.