Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExtendClassUsageSniff
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 2
870
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 process
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
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
11namespace MediaWiki\Sniffs\Usage;
12
13use PHP_CodeSniffer\Files\File;
14use PHP_CodeSniffer\Sniffs\Sniff;
15
16class 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}