Skip to content

Commit 1b5bc4d

Browse files
authored
Hook docs rule (#78)
1 parent c95652e commit 1b5bc4d

10 files changed

+608
-39
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Please see [WooCommerce Stubs](https://github.com/php-stubs/woocommerce-stubs)
6767
- Provides dynamic return type extensions for many core functions
6868
- Defines some core constants
6969
- Handles special functions and classes e.g. `is_wp_error()`
70-
- Uses the optional docblock that precedes a call to `apply_filters()` to treat its return type as certain
70+
- Validates the optional docblock that precedes a call to `apply_filters()` and treats the type of its first `@param` as certain
7171

7272
### Usage of an `apply_filters()` docblock
7373

extension.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ services:
5959
class: SzepeViktor\PHPStan\WordPress\WPErrorParameterDynamicFunctionReturnTypeExtension
6060
tags:
6161
- phpstan.broker.dynamicFunctionReturnTypeExtension
62+
rules:
63+
- SzepeViktor\PHPStan\WordPress\HookDocsRule
6264
parameters:
6365
bootstrapFiles:
6466
- %rootDir%/../../php-stubs/wordpress-stubs/wordpress-stubs.php

phpcs.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<file>src/</file>
55
<file>tests/</file>
66

7+
<exclude-pattern>tests/data</exclude-pattern>
8+
<exclude-pattern>tests/functions.php</exclude-pattern>
9+
710
<rule ref="PSR12NeutronRuleset">
811
<exclude name="Generic.Files.LineLength"/>
912
<exclude name="PEAR.Commenting.ClassComment"/>

src/ApplyFiltersDynamicFunctionReturnTypeExtension.php

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717

1818
class ApplyFiltersDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
1919
{
20-
/** @var \PHPStan\Type\FileTypeMapper */
21-
protected $fileTypeMapper;
20+
/** @var \SzepeViktor\PHPStan\WordPress\HookDocBlock */
21+
protected $hookDocBlock;
2222

2323
public function __construct(FileTypeMapper $fileTypeMapper)
2424
{
25-
$this->fileTypeMapper = $fileTypeMapper;
25+
$this->hookDocBlock = new HookDocBlock($fileTypeMapper);
2626
}
2727

2828
public function isFunctionSupported(FunctionReflection $functionReflection): bool
@@ -42,26 +42,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo
4242
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
4343
{
4444
$default = new MixedType();
45-
$comment = self::getNullableNodeComment($functionCall);
45+
$resolvedPhpDoc = $this->hookDocBlock->getNullableHookDocBlock($functionCall, $scope);
4646

47-
if ($comment === null) {
47+
if ($resolvedPhpDoc === null) {
4848
return $default;
4949
}
5050

51-
// Fetch the docblock contents.
52-
$code = $comment->getText();
53-
54-
// Resolve the docblock in scope.
55-
$classReflection = $scope->getClassReflection();
56-
$traitReflection = $scope->getTraitReflection();
57-
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
58-
$scope->getFile(),
59-
($scope->isInClass() && $classReflection !== null) ? $classReflection->getName() : null,
60-
($scope->isInTrait() && $traitReflection !== null) ? $traitReflection->getName() : null,
61-
$scope->getFunctionName(),
62-
$code
63-
);
64-
6551
// Fetch the `@param` values from the docblock.
6652
$params = $resolvedPhpDoc->getParamTags();
6753

@@ -71,23 +57,4 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
7157

7258
return $default;
7359
}
74-
75-
private static function getNullableNodeComment(FuncCall $node): ?\PhpParser\Comment\Doc
76-
{
77-
$startLine = $node->getStartLine();
78-
79-
while ($node !== null && $node->getStartLine() === $startLine) {
80-
// Fetch the docblock from the node.
81-
$comment = $node->getDocComment();
82-
83-
if ($comment !== null) {
84-
return $comment;
85-
}
86-
87-
/** @var \PhpParser\Node|null */
88-
$node = $node->getAttribute('parent');
89-
}
90-
91-
return null;
92-
}
9360
}

src/HookDocBlock.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/**
4+
* Set return type of apply_filters() based on its optional preceding docblock.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace SzepeViktor\PHPStan\WordPress;
10+
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
14+
use PHPStan\Type\FileTypeMapper;
15+
16+
class HookDocBlock
17+
{
18+
19+
/** @var \PHPStan\Type\FileTypeMapper */
20+
protected $fileTypeMapper;
21+
22+
public function __construct(FileTypeMapper $fileTypeMapper)
23+
{
24+
$this->fileTypeMapper = $fileTypeMapper;
25+
}
26+
27+
public function getNullableHookDocBlock(FuncCall $functionCall, Scope $scope): ?ResolvedPhpDocBlock
28+
{
29+
$comment = self::getNullableNodeComment($functionCall);
30+
31+
if ($comment === null) {
32+
return null;
33+
}
34+
35+
// Fetch the docblock contents.
36+
$code = $comment->getText();
37+
38+
// Resolve the docblock in scope.
39+
$classReflection = $scope->getClassReflection();
40+
$traitReflection = $scope->getTraitReflection();
41+
42+
return $this->fileTypeMapper->getResolvedPhpDoc(
43+
$scope->getFile(),
44+
($scope->isInClass() && $classReflection !== null) ? $classReflection->getName() : null,
45+
($scope->isInTrait() && $traitReflection !== null) ? $traitReflection->getName() : null,
46+
$scope->getFunctionName(),
47+
$code
48+
);
49+
}
50+
51+
private static function getNullableNodeComment(FuncCall $node): ?\PhpParser\Comment\Doc
52+
{
53+
$startLine = $node->getStartLine();
54+
55+
while ($node !== null && $node->getStartLine() === $startLine) {
56+
// Fetch the docblock from the node.
57+
$comment = $node->getDocComment();
58+
59+
if ($comment !== null) {
60+
return $comment;
61+
}
62+
63+
/** @var \PhpParser\Node|null */
64+
$node = $node->getAttribute('parent');
65+
}
66+
67+
return null;
68+
}
69+
}

src/HookDocsParamException.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
/**
4+
* Exception thrown when validating a PHPDoc docblock that precedes a hook.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace SzepeViktor\PHPStan\WordPress;
10+
11+
// phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousExceptionNaming.SuperfluousSuffix
12+
class HookDocsParamException extends \DomainException
13+
{
14+
}

0 commit comments

Comments
 (0)