Skip to content

Commit ee88af9

Browse files
committed
Add MissingOptionalArgumentSniff
1 parent 3282acf commit ee88af9

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Functions;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use SlevomatCodingStandard\Helpers\TokenHelper;
8+
9+
/** Inspired by {@see \SlevomatCodingStandard\Sniffs\Functions\StrictCallSniff}. */
10+
final class MissingOptionalArgumentSniff implements Sniff
11+
{
12+
public const CODE_MISSING_OPTIONAL_ARGUMENT = 'MissingOptionalArgument';
13+
14+
/** @var array<string, int> */
15+
public array $functions = [];
16+
17+
/** @return array<int, (int|string)> */
18+
public function register(): array
19+
{
20+
return TokenHelper::getOnlyNameTokenCodes();
21+
}
22+
23+
/** @inheritDoc */
24+
public function process(File $phpcsFile, $stringPointer): void
25+
{
26+
$tokens = $phpcsFile->getTokens();
27+
28+
$parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1);
29+
if (! is_int($parenthesisOpenerPointer) || $tokens[$parenthesisOpenerPointer]['code'] !== \T_OPEN_PARENTHESIS) {
30+
return;
31+
}
32+
33+
$parenthesisCloserPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_closer'];
34+
assert(is_int($parenthesisCloserPointer));
35+
36+
$functionName = strtolower(ltrim($tokens[$stringPointer]['content'], '\\'));
37+
38+
if (! array_key_exists($functionName, $this->functions)) {
39+
return;
40+
}
41+
42+
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1);
43+
if (in_array($tokens[$previousPointer]['code'], [\T_OBJECT_OPERATOR, \T_DOUBLE_COLON, \T_FUNCTION], true)) {
44+
return;
45+
}
46+
47+
$actualArgumentsNumber = $this->countArguments($phpcsFile, ['opener' => $parenthesisOpenerPointer, 'closer' => $parenthesisCloserPointer]);
48+
$expectedArgumentsNumber = $this->functions[$functionName];
49+
50+
if ($actualArgumentsNumber < $expectedArgumentsNumber) {
51+
$phpcsFile->addError(
52+
sprintf('Missing argument in %s() call: %d arguments used, at least %d expected.', $functionName, $actualArgumentsNumber, $expectedArgumentsNumber),
53+
$stringPointer,
54+
self::CODE_MISSING_OPTIONAL_ARGUMENT
55+
);
56+
}
57+
}
58+
59+
/** @param array{opener: int, closer: int} $parenthesisPointers */
60+
private function countArguments(File $phpcsFile, array $parenthesisPointers): int // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
61+
{
62+
$tokens = $phpcsFile->getTokens();
63+
64+
$commaPointers = [];
65+
for ($i = $parenthesisPointers['opener'] + 1; $i < $parenthesisPointers['closer']; $i++) {
66+
if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS) {
67+
$i = $tokens[$i]['parenthesis_closer'];
68+
continue;
69+
}
70+
71+
if ($tokens[$i]['code'] === \T_OPEN_SHORT_ARRAY) {
72+
$i = $tokens[$i]['bracket_closer'];
73+
continue;
74+
}
75+
76+
if ($tokens[$i]['code'] === \T_COMMA) {
77+
$commaPointers[] = $i;
78+
}
79+
}
80+
81+
$commaPointersCount = count($commaPointers);
82+
83+
$actualArgumentsNumber = $commaPointersCount + 1;
84+
$lastCommaPointer = $commaPointersCount > 0 ? $commaPointers[$commaPointersCount - 1] : null;
85+
86+
if (
87+
$lastCommaPointer !== null
88+
&& TokenHelper::findNextEffective($phpcsFile, $lastCommaPointer + 1, $parenthesisPointers['closer']) === null
89+
) {
90+
$actualArgumentsNumber--;
91+
}
92+
93+
return $actualArgumentsNumber;
94+
}
95+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Functions;
4+
5+
use IxDFCodingStandard\TestCase;
6+
7+
/** @covers \IxDFCodingStandard\Sniffs\Functions\MissingOptionalArgumentSniff */
8+
final class MissingOptionalArgumentSniffTest extends TestCase
9+
{
10+
/** @test */
11+
public function it_does_not_report_when_all_arguments_passed(): void
12+
{
13+
$report = self::checkFile(__DIR__.'/data/missingOptionalArgumentNoErrors.php', [
14+
'functions' => [
15+
'route' => 3,
16+
],
17+
]);
18+
19+
self::assertNoSniffErrorInFile($report);
20+
}
21+
22+
/** @test */
23+
public function it_reports_about_missing_argument(): void
24+
{
25+
$report = self::checkFile(__DIR__.'/data/missingOptionalArgumentErrors.php', [
26+
'functions' => [
27+
'route' => 3,
28+
],
29+
]);
30+
31+
self::assertSame(2, $report->getErrorCount());
32+
self::assertSniffError($report, 3, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
33+
self::assertSniffError($report, 4, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
34+
}
35+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php declare(strict_types=1);
2+
3+
route('name');
4+
route('name', []);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php declare(strict_types=1);
2+
3+
route('name', [], true);
4+
route('name', [], false);

0 commit comments

Comments
 (0)