diff --git a/Magento2/Sniffs/Classes/DiscouragedDependenciesSniff.php b/Magento2/Sniffs/Classes/DiscouragedDependenciesSniff.php new file mode 100644 index 00000000..ed106d4b --- /dev/null +++ b/Magento2/Sniffs/Classes/DiscouragedDependenciesSniff.php @@ -0,0 +1,280 @@ +getFilename(); + if ($this->currentFile != $currentFileName) { + // Clear aliases + $this->aliases = []; + $this->currentFile = $currentFileName; + } + + // Match use statements and constructor (latter matches T_FUNCTION to find constructors) + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] == T_USE) { + $this->processUse($phpcsFile, $stackPtr, $tokens); + } elseif ($tokens[$stackPtr]['code'] == T_FUNCTION) { + $this->processFunction($phpcsFile, $stackPtr, $tokens); + } + } + + /** + * Store plugin/proxy class names for use in matching constructor + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + */ + private function processUse( + File $phpcsFile, + $stackPtr, + $tokens + ) { + // Find end of use statement and position of AS alias if exists + $endPos = $phpcsFile->findNext(T_SEMICOLON, $stackPtr); + $asPos = $phpcsFile->findNext(T_AS, $stackPtr, $endPos); + // Find whether this use statement includes any of the warning words + $includesWarnWord = + $this->includesWarnWordsInSTRINGs( + $phpcsFile, + $stackPtr, + min($asPos, $endPos), + $tokens, + $lastWord + ); + if (! $includesWarnWord) { + return; + } + // If there is an alias then store this explicit alias for matching in constructor + if ($asPos) { + $aliasNamePos = $asPos + 2; + $this->aliases[] = strtolower($tokens[$aliasNamePos]['content']); + } + // Always store last word as alias for checking in constructor + $this->aliases[] = $lastWord; + } + + /** + * If constructor, check for proxy/plugin names and warn + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + */ + private function processFunction( + File $phpcsFile, + $stackPtr, + $tokens + ) { + // Find start and end of constructor signature based on brackets + if (! $this->getConstructorPosition($phpcsFile, $stackPtr, $tokens, $openParenth, $closeParenth)) { + return; + } + $positionInConstrSig = $openParenth; + $lastName = null; + do { + // Find next part of namespace (string) or variable name + $positionInConstrSig = $phpcsFile->findNext( + [T_STRING, T_VARIABLE], + $positionInConstrSig + 1, + $closeParenth + ); + + $currentTokenIsString = $tokens[$positionInConstrSig]['code'] == T_STRING; + + if ($currentTokenIsString) { + // Remember string in case this is last before variable + $lastName = strtolower($tokens[$positionInConstrSig]['content']); + } else { + // If this is a variable, check last word for matches as was end of classname/alias + if ($lastName !== null) { + $namesToWarn = $this->mergedNamesToWarn(true); + if ($this->containsWord($namesToWarn, $lastName)) { + $phpcsFile->addError( + $this->warningMessage, + $positionInConstrSig, + $this->warningCode, + [$lastName] + ); + } + $lastName = null; + } + } + + } while ($positionInConstrSig !== false && $positionInConstrSig < $closeParenth); + } + + /** + * Sets start and end of constructor signature or returns false + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * @param int $openParenth + * @param int $closeParenth + * + * @return bool Whether a constructor + */ + private function getConstructorPosition( + File $phpcsFile, + $stackPtr, + array $tokens, + &$openParenth, + &$closeParenth + ) { + $methodNamePos = $phpcsFile->findNext(T_STRING, $stackPtr - 1); + if ($methodNamePos === false) { + return false; + } + // There is a method name + if ($tokens[$methodNamePos]['content'] != self::CONSTRUCT_METHOD_NAME) { + return false; + } + + // KNOWN: There is a constructor, so get position + + $openParenth = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $methodNamePos); + $closeParenth = $phpcsFile->findNext(T_CLOSE_PARENTHESIS, $openParenth); + if ($openParenth === false || $closeParenth === false) { + return false; + } + + return true; + } + + /** + * Whether $name exactly matches any of $haystacks + * + * @param array $haystacks + * @param string $name + * + * @return bool + */ + private function containsWord($haystacks, $name) + { + return in_array($name, $haystacks); + } + + /** + * Whether warn words are included in STRING tokens in the given range + * + * Populates $lastWord in set to get usable name from namespace + * + * @param File $phpcsFile + * @param int $startPosition + * @param int $endPosition + * @param array $tokens + * @param string|null $lastWord + * + * @return bool + */ + private function includesWarnWordsInSTRINGs( + File $phpcsFile, + $startPosition, + $endPosition, + $tokens, + &$lastWord + ) { + $includesWarnWord = false; + $currentPosition = $startPosition; + do { + $currentPosition = $phpcsFile->findNext(T_STRING, $currentPosition + 1, $endPosition); + if ($currentPosition !== false) { + $word = strtolower($tokens[$currentPosition]['content']); + if ($this->containsWord($this->mergedNamesToWarn(false), $word)) { + $includesWarnWord = true; + } + $lastWord = $word; + } + } while ($currentPosition !== false && $currentPosition < $endPosition); + + return $includesWarnWord; + } + + /** + * Get array of names that if matched should raise warning. + * + * Includes aliases if required + * + * @param bool $includeAliases + * + * @return array + */ + private function mergedNamesToWarn($includeAliases = false) + { + $namesToWarn = $this->incorrectClassNames; + if ($includeAliases) { + $namesToWarn = array_merge($namesToWarn, $this->aliases); + } + + return $namesToWarn; + } +} diff --git a/Magento2/Tests/Classes/DiscouragedDependenciesUnitTest.1.inc b/Magento2/Tests/Classes/DiscouragedDependenciesUnitTest.1.inc new file mode 100644 index 00000000..43ab86ea --- /dev/null +++ b/Magento2/Tests/Classes/DiscouragedDependenciesUnitTest.1.inc @@ -0,0 +1,55 @@ + 1, + 37 => 1, + 44 => 1 + ]; + } elseif ($testFile === 'DiscouragedDependenciesUnitTest.2.inc') { + return [ + 17 => 1, + 37 => 1, + 44 => 1 + ]; + } + + return []; + } + + /** + * @inheritdoc + */ + public function getWarningList() + { + return []; + } +} diff --git a/Magento2/ruleset.xml b/Magento2/ruleset.xml index 7f950bf5..2f9e5f4b 100644 --- a/Magento2/ruleset.xml +++ b/Magento2/ruleset.xml @@ -28,6 +28,11 @@ 10 error + + 10 + error + */Test/* + 10 error