mayflower/mo4-coding-standard

View on GitHub
MO4/Sniffs/Formatting/UnnecessaryNamespaceUsageSniff.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php

/**
 * This file is part of the mo4-coding-standard (phpcs standard)
 *
 * @author  Xaver Loppenstedt <xaver@loppenstedt.de>
 *
 * @license http://spdx.org/licenses/MIT MIT License
 *
 * @link    https://github.com/mayflower/mo4-coding-standard
 */

declare(strict_types=1);

namespace MO4\Sniffs\Formatting;

use MO4\Library\PregLibrary;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens;

/**
 * Unnecessary Namespace Usage sniff.
 *
 * Full namespace declaration should be skipped in favour of the short declaration.
 *
 * @author    Xaver Loppenstedt <xaver@loppenstedt.de>
 * @author    Marco Jantke <marco.jantke@gmail.com>
 * @author    Steffen Ritter <steffenritter1@gmail.com>
 *
 * @copyright 2013 Xaver Loppenstedt, some rights reserved.
 *
 * @license   http://spdx.org/licenses/MIT MIT License
 *
 * @link      https://github.com/mayflower/mo4-coding-standard
 */
class UnnecessaryNamespaceUsageSniff implements Sniff
{
    /**
     * Tokens used in full class name.
     *
     * @var array<int, int>
     */
    private $classNameTokens = [
        T_NS_SEPARATOR,
        T_STRING,
    ];

    /**
     * Registers the tokens that this sniff wants to listen for.
     *
     * @return array<int, int>
     *
     * @see    Tokens.php
     */
    public function register(): array
    {
        return [T_CLASS];
    }

    /**
     * Called when one of the token types that this sniff is listening for
     * is found.
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where the
     *                        token was found.
     * @param int  $stackPtr  The position in the PHP_CodeSniffer
     *                        file's token stack where the token
     *                        was found.
     *
     * @return void
     *
     * @throws RuntimeException
     */
    public function process(File $phpcsFile, $stackPtr): void
    {
        $docCommentTags = [
            '@param'  => 1,
            '@return' => 1,
            '@throws' => 1,
            '@var'    => 2,
        ];
        $scanTokens     = [
            T_NS_SEPARATOR,
            T_DOC_COMMENT_OPEN_TAG,
        ];

        $tokens        = $phpcsFile->getTokens();
        $useStatements = $this->getUseStatements($phpcsFile, 0, ($stackPtr - 1));
        $namespace     = $this->getNamespace($phpcsFile, 0, ($stackPtr - 1));

        $nsSep = $phpcsFile->findNext($scanTokens, ($stackPtr + 1));

        while (false !== $nsSep) {
            $classNameEnd = (int) $phpcsFile->findNext(
                $this->classNameTokens,
                $nsSep,
                null,
                true
            );

            if (T_NS_SEPARATOR === $tokens[$nsSep]['code']) {
                if (T_STRING === $tokens[($nsSep - 1)]['code']) {
                    --$nsSep;
                }

                $className = $phpcsFile->getTokensAsString(
                    $nsSep,
                    ($classNameEnd - $nsSep)
                );

                $this->checkShorthandPossible(
                    $phpcsFile,
                    $useStatements,
                    $className,
                    $namespace,
                    $nsSep,
                    ($classNameEnd - 1)
                );
            } else {
                // Doc comment block.
                foreach ($tokens[$nsSep]['comment_tags'] as $tag) {
                    $content = $tokens[$tag]['content'];

                    if (false === \array_key_exists($content, $docCommentTags)) {
                        continue;
                    }

                    $next = ($tag + 1);
                    // PHP Code Sniffer will magically add  T_DOC_COMMENT_CLOSE_TAG with empty string content.
                    $lineEnd = $phpcsFile->findNext(
                        [
                            T_DOC_COMMENT_CLOSE_TAG,
                            T_DOC_COMMENT_STAR,
                        ],
                        $next
                    );

                    $docCommentStringPtr = $phpcsFile->findNext(
                        [T_DOC_COMMENT_STRING],
                        $next,
                        (int) $lineEnd
                    );

                    if (false === $docCommentStringPtr) {
                        continue;
                    }

                    $docLine = $tokens[$docCommentStringPtr]['content'];

                    $docLineTokens = PregLibrary::MO4PregSplit(
                        '/\s+/',
                        $docLine,
                        -1,
                        PREG_SPLIT_NO_EMPTY
                    );

                    // phpcs:disable
                    /** @var array<string> $docLineTokens */
                    $docLineTokens = \array_slice(
                        $docLineTokens,
                        0,
                        $docCommentTags[$content]
                    );
                    // phpcs:enable

                    foreach ($docLineTokens as $docLineToken) {
                        // phpcs:disable
                        /** @var array<string> $typeTokens */
                        $typeTokens = PregLibrary::MO4PregSplit(
                            '/\|/',
                            $docLineToken,
                            -1,
                            PREG_SPLIT_NO_EMPTY
                        );
                        // phpcs:enable

                        foreach ($typeTokens as $typeToken) {
                            if (true === \in_array($typeToken, $useStatements, true)) {
                                continue;
                            }

                            $this->checkShorthandPossible(
                                $phpcsFile,
                                $useStatements,
                                $typeToken,
                                $namespace,
                                $docCommentStringPtr,
                                $docCommentStringPtr,
                                true
                            );
                        }
                    }
                }
            }

            $nsSep = $phpcsFile->findNext($scanTokens, ($classNameEnd + 1));
        }
    }

    /**
     * Get all use statements in range
     *
     * @param File $phpcsFile PHP CS File
     * @param int  $start     start pointer
     * @param int  $end       end pointer
     *
     * @return array
     */
    protected function getUseStatements(File $phpcsFile, int $start, int $end): array
    {
        $useStatements = [];
        $i             = $start;
        $tokens        = $phpcsFile->getTokens();
        $useTokenPtr   = $phpcsFile->findNext(T_USE, $i, $end);

        while (false !== $useTokenPtr) {
            $classNameStart = (int) $phpcsFile->findNext(
                PHP_CodeSniffer_Tokens::$emptyTokens,
                ($useTokenPtr + 1),
                $end,
                true
            );
            $classNameEnd   = (int) $phpcsFile->findNext(
                $this->classNameTokens,
                ($classNameStart + 1),
                $end,
                true
            );
            $useEnd         = $phpcsFile->findNext(
                [
                    T_SEMICOLON,
                    T_COMMA,
                ],
                $classNameEnd,
                $end
            );

            // Prevent endless loop when 'use ;' is the last use statement.
            if (false === $useEnd) {
                break;
            }

            /** @var int $aliasNamePtr */
            $aliasNamePtr = $phpcsFile->findPrevious(
                PHP_CodeSniffer_Tokens::$emptyTokens,
                ($useEnd - 1),
                0,
                true
            );

            $length    = ($classNameEnd - $classNameStart);
            $className = $phpcsFile->getTokensAsString($classNameStart, $length);

            $className                 = $this->getFullyQualifiedClassName($className);
            $useStatements[$className] = $tokens[$aliasNamePtr]['content'];
            $i                         = ($useEnd + 1);

            $useTokenPtr = T_COMMA === $tokens[$useEnd]['code'] ? $i : $phpcsFile->findNext(T_USE, $i, $end);
        }

        return $useStatements;
    }

    /**
     * Get the namespace of the current class file
     *
     * @param File $phpcsFile PHP CS File
     * @param int  $start     start pointer
     * @param int  $end       end pointer
     *
     * @return string
     */
    protected function getNamespace(File $phpcsFile, int $start, int $end): string
    {
        $namespace      = (int) $phpcsFile->findNext(T_NAMESPACE, $start, $end);
        $namespaceStart = $phpcsFile->findNext(
            PHP_CodeSniffer_Tokens::$emptyTokens,
            ($namespace + 1),
            $end,
            true
        );

        if (false === $namespaceStart) {
            return '';
        }

        $namespaceEnd = (int) $phpcsFile->findNext(
            $this->classNameTokens,
            ($namespaceStart + 1),
            $end,
            true
        );

        $nslen = ($namespaceEnd - $namespaceStart);
        $name  = $phpcsFile->getTokensAsString($namespaceStart, $nslen);

        return "\\{$name}\\";
    }

    /**
     * Return the fully qualified class name, e.g. '\Foo\Bar\Faz'
     *
     * @param string $className class name
     *
     * @return string
     */
    private function getFullyQualifiedClassName(string $className): string
    {
        return '\\' !== $className[0] ? "\\{$className}" : $className;
    }

    /**
     * Check if short hand is possible.
     *
     * @param File   $phpcsFile     PHP CS File
     * @param array  $useStatements array with class use statements
     * @param string $className     class name
     * @param string $namespace     name space
     * @param int    $startPtr      start token pointer
     * @param int    $endPtr        end token pointer
     * @param bool   $isDocBlock    true if fixing doc block
     *
     * @return void
     */
    private function checkShorthandPossible(File $phpcsFile, array $useStatements, string $className, string $namespace, int $startPtr, int $endPtr, bool $isDocBlock = false): void
    {
        $msg              = 'Shorthand possible. Replace "%s" with "%s"';
        $code             = 'UnnecessaryNamespaceUsage';
        $fixable          = false;
        $replaceClassName = false;
        $replacement      = '';

        $fullClassName = $this->getFullyQualifiedClassName($className);

        if (true === \array_key_exists($fullClassName, $useStatements)) {
            $replacement = $useStatements[$fullClassName];

            $data = [
                $className,
                $replacement,
            ];

            $fixable = $phpcsFile->addFixableWarning(
                $msg,
                $startPtr,
                $code,
                $data
            );

            $replaceClassName = true;
        } elseif ('' !== $namespace && 0 === \strpos($fullClassName, $namespace)) {
            $replacement = \substr($fullClassName, \strlen($namespace));

            $data    = [
                $className,
                $replacement,
            ];
            $fixable = $phpcsFile->addFixableWarning(
                $msg,
                $startPtr,
                $code,
                $data
            );
        }

        if (true !== $fixable) {
            return;
        }

        $phpcsFile->fixer->beginChangeset();

        if (true === $isDocBlock) {
            $tokens     = $phpcsFile->getTokens();
            $oldContent = $tokens[$startPtr]['content'];
            $newContent = \str_replace($className, $replacement, $oldContent);
            $phpcsFile->fixer->replaceToken($startPtr, $newContent);
        } else {
            for ($i = $startPtr; $i < $endPtr; $i++) {
                $phpcsFile->fixer->replaceToken($i, '');
            }

            if (true === $replaceClassName) {
                $phpcsFile->fixer->replaceToken($endPtr, $replacement);
            }
        }

        $phpcsFile->fixer->endChangeset();
    }
}