Skip to content

Commit ec8bf88

Browse files
committed
Build/Test Tools: Update PHPStan to 2.3.0.
This also updates `phpstan/phpstan-phpunit` to 2.1.1, which requires PHPStan 2.3. The release analyzes code more precisely, so 12 `variable.undefined` baseline entries no longer match. It also reports two comparisons in `WP_HTML_Processor::run_adoption_agency_algorithm()` as constant: every path through the outer loop body returns or calls `bail()`, so the loop never runs a second time. That loop is scaffolding for parts of the algorithm that are not yet supported, so the comparisons are baselined as work still to do. PHPStan 2.3 deprecates `ResultCacheMetaExtension`, which `HookDocsResultCacheMetaExtension` implemented to discard the whole result cache whenever a hook docblock inherited through a "This filter is documented in" reference changed. Resolving such a reference now declares each candidate file as a dependency of the file being analyzed, so editing a canonical docblock re-analyzes only the files that inherit it, and the scan of the whole tree that ran on every analysis is gone. The extension also hashed the tooling's own sources, which PHPStan 2.3 already does for every registered extension, so it is removed. Developed in #14089. Follow-up to r62939, r64012. Props westonruter, swissspidy. See #65817. git-svn-id: https://develop.svn.wordpress.org/trunk@64237 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 776e07b commit ec8bf88

12 files changed

Lines changed: 96 additions & 311 deletions

‎composer.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
"squizlabs/php_codesniffer": "3.13.5",
5353
"wp-coding-standards/wpcs": "~3.4.1",
5454
"phpcompatibility/phpcompatibility-wp": "~2.1.3",
55-
"phpstan/phpstan": "2.2.16",
56-
"phpstan/phpstan-phpunit": "2.0.18",
55+
"phpstan/phpstan": "2.3.0",
56+
"phpstan/phpstan-phpunit": "2.1.1",
5757
"yoast/phpunit-polyfills": "^1.1.0"
5858
},
5959
"config": {

‎phpstan.neon.dist‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ includes:
4242
- tests/phpstan/baselines/function.alreadyNarrowedType.neon
4343
- tests/phpstan/baselines/function.impossibleType.neon
4444
- tests/phpstan/baselines/function.resultUnused.neon
45+
- tests/phpstan/baselines/greater.alwaysTrue.neon
46+
- tests/phpstan/baselines/greaterOrEqual.alwaysFalse.neon
4547
- tests/phpstan/baselines/greaterOrEqual.alwaysTrue.neon
4648
- tests/phpstan/baselines/identical.alwaysFalse.neon
4749
- tests/phpstan/baselines/identical.alwaysTrue.neon

‎tests/phpstan/ApplyFiltersDynamicFunctionReturnTypeExtension.php‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
namespace WordPress\PHPStan;
2828

2929
use PhpParser\Node\Expr\FuncCall;
30+
use PHPStan\Analyser\DependencyTracker;
3031
use PHPStan\Analyser\Scope;
3132
use PHPStan\Reflection\FunctionReflection;
3233
use PHPStan\ShouldNotHappenException;
@@ -72,9 +73,9 @@ public function isFunctionSupported( FunctionReflection $functionReflection ): b
7273
* @link https://developer.wordpress.org/reference/functions/apply_filters_deprecated/
7374
* @link https://developer.wordpress.org/reference/functions/apply_filters_ref_array/
7475
*
75-
* @param FunctionReflection $functionReflection Function being analyzed.
76-
* @param FuncCall $functionCall The function call node.
77-
* @param Scope $scope Analysis scope.
76+
* @param FunctionReflection $functionReflection Function being analyzed.
77+
* @param FuncCall $functionCall The function call node.
78+
* @param Scope&DependencyTracker $scope Analysis scope.
7879
* @return Type
7980
* @throws ShouldNotHappenException
8081
*/

‎tests/phpstan/HookDocBlock.php‎

Lines changed: 31 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
namespace WordPress\PHPStan;
2929

30-
use FilesystemIterator;
3130
use PhpParser\Comment\Doc;
3231
use PhpParser\Error as PhpParserError;
3332
use PhpParser\ErrorHandler\Collecting;
@@ -42,13 +41,11 @@
4241
use PhpParser\NodeFinder;
4342
use PhpParser\NodeTraverser;
4443
use PhpParser\ParserFactory;
44+
use PHPStan\Analyser\DependencyTracker;
4545
use PHPStan\Analyser\Scope;
4646
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
4747
use PHPStan\ShouldNotHappenException;
4848
use PHPStan\Type\FileTypeMapper;
49-
use RecursiveDirectoryIterator;
50-
use RecursiveIteratorIterator;
51-
use SplFileInfo;
5249

5350
/**
5451
* Bridges the docblock attached to a hook call by HookDocsVisitor to PHPStan's
@@ -104,18 +101,6 @@ class HookDocBlock {
104101
'apply_filters_ref_array',
105102
);
106103

107-
/**
108-
* Directories, relative to the WordPress root, scanned for reference comments
109-
* when hashing the docblocks that call sites can inherit.
110-
*
111-
* @see HookDocBlock::getScannedFiles()
112-
*/
113-
private const SCANNED_DIRECTORIES = array(
114-
'wp-admin',
115-
'wp-includes',
116-
'wp-content/themes',
117-
);
118-
119104
/**
120105
* Problem code: the referenced file does not exist.
121106
*/
@@ -178,110 +163,6 @@ public function __construct( FileTypeMapper $file_type_mapper, ?string $wordpres
178163
$this->canonicalWordpressRoot = str_replace( '\\', '/', $this->canonicalWordpressRoot );
179164
}
180165

181-
/**
182-
* Returns a hash of every hook docblock that a call site can inherit through a
183-
* "documented elsewhere" reference comment.
184-
*
185-
* Those docblocks are read with plain file I/O, so PHPStan's dependency graph
186-
* does not know that the referencing files depend on them: editing a canonical
187-
* docblock re-analyzes only the file it lives in, leaving the cached results of
188-
* every referencing file in place. Folding this hash into the result cache key
189-
* invalidates the cache when an inheritable docblock changes, and only then.
190-
*
191-
* @see HookDocsResultCacheMetaExtension
192-
*
193-
* @return non-falsy-string
194-
*/
195-
public function getReferencedHookDocsHash(): string {
196-
$docs = array();
197-
198-
foreach ( $this->getScannedFiles() as $file ) {
199-
$code = file_get_contents( $file );
200-
201-
if ( false === $code || ! str_contains( $code, 'is documented in' ) ) {
202-
continue;
203-
}
204-
205-
if ( ! preg_match_all( self::REFERENCE_PATTERN, $code, $matches ) ) {
206-
continue;
207-
}
208-
209-
foreach ( $matches[1] as $reference_path ) {
210-
$target = $this->resolveReferencePath( $file, $reference_path );
211-
212-
if ( null === $target ) {
213-
continue;
214-
}
215-
216-
// Key on the path relative to the WordPress root so the hash does not
217-
// depend on where the checkout lives.
218-
$key = $this->getRootRelativePath( $target );
219-
220-
$docs[ $key ] ??= $this->getHookDocs( $target );
221-
}
222-
}
223-
224-
ksort( $docs );
225-
226-
return md5( (string) json_encode( $docs ) );
227-
}
228-
229-
/**
230-
* Returns the files scanned for reference comments: the WordPress directories
231-
* that can contain them, plus the PHP files at the root of the install.
232-
*
233-
* `wp-content/plugins` is deliberately not scanned. A checkout may have plugins
234-
* carrying large `vendor` and `node_modules` trees, and core's reference comments
235-
* only ever point within core.
236-
*
237-
* @return list<string> Absolute file paths.
238-
*/
239-
private function getScannedFiles(): array {
240-
$files = array();
241-
242-
foreach ( self::SCANNED_DIRECTORIES as $directory ) {
243-
$path = $this->wordpressRoot . '/' . $directory;
244-
245-
if ( ! is_dir( $path ) ) {
246-
continue;
247-
}
248-
249-
$iterator = new RecursiveIteratorIterator(
250-
new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS )
251-
);
252-
253-
foreach ( $iterator as $file ) {
254-
if ( $file instanceof SplFileInfo && $file->isFile() && 'php' === strtolower( $file->getExtension() ) ) {
255-
$files[] = $file->getPathname();
256-
}
257-
}
258-
}
259-
260-
$root_files = glob( $this->wordpressRoot . '/*.php' );
261-
262-
if ( is_array( $root_files ) ) {
263-
$files = array_merge( $files, $root_files );
264-
}
265-
266-
return $files;
267-
}
268-
269-
/**
270-
* Expresses an absolute path relative to the WordPress root when it sits inside
271-
* it, so that hashes do not depend on the checkout location.
272-
*
273-
* Paths reached through a reference comment are canonical, so the canonical root is
274-
* what they are relative to.
275-
*
276-
* @param string $path Absolute path.
277-
* @return string
278-
*/
279-
private function getRootRelativePath( string $path ): string {
280-
$prefix = $this->canonicalWordpressRoot . '/';
281-
282-
return str_starts_with( $path, $prefix ) ? substr( $path, strlen( $prefix ) ) : $path;
283-
}
284-
285166
/**
286167
* Resolves the documentation for a hook call: the docblock written above it, or
287168
* the canonical docblock a "documented elsewhere" comment points at.
@@ -295,8 +176,14 @@ private function getRootRelativePath( string $path ): string {
295176
* parameters. `problem` says why it could not be resolved, when the reason is one
296177
* worth reporting.
297178
*
298-
* @param FuncCall $function_call Hook function call node.
299-
* @param Scope $scope Analysis scope.
179+
* A canonical docblock is read with plain file I/O, so PHPStan's dependency graph
180+
* cannot see that the referencing file depends on it. Resolving a reference
181+
* therefore declares that dependency on the scope, and the result cache
182+
* re-analyzes the referencing file when the file it resolved to changes, or when
183+
* a file it would resolve to first is created.
184+
*
185+
* @param FuncCall $function_call Hook function call node.
186+
* @param Scope&DependencyTracker $scope Analysis scope.
300187
* @return HookDocumentation|null Null when no docblock precedes the call.
301188
* @throws ShouldNotHappenException
302189
*/
@@ -336,7 +223,7 @@ public function getHookDoc( FuncCall $function_call, Scope $scope ): ?array {
336223
}
337224

338225
$reference_path = $matches[1];
339-
$target_file = $this->resolveReferencePath( $scope->getFile(), $reference_path );
226+
$target_file = $this->resolveReferencePath( $scope->getFile(), $reference_path, $scope );
340227

341228
// The referenced file could not be located up the directory tree.
342229
if ( null === $target_file ) {
@@ -378,8 +265,8 @@ public function getHookDoc( FuncCall $function_call, Scope $scope ): ?array {
378265
/**
379266
* Resolves the docblock preceding the given function call, if any.
380267
*
381-
* @param FuncCall $function_call Hook function call node.
382-
* @param Scope $scope Analysis scope.
268+
* @param FuncCall $function_call Hook function call node.
269+
* @param Scope&DependencyTracker $scope Analysis scope.
383270
* @return ResolvedPhpDocBlock|null Resolved docblock, or null when none precedes
384271
* the call or a reference cannot be resolved.
385272
* @throws ShouldNotHappenException
@@ -790,16 +677,21 @@ private static function hookNameRegexParts( Expr $expr ): ?array {
790677
*
791678
* Only the single named file is ever tested; no directory is enumerated.
792679
*
793-
* @param string $current_file Absolute path to the file with the reference comment.
794-
* @param string $reference_path Root-relative path (e.g. "wp-includes/media.php").
680+
* Every candidate tested is declared as a dependency of the file being analyzed,
681+
* not only the one that resolves. A candidate tested before the hit decides the
682+
* result by being absent, so creating it changes which file the reference names.
683+
*
684+
* @param string $current_file Absolute path to the file with the reference comment.
685+
* @param string $reference_path Root-relative path (e.g. "wp-includes/media.php").
686+
* @param DependencyTracker $tracker Tracker for the file being analyzed.
795687
* @return string|null Absolute path to the referenced file, or null when it cannot be located.
796688
*/
797-
private function resolveReferencePath( string $current_file, string $reference_path ): ?string {
689+
private function resolveReferencePath( string $current_file, string $reference_path, DependencyTracker $tracker ): ?string {
798690
$reference_path = ltrim( $reference_path, '/' );
799691
$dir = dirname( $current_file );
800692

801693
while ( $dir === $this->wordpressRoot || str_starts_with( $dir, $this->wordpressRoot . '/' ) ) {
802-
$target = $this->resolveWithinRoot( $dir . '/' . $reference_path );
694+
$target = $this->resolveWithinRoot( $dir . '/' . $reference_path, $tracker );
803695
if ( null !== $target ) {
804696
return $target;
805697
}
@@ -813,7 +705,7 @@ private function resolveReferencePath( string $current_file, string $reference_p
813705
}
814706

815707
// The file holding the comment is not in the tree, so resolve against the root.
816-
return $this->resolveWithinRoot( $this->wordpressRoot . '/' . $reference_path );
708+
return $this->resolveWithinRoot( $this->wordpressRoot . '/' . $reference_path, $tracker );
817709
}
818710

819711
/**
@@ -830,10 +722,17 @@ private function resolveReferencePath( string $current_file, string $reference_p
830722
* them working while ensuring a reference cannot reach a file outside it. That
831723
* matters because whatever resolution finds is then read and parsed.
832724
*
833-
* @param string $candidate Absolute candidate path, possibly containing dot segments.
725+
* The candidate is tracked as given rather than canonicalized. It need not exist,
726+
* and when it does, its contents are hashed through any symlink or dot segment,
727+
* which is the file that is read.
728+
*
729+
* @param string $candidate Absolute candidate path, possibly containing dot segments.
730+
* @param DependencyTracker $tracker Tracker for the file being analyzed.
834731
* @return string|null Canonical path, or null when it is not a file inside the tree.
835732
*/
836-
private function resolveWithinRoot( string $candidate ): ?string {
733+
private function resolveWithinRoot( string $candidate, DependencyTracker $tracker ): ?string {
734+
$tracker->trackFileDependency( $candidate );
735+
837736
if ( ! is_file( $candidate ) ) {
838737
return null;
839738
}

‎tests/phpstan/HookDocsResultCacheMetaExtension.php‎

Lines changed: 0 additions & 99 deletions
This file was deleted.

0 commit comments

Comments
 (0)