Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
NestedInlineTernarySniff
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 3
506
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
 process
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
380
 isShortTernary
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Sniffs\Usage;
22
23use PHP_CodeSniffer\Files\File;
24use PHP_CodeSniffer\Sniffs\Sniff;
25use PHP_CodeSniffer\Util\Tokens;
26
27class NestedInlineTernarySniff implements Sniff {
28
29    /**
30     * Tokens that can end an inline ternary statement.
31     *
32     * @var array
33     */
34    private array $endTokens = [];
35
36    /**
37     * @inheritDoc
38     */
39    public function register(): array {
40        $this->endTokens = Tokens::$assignmentTokens + Tokens::$includeTokens + [
41            // Operators having a lower precedence than the ternary operator,
42            // or left associative operators having the same precedence, can
43            // end inline ternary statements. This includes all assignment and
44            // include statements.
45            //
46            // In the PHP source code, the order of precedence can be found
47            // in the file Zend/zend_language_parser.y. To find the ternary
48            // operator in the list, search for "%left '?' ':'".
49            T_INLINE_THEN => T_INLINE_THEN,
50            T_INLINE_ELSE => T_INLINE_ELSE,
51            T_YIELD_FROM => T_YIELD_FROM,
52            T_YIELD => T_YIELD,
53            T_PRINT => T_PRINT,
54            T_LOGICAL_AND => T_LOGICAL_AND,
55            T_LOGICAL_XOR => T_LOGICAL_XOR,
56            T_LOGICAL_OR => T_LOGICAL_OR,
57
58            // Obviously, right brackets, right parentheses, commas, colons,
59            // and semicolons can also end inline ternary statements. There is
60            // a list of corresponding tokens in File::findEndOfStatement(),
61            // which we duplicate here.
62            T_COLON => T_COLON,
63            T_COMMA => T_COMMA,
64            T_SEMICOLON => T_SEMICOLON,
65            T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS,
66            T_CLOSE_SQUARE_BRACKET => T_CLOSE_SQUARE_BRACKET,
67            T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET,
68            T_CLOSE_SHORT_ARRAY => T_CLOSE_SHORT_ARRAY,
69
70            // Less obviously, a foreach loop's array_expression can be
71            // an inline ternary statement, and would be followed by "as".
72            T_AS => T_AS,
73        ];
74
75        return [ T_INLINE_THEN ];
76    }
77
78    /**
79     * @param File $phpcsFile File
80     * @param int $stackPtr Location
81     * @return void
82     */
83    public function process( File $phpcsFile, $stackPtr ) {
84        $tokens = $phpcsFile->getTokens();
85        $elsePtr = null;
86        $thenNestingLevel = 1;
87        $thenNestedPtr = null;
88        $elseNestedPtr = null;
89        for ( $i = $stackPtr + 1; $i < $phpcsFile->numTokens; ++$i ) {
90            // Skip bracketed and parenthesized subexpressions.
91            $inBrackets = isset( $tokens[$i]['bracket_closer'] );
92            if ( $inBrackets && $tokens[$i]['bracket_opener'] === $i ) {
93                $i = $tokens[$i]['bracket_closer'];
94                continue;
95            }
96            $inParentheses = isset( $tokens[$i]['parenthesis_closer'] );
97            if ( $inParentheses && $tokens[$i]['parenthesis_opener'] === $i ) {
98                $i = $tokens[$i]['parenthesis_closer'];
99                continue;
100            }
101
102            if ( $elsePtr === null ) {
103                // In the "then" part of the inline ternary statement:
104                if ( $tokens[$i]['code'] === T_INLINE_THEN ) {
105                    // Let $thenNestedPtr point to the T_INLINE_THEN token
106                    // of the outermost inline ternary statement forming the
107                    // "then" part of the current inline ternary statement.
108                    // Example: $a ? $b ? $c ? $d : $e : $f : $g
109                    // -           ^ stackPtr
110                    // -                ^ thenNestedPtr
111                    if ( ++$thenNestingLevel === 2 ) {
112                        $thenNestedPtr = $i;
113                    }
114                } elseif ( $tokens[$i]['code'] === T_INLINE_ELSE ) {
115                    // Let $elsePtr point to the T_INLINE_ELSE token of the
116                    // current inline ternary statement. See below example.
117                    if ( --$thenNestingLevel === 0 ) {
118                        $elsePtr = $i;
119                    }
120                }
121                // Strictly speaking, checking if the entire "then" part
122                // is an inline ternary statement would involve checking the
123                // token, whenever $thenNestingLevel is 1, against the
124                // list of operators of lower precedence.
125                //
126                // However, we omit this check in order to allow additional
127                // cases to be flagged as needing parentheses for clarity.
128
129            } else {
130                // In the "else" part of the inline ternary statement:
131                if ( isset( $this->endTokens[$tokens[$i]['code']] ) ) {
132                    if ( $tokens[$i]['code'] === T_INLINE_THEN ) {
133                        // Let $elseNestedPtr point to the T_INLINE_THEN token
134                        // of the inline ternary statement having the current
135                        // inline ternary statement as its "if" part.
136                        // Example: $a ? $b : $c ? $d : $e ? $f : $g
137                        // -           ^ stackPtr
138                        // -                ^ elsePtr
139                        // -                     ^ elseNestedPtr
140                        $elseNestedPtr = $i;
141                    }
142                    break;
143                }
144            }
145        }
146
147        // The "then" part of the current inline ternary statement should not
148        // be another inline ternary statement, unless that other inline
149        // ternary statement is parenthesized.
150        if ( $thenNestedPtr !== null && $elsePtr !== null ) {
151            $fix = $phpcsFile->addFixableWarning(
152                'Nested inline ternary statements can be difficult to read without parentheses',
153                $thenNestedPtr,
154                'UnparenthesizedThen'
155            );
156            if ( $fix ) {
157                $phpcsFile->fixer->beginChangeset();
158                $phpcsFile->fixer->addContent( $stackPtr, ' (' );
159                $phpcsFile->fixer->addContentBefore( $elsePtr, ') ' );
160                $phpcsFile->fixer->endChangeset();
161            }
162        }
163
164        // The current inline ternary statement must not be the "if" part of
165        // another inline ternary statement, unless the current inline
166        // ternary statement is parenthesized.
167        if ( $elseNestedPtr !== null && !(
168            // Exception: Stacking is permitted when only the short form of
169            // the ternary operator is used. In this case, the operator's
170            // left associativity is unlikely to matter.
171            $this->isShortTernary( $phpcsFile, $stackPtr ) &&
172            $this->isShortTernary( $phpcsFile, $elseNestedPtr )
173        ) ) {
174            // Report this violation as an error, because it looks like a bug.
175            // For the same reason, don't offer to fix it automatically.
176            $phpcsFile->addError(
177                'Nested inline ternary statements, in PHP, may not behave as you intend ' .
178                'without parentheses',
179                $stackPtr,
180                'UnparenthesizedTernary'
181            );
182        }
183    }
184
185    /**
186     * @param File $phpcsFile File
187     * @param int $i Location of T_INLINE_THEN
188     * @return bool
189     */
190    private function isShortTernary( File $phpcsFile, int $i ): bool {
191        $tokens = $phpcsFile->getTokens();
192        $i = $phpcsFile->findNext( Tokens::$emptyTokens, $i + 1, null, true );
193        return $i !== false && $tokens[$i]['code'] === T_INLINE_ELSE;
194    }
195
196}