diff --git a/SymfonyCustom/Helpers/FixerHelper.php b/SymfonyCustom/Helpers/FixerHelper.php index 408c45b..4b92d68 100644 --- a/SymfonyCustom/Helpers/FixerHelper.php +++ b/SymfonyCustom/Helpers/FixerHelper.php @@ -6,6 +6,8 @@ use PHP_CodeSniffer\Files\File; +use function str_repeat; + /** * Class FixerHelper */ diff --git a/SymfonyCustom/Helpers/SniffHelper.php b/SymfonyCustom/Helpers/SniffHelper.php index e2f92e2..fd83756 100644 --- a/SymfonyCustom/Helpers/SniffHelper.php +++ b/SymfonyCustom/Helpers/SniffHelper.php @@ -7,6 +7,10 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; +use function mb_strtolower; +use function preg_match; +use function trim; + /** * Class SniffHelper */ @@ -187,6 +191,67 @@ public static function isGlobalUse(File $phpcsFile, int $stackPtr): bool return true; } + /** + * @param File $phpcsFile + * @param int $scopePtr + * + * @return array + */ + public static 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; + } + + $use = $phpcsFile->findNext(T_USE, $start + 1, $end); + while (false !== $use && T_USE === $tokens[$use]['code']) { + if ( + !self::isGlobalUse($phpcsFile, $use) + || (null !== $end + && (!isset($tokens[$use]['conditions'][$scopePtr]) + || $tokens[$use]['level'] !== $tokens[$scopePtr]['level'] + 1)) + ) { + $use = $phpcsFile->findNext(Tokens::$emptyTokens, $use + 1, $end, true); + continue; + } + + // find semicolon as the end of the global use scope + $endOfScope = $phpcsFile->findNext(T_SEMICOLON, $use + 1); + + $startOfName = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $use + 1, $endOfScope); + + $type = 'class'; + if (T_STRING === $tokens[$startOfName]['code']) { + $lowerContent = mb_strtolower($tokens[$startOfName]['content']); + if ('function' === $lowerContent || 'const' === $lowerContent) { + $type = $lowerContent; + + $startOfName = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $startOfName + 1, $endOfScope); + } + } + + $uses[] = [ + 'ptrUse' => $use, + 'name' => trim($phpcsFile->getTokensAsString($startOfName, $endOfScope - $startOfName)), + 'ptrEnd' => $endOfScope, + 'string' => trim($phpcsFile->getTokensAsString($use, $endOfScope - $use + 1)), + 'type' => $type, + ]; + + $use = $phpcsFile->findNext(Tokens::$emptyTokens, $endOfScope + 1, $end, true); + } + + return $uses; + } + /** * @param string $content * diff --git a/SymfonyCustom/Sniffs/Arrays/ArrayDeclarationSniff.php b/SymfonyCustom/Sniffs/Arrays/ArrayDeclarationSniff.php index 6493f6c..c650477 100644 --- a/SymfonyCustom/Sniffs/Arrays/ArrayDeclarationSniff.php +++ b/SymfonyCustom/Sniffs/Arrays/ArrayDeclarationSniff.php @@ -9,6 +9,10 @@ use PHP_CodeSniffer\Util\Tokens; use SymfonyCustom\Helpers\FixerHelper; +use function count; +use function in_array; +use function mb_strlen; + /** * A test to ensure that arrays conform to the array coding standard. */ diff --git a/SymfonyCustom/Sniffs/Commenting/DocCommentForbiddenTagsSniff.php b/SymfonyCustom/Sniffs/Commenting/DocCommentForbiddenTagsSniff.php index 86ca211..d57945d 100644 --- a/SymfonyCustom/Sniffs/Commenting/DocCommentForbiddenTagsSniff.php +++ b/SymfonyCustom/Sniffs/Commenting/DocCommentForbiddenTagsSniff.php @@ -7,6 +7,8 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use function in_array; + /** * Throws errors if forbidden tags are met. */ diff --git a/SymfonyCustom/Sniffs/Commenting/DocCommentGroupSameTypeSniff.php b/SymfonyCustom/Sniffs/Commenting/DocCommentGroupSameTypeSniff.php index 1f699be..695a871 100644 --- a/SymfonyCustom/Sniffs/Commenting/DocCommentGroupSameTypeSniff.php +++ b/SymfonyCustom/Sniffs/Commenting/DocCommentGroupSameTypeSniff.php @@ -10,6 +10,9 @@ use SymfonyCustom\Helpers\FixerHelper; use SymfonyCustom\Helpers\SniffHelper; +use function array_merge; +use function in_array; + /** * Throws errors if comments are not grouped by type with one blank line between them. */ diff --git a/SymfonyCustom/Sniffs/Commenting/DocCommentSniff.php b/SymfonyCustom/Sniffs/Commenting/DocCommentSniff.php index 672d0a8..ecf76cf 100644 --- a/SymfonyCustom/Sniffs/Commenting/DocCommentSniff.php +++ b/SymfonyCustom/Sniffs/Commenting/DocCommentSniff.php @@ -8,6 +8,10 @@ use PHP_CodeSniffer\Sniffs\Sniff; use SymfonyCustom\Helpers\FixerHelper; +use function ltrim; +use function rtrim; +use function str_repeat; + /** * Ensures doc blocks follow basic formatting. */ diff --git a/SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php b/SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php index b1eaf8a..6787610 100644 --- a/SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php +++ b/SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php @@ -9,6 +9,10 @@ use PHP_CodeSniffer\Util\Tokens; use SymfonyCustom\Helpers\SniffHelper; +use function count; +use function preg_match; +use function preg_replace; + /** * SymfonyCustom standard customization to PEARs FunctionCommentSniff. */ diff --git a/SymfonyCustom/Sniffs/Commenting/VariableCommentSniff.php b/SymfonyCustom/Sniffs/Commenting/VariableCommentSniff.php index 8907ec2..e1118fa 100644 --- a/SymfonyCustom/Sniffs/Commenting/VariableCommentSniff.php +++ b/SymfonyCustom/Sniffs/Commenting/VariableCommentSniff.php @@ -8,6 +8,9 @@ use PHP_CodeSniffer\Sniffs\AbstractVariableSniff; use SymfonyCustom\Helpers\SniffHelper; +use function preg_match; +use function preg_replace; + /** * Parses and verifies the variable doc comment. */ diff --git a/SymfonyCustom/Sniffs/Formatting/BlankLineBeforeReturnSniff.php b/SymfonyCustom/Sniffs/Formatting/BlankLineBeforeReturnSniff.php index 1bc2e12..2650071 100644 --- a/SymfonyCustom/Sniffs/Formatting/BlankLineBeforeReturnSniff.php +++ b/SymfonyCustom/Sniffs/Formatting/BlankLineBeforeReturnSniff.php @@ -8,6 +8,8 @@ use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Util\Tokens; +use function in_array; + /** * Throws errors if there's no blank line before return statements. */ diff --git a/SymfonyCustom/Sniffs/Formatting/YodaConditionSniff.php b/SymfonyCustom/Sniffs/Formatting/YodaConditionSniff.php index cf9ce4d..1781a8e 100644 --- a/SymfonyCustom/Sniffs/Formatting/YodaConditionSniff.php +++ b/SymfonyCustom/Sniffs/Formatting/YodaConditionSniff.php @@ -8,6 +8,9 @@ use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Util\Tokens; +use function array_merge; +use function in_array; + /** * Enforces Yoda conditional statements. */ diff --git a/SymfonyCustom/Sniffs/Functions/ScopeOrderSniff.php b/SymfonyCustom/Sniffs/Functions/ScopeOrderSniff.php index 4c7f6f7..7d0b689 100644 --- a/SymfonyCustom/Sniffs/Functions/ScopeOrderSniff.php +++ b/SymfonyCustom/Sniffs/Functions/ScopeOrderSniff.php @@ -7,6 +7,9 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use function array_keys; +use function in_array; + /** * Throws warnings if properties are declared after methods */ diff --git a/SymfonyCustom/Sniffs/Namespaces/AlphabeticallySortedUseSniff.php b/SymfonyCustom/Sniffs/Namespaces/AlphabeticallySortedUseSniff.php index 08c5883..1302065 100644 --- a/SymfonyCustom/Sniffs/Namespaces/AlphabeticallySortedUseSniff.php +++ b/SymfonyCustom/Sniffs/Namespaces/AlphabeticallySortedUseSniff.php @@ -6,9 +6,12 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -use PHP_CodeSniffer\Util\Tokens; use SymfonyCustom\Helpers\SniffHelper; +use function explode; +use function str_replace; +use function strcasecmp; + /** * Class AlphabeticallySortedUseSniff */ @@ -40,7 +43,7 @@ public function process(File $phpcsFile, $stackPtr): int } } - $uses = $this->getUseStatements($phpcsFile, $stackPtr); + $uses = SniffHelper::getUseStatements($phpcsFile, $stackPtr); $lastUse = null; foreach ($uses as $use) { @@ -68,67 +71,6 @@ public function process(File $phpcsFile, $stackPtr): int return T_OPEN_TAG === $tokens[$stackPtr]['code'] ? $phpcsFile->numTokens + 1 : $stackPtr + 1; } - /** - * @param File $phpcsFile - * @param int $scopePtr - * - * @return array - */ - 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; - } - - $use = $phpcsFile->findNext(T_USE, $start + 1, $end); - while (false !== $use && T_USE === $tokens[$use]['code']) { - if ( - !SniffHelper::isGlobalUse($phpcsFile, $use) - || (null !== $end - && (!isset($tokens[$use]['conditions'][$scopePtr]) - || $tokens[$use]['level'] !== $tokens[$scopePtr]['level'] + 1)) - ) { - $use = $phpcsFile->findNext(Tokens::$emptyTokens, $use + 1, $end, true); - continue; - } - - // find semicolon as the end of the global use scope - $endOfScope = $phpcsFile->findNext(T_SEMICOLON, $use + 1); - - $startOfName = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $use + 1, $endOfScope); - - $type = 'class'; - if (T_STRING === $tokens[$startOfName]['code']) { - $lowerContent = mb_strtolower($tokens[$startOfName]['content']); - if ('function' === $lowerContent || 'const' === $lowerContent) { - $type = $lowerContent; - - $startOfName = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $startOfName + 1, $endOfScope); - } - } - - $uses[] = [ - 'ptrUse' => $use, - 'name' => trim($phpcsFile->getTokensAsString($startOfName, $endOfScope - $startOfName)), - 'ptrEnd' => $endOfScope, - 'string' => trim($phpcsFile->getTokensAsString($use, $endOfScope - $use + 1)), - 'type' => $type, - ]; - - $use = $phpcsFile->findNext(Tokens::$emptyTokens, $endOfScope + 1, $end, true); - } - - return $uses; - } - /** * @param array $a * @param array $b diff --git a/SymfonyCustom/Sniffs/Namespaces/UnusedUseSniff.php b/SymfonyCustom/Sniffs/Namespaces/UnusedUseSniff.php index 7d470a5..7ec62ff 100644 --- a/SymfonyCustom/Sniffs/Namespaces/UnusedUseSniff.php +++ b/SymfonyCustom/Sniffs/Namespaces/UnusedUseSniff.php @@ -9,6 +9,15 @@ use PHP_CodeSniffer\Util\Tokens; use SymfonyCustom\Helpers\SniffHelper; +use function in_array; +use function mb_strrpos; +use function mb_strtolower; +use function mb_substr; +use function preg_match; +use function preg_quote; +use function strcasecmp; +use function trim; + /** * Checks for "use" statements that are not needed in a file. */ @@ -300,7 +309,7 @@ private function isClassUsed(File $phpcsFile, int $usePtr, int $classPtr): bool if (strcasecmp($namespace, $useNamespace) === 0) { $classUsed = false; } - } elseif (false === $namespacePtr && '' === $useNamespace) { + } elseif ('' === $useNamespace) { $classUsed = false; } } diff --git a/SymfonyCustom/Sniffs/NamingConventions/ValidClassNameSniff.php b/SymfonyCustom/Sniffs/NamingConventions/ValidClassNameSniff.php index 67e4213..9e2b944 100644 --- a/SymfonyCustom/Sniffs/NamingConventions/ValidClassNameSniff.php +++ b/SymfonyCustom/Sniffs/NamingConventions/ValidClassNameSniff.php @@ -7,6 +7,9 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use function mb_strlen; +use function mb_substr; + /** * Throws errors if symfony's naming conventions are not met. */ diff --git a/SymfonyCustom/Sniffs/NamingConventions/ValidFileNameSniff.php b/SymfonyCustom/Sniffs/NamingConventions/ValidFileNameSniff.php index cb16100..1603037 100644 --- a/SymfonyCustom/Sniffs/NamingConventions/ValidFileNameSniff.php +++ b/SymfonyCustom/Sniffs/NamingConventions/ValidFileNameSniff.php @@ -7,6 +7,10 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use function basename; +use function ctype_alnum; +use function mb_strlen; + /** * Checks whether filename contains any other character than alphanumeric and underscores. */ diff --git a/SymfonyCustom/Sniffs/NamingConventions/ValidTypeHintSniff.php b/SymfonyCustom/Sniffs/NamingConventions/ValidTypeHintSniff.php index 8511bd6..7006eb6 100644 --- a/SymfonyCustom/Sniffs/NamingConventions/ValidTypeHintSniff.php +++ b/SymfonyCustom/Sniffs/NamingConventions/ValidTypeHintSniff.php @@ -9,6 +9,20 @@ use PHP_CodeSniffer\Sniffs\Sniff; use SymfonyCustom\Helpers\SniffHelper; +use function array_pop; +use function array_unique; +use function count; +use function implode; +use function in_array; +use function mb_strlen; +use function mb_strpos; +use function mb_strtolower; +use function mb_substr; +use function preg_match; +use function preg_replace; +use function preg_split; +use function usort; + /** * Throws errors if PHPDocs type hint are not valid. */ @@ -233,7 +247,7 @@ private function getValidType(string $typeName): string return self::TYPES[$lowerType] ? $lowerType : $typeName; } - // This can't be case insensitive since this is not reserved keyword + // This can't be case-insensitive since this is not reserved keyword if (isset(self::ALIAS_TYPES[$typeName])) { return self::ALIAS_TYPES[$typeName]; } diff --git a/SymfonyCustom/Sniffs/Operators/OperatorPlacementSniff.php b/SymfonyCustom/Sniffs/Operators/OperatorPlacementSniff.php new file mode 100644 index 0000000..29473f8 --- /dev/null +++ b/SymfonyCustom/Sniffs/Operators/OperatorPlacementSniff.php @@ -0,0 +1,57 @@ +getTokens(); + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + + if ($tokens[$stackPtr]['line'] === $tokens[$next]['line']) { + return; + } + + $content = $tokens[$stackPtr]['content']; + $error = 'Operator "%s" should be on the start of the next line'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'OperatorPlacement', [$content]); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($stackPtr, ''); + $phpcsFile->fixer->addContentBefore($next, $content); + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/SymfonyCustom/Sniffs/PHP/ImportInternalFunctionSniff.php b/SymfonyCustom/Sniffs/PHP/ImportInternalFunctionSniff.php new file mode 100644 index 0000000..c76e4b0 --- /dev/null +++ b/SymfonyCustom/Sniffs/PHP/ImportInternalFunctionSniff.php @@ -0,0 +1,129 @@ +getTokens(); + + $uses = SniffHelper::getUseStatements($phpcsFile, $stackPtr); + $functionUses = array_map( + function (array $use): string { + return $use['name']; + }, + array_filter($uses, function (array $use): bool { + return 'function' === $use['type']; + }) + ); + + $nextString = $phpcsFile->findNext([T_NAMESPACE, T_STRING], $stackPtr + 1); + while (false !== $nextString && T_NAMESPACE !== $tokens[$nextString]['code']) { + $this->processString($phpcsFile, $nextString, $functionUses); + $nextString = $phpcsFile->findNext([T_NAMESPACE, T_STRING], $nextString + 1); + } + } + + /** + * @param File $phpcsFile + * @param int $stackPtr + * @param array $functionUses + * + * @return void + */ + private function processString(File $phpcsFile, int $stackPtr, array $functionUses): void + { + $tokens = $phpcsFile->getTokens(); + + $ignore = [ + T_DOUBLE_COLON => true, + T_OBJECT_OPERATOR => true, + T_NULLSAFE_OBJECT_OPERATOR => true, + T_FUNCTION => true, + T_CONST => true, + T_PUBLIC => true, + T_PRIVATE => true, + T_PROTECTED => true, + T_AS => true, + T_NEW => true, + T_INSTEADOF => true, + T_NS_SEPARATOR => true, + T_IMPLEMENTS => true, + ]; + + $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + + // If function call is directly preceded by a NS_SEPARATOR it points to the + // global namespace, so we should still catch it. + if (T_NS_SEPARATOR === $tokens[$prevToken]['code']) { + $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($prevToken - 1), null, true); + if (T_STRING === $tokens[$prevToken]['code']) { + // Not in the global namespace. + return; + } + } + + if (isset($ignore[$tokens[$prevToken]['code']])) { + // Not a call to a function. + return; + } + + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if (isset($ignore[$tokens[$nextToken]['code']])) { + // Not a call to a function. + return; + } + + if (T_STRING === $tokens[$stackPtr]['code'] && T_OPEN_PARENTHESIS !== $tokens[$nextToken]['code']) { + // Not a call to a function. + return; + } + + $function = mb_strtolower($tokens[$stackPtr]['content']); + + $internalFunctions = get_defined_functions()['internal']; + if (!in_array($function, $internalFunctions)) { + // Not a call to a PHP function. + return; + } + + if (!in_array($function, $functionUses)) { + $phpcsFile->addError( + 'PHP internal function "%s" must be imported', + $stackPtr, + 'IncorrectOrder', + [$function] + ); + } + } +} diff --git a/SymfonyCustom/Sniffs/WhiteSpace/DocCommentTagSpacingSniff.php b/SymfonyCustom/Sniffs/WhiteSpace/DocCommentTagSpacingSniff.php index 34fb39c..19cdb9d 100644 --- a/SymfonyCustom/Sniffs/WhiteSpace/DocCommentTagSpacingSniff.php +++ b/SymfonyCustom/Sniffs/WhiteSpace/DocCommentTagSpacingSniff.php @@ -8,6 +8,8 @@ use PHP_CodeSniffer\Sniffs\Sniff; use SymfonyCustom\Helpers\SniffHelper; +use function in_array; + /** * Checks that there are not 2 empty lines following each other. */ diff --git a/SymfonyCustom/Sniffs/WhiteSpace/OpenBracketSpacingSniff.php b/SymfonyCustom/Sniffs/WhiteSpace/OpenBracketSpacingSniff.php index 09321f7..05a9fac 100644 --- a/SymfonyCustom/Sniffs/WhiteSpace/OpenBracketSpacingSniff.php +++ b/SymfonyCustom/Sniffs/WhiteSpace/OpenBracketSpacingSniff.php @@ -7,6 +7,8 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use function mb_strpos; + /** * Checks that there is no white space after an opening bracket, for "(", "{", and array bracket. * Square Brackets are handled by Squiz_Sniffs_Arrays_ArrayBracketSpacingSniff. diff --git a/SymfonyCustom/Sniffs/WhiteSpace/UnaryOperatorSpacingSniff.php b/SymfonyCustom/Sniffs/WhiteSpace/UnaryOperatorSpacingSniff.php index 452c13e..e0c76da 100644 --- a/SymfonyCustom/Sniffs/WhiteSpace/UnaryOperatorSpacingSniff.php +++ b/SymfonyCustom/Sniffs/WhiteSpace/UnaryOperatorSpacingSniff.php @@ -7,6 +7,8 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use function in_array; + /** * Ensures there are no spaces +/- sign operators. */ diff --git a/SymfonyCustom/Tests/Operators/OperatorPlacementUnitTest.inc b/SymfonyCustom/Tests/Operators/OperatorPlacementUnitTest.inc new file mode 100644 index 0000000..9946f73 --- /dev/null +++ b/SymfonyCustom/Tests/Operators/OperatorPlacementUnitTest.inc @@ -0,0 +1,20 @@ + + */ + protected function getErrorList(): array + { + return [ + 3 => 1, + 6 => 1, + 12 => 1, + 13 => 1, + 16 => 1, + 19 => 1, + ]; + } + + /** + * @return array + */ + protected function getWarningList(): array + { + return []; + } +} diff --git a/SymfonyCustom/Tests/PHP/ImportInternalFunctionUnitTest.inc b/SymfonyCustom/Tests/PHP/ImportInternalFunctionUnitTest.inc new file mode 100644 index 0000000..1ae4b2d --- /dev/null +++ b/SymfonyCustom/Tests/PHP/ImportInternalFunctionUnitTest.inc @@ -0,0 +1,11 @@ + + */ + protected function getErrorList(): array + { + return [ + 11 => 1, + ]; + } + + /** + * @return array + */ + protected function getWarningList(): array + { + return []; + } +}