|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Symfony3Custom\Sniffs\Namespaces; |
| 4 | + |
| 5 | +use PHP_CodeSniffer\Files\File; |
| 6 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 7 | +use PHP_CodeSniffer\Util\Tokens; |
| 8 | + |
| 9 | +/** |
| 10 | + * Checks for "use" statements that are not needed in a file. |
| 11 | + */ |
| 12 | +class UnusedUseSniff implements Sniff |
| 13 | +{ |
| 14 | + /** |
| 15 | + * Returns an array of tokens this test wants to listen for. |
| 16 | + * |
| 17 | + * @return array |
| 18 | + */ |
| 19 | + public function register() |
| 20 | + { |
| 21 | + return array(T_USE); |
| 22 | + } |
| 23 | + |
| 24 | + /** |
| 25 | + * Processes this test, when one of its tokens is encountered. |
| 26 | + * |
| 27 | + * @param File $phpcsFile The file being scanned. |
| 28 | + * @param int $stackPtr The position of the current token in the stack passed in $tokens. |
| 29 | + * |
| 30 | + * @return void |
| 31 | + */ |
| 32 | + public function process(File $phpcsFile, $stackPtr) |
| 33 | + { |
| 34 | + $tokens = $phpcsFile->getTokens(); |
| 35 | + |
| 36 | + // Only check use statements in the global scope. |
| 37 | + if (empty($tokens[$stackPtr]['conditions']) === false) { |
| 38 | + return; |
| 39 | + } |
| 40 | + |
| 41 | + // Seek to the end of the statement and get the string before the semi colon. |
| 42 | + $semiColon = $phpcsFile->findEndOfStatement($stackPtr); |
| 43 | + if (T_SEMICOLON !== $tokens[$semiColon]['code']) { |
| 44 | + return; |
| 45 | + } |
| 46 | + |
| 47 | + $classPtr = $phpcsFile->findPrevious( |
| 48 | + Tokens::$emptyTokens, |
| 49 | + ($semiColon - 1), |
| 50 | + null, |
| 51 | + true |
| 52 | + ); |
| 53 | + |
| 54 | + if (T_STRING !== $tokens[$classPtr]['code']) { |
| 55 | + return; |
| 56 | + } |
| 57 | + |
| 58 | + // Search where the class name is used. PHP treats class names case insensitive, |
| 59 | + // that's why we cannot search for the exact class name string |
| 60 | + // and need to iterate over all T_STRING tokens in the file. |
| 61 | + $classUsed = $phpcsFile->findNext(T_STRING, ($classPtr + 1)); |
| 62 | + $lowerClassName = strtolower($tokens[$classPtr]['content']); |
| 63 | + |
| 64 | + // Check if the referenced class is in the same namespace as the current file. |
| 65 | + // If it is then the use statement is not necessary. |
| 66 | + $namespacePtr = $phpcsFile->findPrevious([T_NAMESPACE], $stackPtr); |
| 67 | + |
| 68 | + // Check if the use statement does aliasing with the "as" keyword. |
| 69 | + // Aliasing is allowed even in the same namespace. |
| 70 | + $aliasUsed = $phpcsFile->findPrevious(T_AS, ($classPtr - 1), $stackPtr); |
| 71 | + |
| 72 | + if (false !== $namespacePtr && false === $aliasUsed) { |
| 73 | + $nsEnd = $phpcsFile->findNext( |
| 74 | + [T_NS_SEPARATOR, T_STRING, T_WHITESPACE], |
| 75 | + ($namespacePtr + 1), |
| 76 | + null, |
| 77 | + true |
| 78 | + ); |
| 79 | + |
| 80 | + $namespace = trim($phpcsFile->getTokensAsString(($namespacePtr + 1), ($nsEnd - $namespacePtr - 1))); |
| 81 | + |
| 82 | + $useNamespacePtr = $phpcsFile->findNext([T_STRING], ($stackPtr + 1)); |
| 83 | + $useNamespaceEnd = $phpcsFile->findNext( |
| 84 | + [T_NS_SEPARATOR, T_STRING], |
| 85 | + ($useNamespacePtr + 1), |
| 86 | + null, |
| 87 | + true |
| 88 | + ); |
| 89 | + |
| 90 | + $useNamespace = rtrim( |
| 91 | + $phpcsFile->getTokensAsString( |
| 92 | + $useNamespacePtr, |
| 93 | + ($useNamespaceEnd - $useNamespacePtr - 1) |
| 94 | + ), |
| 95 | + '\\' |
| 96 | + ); |
| 97 | + |
| 98 | + if (strcasecmp($namespace, $useNamespace) === 0) { |
| 99 | + $classUsed = false; |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + while (false !== $classUsed) { |
| 104 | + if (strtolower($tokens[$classUsed]['content']) === $lowerClassName) { |
| 105 | + // If the name is used in a PHP 7 function return type declaration stop. |
| 106 | + if (T_RETURN_TYPE === $tokens[$classUsed]['code']) { |
| 107 | + return; |
| 108 | + } |
| 109 | + |
| 110 | + $beforeUsage = $phpcsFile->findPrevious( |
| 111 | + Tokens::$emptyTokens, |
| 112 | + ($classUsed - 1), |
| 113 | + null, |
| 114 | + true |
| 115 | + ); |
| 116 | + |
| 117 | + // If a backslash is used before the class name then this is some other use statement. |
| 118 | + if (T_USE !== $tokens[$beforeUsage]['code'] && T_NS_SEPARATOR !== $tokens[$beforeUsage]['code']) { |
| 119 | + return; |
| 120 | + } |
| 121 | + |
| 122 | + // Trait use statement within a class. |
| 123 | + if (T_USE === $tokens[$beforeUsage]['code'] && false === empty($tokens[$beforeUsage]['conditions'])) { |
| 124 | + return; |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + $classUsed = $phpcsFile->findNext( |
| 129 | + [T_STRING, T_RETURN_TYPE], |
| 130 | + ($classUsed + 1) |
| 131 | + ); |
| 132 | + } |
| 133 | + |
| 134 | + // More checks |
| 135 | + foreach ($tokens as $token) { |
| 136 | + // Check for doc params @... |
| 137 | + if ('T_DOC_COMMENT_TAG' === $token['type']) { |
| 138 | + // Handle comment tag as @Route(..) or @ORM\Id |
| 139 | + if (preg_match('/^@'.$lowerClassName.'(?![a-zA-Z])/i', $token['content']) === 1) { |
| 140 | + return; |
| 141 | + }; |
| 142 | + } |
| 143 | + |
| 144 | + // Check for @param Truc or @return Machin |
| 145 | + if ('T_DOC_COMMENT_STRING' === $token['type']) { |
| 146 | + if (trim(strtolower($token['content'])) === $lowerClassName |
| 147 | + // Handle @var Machin[]|Machine|AnotherMachin $machin |
| 148 | + || preg_match('/^'.$lowerClassName.'(\|| |\[)/i', trim($token['content'])) === 1 |
| 149 | + || preg_match('/(\|| )'.$lowerClassName.'(\|| |\[)/i', trim($token['content'])) === 1 |
| 150 | + || preg_match('/(\|| )'.$lowerClassName.'$/i', trim($token['content'])) === 1) { |
| 151 | + $beforeUsage = $phpcsFile->findPrevious( |
| 152 | + Tokens::$emptyTokens, |
| 153 | + ($classUsed - 1), |
| 154 | + null, |
| 155 | + true |
| 156 | + ); |
| 157 | + |
| 158 | + // If a backslash is used before the class name then this is some other use statement. |
| 159 | + if (T_USE !== $tokens[$beforeUsage]['code'] && T_NS_SEPARATOR !== $tokens[$beforeUsage]['code']) { |
| 160 | + return; |
| 161 | + } |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + $fix = $phpcsFile->addFixableError('Unused use statement', $stackPtr, 'UnusedUse'); |
| 167 | + if (true === $fix) { |
| 168 | + // Remove the whole use statement line. |
| 169 | + $phpcsFile->fixer->beginChangeset(); |
| 170 | + for ($i = $stackPtr; $i <= $semiColon; $i++) { |
| 171 | + $phpcsFile->fixer->replaceToken($i, ''); |
| 172 | + } |
| 173 | + |
| 174 | + // Also remove whitespace after the semicolon (new lines). |
| 175 | + while (true === isset($tokens[$i]) && T_WHITESPACE === $tokens[$i]['code']) { |
| 176 | + $phpcsFile->fixer->replaceToken($i, ''); |
| 177 | + if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) { |
| 178 | + break; |
| 179 | + } |
| 180 | + |
| 181 | + $i++; |
| 182 | + } |
| 183 | + |
| 184 | + $phpcsFile->fixer->endChangeset(); |
| 185 | + } |
| 186 | + } |
| 187 | +} |
0 commit comments