Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 77 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
ExtendClassUsageSniff | |
0.00% |
0 / 77 |
|
0.00% |
0 / 2 |
870 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 74 |
|
0.00% |
0 / 1 |
812 |
1 | <?php |
2 | /** |
3 | * Report warnings when a global variable or function is used where there's a better |
4 | * context-aware alternative. |
5 | * |
6 | * Should use $this->msg() rather than wfMessage() on ContextSource extend. |
7 | * Should use $this->getUser() rather than $wgUser on ContextSource extend. |
8 | * Should use $this->getRequest() rather than $wgRequest on ContextSource extend. |
9 | */ |
10 | |
11 | namespace MediaWiki\Sniffs\Usage; |
12 | |
13 | use PHP_CodeSniffer\Files\File; |
14 | use PHP_CodeSniffer\Sniffs\Sniff; |
15 | |
16 | class ExtendClassUsageSniff implements Sniff { |
17 | |
18 | private const MSG_MAP = [ |
19 | T_FUNCTION => 'function', |
20 | T_VARIABLE => 'variable' |
21 | ]; |
22 | |
23 | /** |
24 | * List of globals which cannot be used together with getConfig() because most |
25 | * of these are objects. They are excluded from reporting of this sniff. |
26 | */ |
27 | private const NON_CONFIG_GLOBALS_MEDIAWIKI_CORE = [ |
28 | '$wgAuth', |
29 | '$wgConf', |
30 | '$wgContLang', |
31 | '$wgLang', |
32 | '$wgMemc', |
33 | '$wgOut', |
34 | '$wgParser', |
35 | '$wgRequest', |
36 | '$wgTitle', |
37 | '$wgUser', |
38 | '$wgVersion', |
39 | |
40 | // special global from WebStart.php |
41 | '$IP', |
42 | |
43 | // special global from entry points (index.php, load.php, api.php, etc.) |
44 | '$mediaWiki', |
45 | ]; |
46 | |
47 | /** |
48 | * Allow extensions add to the above list of non-config globals via .phpcs.xml |
49 | * @var string[] |
50 | */ |
51 | public array $nonConfigGlobals = []; |
52 | |
53 | private const CHECK_CONFIG = [ |
54 | // All extended class name. Map of extended class name to the checklist that |
55 | // should be used. |
56 | // Note that the SpecialPage class does NOT actually extend ContextSource, |
57 | // but all of the checks for ContextSource here also apply equally to SpecialPage |
58 | 'extendsCls' => [ |
59 | 'ContextSource' => 'ContextSource', |
60 | 'SpecialPage' => 'ContextSource', |
61 | |
62 | // Subclasses of ContextSource |
63 | 'ApiBase' => 'ContextSource', |
64 | 'ApiQueryBase' => 'ContextSource', |
65 | 'ApiQueryGeneratorBase' => 'ContextSource', |
66 | 'ApiQueryRevisionsBase' => 'ContextSource', |
67 | 'DifferenceEngine' => 'ContextSource', |
68 | 'HTMLForm' => 'ContextSource', |
69 | 'IndexPager' => 'ContextSource', |
70 | 'Skin' => 'ContextSource', |
71 | |
72 | // Subclasses of SpecialPage |
73 | 'AuthManagerSpecialPage' => 'ContextSource', |
74 | 'FormSpecialPage' => 'ContextSource', |
75 | 'ImageQueryPage' => 'ContextSource', |
76 | 'IncludableSpecialPage' => 'ContextSource', |
77 | 'PageQueryPage' => 'ContextSource', |
78 | 'QueryPage' => 'ContextSource', |
79 | 'UnlistedSpecialPage' => 'ContextSource', |
80 | 'WantedQueryPage' => 'ContextSource', |
81 | |
82 | // Subclasses of IndexPager |
83 | 'AlphabeticPager' => 'ContextSource', |
84 | 'RangeChronologicalPager' => 'ContextSource', |
85 | 'ReverseChronologicalPager' => 'ContextSource', |
86 | 'TablePager' => 'ContextSource', |
87 | ], |
88 | // All details of usage need to be check. |
89 | 'checkList' => [ |
90 | // Checklist name, usually the extended class |
91 | 'ContextSource' => [ |
92 | [ |
93 | // The check content. |
94 | 'content' => 'wfMessage', |
95 | // The content shows on report message. |
96 | 'msg_content' => 'wfMessage()', |
97 | // The check content code. |
98 | 'code' => T_FUNCTION, |
99 | // The expected content. |
100 | 'expect_content' => '$this->msg()', |
101 | // The expected content code. |
102 | 'expect_code' => T_FUNCTION |
103 | ], |
104 | [ |
105 | 'content' => '$wgUser', |
106 | 'msg_content' => '$wgUser', |
107 | 'code' => T_VARIABLE, |
108 | 'expect_content' => '$this->getUser()', |
109 | 'expect_code' => T_FUNCTION |
110 | ], |
111 | [ |
112 | 'content' => '$wgRequest', |
113 | 'msg_content' => '$wgRequest', |
114 | 'code' => T_VARIABLE, |
115 | 'expect_content' => '$this->getRequest()', |
116 | 'expect_code' => T_FUNCTION |
117 | ], |
118 | [ |
119 | 'content' => '$wgOut', |
120 | 'msg_content' => '$wgOut', |
121 | 'code' => T_VARIABLE, |
122 | 'expect_content' => '$this->getOutput()', |
123 | 'expect_code' => T_FUNCTION |
124 | ], |
125 | [ |
126 | 'content' => '$wgLang', |
127 | 'msg_content' => '$wgLang', |
128 | 'code' => T_VARIABLE, |
129 | 'expect_content' => '$this->getLanguage()', |
130 | 'expect_code' => T_FUNCTION |
131 | ], |
132 | ] |
133 | ] |
134 | ]; |
135 | |
136 | /** |
137 | * @inheritDoc |
138 | */ |
139 | public function register(): array { |
140 | return [ |
141 | T_CLASS |
142 | ]; |
143 | } |
144 | |
145 | /** |
146 | * @param File $phpcsFile |
147 | * @param int $stackPtr The current token index. |
148 | * @return void |
149 | */ |
150 | public function process( File $phpcsFile, $stackPtr ) { |
151 | $extClsContent = $phpcsFile->findExtendedClassName( $stackPtr ); |
152 | if ( $extClsContent === false ) { |
153 | // No extends token found |
154 | return; |
155 | } |
156 | |
157 | // Ignore namespace separator at the beginning |
158 | $extClsContent = ltrim( $extClsContent, '\\' ); |
159 | |
160 | // This should be replaced with a mechanism that check if |
161 | // the base class is in the list of restricted classes |
162 | if ( !isset( self::CHECK_CONFIG['extendsCls'][$extClsContent] ) ) { |
163 | return; |
164 | } |
165 | |
166 | $tokens = $phpcsFile->getTokens(); |
167 | $currToken = $tokens[$stackPtr]; |
168 | $nonConfigGlobals = array_flip( array_merge( |
169 | self::NON_CONFIG_GLOBALS_MEDIAWIKI_CORE, $this->nonConfigGlobals |
170 | ) ); |
171 | |
172 | $checkListName = self::CHECK_CONFIG['extendsCls'][$extClsContent]; |
173 | $extClsCheckList = self::CHECK_CONFIG['checkList'][$checkListName]; |
174 | // Loop over all tokens of the class to check each function |
175 | $i = $currToken['scope_opener']; |
176 | $end = $currToken['scope_closer']; |
177 | $eligibleFunc = null; |
178 | $endOfGlobal = null; |
179 | while ( $i !== false && $i < $end ) { |
180 | $iToken = $tokens[$i]; |
181 | if ( $iToken['code'] === T_FUNCTION ) { |
182 | $eligibleFunc = null; |
183 | // If this is a function, make sure it's eligible |
184 | // (i.e. not static or abstract, and has a body). |
185 | $methodProps = $phpcsFile->getMethodProperties( $i ); |
186 | $isStaticOrAbstract = $methodProps['is_static'] || $methodProps['is_abstract']; |
187 | $hasBody = isset( $iToken['scope_opener'] ) |
188 | && isset( $iToken['scope_closer'] ); |
189 | if ( !$isStaticOrAbstract && $hasBody ) { |
190 | $funcNamePtr = $phpcsFile->findNext( T_STRING, $i ); |
191 | $eligibleFunc = [ |
192 | 'name' => $tokens[$funcNamePtr]['content'], |
193 | 'scope_start' => $iToken['scope_opener'], |
194 | 'scope_end' => $iToken['scope_closer'] |
195 | ]; |
196 | } |
197 | } |
198 | |
199 | if ( $eligibleFunc !== null |
200 | && $i > $eligibleFunc['scope_start'] |
201 | && $i < $eligibleFunc['scope_end'] |
202 | ) { |
203 | // Inside eligible function, check the |
204 | // current token against the checklist |
205 | foreach ( $extClsCheckList as $value ) { |
206 | $condition = false; |
207 | if ( $value['code'] === T_FUNCTION |
208 | && strcasecmp( $iToken['content'], $value['content'] ) === 0 |
209 | ) { |
210 | $condition = true; |
211 | } |
212 | if ( $value['code'] === T_VARIABLE |
213 | && $iToken['content'] === $value['content'] |
214 | ) { |
215 | $condition = true; |
216 | } |
217 | if ( $condition ) { |
218 | $phpcsFile->addWarning( |
219 | 'Should use %s %s rather than %s %s', |
220 | $i, |
221 | 'FunctionVarUsage', |
222 | [ |
223 | self::MSG_MAP[$value['expect_code']], |
224 | $value['expect_content'], |
225 | self::MSG_MAP[$value['code']], |
226 | $value['msg_content'] |
227 | ] |
228 | ); |
229 | } |
230 | } |
231 | |
232 | // Handle globals to check for use of getConfig() |
233 | if ( $iToken['code'] === T_GLOBAL ) { |
234 | $endOfGlobal = $phpcsFile->findEndOfStatement( $i, T_COMMA ); |
235 | } elseif ( $endOfGlobal !== null && $i >= $endOfGlobal ) { |
236 | $endOfGlobal = null; |
237 | } |
238 | if ( $endOfGlobal !== null && |
239 | $iToken['code'] === T_VARIABLE && |
240 | !isset( $nonConfigGlobals[$iToken['content']] ) |
241 | ) { |
242 | $phpcsFile->addWarning( |
243 | 'Should use function %s rather than global %s', |
244 | $i, |
245 | 'FunctionConfigUsage', |
246 | [ '$this->getConfig()->get()', $iToken['content'] ] |
247 | ); |
248 | } |
249 | } |
250 | // Jump to the next function |
251 | if ( $eligibleFunc === null |
252 | || $i >= $eligibleFunc['scope_end'] |
253 | ) { |
254 | $start = $eligibleFunc === null ? $i : $eligibleFunc['scope_end']; |
255 | $i = $phpcsFile->findNext( T_FUNCTION, $start + 1, $end ); |
256 | continue; |
257 | } |
258 | // Find next token to work with |
259 | $i = $phpcsFile->findNext( [ T_STRING, T_VARIABLE, T_FUNCTION, T_GLOBAL ], $i + 1, $end ); |
260 | } |
261 | } |
262 | } |