Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 67 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
NestedInlineTernarySniff | |
0.00% |
0 / 67 |
|
0.00% |
0 / 3 |
506 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
380 | |||
isShortTernary | |
0.00% |
0 / 3 |
|
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 | |
21 | namespace MediaWiki\Sniffs\Usage; |
22 | |
23 | use PHP_CodeSniffer\Files\File; |
24 | use PHP_CodeSniffer\Sniffs\Sniff; |
25 | use PHP_CodeSniffer\Util\Tokens; |
26 | |
27 | class 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 | } |