44
55use PHP_CodeSniffer \Files \File ;
66use PHP_CodeSniffer \Sniffs \Sniff ;
7+ use PHP_CodeSniffer \Util \Tokens ;
78use 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}
0 commit comments