Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
StaticClosureSniff
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 3
1190
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 1
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
756
 isStaticClassProperty
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3/**
4 * Use static closure when the inner body does not use $this
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 */
23
24namespace MediaWiki\Sniffs\Usage;
25
26use PHP_CodeSniffer\Files\File;
27use PHP_CodeSniffer\Sniffs\Sniff;
28use PHP_CodeSniffer\Util\Tokens;
29
30class StaticClosureSniff implements Sniff {
31
32    /**
33     * @inheritDoc
34     */
35    public function register(): array {
36        return [ T_CLOSURE ];
37    }
38
39    /**
40     * @param File $phpcsFile
41     * @param int $stackPtr The current token index.
42     */
43    public function process( File $phpcsFile, $stackPtr ) {
44        $prevClosureToken = $phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true );
45        if ( $prevClosureToken === false ) {
46            return;
47        }
48
49        $tokens = $phpcsFile->getTokens();
50        $containsNonStaticStatements = false;
51        $unclearSituation = false;
52
53        $searchToken = [
54            T_VARIABLE,
55            T_DOUBLE_QUOTED_STRING,
56            T_HEREDOC,
57            T_PARENT,
58            T_SELF,
59            T_STATIC,
60            T_CLOSURE,
61            T_ANON_CLASS,
62        ];
63        $end = $tokens[$stackPtr]['scope_closer'];
64
65        // Search for tokens which indicates that this cannot be a static closure
66        $next = $phpcsFile->findNext( $searchToken, $tokens[$stackPtr]['scope_opener'] + 1, $end );
67        while ( $next !== false ) {
68            $code = $tokens[$next]['code'];
69            switch ( $code ) {
70                case T_VARIABLE:
71                    if ( $tokens[$next]['content'] === '$this' ) {
72                        $containsNonStaticStatements = true;
73                    }
74                    break;
75
76                case T_DOUBLE_QUOTED_STRING:
77                case T_HEREDOC:
78                    if ( preg_match( '/\${?this\b/', $tokens[$next]['content'] ) ) {
79                        $containsNonStaticStatements = true;
80                    }
81                    break;
82
83                case T_PARENT:
84                case T_SELF:
85                case T_STATIC:
86                    // Use of consts are allowed in static closures
87                    $nextToken = $phpcsFile->findNext( Tokens::$emptyTokens, $next + 1, $end, true );
88                    // In case of T_STATIC ignore the static keyword on closures
89                    if ( $nextToken !== false
90                        && $tokens[$nextToken]['code'] !== T_CLOSURE
91                        && !$this->isStaticClassProperty( $phpcsFile, $tokens, $nextToken, $end )
92                    ) {
93                        $prevToken = $phpcsFile->findPrevious( Tokens::$emptyTokens, $next - 1, null, true );
94                        // Okay on "new self"
95                        if ( $prevToken === false || $tokens[$prevToken]['code'] !== T_NEW ) {
96                            // php allows to call non-static method with self:: or parent:: or static::
97                            // That is normally a static call, but keep it as is, because it is unclear
98                            // and can break when changing.
99                            // Also keep unknown token sequences or unclear syntax
100                            $unclearSituation = true;
101                        }
102                    }
103                    if ( $nextToken !== false ) {
104                        // Skip over analyzed tokens
105                        $next = $nextToken;
106                    }
107                    break;
108
109                case T_CLOSURE:
110                    // Skip arguments and use parameter for closure, which can contains T_SELF as type hint
111                    // But search also inside nested closures for $this
112                    if ( isset( $tokens[$next]['scope_opener'] ) ) {
113                        $next = $tokens[$next]['scope_opener'];
114                    }
115                    break;
116
117                case T_ANON_CLASS:
118                    if ( isset( $tokens[$next]['scope_closer'] ) ) {
119                        // Skip to the end of the anon class because $this in anon is not relevant for this sniff
120                        $next = $tokens[$next]['scope_closer'];
121                    }
122                    break;
123            }
124            $next = $phpcsFile->findNext( $searchToken, $next + 1, $end );
125        }
126
127        if ( $unclearSituation ) {
128            // Keep everything as is
129            return;
130        }
131
132        if ( $tokens[$prevClosureToken]['code'] === T_STATIC ) {
133            if ( $containsNonStaticStatements ) {
134                $fix = $phpcsFile->addFixableError(
135                    'Cannot not use static closure in class context',
136                    $stackPtr,
137                    'NonStaticClosure'
138                );
139                if ( $fix ) {
140                    $phpcsFile->fixer->beginChangeset();
141
142                    do {
143                        $phpcsFile->fixer->replaceToken( $prevClosureToken, '' );
144                        $prevClosureToken++;
145                    } while ( $prevClosureToken < $stackPtr );
146
147                    $phpcsFile->fixer->endChangeset();
148                }
149            }
150        } elseif ( !$containsNonStaticStatements ) {
151            $fix = $phpcsFile->addFixableWarning(
152                'Use static closure',
153                $stackPtr,
154                'StaticClosure'
155            );
156            if ( $fix ) {
157                $phpcsFile->fixer->addContentBefore( $stackPtr, 'static ' );
158            }
159        }
160
161        // also check inner closure for static
162    }
163
164    /**
165     * Check if this is a class property like const or static field of format self::const
166     * @param File $phpcsFile
167     * @param array $tokens
168     * @param int &$stackPtr Non-empty token after self/parent/static
169     * @param int $end
170     * @return bool
171     */
172    private function isStaticClassProperty( File $phpcsFile, array $tokens, int &$stackPtr, int $end ): bool {
173        // No ::, no const
174        if ( $tokens[$stackPtr]['code'] !== T_DOUBLE_COLON ) {
175            return false;
176        }
177
178        // the const is a T_STRING, but also method calls are T_STRING
179        // okay with (static) variables
180        $stackPtr = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, $end, true );
181        if ( $stackPtr === false || $tokens[$stackPtr]['code'] !== T_STRING ) {
182            return $stackPtr !== false && $tokens[$stackPtr]['code'] === T_VARIABLE;
183        }
184
185        // const is a T_STRING without parenthesis after it
186        $stackPtr = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, $end, true );
187        return $stackPtr !== false && $tokens[$stackPtr]['code'] !== T_OPEN_PARENTHESIS;
188    }
189}