Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
64 / 64
NestedInlineTernarySniff
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
3 / 3
22
100.00% covered (success)
100.00%
64 / 64
 register
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
18 / 18
 process
100.00% covered (success)
100.00%
1 / 1
19
100.00% covered (success)
100.00%
43 / 43
 isShortTernary
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
<?php
/**
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 */
namespace MediaWiki\Sniffs\Usage;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class NestedInlineTernarySniff implements Sniff {
    /**
     * Tokens that can end an inline ternary statement.
     *
     * @var array
     */
    private $endTokens = [];
    /**
     * @inheritDoc
     */
    public function register() : array {
        $this->endTokens = Tokens::$assignmentTokens + Tokens::$includeTokens + [
            // Operators having a lower precedence than the ternary operator,
            // or left associative operators having the same precedence, can
            // end inline ternary statements. This includes all assignment and
            // include statements.
            //
            // In the PHP source code, the order of precedence can be found
            // in the file Zend/zend_language_parser.y. To find the ternary
            // operator in the list, search for "%left '?' ':'".
            T_INLINE_THEN => T_INLINE_THEN,
            T_INLINE_ELSE => T_INLINE_ELSE,
            T_YIELD_FROM => T_YIELD_FROM,
            T_YIELD => T_YIELD,
            T_PRINT => T_PRINT,
            T_LOGICAL_AND => T_LOGICAL_AND,
            T_LOGICAL_XOR => T_LOGICAL_XOR,
            T_LOGICAL_OR => T_LOGICAL_OR,
            // Obviously, right brackets, right parentheses, commas, colons,
            // and semicolons can also end inline ternary statements. There is
            // a list of corresponding tokens in File::findEndOfStatement(),
            // which we duplicate here.
            T_COLON => T_COLON,
            T_COMMA => T_COMMA,
            T_SEMICOLON => T_SEMICOLON,
            T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS,
            T_CLOSE_SQUARE_BRACKET => T_CLOSE_SQUARE_BRACKET,
            T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET,
            T_CLOSE_SHORT_ARRAY => T_CLOSE_SHORT_ARRAY,
            // Less obviously, a foreach loop's array_expression can be
            // an inline ternary statement, and would be followed by "as".
            T_AS => T_AS,
        ];
        return [ T_INLINE_THEN ];
    }
    /**
     * @param File $phpcsFile File
     * @param int $stackPtr Location
     * @return void
     */
    public function process( File $phpcsFile, $stackPtr ) {
        $tokens = $phpcsFile->getTokens();
        $elsePtr = null;
        $thenNestingLevel = 1;
        $thenNestedPtr = null;
        $elseNestedPtr = null;
        for ( $i = $stackPtr + 1; $i < $phpcsFile->numTokens; ++$i ) {
            // Skip bracketed and parenthesized subexpressions.
            $inBrackets = isset( $tokens[$i]['bracket_closer'] );
            if ( $inBrackets && $tokens[$i]['bracket_opener'] === $i ) {
                $i = $tokens[$i]['bracket_closer'];
                continue;
            }
            $inParentheses = isset( $tokens[$i]['parenthesis_closer'] );
            if ( $inParentheses && $tokens[$i]['parenthesis_opener'] === $i ) {
                $i = $tokens[$i]['parenthesis_closer'];
                continue;
            }
            if ( $elsePtr === null ) {
                // In the "then" part of the inline ternary statement:
                if ( $tokens[$i]['code'] === T_INLINE_THEN ) {
                    // Let $thenNestedPtr point to the T_INLINE_THEN token
                    // of the outermost inline ternary statement forming the
                    // "then" part of the current inline ternary statement.
                    // Example: $a ? $b ? $c ? $d : $e : $f : $g
                    // -           ^ stackPtr
                    // -                ^ thenNestedPtr
                    if ( ++$thenNestingLevel === 2 ) {
                        $thenNestedPtr = $i;
                    }
                } elseif ( $tokens[$i]['code'] === T_INLINE_ELSE ) {
                    // Let $elsePtr point to the T_INLINE_ELSE token of the
                    // current inline ternary statement. See below example.
                    if ( --$thenNestingLevel === 0 ) {
                        $elsePtr = $i;
                    }
                }
                // Strictly speaking, checking if the entire "then" part
                // is an inline ternary statement would involve checking the
                // token, whenever $thenNestingLevel is 1, against the
                // list of operators of lower precedence.
                //
                // However, we omit this check in order to allow additional
                // cases to be flagged as needing parentheses for clarity.
            } else {
                // In the "else" part of the inline ternary statement:
                if ( isset( $this->endTokens[$tokens[$i]['code']] ) ) {
                    if ( $tokens[$i]['code'] === T_INLINE_THEN ) {
                        // Let $elseNestedPtr point to the T_INLINE_THEN token
                        // of the inline ternary statement having the current
                        // inline ternary statement as its "if" part.
                        // Example: $a ? $b : $c ? $d : $e ? $f : $g
                        // -           ^ stackPtr
                        // -                ^ elsePtr
                        // -                     ^ elseNestedPtr
                        $elseNestedPtr = $i;
                    }
                    break;
                }
            }
        }
        // The "then" part of the current inline ternary statement should not
        // be another inline ternary statement, unless that other inline
        // ternary statement is parenthesized.
        if ( $thenNestedPtr !== null && $elsePtr !== null ) {
            $fix = $phpcsFile->addFixableWarning(
                'Nested inline ternary statements can be difficult to read without parentheses',
                $thenNestedPtr,
                'UnparenthesizedThen'
            );
            if ( $fix ) {
                $phpcsFile->fixer->beginChangeset();
                $phpcsFile->fixer->addContent( $stackPtr, ' (' );
                $phpcsFile->fixer->addContentBefore( $elsePtr, ') ' );
                $phpcsFile->fixer->endChangeset();
            }
        }
        // The current inline ternary statement must not be the "if" part of
        // another inline ternary statement, unless the current inline
        // ternary statement is parenthesized.
        if ( $elseNestedPtr !== null && !(
            // Exception: Stacking is permitted when only the short form of
            // the ternary operator is used. In this case, the operator's
            // left associativity is unlikely to matter.
            $this->isShortTernary( $phpcsFile, $stackPtr ) &&
            $this->isShortTernary( $phpcsFile, $elseNestedPtr )
        ) ) {
            // Report this violation as an error, because it looks like a bug.
            // For the same reason, don't offer to fix it automatically.
            $phpcsFile->addError(
                'Nested inline ternary statements, in PHP, may not behave as you intend ' .
                'without parentheses',
                $stackPtr,
                'UnparenthesizedTernary'
            );
        }
    }
    /**
     * @param File $phpcsFile File
     * @param int $i Location of T_INLINE_THEN
     * @return bool
     */
    private function isShortTernary( File $phpcsFile, int $i ) : bool {
        $tokens = $phpcsFile->getTokens();
        $i = $phpcsFile->findNext( Tokens::$emptyTokens, $i + 1, null, true );
        return $i !== false && $tokens[$i]['code'] === T_INLINE_ELSE;
    }
}