4
4
5
5
use PHP_CodeSniffer \Files \File ;
6
6
use PHP_CodeSniffer \Sniffs \Sniff ;
7
+ use PHP_CodeSniffer \Util \Tokens ;
7
8
use SlevomatCodingStandard \Helpers \TokenHelper ;
8
9
9
10
/** Inspired by {@see \SlevomatCodingStandard\Sniffs\Functions\StrictCallSniff}. */
@@ -14,14 +15,17 @@ final class MissingOptionalArgumentSniff implements Sniff
14
15
/** @var array<string, int> */
15
16
public array $ functions = [];
16
17
18
+ /** @var array<string, int> */
19
+ public array $ staticMethods = [];
20
+
17
21
/** @return array<int, (int|string)> */
18
22
public function register (): array
19
23
{
20
24
return TokenHelper::getOnlyNameTokenCodes ();
21
25
}
22
26
23
27
/** @inheritDoc */
24
- public function process (File $ phpcsFile , $ stringPointer ): void
28
+ public function process (File $ phpcsFile , $ stringPointer ): void // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
25
29
{
26
30
$ tokens = $ phpcsFile ->getTokens ();
27
31
@@ -35,21 +39,35 @@ public function process(File $phpcsFile, $stringPointer): void
35
39
36
40
$ functionName = strtolower (ltrim ($ tokens [$ stringPointer ]['content ' ], '\\' ));
37
41
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
40
46
}
41
47
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 {
44
63
return ;
45
64
}
46
65
47
66
$ actualArgumentsNumber = $ this ->countArguments ($ phpcsFile , ['opener ' => $ parenthesisOpenerPointer , 'closer ' => $ parenthesisCloserPointer ]);
48
- $ expectedArgumentsNumber = $ this ->functions [$ functionName ];
49
67
50
68
if ($ actualArgumentsNumber < $ expectedArgumentsNumber ) {
51
69
$ 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 ),
53
71
$ stringPointer ,
54
72
self ::CODE_MISSING_OPTIONAL_ARGUMENT
55
73
);
@@ -92,4 +110,51 @@ private function countArguments(File $phpcsFile, array $parenthesisPointers): in
92
110
93
111
return $ actualArgumentsNumber ;
94
112
}
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
+ }
95
160
}
0 commit comments