From 03e9bcade5f074bdab874afc353419790aa55c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bundyra?= Date: Sun, 10 Nov 2019 11:47:36 +0000 Subject: [PATCH 1/2] Feature: Namespaces\UniqueImport sniff Check if the class/function/constant is imported only once. Check if the name is used only once. The same name can be used for class, function and constant. Class and function names are case insensitive, constant names are case sensitive, and also namespaces are case sensitive. --- .../Sniffs/Namespaces/UniqueImportSniff.php | 168 ++++++++++++++++++ .../Namespaces/UniqueImportUnitTest.1.inc | 37 ++++ .../Namespaces/UniqueImportUnitTest.2.inc | 43 +++++ .../Namespaces/UniqueImportUnitTest.inc | 33 ++++ .../Namespaces/UniqueImportUnitTest.php | 48 +++++ 5 files changed, 329 insertions(+) create mode 100644 src/WebimpressCodingStandard/Sniffs/Namespaces/UniqueImportSniff.php create mode 100644 test/Sniffs/Namespaces/UniqueImportUnitTest.1.inc create mode 100644 test/Sniffs/Namespaces/UniqueImportUnitTest.2.inc create mode 100644 test/Sniffs/Namespaces/UniqueImportUnitTest.inc create mode 100644 test/Sniffs/Namespaces/UniqueImportUnitTest.php 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 []; + } +} From b6158f64acc0a94374e2a32fc65263f37f2620e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bundyra?= Date: Mon, 11 Nov 2019 13:25:39 +0000 Subject: [PATCH 2/2] Adds CHANGELOG entry for #54 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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.