Skip to content

Commit be2c146

Browse files
authored
Merge branch 'WordPress:trunk' into trunk
2 parents 8ddc01f + ec8bf88 commit be2c146

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)