Skip to content

Commit ef3453a

Browse files
committed
MissingOptionalArgumentSniff: support static method calls
native implementation getClassNameOfMethodCall can be simplfied
1 parent cf65208 commit ef3453a

File tree

3 files changed

+88
-8
lines changed

3 files changed

+88
-8
lines changed

IxDFCodingStandard/Sniffs/Functions/MissingOptionalArgumentSniff.php

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHP_CodeSniffer\Files\File;
66
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use PHP_CodeSniffer\Util\Tokens;
78
use SlevomatCodingStandard\Helpers\TokenHelper;
89

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

18+
/** @var array<string, int> */
19+
public array $staticMethods = [];
20+
1721
/** @return array<int, (int|string)> */
1822
public function register(): array
1923
{
2024
return TokenHelper::getOnlyNameTokenCodes();
2125
}
2226

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

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

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

38-
if (! array_key_exists($functionName, $this->functions)) {
39-
return;
42+
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1);
43+
44+
if (in_array($tokens[$previousPointer]['code'], [...Tokens::$methodPrefixes, \T_FUNCTION], true)) {
45+
return; // skip function/methods declarations
4046
}
4147

42-
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1);
43-
if (in_array($tokens[$previousPointer]['code'], [\T_OBJECT_OPERATOR, \T_DOUBLE_COLON, \T_FUNCTION], true)) {
48+
$isMethodCall = in_array($tokens[$previousPointer]['code'], [\T_OBJECT_OPERATOR, \T_DOUBLE_COLON], true);
49+
$fullyQualifiedFunctionName = $functionName;
50+
51+
if ($isMethodCall) {
52+
$fqcn = $this->getClassNameOfMethodCall($phpcsFile, $stringPointer);
53+
$fullyQualifiedFunctionName = "$fqcn::$functionName";
54+
55+
if (! array_key_exists($fullyQualifiedFunctionName, $this->staticMethods)) {
56+
return;
57+
}
58+
59+
$expectedArgumentsNumber = $this->staticMethods[$fullyQualifiedFunctionName];
60+
} elseif (array_key_exists($functionName, $this->functions)) {
61+
$expectedArgumentsNumber = $this->functions[$functionName];
62+
} else {
4463
return;
4564
}
4665

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

5068
if ($actualArgumentsNumber < $expectedArgumentsNumber) {
5169
$phpcsFile->addError(
52-
sprintf('Missing argument in %s() call: %d arguments used, at least %d expected.', $functionName, $actualArgumentsNumber, $expectedArgumentsNumber),
70+
sprintf('Missing argument in %s() call: %d arguments used, at least %d expected.', $fullyQualifiedFunctionName, $actualArgumentsNumber, $expectedArgumentsNumber),
5371
$stringPointer,
5472
self::CODE_MISSING_OPTIONAL_ARGUMENT
5573
);
@@ -92,4 +110,51 @@ private function countArguments(File $phpcsFile, array $parenthesisPointers): in
92110

93111
return $actualArgumentsNumber;
94112
}
113+
114+
/**
115+
* Given a position of a method call token, find the class name it belongs to.
116+
* @param int $stackPointer The position of the token in the stack passed in $tokens.
117+
* @return class-string|null Returns class name if found, null otherwise.
118+
*/
119+
private function getClassNameOfMethodCall(File $phpcsFile, int $stackPointer): ?string // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
120+
{
121+
$tokens = $phpcsFile->getTokens();
122+
123+
// Go back and find the object operator or double colon
124+
$operator = $phpcsFile->findPrevious(
125+
[\T_OBJECT_OPERATOR, \T_DOUBLE_COLON],
126+
$stackPointer - 1
127+
);
128+
129+
if ($operator === false) {
130+
return null; // It's not a method call on an object or static class method call
131+
}
132+
133+
// For static calls using ::
134+
if ($tokens[$operator]['code'] === \T_DOUBLE_COLON) {
135+
// Get the string before the double colon, which should be the class name or self, parent, etc.
136+
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $operator - 1, null, true);
137+
if (
138+
$tokens[$prev]['code'] === \T_STRING
139+
|| $tokens[$prev]['code'] === \T_SELF
140+
|| $tokens[$prev]['code'] === \T_PARENT
141+
|| $tokens[$prev]['code'] === \T_STATIC
142+
) {
143+
return $tokens[$prev]['content'];
144+
}
145+
}
146+
147+
// For object instance calls using ->
148+
if ($tokens[$operator]['code'] === \T_OBJECT_OPERATOR) {
149+
// Finding the variable or the string before -> which could be the object instance
150+
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $operator - 1, null, true);
151+
if ($tokens[$prev]['code'] === \T_VARIABLE) {
152+
// Classname presented as a variable, getting actual class name for an instance variable
153+
// is complex and may require more in-depth analysis or static code analysis tools.
154+
return null;
155+
}
156+
}
157+
158+
return null;
159+
}
95160
}

tests/Sniffs/Functions/MissingOptionalArgumentSniffTest.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function it_does_not_report_when_all_arguments_passed(): void
2020
}
2121

2222
/** @test */
23-
public function it_reports_about_missing_argument(): void
23+
public function it_reports_about_missing_function_argument(): void
2424
{
2525
$report = self::checkFile(__DIR__.'/data/missingOptionalArgumentErrors.php', [
2626
'functions' => [
@@ -32,4 +32,17 @@ public function it_reports_about_missing_argument(): void
3232
self::assertSniffError($report, 3, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
3333
self::assertSniffError($report, 4, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
3434
}
35+
36+
/** @test */
37+
public function it_reports_about_missing_method_argument(): void
38+
{
39+
$report = self::checkFile(__DIR__.'/data/missingOptionalArgumentErrors.php', [
40+
'staticMethods' => [
41+
'Some::route' => 3,
42+
],
43+
]);
44+
45+
self::assertSame(1, $report->getErrorCount());
46+
self::assertSniffError($report, 6, MissingOptionalArgumentSniff::CODE_MISSING_OPTIONAL_ARGUMENT);
47+
}
3548
}

tests/Sniffs/Functions/data/missingOptionalArgumentErrors.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22

33
route('name');
44
route('name', []);
5+
6+
Some::route('name', []);

0 commit comments

Comments
 (0)