From 84e07f4d3a108bd0a75f285df17c93cd31714af8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 7 Oct 2026 13:58:10 -0700 Subject: [PATCH 1/2] Build/Test Tools: Update PHPStan to 2.3.0 and phpstan-phpunit to 2.1.1. phpstan-phpunit 2.1 requires PHPStan 2.3, so the two move together. The baselines are regenerated for what the new release reports differently: * 12 `variable.undefined` entries no longer match, as the rewritten engine now sees these variables are always defined. * `WP_HTML_Processor::run_adoption_agency_algorithm()` gains a `greater.alwaysTrue` and a `greaterOrEqual.alwaysFalse` entry. Every path through its outer loop body returns or calls `bail()`, which never returns, so the loop cannot iterate twice and its counters never move. PHPStan 2.3 now builds the loop's back edge only from reachable `continue` statements and so reports the comparisons as constant. The loop is scaffolding for the parts of the algorithm that are not yet supported, so this is baselined as work to do rather than ignored. Co-Authored-By: Claude Opus 5.5 --- composer.json | 4 +- phpstan.neon.dist | 2 + .../phpstan/baselines/greater.alwaysTrue.neon | 25 ++++++++ .../baselines/greaterOrEqual.alwaysFalse.neon | 25 ++++++++ .../phpstan/baselines/variable.undefined.neon | 60 ------------------- 5 files changed, 54 insertions(+), 62 deletions(-) create mode 100644 tests/phpstan/baselines/greater.alwaysTrue.neon create mode 100644 tests/phpstan/baselines/greaterOrEqual.alwaysFalse.neon diff --git a/composer.json b/composer.json index 68c25f6f125ed..f61b6e5e72854 100644 --- a/composer.json +++ b/composer.json @@ -52,8 +52,8 @@ "squizlabs/php_codesniffer": "3.13.5", "wp-coding-standards/wpcs": "~3.4.1", "phpcompatibility/phpcompatibility-wp": "~2.1.3", - "phpstan/phpstan": "2.2.16", - "phpstan/phpstan-phpunit": "2.0.18", + "phpstan/phpstan": "2.3.0", + "phpstan/phpstan-phpunit": "2.1.1", "yoast/phpunit-polyfills": "^1.1.0" }, "config": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b8eb7a5ccd898..e61c7d525fe91 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -42,6 +42,8 @@ includes: - tests/phpstan/baselines/function.alreadyNarrowedType.neon - tests/phpstan/baselines/function.impossibleType.neon - tests/phpstan/baselines/function.resultUnused.neon + - tests/phpstan/baselines/greater.alwaysTrue.neon + - tests/phpstan/baselines/greaterOrEqual.alwaysFalse.neon - tests/phpstan/baselines/greaterOrEqual.alwaysTrue.neon - tests/phpstan/baselines/identical.alwaysFalse.neon - tests/phpstan/baselines/identical.alwaysTrue.neon diff --git a/tests/phpstan/baselines/greater.alwaysTrue.neon b/tests/phpstan/baselines/greater.alwaysTrue.neon new file mode 100644 index 0000000000000..897d88c5ee83f --- /dev/null +++ b/tests/phpstan/baselines/greater.alwaysTrue.neon @@ -0,0 +1,25 @@ +# PHPStan baseline for the `greater.alwaysTrue` errors in WordPress core. +# +# https://phpstan.org/error-identifiers/greater.alwaysTrue +# +# Each entry is scoped to a single file and carries an exact occurrence count, +# so that a new instance is reported as a new error rather than being absorbed +# silently. Fixing an occurrence therefore means decrementing or removing its +# entry here as part of the same change. +# +# The goal is to empty this file and delete it, along with the `includes` entry +# for it in phpstan.neon.dist. +# +# Generated by `composer phpstan:baselines`. Do not edit by hand; regenerate with +# +# composer phpstan:baselines -- --identifier=greater.alwaysTrue +# +# which reruns the analysis with this file suppressed so the errors surface again. + +parameters: + ignoreErrors: + - + message: '#^Comparison operation "\>" between 999\|1000 and 0 is always true\.$#' + identifier: greater.alwaysTrue + count: 1 + path: ../../../src/wp-includes/html-api/class-wp-html-processor.php diff --git a/tests/phpstan/baselines/greaterOrEqual.alwaysFalse.neon b/tests/phpstan/baselines/greaterOrEqual.alwaysFalse.neon new file mode 100644 index 0000000000000..8010e78a6979a --- /dev/null +++ b/tests/phpstan/baselines/greaterOrEqual.alwaysFalse.neon @@ -0,0 +1,25 @@ +# PHPStan baseline for the `greaterOrEqual.alwaysFalse` errors in WordPress core. +# +# https://phpstan.org/error-identifiers/greaterOrEqual.alwaysFalse +# +# Each entry is scoped to a single file and carries an exact occurrence count, +# so that a new instance is reported as a new error rather than being absorbed +# silently. Fixing an occurrence therefore means decrementing or removing its +# entry here as part of the same change. +# +# The goal is to empty this file and delete it, along with the `includes` entry +# for it in phpstan.neon.dist. +# +# Generated by `composer phpstan:baselines`. Do not edit by hand; regenerate with +# +# composer phpstan:baselines -- --identifier=greaterOrEqual.alwaysFalse +# +# which reruns the analysis with this file suppressed so the errors surface again. + +parameters: + ignoreErrors: + - + message: '#^Comparison operation "\>\=" between 0 and 8 is always false\.$#' + identifier: greaterOrEqual.alwaysFalse + count: 1 + path: ../../../src/wp-includes/html-api/class-wp-html-processor.php diff --git a/tests/phpstan/baselines/variable.undefined.neon b/tests/phpstan/baselines/variable.undefined.neon index ce112fdbc0807..1a7f497b923aa 100644 --- a/tests/phpstan/baselines/variable.undefined.neon +++ b/tests/phpstan/baselines/variable.undefined.neon @@ -313,11 +313,6 @@ parameters: identifier: variable.undefined count: 9 path: ../../../src/wp-admin/maint/repair.php - - - message: '#^Variable \$update_data might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: ../../../src/wp-admin/menu.php - message: '#^Variable \$current_user might not be defined\.$#' identifier: variable.undefined @@ -468,11 +463,6 @@ parameters: identifier: variable.undefined count: 1 path: ../../../src/wp-admin/themes.php - - - message: '#^Variable \$auto_update_notice might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: ../../../src/wp-admin/update-core.php - message: '#^Variable \$wp_version might not be defined\.$#' identifier: variable.undefined @@ -678,11 +668,6 @@ parameters: identifier: variable.undefined count: 1 path: ../../../src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$comment_opening_at might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: ../../../src/wp-includes/class-wp-block-processor.php - message: '#^Variable \$has_closer might not be defined\.$#' identifier: variable.undefined @@ -718,11 +703,6 @@ parameters: identifier: variable.undefined count: 5 path: ../../../src/wp-includes/class-wp-block-processor.php - - - message: '#^Variable \$section_id might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: ../../../src/wp-includes/class-wp-customize-widgets.php - message: '#^Variable \$filter_id might not be defined\.$#' identifier: variable.undefined @@ -743,11 +723,6 @@ parameters: identifier: variable.undefined count: 1 path: ../../../src/wp-includes/class-wp-image-editor.php - - - message: '#^Variable \$network might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: ../../../src/wp-includes/class-wp-network.php - message: '#^Variable \$cache_found might not be defined\.$#' identifier: variable.undefined @@ -823,31 +798,11 @@ parameters: identifier: variable.undefined count: 1 path: ../../../src/wp-includes/general-template.php - - - message: '#^Variable \$cache_key might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: ../../../src/wp-includes/global-styles-and-settings.php - - - message: '#^Variable \$cached might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: ../../../src/wp-includes/global-styles-and-settings.php - message: '#^Variable \$node might not be defined\.$#' identifier: variable.undefined count: 2 path: ../../../src/wp-includes/html-api/class-wp-html-processor.php - - - message: '#^Variable \$parts might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: ../../../src/wp-includes/kses.php - - - message: '#^Variable \$post might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: ../../../src/wp-includes/link-template.php - message: '#^Variable \$current_blog might not be defined\.$#' identifier: variable.undefined @@ -868,11 +823,6 @@ parameters: identifier: variable.undefined count: 3 path: ../../../src/wp-includes/ms-settings.php - - - message: '#^Variable \$icon might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: ../../../src/wp-includes/post.php - message: '#^Variable \$wp_version might not be defined\.$#' identifier: variable.undefined @@ -893,16 +843,6 @@ parameters: identifier: variable.undefined count: 1 path: ../../../src/wp-login.php - - - message: '#^Variable \$rp_key might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: ../../../src/wp-login.php - - - message: '#^Variable \$rp_login might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: ../../../src/wp-login.php - message: '#^Variable \$user_login might not be defined\.$#' identifier: variable.undefined From a46bf0145e49697534cfb6ff1c9e17e88ff65d5e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 7 Oct 2026 14:04:25 -0700 Subject: [PATCH 2/2] Build/Test Tools: Track hook docblock references as file dependencies. PHPStan 2.3 deprecates `ResultCacheMetaExtension` in favor of dependencies declared on the scope, which re-analyze only the files that declared them instead of discarding the whole result cache. `HookDocsResultCacheMetaExtension` folded two things into the cache key: * A hash of every canonical hook docblock reachable through a "This filter is documented in " reference, found by scanning wp-admin, wp-includes and the bundled themes on every run. Resolving a reference now calls `trackFileDependency()` on each candidate path it tests, so editing a canonical docblock re-analyzes only the files that reference it. A candidate tested before the hit is tracked too, since creating it would change which file the reference names. * A hash of the extension sources in tests/phpstan. PHPStan 2.3 already runs a full analysis when the source of any registered extension or bootstrap file changes, so this half was redundant. With both covered, the extension, its whole-tree scan, and the hashing helpers in `HookDocBlock` are removed. The rules and the `apply_filters()` return type extension document their scope as `Scope&DependencyTracker`, which is what PHPStan passes them, and `HookDocBlock` takes that type in turn. Verified by adding a `@param` to the canonical `the_title` docblock in wp-includes/post-template.php with a warm cache: 22 files were re-analyzed, and every call site inheriting that docblock reported the parameter count mismatch. Co-Authored-By: Claude Opus 5.5 --- ...tersDynamicFunctionReturnTypeExtension.php | 7 +- tests/phpstan/HookDocBlock.php | 163 ++++-------------- .../HookDocsResultCacheMetaExtension.php | 99 ----------- tests/phpstan/HookDocumentationRule.php | 5 +- tests/phpstan/HookParamCountRule.php | 5 +- tests/phpstan/README.md | 2 +- tests/phpstan/base.neon | 10 -- 7 files changed, 42 insertions(+), 249 deletions(-) delete mode 100644 tests/phpstan/HookDocsResultCacheMetaExtension.php diff --git a/tests/phpstan/ApplyFiltersDynamicFunctionReturnTypeExtension.php b/tests/phpstan/ApplyFiltersDynamicFunctionReturnTypeExtension.php index 4d708aa7757df..35038ec079de1 100644 --- a/tests/phpstan/ApplyFiltersDynamicFunctionReturnTypeExtension.php +++ b/tests/phpstan/ApplyFiltersDynamicFunctionReturnTypeExtension.php @@ -27,6 +27,7 @@ namespace WordPress\PHPStan; use PhpParser\Node\Expr\FuncCall; +use PHPStan\Analyser\DependencyTracker; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; @@ -72,9 +73,9 @@ public function isFunctionSupported( FunctionReflection $functionReflection ): b * @link https://developer.wordpress.org/reference/functions/apply_filters_deprecated/ * @link https://developer.wordpress.org/reference/functions/apply_filters_ref_array/ * - * @param FunctionReflection $functionReflection Function being analyzed. - * @param FuncCall $functionCall The function call node. - * @param Scope $scope Analysis scope. + * @param FunctionReflection $functionReflection Function being analyzed. + * @param FuncCall $functionCall The function call node. + * @param Scope&DependencyTracker $scope Analysis scope. * @return Type * @throws ShouldNotHappenException */ diff --git a/tests/phpstan/HookDocBlock.php b/tests/phpstan/HookDocBlock.php index 0109bdd45bfb0..05f75ae85d927 100644 --- a/tests/phpstan/HookDocBlock.php +++ b/tests/phpstan/HookDocBlock.php @@ -27,7 +27,6 @@ namespace WordPress\PHPStan; -use FilesystemIterator; use PhpParser\Comment\Doc; use PhpParser\Error as PhpParserError; use PhpParser\ErrorHandler\Collecting; @@ -42,13 +41,11 @@ use PhpParser\NodeFinder; use PhpParser\NodeTraverser; use PhpParser\ParserFactory; +use PHPStan\Analyser\DependencyTracker; use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; -use SplFileInfo; /** * Bridges the docblock attached to a hook call by HookDocsVisitor to PHPStan's @@ -104,18 +101,6 @@ class HookDocBlock { 'apply_filters_ref_array', ); - /** - * Directories, relative to the WordPress root, scanned for reference comments - * when hashing the docblocks that call sites can inherit. - * - * @see HookDocBlock::getScannedFiles() - */ - private const SCANNED_DIRECTORIES = array( - 'wp-admin', - 'wp-includes', - 'wp-content/themes', - ); - /** * Problem code: the referenced file does not exist. */ @@ -178,110 +163,6 @@ public function __construct( FileTypeMapper $file_type_mapper, ?string $wordpres $this->canonicalWordpressRoot = str_replace( '\\', '/', $this->canonicalWordpressRoot ); } - /** - * Returns a hash of every hook docblock that a call site can inherit through a - * "documented elsewhere" reference comment. - * - * Those docblocks are read with plain file I/O, so PHPStan's dependency graph - * does not know that the referencing files depend on them: editing a canonical - * docblock re-analyzes only the file it lives in, leaving the cached results of - * every referencing file in place. Folding this hash into the result cache key - * invalidates the cache when an inheritable docblock changes, and only then. - * - * @see HookDocsResultCacheMetaExtension - * - * @return non-falsy-string - */ - public function getReferencedHookDocsHash(): string { - $docs = array(); - - foreach ( $this->getScannedFiles() as $file ) { - $code = file_get_contents( $file ); - - if ( false === $code || ! str_contains( $code, 'is documented in' ) ) { - continue; - } - - if ( ! preg_match_all( self::REFERENCE_PATTERN, $code, $matches ) ) { - continue; - } - - foreach ( $matches[1] as $reference_path ) { - $target = $this->resolveReferencePath( $file, $reference_path ); - - if ( null === $target ) { - continue; - } - - // Key on the path relative to the WordPress root so the hash does not - // depend on where the checkout lives. - $key = $this->getRootRelativePath( $target ); - - $docs[ $key ] ??= $this->getHookDocs( $target ); - } - } - - ksort( $docs ); - - return md5( (string) json_encode( $docs ) ); - } - - /** - * Returns the files scanned for reference comments: the WordPress directories - * that can contain them, plus the PHP files at the root of the install. - * - * `wp-content/plugins` is deliberately not scanned. A checkout may have plugins - * carrying large `vendor` and `node_modules` trees, and core's reference comments - * only ever point within core. - * - * @return list Absolute file paths. - */ - private function getScannedFiles(): array { - $files = array(); - - foreach ( self::SCANNED_DIRECTORIES as $directory ) { - $path = $this->wordpressRoot . '/' . $directory; - - if ( ! is_dir( $path ) ) { - continue; - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS ) - ); - - foreach ( $iterator as $file ) { - if ( $file instanceof SplFileInfo && $file->isFile() && 'php' === strtolower( $file->getExtension() ) ) { - $files[] = $file->getPathname(); - } - } - } - - $root_files = glob( $this->wordpressRoot . '/*.php' ); - - if ( is_array( $root_files ) ) { - $files = array_merge( $files, $root_files ); - } - - return $files; - } - - /** - * Expresses an absolute path relative to the WordPress root when it sits inside - * it, so that hashes do not depend on the checkout location. - * - * Paths reached through a reference comment are canonical, so the canonical root is - * what they are relative to. - * - * @param string $path Absolute path. - * @return string - */ - private function getRootRelativePath( string $path ): string { - $prefix = $this->canonicalWordpressRoot . '/'; - - return str_starts_with( $path, $prefix ) ? substr( $path, strlen( $prefix ) ) : $path; - } - /** * Resolves the documentation for a hook call: the docblock written above it, or * the canonical docblock a "documented elsewhere" comment points at. @@ -295,8 +176,14 @@ private function getRootRelativePath( string $path ): string { * parameters. `problem` says why it could not be resolved, when the reason is one * worth reporting. * - * @param FuncCall $function_call Hook function call node. - * @param Scope $scope Analysis scope. + * A canonical docblock is read with plain file I/O, so PHPStan's dependency graph + * cannot see that the referencing file depends on it. Resolving a reference + * therefore declares that dependency on the scope, and the result cache + * re-analyzes the referencing file when the file it resolved to changes, or when + * a file it would resolve to first is created. + * + * @param FuncCall $function_call Hook function call node. + * @param Scope&DependencyTracker $scope Analysis scope. * @return HookDocumentation|null Null when no docblock precedes the call. * @throws ShouldNotHappenException */ @@ -336,7 +223,7 @@ public function getHookDoc( FuncCall $function_call, Scope $scope ): ?array { } $reference_path = $matches[1]; - $target_file = $this->resolveReferencePath( $scope->getFile(), $reference_path ); + $target_file = $this->resolveReferencePath( $scope->getFile(), $reference_path, $scope ); // The referenced file could not be located up the directory tree. if ( null === $target_file ) { @@ -378,8 +265,8 @@ public function getHookDoc( FuncCall $function_call, Scope $scope ): ?array { /** * Resolves the docblock preceding the given function call, if any. * - * @param FuncCall $function_call Hook function call node. - * @param Scope $scope Analysis scope. + * @param FuncCall $function_call Hook function call node. + * @param Scope&DependencyTracker $scope Analysis scope. * @return ResolvedPhpDocBlock|null Resolved docblock, or null when none precedes * the call or a reference cannot be resolved. * @throws ShouldNotHappenException @@ -790,16 +677,21 @@ private static function hookNameRegexParts( Expr $expr ): ?array { * * Only the single named file is ever tested; no directory is enumerated. * - * @param string $current_file Absolute path to the file with the reference comment. - * @param string $reference_path Root-relative path (e.g. "wp-includes/media.php"). + * Every candidate tested is declared as a dependency of the file being analyzed, + * not only the one that resolves. A candidate tested before the hit decides the + * result by being absent, so creating it changes which file the reference names. + * + * @param string $current_file Absolute path to the file with the reference comment. + * @param string $reference_path Root-relative path (e.g. "wp-includes/media.php"). + * @param DependencyTracker $tracker Tracker for the file being analyzed. * @return string|null Absolute path to the referenced file, or null when it cannot be located. */ - private function resolveReferencePath( string $current_file, string $reference_path ): ?string { + private function resolveReferencePath( string $current_file, string $reference_path, DependencyTracker $tracker ): ?string { $reference_path = ltrim( $reference_path, '/' ); $dir = dirname( $current_file ); while ( $dir === $this->wordpressRoot || str_starts_with( $dir, $this->wordpressRoot . '/' ) ) { - $target = $this->resolveWithinRoot( $dir . '/' . $reference_path ); + $target = $this->resolveWithinRoot( $dir . '/' . $reference_path, $tracker ); if ( null !== $target ) { return $target; } @@ -813,7 +705,7 @@ private function resolveReferencePath( string $current_file, string $reference_p } // The file holding the comment is not in the tree, so resolve against the root. - return $this->resolveWithinRoot( $this->wordpressRoot . '/' . $reference_path ); + return $this->resolveWithinRoot( $this->wordpressRoot . '/' . $reference_path, $tracker ); } /** @@ -830,10 +722,17 @@ private function resolveReferencePath( string $current_file, string $reference_p * them working while ensuring a reference cannot reach a file outside it. That * matters because whatever resolution finds is then read and parsed. * - * @param string $candidate Absolute candidate path, possibly containing dot segments. + * The candidate is tracked as given rather than canonicalized. It need not exist, + * and when it does, its contents are hashed through any symlink or dot segment, + * which is the file that is read. + * + * @param string $candidate Absolute candidate path, possibly containing dot segments. + * @param DependencyTracker $tracker Tracker for the file being analyzed. * @return string|null Canonical path, or null when it is not a file inside the tree. */ - private function resolveWithinRoot( string $candidate ): ?string { + private function resolveWithinRoot( string $candidate, DependencyTracker $tracker ): ?string { + $tracker->trackFileDependency( $candidate ); + if ( ! is_file( $candidate ) ) { return null; } diff --git a/tests/phpstan/HookDocsResultCacheMetaExtension.php b/tests/phpstan/HookDocsResultCacheMetaExtension.php deleted file mode 100644 index ff7b3a9fa068b..0000000000000 --- a/tests/phpstan/HookDocsResultCacheMetaExtension.php +++ /dev/null @@ -1,99 +0,0 @@ - *\/` convention and read with plain - * file I/O. A referencing file has no symbol dependency on its reference target, - * so editing a canonical docblock re-analyzes only the file that docblock lives - * in; every call site inheriting it keeps its cached result. - * 2. The tooling's own source files. PHPStan hashes its configuration, but changing - * a rule's logic does not invalidate results that rule already produced. - * - * Both are folded into the result cache key here, so cached results are discarded - * when — and only when — an inheritable docblock or the tooling itself changes. - * - * @see HookDocBlock::getReferencedHookDocsHash() - * - * @package WordPress - */ - -declare(strict_types=1); - -namespace WordPress\PHPStan; - -use PHPStan\Analyser\ResultCache\ResultCacheMetaExtension; - -/** - * Invalidates the result cache when hook documentation read from another file, or - * the tooling reading it, changes. - */ -final class HookDocsResultCacheMetaExtension implements ResultCacheMetaExtension { - - /** - * Hook docblock resolver. - * - * @var HookDocBlock - */ - private HookDocBlock $hookDocBlock; - - /** - * Constructor. - * - * @param HookDocBlock $hook_doc_block Hook docblock resolver. - */ - public function __construct( HookDocBlock $hook_doc_block ) { - $this->hookDocBlock = $hook_doc_block; - } - - /** - * Returns the key identifying this metadata source. - * - * @return non-empty-string - */ - public function getKey(): string { - return 'wordpressHookDocs'; - } - - /** - * Returns a hash of the inheritable hook documentation and of the tooling that - * reads it. - * - * @return non-falsy-string - */ - public function getHash(): string { - return md5( self::getToolingHash() . '|' . $this->hookDocBlock->getReferencedHookDocsHash() ); - } - - /** - * Hashes the PHPStan extension sources in this directory. - * - * @return non-falsy-string - */ - private static function getToolingHash(): string { - $files = glob( __DIR__ . '/*.php' ); - - if ( ! is_array( $files ) ) { - $files = array(); - } - - sort( $files ); - - $parts = array(); - - foreach ( $files as $file ) { - $contents = file_get_contents( $file ); - - if ( false === $contents ) { - continue; - } - - $parts[] = basename( $file ) . ':' . md5( $contents ); - } - - return md5( implode( '|', $parts ) ); - } -} diff --git a/tests/phpstan/HookDocumentationRule.php b/tests/phpstan/HookDocumentationRule.php index 7eead6a371321..b76cfabfcebd2 100644 --- a/tests/phpstan/HookDocumentationRule.php +++ b/tests/phpstan/HookDocumentationRule.php @@ -21,6 +21,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; +use PHPStan\Analyser\DependencyTracker; use PHPStan\Analyser\Scope; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -62,8 +63,8 @@ public function getNodeType(): string { /** * Processes a function call node. * - * @param Node $node Function call node. - * @param Scope $scope Analysis scope. + * @param Node $node Function call node. + * @param Scope&DependencyTracker $scope Analysis scope. * @return list * @throws ShouldNotHappenException */ diff --git a/tests/phpstan/HookParamCountRule.php b/tests/phpstan/HookParamCountRule.php index 4cebe197a7af4..2529aa1283ab6 100644 --- a/tests/phpstan/HookParamCountRule.php +++ b/tests/phpstan/HookParamCountRule.php @@ -24,6 +24,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; +use PHPStan\Analyser\DependencyTracker; use PHPStan\Analyser\Scope; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -86,8 +87,8 @@ public function getNodeType(): string { /** * Processes a function call node. * - * @param Node $node Function call node. - * @param Scope $scope Analysis scope. + * @param Node $node Function call node. + * @param Scope&DependencyTracker $scope Analysis scope. * @return list * @throws ShouldNotHappenException */ diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index e131da474e78e..c9ff5762d9f51 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -209,7 +209,7 @@ A cache written before one of them changed therefore answers with what the old c rm -rf .cache ``` -The 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). +The results cache alone is not the problem. PHPStan invalidates that itself when the configuration changes, and also when a bootstrap file or the source of any extension the configuration registers is edited, which covers every file in this directory that runs during analysis. 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). Analyzing 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. diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 4f1f4d4f7be4b..863d2b684d0ac 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -53,15 +53,6 @@ services: tags: - phpstan.rules.rule - # Docblocks inherited from another file, and the sources above that read them, - # are invisible to PHPStan's dependency graph, so both are folded into the result - # cache key. Without this, editing a canonical hook docblock leaves the cached - # results of every call site inheriting it in place. - - - class: WordPress\PHPStan\HookDocsResultCacheMetaExtension - tags: - - phpstan.resultCacheMetaExtension - # Runs the visitors above over every file PHPStan parses, not only the ones it # analyzes. # @@ -213,7 +204,6 @@ parameters: - ApplyFiltersDynamicFunctionReturnTypeExtension.php - HookDocumentationRule.php - HookParamCountRule.php - - HookDocsResultCacheMetaExtension.php bootstrapFiles: - bootstrap.php scanFiles: