Skip to content

Commit 56cec45

Browse files
committed
Add support for array generics as required by PHPStan.
1 parent 5bac909 commit 56cec45

8 files changed

+185
-14
lines changed

custom-standards/Flyeralarm/Sniffs/Docblock/ReturnTypeSniff.php

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,13 @@ public function process(File $phpcsFile, $stackPtr)
5858
$tokens = $phpcsFile->getTokens();
5959
$returnTypePtr = $this->getDocReturnTypePtr($phpcsFile, $stackPtr);
6060
$returnTypeString = $tokens[$returnTypePtr]['content'];
61-
$returnTypes = explode('|', $returnTypeString);
62-
63-
foreach ($returnTypes as $returnType) {
64-
$returnType = trim($returnType);
65-
if (in_array($returnType, $this->returnTypeScalarWhitelist, true)) {
66-
continue;
67-
}
68-
if (in_array($returnType, $this->returnTypeClassWhitelist, true)) {
69-
continue;
70-
}
71-
if ($this->isStartingWithUppercaseLetter($returnType)) {
72-
continue;
73-
}
7461

62+
try {
63+
$this->checkReturnTypeShape($returnTypeString);
64+
}
65+
catch (\InvalidArgumentException $exception) {
7566
$phpcsFile->addError(
76-
sprintf('Return type "%s" is discouraged', $returnType),
67+
$exception->getMessage(),
7768
$returnTypePtr,
7869
'ProhibitedReturnType'
7970
);
@@ -145,4 +136,50 @@ private function isStartingWithUppercaseLetter($singleCharacter)
145136

146137
return false;
147138
}
139+
140+
/**
141+
* @param string $subject The return type string to check.
142+
* @return void
143+
*/
144+
private function checkReturnTypeShape(string $subject)
145+
{
146+
preg_match_all('#(?<separator>\s*\|\s*)?(?<atom>[^<>\|]+)(?<generic><(?<nested>.*)>)?#', $subject, $matches);
147+
148+
if (implode('', $matches[0]) !== $subject) {
149+
throw new \InvalidArgumentException('Invalid structure in return type "' . $subject . '"');
150+
}
151+
152+
if (strpos($matches['separator'][0], '|') !== false) {
153+
throw new \InvalidArgumentException('Missing return type in first alternative of type "' . $subject . '"');
154+
}
155+
156+
foreach ($matches['nested'] as $index => $match) {
157+
if (!empty($matches['generic'][$index])) {
158+
if (trim($matches['atom'][$index]) !== 'array') {
159+
throw new \InvalidArgumentException('Unexpected generic specification in type "' . $matches[0][$index] . '"');
160+
}
161+
162+
$match = trim($match);
163+
if ($match === '') {
164+
throw new \InvalidArgumentException('Generic specification may not be empty in type "' . $matches[0][$index] . '"');
165+
}
166+
167+
$this->checkReturnTypeShape($match);
168+
}
169+
170+
// Check if atom is in whitelist.
171+
$returnType = trim($matches['atom'][$index]);
172+
if (in_array($returnType, $this->returnTypeScalarWhitelist, true)) {
173+
continue;
174+
}
175+
if (in_array($returnType, $this->returnTypeClassWhitelist, true)) {
176+
continue;
177+
}
178+
if ($this->isStartingWithUppercaseLetter($returnType)) {
179+
continue;
180+
}
181+
182+
throw new \InvalidArgumentException('Return type "' . $returnType . '" is discouraged');
183+
}
184+
}
148185
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
// @expectedPass
4+
5+
namespace flyeralarm\Test;
6+
7+
class FooTest
8+
{
9+
/**
10+
* @return array
11+
*/
12+
public function testArray()
13+
{
14+
}
15+
16+
/**
17+
* @return array<string>
18+
*/
19+
public function testWithGeneric()
20+
{
21+
}
22+
23+
/**
24+
* @return array<string | int>
25+
*/
26+
public function testWithAlternativeInside()
27+
{
28+
}
29+
30+
/**
31+
* @return int | array<array<string>>
32+
*/
33+
public function testWithAlternativeOutside()
34+
{
35+
}
36+
37+
/**
38+
* @return array<array<string>> | null
39+
*/
40+
public function testWithAlternativeAtTheEnd()
41+
{
42+
}
43+
44+
/**
45+
* @return int | array<string | array<string>>
46+
*/
47+
public function testWithMultipleAlternatives()
48+
{
49+
}
50+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// @expectedError Missing return type in first alternative of type "| string"
4+
5+
class EmptyAlternativeInReturnTypeInDocComment
6+
{
7+
/**
8+
* @return | string
9+
*/
10+
public function foo()
11+
{
12+
return true;
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// @expectedError Generic specification may not be empty in type "array<>"
4+
5+
class EmptyGenericReturnTypeInDocComment
6+
{
7+
/**
8+
* @return array<>
9+
*/
10+
public function foo()
11+
{
12+
return true;
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// @expectedError Invalid structure in return type "array<int"
4+
5+
class InvalidStructureInReturnTypeInDocComment
6+
{
7+
/**
8+
* @return array<int
9+
*/
10+
public function foo()
11+
{
12+
return true;
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// @expectedError Return type "array int" is discouraged
4+
5+
class MissingAlternativeSeparatorInReturnTypeInDocComment
6+
{
7+
/**
8+
* @return array int
9+
*/
10+
public function foo()
11+
{
12+
return true;
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// @expectedError Unexpected generic specification in type "int<array>"
4+
5+
class UnknownGenericReturnTypeInDocComment
6+
{
7+
/**
8+
* @return int<array>
9+
*/
10+
public function foo()
11+
{
12+
return true;
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// @expectedError Return type "foo" is discouraged
4+
5+
class UnknownGenericSubReturnTypeInDocComment
6+
{
7+
/**
8+
* @return array<foo>
9+
*/
10+
public function foo()
11+
{
12+
return true;
13+
}
14+
}

0 commit comments

Comments
 (0)