Skip to content

Commit

Permalink
MissingOptionalArgumentSniff: support static method calls
Browse files Browse the repository at this point in the history
native implementation

getClassNameOfMethodCall can be simplfied
  • Loading branch information
alies-dev committed Aug 9, 2023
1 parent cf65208 commit ef3453a
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

/** Inspired by {@see \SlevomatCodingStandard\Sniffs\Functions\StrictCallSniff}. */
Expand All @@ -14,14 +15,17 @@ final class MissingOptionalArgumentSniff implements Sniff
/** @var array<string, int> */
public array $functions = [];

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

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

/** @inheritDoc */
public function process(File $phpcsFile, $stringPointer): void
public function process(File $phpcsFile, $stringPointer): void // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
{
$tokens = $phpcsFile->getTokens();

Expand All @@ -35,21 +39,35 @@ public function process(File $phpcsFile, $stringPointer): void

$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'], [...Tokens::$methodPrefixes, \T_FUNCTION], true)) {
return; // skip function/methods declarations
}

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

if ($isMethodCall) {
$fqcn = $this->getClassNameOfMethodCall($phpcsFile, $stringPointer);
$fullyQualifiedFunctionName = "$fqcn::$functionName";

if (! array_key_exists($fullyQualifiedFunctionName, $this->staticMethods)) {
return;
}

$expectedArgumentsNumber = $this->staticMethods[$fullyQualifiedFunctionName];
} elseif (array_key_exists($functionName, $this->functions)) {
$expectedArgumentsNumber = $this->functions[$functionName];
} else {
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),
sprintf('Missing argument in %s() call: %d arguments used, at least %d expected.', $fullyQualifiedFunctionName, $actualArgumentsNumber, $expectedArgumentsNumber),
$stringPointer,
self::CODE_MISSING_OPTIONAL_ARGUMENT
);
Expand Down Expand Up @@ -92,4 +110,51 @@ private function countArguments(File $phpcsFile, array $parenthesisPointers): in

return $actualArgumentsNumber;
}

/**
* Given a position of a method call token, find the class name it belongs to.
* @param int $stackPointer The position of the token in the stack passed in $tokens.
* @return class-string|null Returns class name if found, null otherwise.
*/
private function getClassNameOfMethodCall(File $phpcsFile, int $stackPointer): ?string // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
{
$tokens = $phpcsFile->getTokens();

// Go back and find the object operator or double colon
$operator = $phpcsFile->findPrevious(
[\T_OBJECT_OPERATOR, \T_DOUBLE_COLON],
$stackPointer - 1
);

if ($operator === false) {
return null; // It's not a method call on an object or static class method call
}

// For static calls using ::
if ($tokens[$operator]['code'] === \T_DOUBLE_COLON) {
// Get the string before the double colon, which should be the class name or self, parent, etc.
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $operator - 1, null, true);
if (
$tokens[$prev]['code'] === \T_STRING
|| $tokens[$prev]['code'] === \T_SELF
|| $tokens[$prev]['code'] === \T_PARENT
|| $tokens[$prev]['code'] === \T_STATIC
) {
return $tokens[$prev]['content'];
}
}

// For object instance calls using ->
if ($tokens[$operator]['code'] === \T_OBJECT_OPERATOR) {
// Finding the variable or the string before -> which could be the object instance
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $operator - 1, null, true);
if ($tokens[$prev]['code'] === \T_VARIABLE) {
// Classname presented as a variable, getting actual class name for an instance variable
// is complex and may require more in-depth analysis or static code analysis tools.
return null;
}
}

return null;
}
}
15 changes: 14 additions & 1 deletion tests/Sniffs/Functions/MissingOptionalArgumentSniffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function it_does_not_report_when_all_arguments_passed(): void
}

/** @test */
public function it_reports_about_missing_argument(): void
public function it_reports_about_missing_function_argument(): void
{
$report = self::checkFile(__DIR__.'/data/missingOptionalArgumentErrors.php', [
'functions' => [
Expand All @@ -32,4 +32,17 @@ public function it_reports_about_missing_argument(): void
self::assertSniffError($report, 3, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
self::assertSniffError($report, 4, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
}

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

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

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

Some::route('name', []);

0 comments on commit ef3453a

Please sign in to comment.