Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
68 / 68
UnsortedUseStatementsSniff
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
29
100.00% covered (success)
100.00%
68 / 68
 register
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 process
100.00% covered (success)
100.00%
1 / 1
10
100.00% covered (success)
100.00%
26 / 26
 makeUseStatementList
100.00% covered (success)
100.00%
1 / 1
16
100.00% covered (success)
100.00%
36 / 36
 sortByFullQualifiedClassName
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
<?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\Classes;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class UnsortedUseStatementsSniff implements Sniff {
    // Preferred order is classes → functions → constants
    private const ORDER = [ 'function' => 1, 'const' => 2 ];
    /**
     * @inheritDoc
     */
    public function register() : array {
        return [ T_USE ];
    }
    /**
     * @inheritDoc
     *
     * @param File $phpcsFile
     * @param int $stackPtr
     * @return int|void
     */
    public function process( File $phpcsFile, $stackPtr ) {
        $tokens = $phpcsFile->getTokens();
        // Only check use statements in the global scope.
        if ( !empty( $tokens[$stackPtr]['conditions'] ) ) {
            // TODO: Use array_key_first() if available
            $scope = key( $tokens[$stackPtr]['conditions'] );
            return $tokens[$scope]['scope_closer'] ?? $stackPtr;
        }
        $useStatementList = $this->makeUseStatementList( $phpcsFile, $stackPtr );
        // Nothing to do, bail out as fast as possible
        if ( count( $useStatementList ) <= 1 ) {
            return;
        }
        $sortedStatements = $this->sortByFullQualifiedClassName( $useStatementList );
        if ( $useStatementList !== $sortedStatements ) {
            $fix = $phpcsFile->addFixableWarning(
                'Use statements are not alphabetically sorted',
                $stackPtr,
                'UnsortedUse'
            );
            if ( $fix ) {
                $phpcsFile->fixer->beginChangeset();
                foreach ( $useStatementList as $statement ) {
                    for ( $i = $statement['start']; $i <= $statement['end']; $i++ ) {
                        $phpcsFile->fixer->replaceToken( $i, '' );
                    }
                    // Also remove the newline at the end of the line, if there is one
                    if ( $tokens[$i]['code'] === T_WHITESPACE
                        && $tokens[$i]['line'] < $tokens[$i + 1]['line']
                    ) {
                        $phpcsFile->fixer->replaceToken( $i, '' );
                    }
                }
                foreach ( $sortedStatements as $statement ) {
                    $phpcsFile->fixer->addContent( $stackPtr, $statement['originalContent'] );
                    $phpcsFile->fixer->addNewline( $stackPtr );
                }
                $phpcsFile->fixer->endChangeset();
            }
        }
        // Continue *after* the last use token, to not process it twice
        return end( $useStatementList )['end'] + 1;
    }
    /**
     * @param File $phpcsFile
     * @param int $stackPtr
     * @return array[]
     */
    private function makeUseStatementList( File $phpcsFile, int $stackPtr ) : array {
        $tokens = $phpcsFile->getTokens();
        $next = $stackPtr;
        $list = [];
        do {
            $originalContent = '';
            $group = 0;
            $sortKey = '';
            $collectSortKey = false;
            $start = $next;
            // The end condition here is for when a file ends directly after a "use"
            for ( ; $next < $phpcsFile->numTokens; $next++ ) {
                [ 'code' => $code, 'content' => $content ] = $tokens[$next];
                $originalContent .= $content;
                if ( $code === T_STRING ) {
                    // Reserved keywords "function" and "const" can not appear anywhere else
                    if ( strcasecmp( $content, 'function' ) === 0
                        || strcasecmp( $content, 'const' ) === 0
                    ) {
                        $group = self::ORDER[ strtolower( $content ) ];
                    } elseif ( !$sortKey ) {
                        // The first non-reserved string is where the class name starts
                        $collectSortKey = true;
                    }
                } elseif ( $code === T_AS ) {
                    // The string after an "as" is not part of the class name any more
                    $collectSortKey = false;
                } elseif ( $code === T_SEMICOLON && $sortKey ) {
                    $list[] = [
                        'start' => $start,
                        'end' => $next,
                        'originalContent' => $originalContent,
                        'group' => $group,
                        // No need to trim(), no spaces or leading backslashes have been collected
                        'sortKey' => strtolower( $sortKey ),
                    ];
                    // Try to find the next "use" token after the current one
                    $next = $phpcsFile->findNext( Tokens::$emptyTokens, $next + 1, null, true );
                    break;
                } elseif ( isset( Tokens::$emptyTokens[$code] ) ) {
                    // We never want any space or comment in the sort key
                    continue;
                } elseif ( $code !== T_USE && $code !== T_NS_SEPARATOR ) {
                    // Unexpected token, stop searching for more "use" keywords
                    break 2;
                }
                if ( $collectSortKey ) {
                    $sortKey .= $content;
                }
            }
        } while ( $next && isset( $tokens[$next] ) && $tokens[$next]['code'] === T_USE );
        return $list;
    }
    /**
     * @param array[] $list
     * @return array[]
     */
    private function sortByFullQualifiedClassName( array $list ) : array {
        usort( $list, static function ( array $a, array $b ) {
            if ( $a['group'] !== $b['group'] ) {
                return $a['group'] <=> $b['group'];
            }
            // Can't use strnatcasecmp() because it behaves different, compared to e.g. PHPStorm
            return strnatcmp( $a['sortKey'], $b['sortKey'] );
        } );
        return $list;
    }
}