Skip to content

Commit 2f337b0

Browse files
pepakrizkukulich
authored andcommitted
Check void return types on closures
1 parent edfea92 commit 2f337b0

9 files changed

+177
-5
lines changed

SlevomatCodingStandard/Helpers/FunctionHelper.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,12 @@ public static function returnsValue(\PHP_CodeSniffer\Files\File $codeSnifferFile
127127

128128
$isInSameLevel = function (int $pointer) use ($functionPointer, $tokens): bool {
129129
foreach (array_reverse($tokens[$pointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) {
130+
if ($conditionPointer === $functionPointer) {
131+
break;
132+
}
133+
130134
if ($conditionTokenCode === T_CLOSURE || $conditionTokenCode === T_ANON_CLASS) {
131135
return false;
132-
} elseif ($conditionPointer === $functionPointer) {
133-
break;
134136
}
135137
}
136138
return true;

SlevomatCodingStandard/Sniffs/TypeHints/TypeHintDeclarationSniff.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class TypeHintDeclarationSniff implements \PHP_CodeSniffer\Sniffs\Sniff
3535

3636
const CODE_USELESS_RETURN_ANNOTATION = 'UselessReturnAnnotation';
3737

38+
const CODE_INCORRECT_RETURN_TYPE_HINT = 'IncorrectReturnTypeHint';
39+
3840
const CODE_USELESS_DOC_COMMENT = 'UselessDocComment';
3941

4042
/** @var bool */
@@ -71,6 +73,8 @@ public function process(\PHP_CodeSniffer\Files\File $phpcsFile, $pointer)
7173
$this->checkParametersTypeHints($phpcsFile, $pointer);
7274
$this->checkReturnTypeHints($phpcsFile, $pointer);
7375
$this->checkUselessDocComment($phpcsFile, $pointer);
76+
} elseif ($token['code'] === T_CLOSURE) {
77+
$this->checkClosure($phpcsFile, $pointer);
7478
} elseif ($token['code'] === T_VARIABLE && PropertyHelper::isProperty($phpcsFile, $pointer)) {
7579
$this->checkPropertyTypeHint($phpcsFile, $pointer);
7680
}
@@ -83,6 +87,7 @@ public function register(): array
8387
{
8488
return [
8589
T_FUNCTION,
90+
T_CLOSURE,
8691
T_VARIABLE,
8792
];
8893
}
@@ -473,6 +478,51 @@ private function checkReturnTypeHints(\PHP_CodeSniffer\Files\File $phpcsFile, in
473478
}
474479
}
475480

481+
private function checkClosure(\PHP_CodeSniffer\Files\File $phpcsFile, int $closurePointer)
482+
{
483+
$returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $closurePointer);
484+
$returnsValue = FunctionHelper::returnsValue($phpcsFile, $closurePointer);
485+
486+
if (!$returnsValue && $returnTypeHint !== null && $returnTypeHint->getTypeHint() !== 'void') {
487+
$fix = $phpcsFile->addFixableError(
488+
'Closure has incorrect return type hint.',
489+
$closurePointer,
490+
self::CODE_INCORRECT_RETURN_TYPE_HINT
491+
);
492+
493+
if ($fix) {
494+
$tokens = $phpcsFile->getTokens();
495+
$closeParenthesisPosition = TokenHelper::findPrevious($phpcsFile, [T_CLOSE_PARENTHESIS], $tokens[$closurePointer]['scope_opener'] - 1, $closurePointer);
496+
497+
$phpcsFile->fixer->beginChangeset();
498+
for ($i = $closeParenthesisPosition + 1; $i < $tokens[$closurePointer]['scope_opener']; $i++) {
499+
$phpcsFile->fixer->replaceToken($i, '');
500+
}
501+
$phpcsFile->fixer->replaceToken($closeParenthesisPosition, $this->enableVoidTypeHint ? '): void ' : ') ');
502+
$phpcsFile->fixer->endChangeset();
503+
}
504+
505+
return;
506+
}
507+
508+
if ($this->enableVoidTypeHint && !$returnsValue && $returnTypeHint === null) {
509+
$fix = $phpcsFile->addFixableError(
510+
'Closure does not have void return type hint.',
511+
$closurePointer,
512+
self::CODE_MISSING_RETURN_TYPE_HINT
513+
);
514+
515+
if ($fix) {
516+
$tokens = $phpcsFile->getTokens();
517+
$position = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$closurePointer]['scope_opener'] - 1, $closurePointer);
518+
519+
$phpcsFile->fixer->beginChangeset();
520+
$phpcsFile->fixer->addContent($position, ': void');
521+
$phpcsFile->fixer->endChangeset();
522+
}
523+
}
524+
}
525+
476526
private function checkUselessDocComment(\PHP_CodeSniffer\Files\File $phpcsFile, int $functionPointer)
477527
{
478528
$docCommentSniffSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $this->getSniffName(self::CODE_USELESS_DOC_COMMENT));

tests/Sniffs/TypeHints/TypeHintDeclarationSniffTest.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,16 @@ public function testEnabledVoidTypeHintErrors()
140140
'enableVoidTypeHint' => true,
141141
]);
142142

143-
$this->assertSame(4, $report->getErrorCount());
143+
$this->assertSame(8, $report->getErrorCount());
144144

145145
$this->assertSniffError($report, 3, TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT);
146146
$this->assertSniffError($report, 14, TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT);
147147
$this->assertSniffError($report, 16, TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT);
148148
$this->assertSniffError($report, 24, TypeHintDeclarationSniff::CODE_USELESS_DOC_COMMENT);
149+
$this->assertSniffError($report, 31, TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT);
150+
$this->assertSniffError($report, 35, TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT);
151+
$this->assertSniffError($report, 39, TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT);
152+
$this->assertSniffError($report, 51, TypeHintDeclarationSniff::CODE_INCORRECT_RETURN_TYPE_HINT);
149153
}
150154

151155
public function testEnabledNullableTypeHintsNoErrors()
@@ -259,7 +263,7 @@ public function testFixableReturnTypeHintsWithEnabledVoid()
259263
$report = $this->checkFile(__DIR__ . '/data/fixableReturnTypeHintsWithEnabledVoid.php', [
260264
'enableNullableTypeHints' => false,
261265
'enableVoidTypeHint' => true,
262-
], [TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT]);
266+
], [TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT, TypeHintDeclarationSniff::CODE_INCORRECT_RETURN_TYPE_HINT]);
263267

264268
$this->assertAllFixedInFile($report);
265269
}
@@ -269,7 +273,7 @@ public function testFixableReturnTypeHintsWithDisabledVoid()
269273
$report = $this->checkFile(__DIR__ . '/data/fixableReturnTypeHintsWithDisabledVoid.php', [
270274
'enableNullableTypeHints' => false,
271275
'enableVoidTypeHint' => false,
272-
], [TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT]);
276+
], [TypeHintDeclarationSniff::CODE_MISSING_RETURN_TYPE_HINT, TypeHintDeclarationSniff::CODE_INCORRECT_RETURN_TYPE_HINT]);
273277

274278
$this->assertAllFixedInFile($report);
275279
}

tests/Sniffs/TypeHints/data/enabledVoidTypeHintErrors.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,27 @@ public function withSuppress(): void
2727
}
2828

2929
}
30+
31+
function () {
32+
33+
};
34+
35+
function () {
36+
return;
37+
};
38+
39+
function () {
40+
function (): bool {
41+
return true;
42+
};
43+
new class {
44+
public function foo(): bool
45+
{
46+
return true;
47+
}
48+
};
49+
};
50+
51+
function (): bool {
52+
53+
};

tests/Sniffs/TypeHints/data/enabledVoidTypeHintNoErrors.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,31 @@ public function withSuppress()
4343
}
4444

4545
}
46+
47+
function (): void {
48+
49+
};
50+
51+
function (): void {
52+
return;
53+
};
54+
55+
function (): void {
56+
function (): bool {
57+
return true;
58+
};
59+
new class {
60+
public function foo(): bool
61+
{
62+
return true;
63+
}
64+
};
65+
};
66+
67+
function () {
68+
return true;
69+
};
70+
71+
function (): bool {
72+
return true;
73+
};

tests/Sniffs/TypeHints/data/fixableReturnTypeHintsWithDisabledVoid.fixed.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,19 @@ public function voidAnnotation()
6161
}
6262

6363
}
64+
65+
function () {
66+
67+
};
68+
69+
function () {
70+
return;
71+
};
72+
73+
function () {
74+
75+
};
76+
77+
function () use (& $foo) {
78+
79+
};

tests/Sniffs/TypeHints/data/fixableReturnTypeHintsWithDisabledVoid.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,19 @@ public function voidAnnotation()
6161
}
6262

6363
}
64+
65+
function () {
66+
67+
};
68+
69+
function () {
70+
return;
71+
};
72+
73+
function (): bool {
74+
75+
};
76+
77+
function () use (& $foo): \Foo\Bar {
78+
79+
};

tests/Sniffs/TypeHints/data/fixableReturnTypeHintsWithEnabledVoid.fixed.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,19 @@ protected function returnsNothing(): void
5353
public abstract function voidAnnotation(): void;
5454

5555
}
56+
57+
function (): void {
58+
59+
};
60+
61+
function (): void {
62+
return;
63+
};
64+
65+
function (): void {
66+
67+
};
68+
69+
function () use (& $foo): void {
70+
71+
};

tests/Sniffs/TypeHints/data/fixableReturnTypeHintsWithEnabledVoid.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,19 @@ protected function returnsNothing()
5353
public abstract function voidAnnotation();
5454

5555
}
56+
57+
function () {
58+
59+
};
60+
61+
function () {
62+
return;
63+
};
64+
65+
function (): bool {
66+
67+
};
68+
69+
function () use (& $foo): \Foo\Bar {
70+
71+
};

0 commit comments

Comments
 (0)