Skip to content

Commit c6898a1

Browse files
paulbalandanondrejmirtes
authored andcommitted
Introduce InvalidTypesInUnionRule
1 parent 993c0a2 commit c6898a1

File tree

7 files changed

+287
-0
lines changed

7 files changed

+287
-0
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ lint:
6060
--exclude tests/PHPStan/Rules/Methods/data/bug-10101.php \
6161
--exclude tests/PHPStan/Rules/Methods/data/final-method-by-phpdoc.php \
6262
--exclude tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php \
63+
--exclude tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php \
64+
--exclude tests/PHPStan/Rules/Types/data/invalid-union-with-never.php \
65+
--exclude tests/PHPStan/Rules/Types/data/invalid-union-with-void.php \
6366
--exclude tests/PHPStan/Rules/Constants/data/dynamic-class-constant-fetch.php \
6467
src tests
6568

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ rules:
103103
- PHPStan\Rules\Properties\ReadOnlyPropertyRule
104104
- PHPStan\Rules\Traits\ConflictingTraitConstantsRule
105105
- PHPStan\Rules\Traits\ConstantsInTraitsRule
106+
- PHPStan\Rules\Types\InvalidTypesInUnionRule
106107
- PHPStan\Rules\Variables\UnsetRule
107108
- PHPStan\Rules\Whitespace\FileWhitespaceRule
108109

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Types;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\ClassPropertyNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleError;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use function array_merge;
12+
use function in_array;
13+
use function sprintf;
14+
15+
/**
16+
* @implements Rule<Node>
17+
*/
18+
class InvalidTypesInUnionRule implements Rule
19+
{
20+
21+
private const ONLY_STANDALONE_TYPES = [
22+
'mixed',
23+
'never',
24+
'void',
25+
];
26+
27+
public function getNodeType(): string
28+
{
29+
return Node::class;
30+
}
31+
32+
public function processNode(Node $node, Scope $scope): array
33+
{
34+
if (!$node instanceof Node\FunctionLike && !$node instanceof ClassPropertyNode) {
35+
return [];
36+
}
37+
38+
if ($node instanceof Node\FunctionLike) {
39+
return $this->processFunctionLikeNode($node);
40+
}
41+
42+
return $this->processClassPropertyNode($node);
43+
}
44+
45+
/**
46+
* @return list<RuleError>
47+
*/
48+
private function processFunctionLikeNode(Node\FunctionLike $functionLike): array
49+
{
50+
$errors = [];
51+
52+
foreach ($functionLike->getParams() as $param) {
53+
if (!$param->type instanceof Node\ComplexType) {
54+
continue;
55+
}
56+
57+
$errors = array_merge($errors, $this->processComplexType($param->type));
58+
}
59+
60+
if ($functionLike->getReturnType() instanceof Node\ComplexType) {
61+
$errors = array_merge($errors, $this->processComplexType($functionLike->getReturnType()));
62+
}
63+
64+
return $errors;
65+
}
66+
67+
/**
68+
* @return list<RuleError>
69+
*/
70+
private function processClassPropertyNode(ClassPropertyNode $classPropertyNode): array
71+
{
72+
if (!$classPropertyNode->getNativeType() instanceof Node\ComplexType) {
73+
return [];
74+
}
75+
76+
return $this->processComplexType($classPropertyNode->getNativeType());
77+
}
78+
79+
/**
80+
* @return list<RuleError>
81+
*/
82+
private function processComplexType(Node\ComplexType $complexType): array
83+
{
84+
if (!$complexType instanceof Node\UnionType && !$complexType instanceof Node\NullableType) {
85+
return [];
86+
}
87+
88+
if ($complexType instanceof Node\UnionType) {
89+
foreach ($complexType->types as $type) {
90+
if ($type instanceof Node\Identifier && in_array($type->toString(), self::ONLY_STANDALONE_TYPES, true)) {
91+
return [
92+
RuleErrorBuilder::message(sprintf('Type %s cannot be part of a union type declaration.', $type->toString()))
93+
->line($complexType->getLine())
94+
->nonIgnorable()
95+
->build(),
96+
];
97+
}
98+
}
99+
100+
return [];
101+
}
102+
103+
if ($complexType->type instanceof Node\Identifier && in_array($complexType->type->toString(), self::ONLY_STANDALONE_TYPES, true)) {
104+
return [
105+
RuleErrorBuilder::message(sprintf('Type %s cannot be part of a nullable type declaration.', $complexType->type->toString()))
106+
->line($complexType->getLine())
107+
->nonIgnorable()
108+
->build(),
109+
];
110+
}
111+
112+
return [];
113+
}
114+
115+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Types;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<InvalidTypesInUnionRule>
10+
*/
11+
class InvalidTypesInUnionRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new InvalidTypesInUnionRule();
17+
}
18+
19+
public function testRuleOnUnionWithVoid(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/invalid-union-with-void.php'], [
22+
[
23+
'Type void cannot be part of a union type declaration.',
24+
11,
25+
],
26+
[
27+
'Type void cannot be part of a nullable type declaration.',
28+
15,
29+
],
30+
]);
31+
}
32+
33+
/**
34+
* @requires PHP 8.0
35+
*/
36+
public function testRuleOnUnionWithMixed(): void
37+
{
38+
$this->analyse([__DIR__ . '/data/invalid-union-with-mixed.php'], [
39+
[
40+
'Type mixed cannot be part of a nullable type declaration.',
41+
9,
42+
],
43+
[
44+
'Type mixed cannot be part of a union type declaration.',
45+
12,
46+
],
47+
[
48+
'Type mixed cannot be part of a union type declaration.',
49+
16,
50+
],
51+
[
52+
'Type mixed cannot be part of a union type declaration.',
53+
17,
54+
],
55+
[
56+
'Type mixed cannot be part of a union type declaration.',
57+
22,
58+
],
59+
[
60+
'Type mixed cannot be part of a nullable type declaration.',
61+
29,
62+
],
63+
[
64+
'Type mixed cannot be part of a nullable type declaration.',
65+
29,
66+
],
67+
[
68+
'Type mixed cannot be part of a nullable type declaration.',
69+
34,
70+
],
71+
]);
72+
}
73+
74+
/**
75+
* @requires PHP 8.1
76+
*/
77+
public function testRuleOnUnionWithNever(): void
78+
{
79+
$this->analyse([__DIR__ . '/data/invalid-union-with-never.php'], [
80+
[
81+
'Type never cannot be part of a nullable type declaration.',
82+
7,
83+
],
84+
[
85+
'Type never cannot be part of a union type declaration.',
86+
16,
87+
],
88+
]);
89+
}
90+
91+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace InvalidUnionWithMixed;
4+
5+
class Foo
6+
{
7+
8+
public ?string $bar = null;
9+
public ?mixed $baz = null;
10+
11+
public int|null $lorem = null;
12+
public mixed|string $ipsum = '';
13+
14+
public function dolor(
15+
string|int $sit,
16+
bool|mixed $amet
17+
): mixed|string
18+
{
19+
return '';
20+
}
21+
22+
public function lorem(): mixed|string
23+
{
24+
return '';
25+
}
26+
27+
}
28+
29+
function funcWithMixed(mixed $a, ?mixed $b): ?mixed
30+
{
31+
return $a;
32+
}
33+
34+
static fn (int $a): ?mixed => $a;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace InvalidUnionWithNever;
4+
5+
class Foo
6+
{
7+
public function bar(string $a): ?never
8+
{
9+
if ($a === null) {
10+
return null;
11+
}
12+
13+
throw new \RuntimeException($a);
14+
}
15+
16+
public function baz(string $b): never|string
17+
{
18+
if ($b === '') {
19+
throw new \RuntimeException('Error.');
20+
}
21+
22+
return $b;
23+
}
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace InvalidUnionWithVoid;
4+
5+
class Foo
6+
{
7+
8+
public function prepare(
9+
string $query,
10+
string ...$args
11+
): string|void
12+
{
13+
}
14+
15+
public function execute(string $query): ?void
16+
{
17+
}
18+
19+
}

0 commit comments

Comments
 (0)