diff --git a/CHANGELOG.md b/CHANGELOG.md index a5ed5d1a..534103f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ All notable changes to this project will be documented in this file, in reverse - [#51](https://github.com/webimpress/coding-standard/pull/51) adds check for blank lines and comments before arrow in arrays in `Array\Format` sniff. Arrow must be after the index value, can be in new line, but any additional lines or comments are disallowed. +- [#54](https://github.com/webimpress/coding-standard/pull/54) adds `Namespaces\UniqueImport` sniff to detect if class/function/constant is imported only once. + Sniff checks also if the name is used only once. The same name can be used for class/function/constant, and constant names are case sensitive. + ### Changed - [#42](https://github.com/webimpress/coding-standard/pull/42) changes `NamingConventions\ValidVariableName` to require variable names be in strict camelCase. It means two capital letters next to each other are not allowed. diff --git a/src/WebimpressCodingStandard/Sniffs/Namespaces/UniqueImportSniff.php b/src/WebimpressCodingStandard/Sniffs/Namespaces/UniqueImportSniff.php new file mode 100644 index 00000000..23eaae44 --- /dev/null +++ b/src/WebimpressCodingStandard/Sniffs/Namespaces/UniqueImportSniff.php @@ -0,0 +1,168 @@ +getTokens(); + + if ($tokens[$stackPtr]['code'] === T_OPEN_TAG) { + $namespace = $phpcsFile->findNext(T_NAMESPACE, $stackPtr + 1); + if ($namespace) { + return $namespace; + } + } + + $uses = $this->getUseStatements($phpcsFile, $stackPtr); + + foreach ($uses as $type => $data) { + foreach ($data as $name => $ptrs) { + if (isset($ptrs[1])) { + $ptr = max($ptrs); + if ($type === 'full') { + $error = 'The same %s %s is already imported'; + $data = [explode('::', $name)[0], $tokens[$ptr]['content']]; + $phpcsFile->addError($error, $ptr, 'DuplicateImport', $data); + } else { + $error = 'The name %s is already used for another %s'; + $data = [$tokens[$ptr]['content'], $type]; + $phpcsFile->addError($error, $ptr, 'DuplicateAlias', $data); + } + } + } + } + + return $tokens[$stackPtr]['code'] === T_OPEN_TAG + ? $phpcsFile->numTokens + 1 + : $stackPtr + 1; + } + + /** + * @return string[][] + */ + private function getUseStatements(File $phpcsFile, int $scopePtr) : array + { + $tokens = $phpcsFile->getTokens(); + + $uses = []; + + if (isset($tokens[$scopePtr]['scope_opener'])) { + $start = $tokens[$scopePtr]['scope_opener']; + $end = $tokens[$scopePtr]['scope_closer']; + } else { + $start = $scopePtr; + $end = null; + } + + while ($use = $phpcsFile->findNext(T_USE, $start + 1, $end)) { + if (! CodingStandard::isGlobalUse($phpcsFile, $use)) { + $start = $use; + continue; + } + + $semicolon = $phpcsFile->findNext(T_SEMICOLON, $use + 1); + + $type = 'class'; + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $use + 1, null, true); + if ($tokens[$next]['code'] === T_STRING + && in_array(strtolower($tokens[$next]['content']), ['const', 'function'], true) + ) { + $type = strtolower($tokens[$next]['content']); + + $use = $next + 1; + } + + $current = $semicolon; + while ($namePtr = $phpcsFile->findPrevious(T_STRING, $current, $use)) { + $key = $tokens[$namePtr]['content']; + if ($type !== 'const') { + $key = strtolower($key); + } + $uses[$type][$key][] = $namePtr; + + $lastPtr = $namePtr; + $as = $phpcsFile->findPrevious(Tokens::$emptyTokens, $namePtr - 1, null, true); + if ($tokens[$as]['code'] === T_AS) { + $lastPtr = $phpcsFile->findPrevious(T_STRING, $as - 1); + $key = $tokens[$lastPtr]['content']; + if ($type !== 'const') { + $key = strtolower($key); + } + } + + $from = $phpcsFile->findPrevious( + Tokens::$emptyTokens + [ + T_STRING => T_STRING, + T_NS_SEPARATOR => T_NS_SEPARATOR, + ], + $lastPtr - 1, + $use, + true + ) ?: $use; + + $full = ''; + for ($i = $from + 1; $i < $lastPtr; ++$i) { + if ($tokens[$i]['code'] === T_STRING || $tokens[$i]['code'] === T_NS_SEPARATOR) { + $full .= $tokens[$i]['content']; + } + } + $full .= $key; + + $prev = $phpcsFile->findPrevious(T_OPEN_USE_GROUP, $lastPtr - 1, $use); + if ($prev) { + for ($i = $prev - 1; $i > $use; --$i) { + if ($tokens[$i]['code'] === T_STRING || $tokens[$i]['code'] === T_NS_SEPARATOR) { + $full = $tokens[$i]['content'] . $full; + } + } + } + + $uses['full'][$type . '::' . $full][] = $lastPtr; + + $current = $phpcsFile->findPrevious(T_COMMA, $namePtr - 1, $use); + if ($current === false) { + break; + } + } + + $start = $semicolon; + } + + return $uses; + } +} diff --git a/test/Sniffs/Namespaces/UniqueImportUnitTest.1.inc b/test/Sniffs/Namespaces/UniqueImportUnitTest.1.inc new file mode 100644 index 00000000..f37df0fa --- /dev/null +++ b/test/Sniffs/Namespaces/UniqueImportUnitTest.1.inc @@ -0,0 +1,37 @@ +closure = function() use($a) {}; + } +} diff --git a/test/Sniffs/Namespaces/UniqueImportUnitTest.2.inc b/test/Sniffs/Namespaces/UniqueImportUnitTest.2.inc new file mode 100644 index 00000000..c4776f6f --- /dev/null +++ b/test/Sniffs/Namespaces/UniqueImportUnitTest.2.inc @@ -0,0 +1,43 @@ +closure = function() use($a) {}; + } + } +} diff --git a/test/Sniffs/Namespaces/UniqueImportUnitTest.inc b/test/Sniffs/Namespaces/UniqueImportUnitTest.inc new file mode 100644 index 00000000..3e41f6d8 --- /dev/null +++ b/test/Sniffs/Namespaces/UniqueImportUnitTest.inc @@ -0,0 +1,33 @@ +closure = function() use($a) {}; + } +} diff --git a/test/Sniffs/Namespaces/UniqueImportUnitTest.php b/test/Sniffs/Namespaces/UniqueImportUnitTest.php new file mode 100644 index 00000000..a753639c --- /dev/null +++ b/test/Sniffs/Namespaces/UniqueImportUnitTest.php @@ -0,0 +1,48 @@ + 1, + 11 => 1, + 12 => 1, + 15 => 1, + 19 => 1, + 23 => 1, + ]; + case 'UniqueImportUnitTest.2.inc': + return [ + 6 => 1, + 10 => 1, + 11 => 1, + 14 => 1, + 18 => 1, + 22 => 1, + ]; + } + + return [ + 5 => 1, + 9 => 1, + 10 => 1, + 13 => 1, + 17 => 1, + 21 => 1, + ]; + } + + protected function getWarningList(string $testFile = '') : array + { + return []; + } +}