Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 75 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
ForbiddenFunctionsSniff | |
0.00% |
0 / 75 |
|
0.00% |
0 / 3 |
600 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
182 | |||
argCount | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
110 |
1 | <?php |
2 | /** |
3 | * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org> |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
18 | */ |
19 | |
20 | namespace MediaWiki\Sniffs\Usage; |
21 | |
22 | use PHP_CodeSniffer\Files\File; |
23 | use PHP_CodeSniffer\Sniffs\Sniff; |
24 | use PHP_CodeSniffer\Util\Tokens; |
25 | |
26 | /** |
27 | * Use e.g. <exclude name="MediaWiki.Usage.ForbiddenFunctions.eval" /> in your .phpcs.xml to remove |
28 | * a function from the predefined list of forbidden functions. |
29 | * |
30 | * You can also add entries or modify existing ones. Note that an empty `value=""` won't work. Use |
31 | * "null" for forbidden functions and any other non-empty value for replacements. |
32 | * |
33 | * <rule ref="MediaWiki.Usage.ForbiddenFunctions"> |
34 | * <properties> |
35 | * <property name="forbiddenFunctions" type="array"> |
36 | * <element key="eval" value="null" /> |
37 | * <element key="sizeof" value="count" /> |
38 | * </property> |
39 | * </properties> |
40 | * </rule> |
41 | */ |
42 | class ForbiddenFunctionsSniff implements Sniff { |
43 | |
44 | /** |
45 | * Predefined list of deprecated functions and their replacements, or any empty value for |
46 | * forbidden functions. |
47 | */ |
48 | private const FORBIDDEN_FUNCTIONS = [ |
49 | 'chop' => 'rtrim', |
50 | 'diskfreespace' => 'disk_free_space', |
51 | 'doubleval' => 'floatval', |
52 | 'ini_alter' => 'ini_set', |
53 | 'is_integer' => 'is_int', |
54 | 'is_long' => 'is_int', |
55 | 'is_double' => 'is_float', |
56 | 'is_real' => 'is_float', |
57 | 'is_writeable' => 'is_writable', |
58 | 'join' => 'implode', |
59 | 'key_exists' => 'array_key_exists', |
60 | 'pos' => 'current', |
61 | 'sizeof' => 'count', |
62 | 'strchr' => 'strstr', |
63 | 'assert' => false, |
64 | 'eval' => false, |
65 | 'extract' => false, |
66 | 'compact' => false, |
67 | // Deprecated in PHP 7.2 |
68 | 'create_function' => false, |
69 | 'each' => false, |
70 | 'parse_str' => false, |
71 | 'mb_parse_str' => false, |
72 | // MediaWiki wrappers for external program execution should be used, |
73 | // forbid PHP's (https://secure.php.net/manual/en/ref.exec.php) |
74 | 'escapeshellarg' => false, |
75 | 'escapeshellcmd' => false, |
76 | 'exec' => false, |
77 | 'passthru' => false, |
78 | 'popen' => false, |
79 | 'proc_open' => false, |
80 | 'shell_exec' => false, |
81 | 'system' => false, |
82 | 'isset' => false, |
83 | // resource type is going away in PHP 8.0+ (T260735) |
84 | 'is_resource' => false, |
85 | // define third parameter is deprecated in 7.3 |
86 | 'define' => false, |
87 | ]; |
88 | |
89 | /** |
90 | * Functions that are forbidden (per above) but allowed with a specific number of arguments |
91 | */ |
92 | private const ALLOWED_ARG_COUNT = [ |
93 | 'parse_str' => 2, |
94 | 'mb_parse_str' => 2, |
95 | 'isset' => 1, |
96 | 'define' => 2, |
97 | ]; |
98 | |
99 | /** |
100 | * @var string[] Key-value pairs as provided via .phpcs.xml. Maps deprecated function names to |
101 | * their replacement, or the literal string "null" for forbidden functions. |
102 | */ |
103 | public $forbiddenFunctions = []; |
104 | |
105 | /** |
106 | * @inheritDoc |
107 | */ |
108 | public function register(): array { |
109 | return [ T_STRING, T_EVAL, T_ISSET ]; |
110 | } |
111 | |
112 | /** |
113 | * @param File $phpcsFile |
114 | * @param int $stackPtr The current token index. |
115 | * @return void |
116 | */ |
117 | public function process( File $phpcsFile, $stackPtr ) { |
118 | $tokens = $phpcsFile->getTokens(); |
119 | |
120 | $nextToken = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); |
121 | if ( $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS || |
122 | !isset( $tokens[$nextToken]['parenthesis_closer'] ) |
123 | ) { |
124 | return; |
125 | } |
126 | |
127 | // Check if the function is one of the bad ones |
128 | $funcName = $tokens[$stackPtr]['content']; |
129 | if ( array_key_exists( $funcName, $this->forbiddenFunctions ) ) { |
130 | $replacement = $this->forbiddenFunctions[$funcName]; |
131 | if ( $replacement === $funcName ) { |
132 | return; |
133 | } |
134 | } elseif ( array_key_exists( $funcName, self::FORBIDDEN_FUNCTIONS ) ) { |
135 | $replacement = self::FORBIDDEN_FUNCTIONS[$funcName]; |
136 | } else { |
137 | return; |
138 | } |
139 | |
140 | $ignore = [ |
141 | T_DOUBLE_COLON => true, |
142 | T_OBJECT_OPERATOR => true, |
143 | T_NULLSAFE_OBJECT_OPERATOR => true, |
144 | T_FUNCTION => true, |
145 | T_CONST => true, |
146 | ]; |
147 | |
148 | // Check to make sure it's a PHP function (not $this->, etc.) |
149 | $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); |
150 | if ( isset( $ignore[$tokens[$prevToken]['code']] ) ) { |
151 | return; |
152 | } |
153 | |
154 | // Check argument count |
155 | $allowedArgCount = self::ALLOWED_ARG_COUNT[$funcName] ?? null; |
156 | if ( $allowedArgCount !== null && |
157 | $this->argCount( $phpcsFile, $nextToken ) == $allowedArgCount |
158 | ) { |
159 | // Nothing to replace |
160 | return; |
161 | } |
162 | |
163 | // The hard-coded FORBIDDEN_FUNCTIONS can use false, but values from .phpcs.xml are always |
164 | // strings. We use the same special string "null" as in the Generic.PHP.ForbiddenFunctions |
165 | // sniff. |
166 | if ( $replacement && $replacement !== 'null' ) { |
167 | $fix = $phpcsFile->addFixableWarning( |
168 | 'Use %s() instead of %s', |
169 | $stackPtr, |
170 | $funcName, |
171 | [ $replacement, $funcName ] |
172 | ); |
173 | if ( $fix ) { |
174 | $phpcsFile->fixer->replaceToken( $stackPtr, $replacement ); |
175 | } |
176 | } else { |
177 | $phpcsFile->addWarning( |
178 | $allowedArgCount !== null |
179 | ? '%s should be used with %s argument(s)' |
180 | : '%s should not be used', |
181 | $stackPtr, |
182 | $funcName, |
183 | [ $funcName, $allowedArgCount ] |
184 | ); |
185 | } |
186 | } |
187 | |
188 | /** |
189 | * Return the number of arguments between the $parenthesis as opener and its closer |
190 | * Ignoring commas between brackets to support nested argument lists |
191 | * |
192 | * @param File $phpcsFile |
193 | * @param int $parenthesis The parenthesis token index. |
194 | * @return int |
195 | */ |
196 | private function argCount( File $phpcsFile, int $parenthesis ): int { |
197 | $tokens = $phpcsFile->getTokens(); |
198 | $end = $tokens[$parenthesis]['parenthesis_closer']; |
199 | $next = $phpcsFile->findNext( Tokens::$emptyTokens, $parenthesis + 1, $end, true ); |
200 | $argCount = 0; |
201 | |
202 | if ( $next !== false ) { |
203 | // Something found, there is at least one argument |
204 | $argCount++; |
205 | |
206 | $searchTokens = [ |
207 | T_OPEN_CURLY_BRACKET, |
208 | T_OPEN_SQUARE_BRACKET, |
209 | T_OPEN_SHORT_ARRAY, |
210 | T_OPEN_PARENTHESIS, |
211 | T_COMMA |
212 | ]; |
213 | while ( $next !== false ) { |
214 | switch ( $tokens[$next]['code'] ) { |
215 | case T_OPEN_CURLY_BRACKET: |
216 | case T_OPEN_SQUARE_BRACKET: |
217 | case T_OPEN_PARENTHESIS: |
218 | if ( isset( $tokens[$next]['parenthesis_closer'] ) ) { |
219 | // jump to closing parenthesis to ignore commas between opener and closer |
220 | $next = $tokens[$next]['parenthesis_closer']; |
221 | } |
222 | break; |
223 | case T_OPEN_SHORT_ARRAY: |
224 | if ( isset( $tokens[$next]['bracket_closer'] ) ) { |
225 | // jump to closing bracket to ignore commas between opener and closer |
226 | $next = $tokens[$next]['bracket_closer']; |
227 | } |
228 | break; |
229 | case T_COMMA: |
230 | $argCount++; |
231 | break; |
232 | } |
233 | |
234 | $next = $phpcsFile->findNext( $searchTokens, $next + 1, $end ); |
235 | } |
236 | } |
237 | |
238 | return $argCount; |
239 | } |
240 | |
241 | } |