Skip to content

Commit 6a24aca

Browse files
committed
Allow skipping no return type warning via doc bloc
1 parent ec4d588 commit 6a24aca

File tree

3 files changed

+242
-30
lines changed

3 files changed

+242
-30
lines changed

Inpsyde/Helpers.php

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
use PHP_CodeSniffer\Exceptions\RuntimeException as CodeSnifferRuntimeException;
1919
use PHP_CodeSniffer\Files\File;
20+
use PHP_CodeSniffer\Util\Tokens;
2021

2122
/**
2223
* @package php-coding-standards
@@ -437,7 +438,7 @@ public static function countReturns(File $file, int $functionPosition): array
437438
return [0, 0];
438439
}
439440

440-
$nonVoidReturnCount = $voidReturnCount = 0;
441+
$nonVoidReturnCount = $voidReturnCount = $nullReturnCount = 0;
441442
$scopeClosers = new \SplStack();
442443
$tokens = $file->getTokens();
443444
for ($i = $functionStart + 1; $i < $functionEnd; $i++) {
@@ -452,10 +453,11 @@ public static function countReturns(File $file, int $functionPosition): array
452453

453454
if (!$scopeClosers->count() && $tokens[$i]['code'] === T_RETURN) {
454455
Helpers::isVoidReturn($file, $i) ? $voidReturnCount++ : $nonVoidReturnCount++;
456+
Helpers::isNullReturn($file, $i) and $nullReturnCount++;
455457
}
456458
}
457459

458-
return [$nonVoidReturnCount, $voidReturnCount];
460+
return [$nonVoidReturnCount, $voidReturnCount, $nullReturnCount];
459461
}
460462

461463
/**
@@ -473,7 +475,88 @@ public static function isVoidReturn(File $file, int $returnPosition): bool
473475

474476
$returnPosition++;
475477
$nextToReturn = $file->findNext([T_WHITESPACE], $returnPosition, null, true, null, true);
478+
$nextToReturnType = $tokens[$nextToReturn]['code'] ?? '';
476479

477-
return $nextToReturn && ($tokens[$nextToReturn]['type'] ?? '') === 'T_SEMICOLON';
480+
return in_array($nextToReturnType, [T_SEMICOLON, T_NULL], true);
481+
}
482+
483+
/**
484+
* @param File $file
485+
* @param int $returnPosition
486+
* @return bool
487+
*/
488+
public static function isNullReturn(File $file, int $returnPosition): bool
489+
{
490+
$tokens = $file->getTokens();
491+
492+
if (($tokens[$returnPosition]['code'] ?? '') !== T_RETURN) {
493+
return false;
494+
}
495+
496+
$returnPosition++;
497+
$nextToReturn = $file->findNext([T_WHITESPACE], $returnPosition, null, true, null, true);
498+
$nextToReturnType = $tokens[$nextToReturn]['code'] ?? '';
499+
500+
return $nextToReturnType === T_NULL;
501+
}
502+
503+
/**
504+
* @param string $tag
505+
* @param File $file
506+
* @param int $functionPosition
507+
* @return string[]
508+
*/
509+
public static function functionDocBlockTag(
510+
string $tag,
511+
File $file,
512+
int $functionPosition
513+
): array {
514+
515+
$tokens = $file->getTokens();
516+
if (!array_key_exists($functionPosition, $tokens)
517+
|| !in_array($tokens[$functionPosition]['code'], [T_FUNCTION, T_CLOSURE], true)
518+
) {
519+
return [];
520+
}
521+
522+
$exclude = array_values(Tokens::$methodPrefixes);
523+
$exclude[] = T_WHITESPACE;
524+
525+
$lastBeforeFunc = $file->findPrevious($exclude, $functionPosition - 1, null, true);
526+
527+
if (!$lastBeforeFunc
528+
|| !array_key_exists($lastBeforeFunc, $tokens)
529+
|| $tokens[$lastBeforeFunc]['code'] !== T_DOC_COMMENT_CLOSE_TAG
530+
|| empty($tokens[$lastBeforeFunc]['comment_opener'])
531+
|| $tokens[$lastBeforeFunc]['comment_opener'] >= $lastBeforeFunc
532+
) {
533+
return [];
534+
}
535+
536+
$tags = [];
537+
$inTag = false;
538+
$start = $tokens[$lastBeforeFunc]['comment_opener'] + 1;
539+
$end = $lastBeforeFunc - 1;
540+
541+
for ($i = $start; $i < $end; $i++) {
542+
543+
if ($inTag && $tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
544+
$tags[] .= $tokens[$i]['content'];
545+
continue;
546+
}
547+
548+
if ($inTag && $tokens[$i]['code'] !== T_DOC_COMMENT_WHITESPACE) {
549+
$inTag = false;
550+
continue;
551+
}
552+
553+
if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG
554+
&& (ltrim($tokens[$i]['content'], '@') === ltrim($tag, '@'))
555+
) {
556+
$inTag = true;
557+
}
558+
}
559+
560+
return $tags;
478561
}
479562
}

Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Inpsyde\InpsydeCodingStandard\Helpers;
1919
use PHP_CodeSniffer\Sniffs\Sniff;
2020
use PHP_CodeSniffer\Files\File;
21+
use PHPCompatibility;
2122

2223
class ReturnTypeDeclarationSniff implements Sniff
2324
{
@@ -55,14 +56,18 @@ public function process(File $file, $position)
5556
$position
5657
);
5758

58-
list($nonVoidReturnCount, $voidReturnCount) = Helpers::countReturns($file, $position);
59+
list($nonVoidReturnCount, $voidReturnCount, $nullReturnCount) = Helpers::countReturns(
60+
$file,
61+
$position
62+
);
5963

6064
$this->maybeErrors(
6165
$hasNonVoidReturnType,
6266
$hasVoidReturnType,
6367
$hasNoReturnType,
6468
$nonVoidReturnCount,
6569
$voidReturnCount,
70+
$nullReturnCount,
6671
$file,
6772
$position
6873
);
@@ -74,6 +79,7 @@ public function process(File $file, $position)
7479
* @param bool $hasNoReturnType
7580
* @param int $nonVoidReturnCount
7681
* @param int $voidReturnCount
82+
* @param int $nullReturnCount
7783
* @param File $file
7884
* @param int $position
7985
*/
@@ -83,6 +89,7 @@ private function maybeErrors(
8389
bool $hasNoReturnType,
8490
int $nonVoidReturnCount,
8591
int $voidReturnCount,
92+
int $nullReturnCount,
8693
File $file,
8794
int $position
8895
) {
@@ -111,6 +118,15 @@ private function maybeErrors(
111118
return;
112119
}
113120

121+
if ($nullReturnCount
122+
&& $nonVoidReturnCount
123+
&& ($nullReturnCount === $voidReturnCount)
124+
&& !$this->areNullableReturnTypesSupported()
125+
&& $this->hasReturnNullDocBloc($file, $position)
126+
) {
127+
return;
128+
}
129+
114130
if ($hasNoReturnType) {
115131
$file->addWarning('Return type is missing', $position, 'NoReturnType');
116132
}
@@ -143,4 +159,45 @@ private function returnTypeInfo(File $file, int $functionPosition): array
143159

144160
return [$hasNonVoidReturnType, $hasVoidReturnType, $hasNoReturnType];
145161
}
162+
163+
/**
164+
* @param File $file
165+
* @param int $functionPosition
166+
* @return bool
167+
*/
168+
private function hasReturnNullDocBloc(File $file, int $functionPosition): bool
169+
{
170+
$return = Helpers::functionDocBlockTag('@return', $file, $functionPosition);
171+
if (!$return) {
172+
return false;
173+
}
174+
175+
$returnContentParts = preg_split('~\s+~', reset($return));
176+
$returnTypes = $returnContentParts ? explode('|', reset($returnContentParts)) : [];
177+
$returnTypes and $returnTypes = array_map('strtolower', $returnTypes);
178+
179+
return
180+
$returnTypes
181+
&& count($returnTypes) < 3
182+
&& !in_array('mixed', $returnTypes, true)
183+
&& in_array('null', $returnTypes, true);
184+
}
185+
186+
/**
187+
* Return true if _min_ supported version is PHP 7.1.
188+
*
189+
* @return bool
190+
*/
191+
private function areNullableReturnTypesSupported(): bool
192+
{
193+
$testVersion = trim(PHPCompatibility\PHPCSHelper::getConfigData('testVersion') ?: '');
194+
if (!$testVersion) {
195+
return false;
196+
}
197+
198+
preg_match('`^(\d+\.\d+)(?:\s*-\s*(?:\d+\.\d+)?)?$`', $testVersion, $matches);
199+
$min = $matches[1] ?? null;
200+
201+
return $min && version_compare($min, '7.1', '>=');
202+
}
146203
}

0 commit comments

Comments
 (0)