Skip to content

Commit

Permalink
Add MissingOptionalArgumentSniff
Browse files Browse the repository at this point in the history
  • Loading branch information
alies-dev committed Aug 9, 2023
1 parent 3282acf commit ee88af9
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php declare(strict_types=1);

namespace IxDFCodingStandard\Sniffs\Functions;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\TokenHelper;

/** Inspired by {@see \SlevomatCodingStandard\Sniffs\Functions\StrictCallSniff}. */
final class MissingOptionalArgumentSniff implements Sniff
{
public const CODE_MISSING_OPTIONAL_ARGUMENT = 'MissingOptionalArgument';

/** @var array<string, int> */
public array $functions = [];

/** @return array<int, (int|string)> */
public function register(): array
{
return TokenHelper::getOnlyNameTokenCodes();
}

/** @inheritDoc */
public function process(File $phpcsFile, $stringPointer): void
{
$tokens = $phpcsFile->getTokens();

$parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1);
if (! is_int($parenthesisOpenerPointer) || $tokens[$parenthesisOpenerPointer]['code'] !== \T_OPEN_PARENTHESIS) {
return;
}

$parenthesisCloserPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_closer'];
assert(is_int($parenthesisCloserPointer));

$functionName = strtolower(ltrim($tokens[$stringPointer]['content'], '\\'));

if (! array_key_exists($functionName, $this->functions)) {
return;
}

$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1);
if (in_array($tokens[$previousPointer]['code'], [\T_OBJECT_OPERATOR, \T_DOUBLE_COLON, \T_FUNCTION], true)) {
return;
}

$actualArgumentsNumber = $this->countArguments($phpcsFile, ['opener' => $parenthesisOpenerPointer, 'closer' => $parenthesisCloserPointer]);
$expectedArgumentsNumber = $this->functions[$functionName];

if ($actualArgumentsNumber < $expectedArgumentsNumber) {
$phpcsFile->addError(
sprintf('Missing argument in %s() call: %d arguments used, at least %d expected.', $functionName, $actualArgumentsNumber, $expectedArgumentsNumber),
$stringPointer,
self::CODE_MISSING_OPTIONAL_ARGUMENT
);
}
}

/** @param array{opener: int, closer: int} $parenthesisPointers */
private function countArguments(File $phpcsFile, array $parenthesisPointers): int // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
{
$tokens = $phpcsFile->getTokens();

$commaPointers = [];
for ($i = $parenthesisPointers['opener'] + 1; $i < $parenthesisPointers['closer']; $i++) {
if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS) {
$i = $tokens[$i]['parenthesis_closer'];
continue;
}

if ($tokens[$i]['code'] === \T_OPEN_SHORT_ARRAY) {
$i = $tokens[$i]['bracket_closer'];
continue;
}

if ($tokens[$i]['code'] === \T_COMMA) {
$commaPointers[] = $i;
}
}

$commaPointersCount = count($commaPointers);

$actualArgumentsNumber = $commaPointersCount + 1;
$lastCommaPointer = $commaPointersCount > 0 ? $commaPointers[$commaPointersCount - 1] : null;

if (
$lastCommaPointer !== null
&& TokenHelper::findNextEffective($phpcsFile, $lastCommaPointer + 1, $parenthesisPointers['closer']) === null
) {
$actualArgumentsNumber--;
}

return $actualArgumentsNumber;
}
}
35 changes: 35 additions & 0 deletions tests/Sniffs/Functions/MissingOptionalArgumentSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types=1);

namespace IxDFCodingStandard\Sniffs\Functions;

use IxDFCodingStandard\TestCase;

/** @covers \IxDFCodingStandard\Sniffs\Functions\MissingOptionalArgumentSniff */
final class MissingOptionalArgumentSniffTest extends TestCase
{
/** @test */
public function it_does_not_report_when_all_arguments_passed(): void
{
$report = self::checkFile(__DIR__.'/data/missingOptionalArgumentNoErrors.php', [
'functions' => [
'route' => 3,
],
]);

self::assertNoSniffErrorInFile($report);
}

/** @test */
public function it_reports_about_missing_argument(): void
{
$report = self::checkFile(__DIR__.'/data/missingOptionalArgumentErrors.php', [
'functions' => [
'route' => 3,
],
]);

self::assertSame(2, $report->getErrorCount());
self::assertSniffError($report, 3, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
self::assertSniffError($report, 4, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
}
}
4 changes: 4 additions & 0 deletions tests/Sniffs/Functions/data/missingOptionalArgumentErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php declare(strict_types=1);

route('name');
route('name', []);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php declare(strict_types=1);

route('name', [], true);
route('name', [], false);

0 comments on commit ee88af9

Please sign in to comment.