Skip to content

Commit d4dbd21

Browse files
authored
is_wp_error() rule (#95)
1 parent 42d1c4c commit d4dbd21

File tree

4 files changed

+176
-0
lines changed

4 files changed

+176
-0
lines changed

extension.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ services:
7777
- phpstan.broker.dynamicFunctionReturnTypeExtension
7878
rules:
7979
- SzepeViktor\PHPStan\WordPress\HookDocsRule
80+
- SzepeViktor\PHPStan\WordPress\IsWpErrorRule
8081
parameters:
8182
bootstrapFiles:
8283
- %rootDir%/../../php-stubs/wordpress-stubs/wordpress-stubs.php

src/IsWpErrorRule.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/**
4+
* Custom rule to validate usage of `is_wp_error()`.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace SzepeViktor\PHPStan\WordPress;
10+
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PhpParser\Node;
13+
use PHPStan\Analyser\Scope;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use PHPStan\Rules\RuleLevelHelper;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\VerbosityLevel;
18+
19+
use function sprintf;
20+
21+
/**
22+
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall>
23+
*/
24+
class IsWpErrorRule implements \PHPStan\Rules\Rule
25+
{
26+
/** @var \PHPStan\Rules\RuleLevelHelper */
27+
protected $ruleLevelHelper;
28+
29+
public function __construct(
30+
RuleLevelHelper $ruleLevelHelper
31+
) {
32+
$this->ruleLevelHelper = $ruleLevelHelper;
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return FuncCall::class;
38+
}
39+
40+
/**
41+
* @param \PhpParser\Node\Expr\FuncCall $node
42+
* @param \PHPStan\Analyser\Scope $scope
43+
* @return array<int, \PHPStan\Rules\RuleError>
44+
*/
45+
public function processNode(Node $node, Scope $scope): array
46+
{
47+
$name = $node->name;
48+
49+
if (! ($name instanceof \PhpParser\Node\Name)) {
50+
return [];
51+
}
52+
53+
if ($name->toString() !== 'is_wp_error') {
54+
return [];
55+
}
56+
57+
$args = $node->getArgs();
58+
59+
if (! isset($args[0])) {
60+
return [];
61+
}
62+
63+
$argumentType = $scope->getType($args[0]->value);
64+
$accepted = $this->ruleLevelHelper->accepts(
65+
$argumentType,
66+
new ObjectType('WP_Error'),
67+
$scope->isDeclareStrictTypes()
68+
);
69+
70+
if (!$accepted) {
71+
return [
72+
RuleErrorBuilder::message(
73+
sprintf(
74+
'is_wp_error(%s) will always evaluate to false.',
75+
$argumentType->describe(VerbosityLevel::typeOnly())
76+
)
77+
)->build(),
78+
];
79+
}
80+
81+
if (($argumentType instanceof ObjectType) && ($argumentType->getClassName() === 'WP_Error')) {
82+
return [
83+
RuleErrorBuilder::message(
84+
'is_wp_error(WP_Error) will always evaluate to true.'
85+
)->build(),
86+
];
87+
}
88+
89+
return [];
90+
}
91+
}

tests/IsWpErrorRuleTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SzepeViktor\PHPStan\WordPress\Tests;
6+
7+
use PHPStan\Rules\RuleLevelHelper;
8+
use SzepeViktor\PHPStan\WordPress\IsWpErrorRule;
9+
10+
/**
11+
* @extends \PHPStan\Testing\RuleTestCase<\SzepeViktor\PHPStan\WordPress\IsWpErrorRule>
12+
*/
13+
class IsWpErrorRuleTest extends \PHPStan\Testing\RuleTestCase
14+
{
15+
protected function getRule(): \PHPStan\Rules\Rule
16+
{
17+
/** @var \PHPStan\Rules\RuleLevelHelper */
18+
$ruleLevelHelper = self::getContainer()->getByType(RuleLevelHelper::class);
19+
20+
// getRule() method needs to return an instance of the tested rule
21+
return new IsWpErrorRule($ruleLevelHelper);
22+
}
23+
24+
// phpcs:ignore NeutronStandard.Functions.LongFunction.LongFunction
25+
public function testRule(): void
26+
{
27+
// first argument: path to the example file that contains some errors that should be reported by IsWpErrorRule
28+
// second argument: an array of expected errors,
29+
// each error consists of the asserted error message, and the asserted error file line
30+
$this->analyse(
31+
[
32+
__DIR__ . '/data/is_wp_error.php',
33+
],
34+
[
35+
[
36+
'is_wp_error(int) will always evaluate to false.',
37+
23,
38+
],
39+
[
40+
'is_wp_error(int|false) will always evaluate to false.',
41+
24,
42+
],
43+
[
44+
'is_wp_error(WP_Error) will always evaluate to true.',
45+
25,
46+
],
47+
[
48+
'is_wp_error(WP_Post) will always evaluate to false.',
49+
26,
50+
],
51+
]
52+
);
53+
}
54+
}

tests/data/is_wp_error.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SzepeViktor\PHPStan\WordPress\Tests;
6+
7+
/** @var mixed $mixed */
8+
$mixed = $mixed;
9+
10+
/** @var \WP_Error|int $yes */
11+
$yes = $yes;
12+
13+
/** @var int|false $no */
14+
$no = $no;
15+
16+
/** @var \WP_Post $post */
17+
$post = $post;
18+
19+
/** @var \WP_Error $always */
20+
$always = $always;
21+
22+
// Incorrect usage:
23+
is_wp_error(123);
24+
is_wp_error($no);
25+
is_wp_error($always);
26+
is_wp_error($post);
27+
28+
// Correct usage:
29+
is_wp_error($mixed);
30+
is_wp_error($yes);

0 commit comments

Comments
 (0)