Skip to content

Commit 193ab98

Browse files
committed
Rule for testing assertType() calls etc. directly
1 parent d069408 commit 193ab98

File tree

4 files changed

+283
-0
lines changed

4 files changed

+283
-0
lines changed

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ parametersSchema:
305305

306306
rules:
307307
- PHPStan\Rules\Debug\DumpTypeRule
308+
- PHPStan\Rules\Debug\FileAssertRule
308309

309310
services:
310311
-

src/Rules/Debug/FileAssertRule.php

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Debug;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\StaticCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\ReflectionProvider;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleError;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\TrinaryLogic;
13+
use PHPStan\Type\Constant\ConstantStringType;
14+
use PHPStan\Type\VerbosityLevel;
15+
16+
/**
17+
* @implements Rule<Node\Expr\FuncCall>
18+
*/
19+
class FileAssertRule implements Rule
20+
{
21+
22+
private ReflectionProvider $reflectionProvider;
23+
24+
public function __construct(ReflectionProvider $reflectionProvider)
25+
{
26+
$this->reflectionProvider = $reflectionProvider;
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return Node\Expr\FuncCall::class;
32+
}
33+
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
if (!$node->name instanceof Node\Name) {
37+
return [];
38+
}
39+
40+
if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
41+
return [];
42+
}
43+
44+
$function = $this->reflectionProvider->getFunction($node->name, $scope);
45+
if ($function->getName() === 'PHPStan\\Testing\\assertType') {
46+
return $this->processAssertType($node->args, $scope);
47+
}
48+
49+
if ($function->getName() === 'PHPStan\\Testing\\assertNativeType') {
50+
return $this->processAssertNativeType($node->args, $scope);
51+
}
52+
53+
if ($function->getName() === 'PHPStan\\Testing\\assertVariableCertainty') {
54+
return $this->processAssertVariableCertainty($node->args, $scope);
55+
}
56+
57+
return [];
58+
}
59+
60+
/**
61+
* @param Node\Arg[] $args
62+
* @param Scope $scope
63+
* @return RuleError[]
64+
*/
65+
private function processAssertType(array $args, Scope $scope): array
66+
{
67+
if (count($args) !== 2) {
68+
return [];
69+
}
70+
71+
$expectedTypeString = $scope->getType($args[0]->value);
72+
if (!$expectedTypeString instanceof ConstantStringType) {
73+
return [
74+
RuleErrorBuilder::message('Expected type must be a literal string.')->nonIgnorable()->build(),
75+
];
76+
}
77+
78+
$expressionType = $scope->getType($args[1]->value)->describe(VerbosityLevel::precise());
79+
if ($expectedTypeString->getValue() === $expressionType) {
80+
return [];
81+
}
82+
83+
return [
84+
RuleErrorBuilder::message(sprintf('Expected type %s, actual: %s', $expectedTypeString->getValue(), $expressionType))->nonIgnorable()->build(),
85+
];
86+
}
87+
88+
/**
89+
* @param Node\Arg[] $args
90+
* @param Scope $scope
91+
* @return RuleError[]
92+
*/
93+
private function processAssertNativeType(array $args, Scope $scope): array
94+
{
95+
if (count($args) !== 2) {
96+
return [];
97+
}
98+
99+
$scope = $scope->doNotTreatPhpDocTypesAsCertain();
100+
$expectedTypeString = $scope->getNativeType($args[0]->value);
101+
if (!$expectedTypeString instanceof ConstantStringType) {
102+
return [
103+
RuleErrorBuilder::message('Expected native type must be a literal string.')->nonIgnorable()->build(),
104+
];
105+
}
106+
107+
$expressionType = $scope->getNativeType($args[1]->value)->describe(VerbosityLevel::precise());
108+
if ($expectedTypeString->getValue() === $expressionType) {
109+
return [];
110+
}
111+
112+
return [
113+
RuleErrorBuilder::message(sprintf('Expected native type %s, actual: %s', $expectedTypeString->getValue(), $expressionType))->nonIgnorable()->build(),
114+
];
115+
}
116+
117+
/**
118+
* @param Node\Arg[] $args
119+
* @param Scope $scope
120+
* @return RuleError[]
121+
*/
122+
private function processAssertVariableCertainty(array $args, Scope $scope): array
123+
{
124+
if (count($args) !== 2) {
125+
return [];
126+
}
127+
128+
$certainty = $args[0]->value;
129+
if (!$certainty instanceof StaticCall) {
130+
return [
131+
RuleErrorBuilder::message('First argument of %s() must be TrinaryLogic call')
132+
->nonIgnorable()
133+
->build(),
134+
];
135+
}
136+
if (!$certainty->class instanceof Node\Name) {
137+
return [
138+
RuleErrorBuilder::message('Invalid TrinaryLogic call.')
139+
->nonIgnorable()
140+
->build(),
141+
];
142+
}
143+
144+
if ($certainty->class->toString() !== 'PHPStan\\TrinaryLogic') {
145+
return [
146+
RuleErrorBuilder::message('Invalid TrinaryLogic call.')
147+
->nonIgnorable()
148+
->build(),
149+
];
150+
}
151+
152+
if (!$certainty->name instanceof Node\Identifier) {
153+
return [
154+
RuleErrorBuilder::message('Invalid TrinaryLogic call.')
155+
->nonIgnorable()
156+
->build(),
157+
];
158+
}
159+
160+
// @phpstan-ignore-next-line
161+
$expectedCertaintyValue = TrinaryLogic::{$certainty->name->toString()}();
162+
$variable = $args[1]->value;
163+
if (!$variable instanceof Node\Expr\Variable) {
164+
return [
165+
RuleErrorBuilder::message('Invalid assertVariableCertainty call.')
166+
->nonIgnorable()
167+
->build(),
168+
];
169+
}
170+
if (!is_string($variable->name)) {
171+
return [
172+
RuleErrorBuilder::message('Invalid assertVariableCertainty call.')
173+
->nonIgnorable()
174+
->build(),
175+
];
176+
}
177+
178+
$actualCertaintyValue = $scope->hasVariableType($variable->name);
179+
if ($expectedCertaintyValue->equals($actualCertaintyValue)) {
180+
return [];
181+
}
182+
183+
return [
184+
RuleErrorBuilder::message(sprintf('Expected variable certainty %s, actual: %s', $expectedCertaintyValue->describe(), $actualCertaintyValue->describe()))->nonIgnorable()->build(),
185+
];
186+
}
187+
188+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Debug;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<FileAssertRule>
10+
*/
11+
class FileAssertRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new FileAssertRule($this->createReflectionProvider());
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/file-asserts.php'], [
22+
[
23+
'Expected type array<string>, actual: array<int>',
24+
19,
25+
],
26+
[
27+
'Expected native type false, actual: bool',
28+
36,
29+
],
30+
[
31+
'Expected native type true, actual: bool',
32+
37,
33+
],
34+
[
35+
'Expected variable certainty Yes, actual: No',
36+
45,
37+
],
38+
[
39+
'Expected variable certainty Maybe, actual: No',
40+
46,
41+
],
42+
]);
43+
}
44+
45+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace FileAsserts;
4+
5+
use PHPStan\TrinaryLogic;
6+
use function PHPStan\Testing\assertNativeType;
7+
use function PHPStan\Testing\assertType;
8+
use function PHPStan\Testing\assertVariableCertainty;
9+
10+
class Foo
11+
{
12+
13+
/**
14+
* @param array<int> $a
15+
*/
16+
public function doFoo(array $a): void
17+
{
18+
assertType('array<int>', $a);
19+
assertType('array<string>', $a);
20+
}
21+
22+
/**
23+
* @param non-empty-array<int> $a
24+
*/
25+
public function doBar(array $a): void
26+
{
27+
assertType('array<int>&nonEmpty', $a);
28+
assertNativeType('array', $a);
29+
30+
assertType('false', $a === []);
31+
assertType('true', $a !== []);
32+
33+
assertNativeType('bool', $a === []);
34+
assertNativeType('bool', $a !== []);
35+
36+
assertNativeType('false', $a === []);
37+
assertNativeType('true', $a !== []);
38+
}
39+
40+
public function doBaz($a): void
41+
{
42+
assertVariableCertainty(TrinaryLogic::createYes(), $a);
43+
assertVariableCertainty(TrinaryLogic::createNo(), $b);
44+
45+
assertVariableCertainty(TrinaryLogic::createYes(), $b);
46+
assertVariableCertainty(TrinaryLogic::createMaybe(), $b);
47+
}
48+
49+
}

0 commit comments

Comments
 (0)