├── src ├── Composer │ ├── templates │ │ └── git │ │ │ └── hooks │ │ │ └── pre-commit-phpcs │ └── ScriptHandler.php └── ZenifyCodingStandard │ ├── Helper │ ├── Whitespace │ │ ├── WhitespaceFinder.php │ │ ├── EmptyLinesResizer.php │ │ └── ClassMetrics.php │ ├── PositionFinder.php │ └── Commenting │ │ ├── MethodDocBlock.php │ │ └── FunctionHelper.php │ ├── Sniffs │ ├── Debug │ │ └── DebugFunctionCallSniff.php │ ├── Naming │ │ ├── InterfaceNameSniff.php │ │ └── AbstractClassNameSniff.php │ ├── Namespaces │ │ ├── ClassNamesWithoutPreSlashSniff.php │ │ ├── UseDeclarationSniff.php │ │ └── NamespaceDeclarationSniff.php │ ├── WhiteSpace │ │ ├── ExclamationMarkSniff.php │ │ ├── DocBlockSniff.php │ │ ├── IfElseTryCatchFinallySniff.php │ │ ├── PropertiesMethodsMutualSpacingSniff.php │ │ └── InBetweenMethodSpacingSniff.php │ ├── Commenting │ │ ├── MethodCommentReturnTagSniff.php │ │ ├── MethodCommentSniff.php │ │ ├── VarPropertyCommentSniff.php │ │ ├── ComponentFactoryCommentSniff.php │ │ └── BlockPropertyCommentSniff.php │ ├── ControlStructures │ │ ├── YodaConditionSniff.php │ │ ├── NewClassSniff.php │ │ └── SwitchDeclarationSniff.php │ └── Classes │ │ ├── FinalInterfaceSniff.php │ │ └── ClassDeclarationSniff.php │ └── ruleset.xml ├── composer.json └── LICENSE /src/Composer/templates/git/hooks/pre-commit-phpcs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # get changed .php files, ready to be commited, all but deleted 4 | FILES=$(git diff --name-only --cached --diff-filter=ACMRTUXB | grep .php); 5 | 6 | # run phpcs 7 | if [ ! -z "$FILES" ]; then 8 | printf "Running Code Sniffer..." 9 | vendor/bin/phpcs $FILES --standard=vendor/zenify/coding-standard/src/ZenifyCodingStandard/ruleset.xml 10 | 11 | if [ $? -ne 0 ] 12 | then 13 | printf "\033[0;41;37mFix coding standards before commit!\033[0m\n" 14 | exit 1 15 | fi 16 | fi 17 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Helper/Whitespace/WhitespaceFinder.php: -------------------------------------------------------------------------------- 1 | getTokens(); 21 | 22 | $currentLine = $tokens[$position]['line']; 23 | $nextLinePosition = $position; 24 | do { 25 | ++$nextLinePosition; 26 | $nextLine = $tokens[$nextLinePosition]['line']; 27 | } while ($currentLine === $nextLine); 28 | 29 | return $nextLinePosition; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zenify/coding-standard", 3 | "description": "Set of rules for PHP_CodeSniffer preferring tabs and based on Nette coding standard.", 4 | "license": "MIT", 5 | "authors": [ 6 | { "name": "Tomáš Votruba", "email": "tomas.vot@gmail.com", "homepage": "http://tomasvotruba.cz" } 7 | ], 8 | "require": { 9 | "php": "^7.0", 10 | "squizlabs/php_codesniffer": "~2.7" 11 | }, 12 | "require-dev": { 13 | "phpunit/phpunit": "^5.6" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Zenify\\CodingStandard\\": "src", 18 | "ZenifyCodingStandard\\": "src/ZenifyCodingStandard" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Zenify\\CodingStandard\\Tests\\": "tests" 24 | } 25 | }, 26 | "scripts": { 27 | "check-cs": "vendor/bin/phpcs src tests -p -sw --standard=src/ZenifyCodingStandard/ruleset.xml --ignore=wrong,correct" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Composer/ScriptHandler.php: -------------------------------------------------------------------------------- 1 | string|NULL) 34 | */ 35 | public $forbiddenFunctions = [ 36 | 'd' => NULL, 37 | 'dd' => NULL, 38 | 'dump' => NULL, 39 | 'var_dump' => NULL 40 | ]; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Helper/PositionFinder.php: -------------------------------------------------------------------------------- 1 | getTokens()[$position]['line']; 23 | while ($file->getTokens()[$currentPosition]['line'] === $line) { 24 | $currentPosition--; 25 | } 26 | 27 | return $currentPosition; 28 | } 29 | 30 | 31 | public static function findLastPositionInCurrentLine(PHP_CodeSniffer_File $file, int $position) : int 32 | { 33 | $currentPosition = $position; 34 | 35 | $line = $file->getTokens()[$position]['line']; 36 | while ($file->getTokens()[$currentPosition]['line'] === $line) { 37 | $currentPosition++; 38 | } 39 | 40 | return $currentPosition; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | --------------- 3 | 4 | Copyright (c) 2012 Tomáš Votruba (http://tomasvotruba.cz) 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Helper/Whitespace/EmptyLinesResizer.php: -------------------------------------------------------------------------------- 1 | $desiredLineCount) { 25 | self::reduceBlankLines($file, $position, $currentLineCount, $desiredLineCount); 26 | 27 | } else { 28 | self::increaseBlankLines($file, $position, $currentLineCount, $desiredLineCount); 29 | } 30 | } 31 | 32 | 33 | private static function reduceBlankLines(PHP_CodeSniffer_File $file, int $position, int $from, int $to) 34 | { 35 | for ($i = $from; $i > $to; $i--) { 36 | $file->fixer->replaceToken($position, ''); 37 | $position++; 38 | } 39 | } 40 | 41 | 42 | private static function increaseBlankLines(PHP_CodeSniffer_File $file, int $position, int $from, int $to) 43 | { 44 | for ($i = $from; $i < $to; $i++) { 45 | $file->fixer->addContentBefore($position, PHP_EOL); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Helper/Commenting/MethodDocBlock.php: -------------------------------------------------------------------------------- 1 | getTokens(); 21 | $currentToken = $tokens[$position]; 22 | $docBlockClosePosition = $file->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $position); 23 | 24 | if ($docBlockClosePosition === FALSE) { 25 | return FALSE; 26 | } 27 | 28 | $docBlockCloseToken = $tokens[$docBlockClosePosition]; 29 | if ($docBlockCloseToken['line'] === ($currentToken['line'] - 1)) { 30 | return TRUE; 31 | } 32 | 33 | return FALSE; 34 | } 35 | 36 | 37 | public static function getMethodDocBlock(PHP_CodeSniffer_File $file, int $position) : string 38 | { 39 | if ( ! self::hasMethodDocBlock($file, $position)) { 40 | return ''; 41 | } 42 | 43 | $commentStart = $file->findPrevious(T_DOC_COMMENT_OPEN_TAG, $position - 1); 44 | $commentEnd = $file->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $position - 1); 45 | return $file->getTokensAsString($commentStart, $commentEnd - $commentStart + 1); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Naming/InterfaceNameSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 55 | $this->position = $position; 56 | 57 | $interfaceName = $this->getInterfaceName(); 58 | if ((strlen($interfaceName) - strlen('Interface')) === strrpos($interfaceName, 'Interface')) { 59 | return; 60 | } 61 | 62 | $file->addError('Interface should have suffix "Interface".', $position); 63 | } 64 | 65 | 66 | /** 67 | * @return string|FALSE 68 | */ 69 | private function getInterfaceName() 70 | { 71 | $namePosition = $this->file->findNext(T_STRING, $this->position); 72 | if ( ! $namePosition) { 73 | return FALSE; 74 | } 75 | 76 | return $this->file->getTokens()[$namePosition]['content']; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Namespaces/ClassNamesWithoutPreSlashSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 52 | $classNameStart = $tokens[$position + 2]['content']; 53 | 54 | if ($classNameStart === '\\') { 55 | if ($this->isExcludedClassName($tokens[$position + 3]['content'])) { 56 | return; 57 | } 58 | $file->addError('Class name after new/instanceof should not start with slash.', $position); 59 | } 60 | } 61 | 62 | 63 | /** 64 | * @param string $className 65 | * @return bool 66 | */ 67 | private function isExcludedClassName($className) 68 | { 69 | if (in_array($className, $this->excludedClassNames)) { 70 | return TRUE; 71 | } 72 | return FALSE; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/WhiteSpace/ExclamationMarkSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 50 | 51 | $tokens = $file->getTokens(); 52 | $hasSpaceBefore = $tokens[$position - 1]['code'] === T_WHITESPACE; 53 | $hasSpaceAfter = $tokens[$position + 1]['code'] === T_WHITESPACE; 54 | 55 | if ( ! $hasSpaceBefore || ! $hasSpaceAfter) { 56 | $error = 'Not operator (!) should be surrounded by spaces.'; 57 | $fix = $file->addFixableError($error, $position); 58 | if ($fix) { 59 | $this->fixSpacesAroundExclamationMark($position, $hasSpaceBefore, $hasSpaceAfter); 60 | } 61 | } 62 | } 63 | 64 | 65 | private function fixSpacesAroundExclamationMark(int $position, bool $isSpaceBefore, bool $isSpaceAfter) 66 | { 67 | if ( ! $isSpaceBefore) { 68 | $this->file->fixer->addContentBefore($position, ' '); 69 | } 70 | 71 | if ( ! $isSpaceAfter) { 72 | $this->file->fixer->addContentBefore($position + 1, ' '); 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Naming/AbstractClassNameSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 55 | $this->position = $position; 56 | 57 | if ( ! $this->isClassAbstract()) { 58 | return; 59 | } 60 | 61 | if (strpos($this->getClassName(), 'Abstract') === 0) { 62 | return; 63 | } 64 | 65 | $file->addError('Abstract class should have prefix "Abstract".', $position); 66 | } 67 | 68 | 69 | private function isClassAbstract() : bool 70 | { 71 | $classProperties = $this->file->getClassProperties($this->position); 72 | return $classProperties['is_abstract']; 73 | } 74 | 75 | 76 | /** 77 | * @return string|FALSE 78 | */ 79 | private function getClassName() 80 | { 81 | $namePosition = $this->file->findNext(T_STRING, $this->position); 82 | if ( ! $namePosition) { 83 | return FALSE; 84 | } 85 | 86 | return $this->file->getTokens()[$namePosition]['content']; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Helper/Commenting/FunctionHelper.php: -------------------------------------------------------------------------------- 1 | getTokens(); 27 | $isAbstract = self::isAbstract($codeSnifferFile, $functionPointer); 28 | $colonToken = $isAbstract 29 | ? $codeSnifferFile->findNext( 30 | T_COLON, $tokens[$functionPointer]['parenthesis_closer'] + 1, NULL, FALSE, NULL, TRUE 31 | ) 32 | : $codeSnifferFile->findNext( 33 | T_COLON, $tokens[$functionPointer]['parenthesis_closer'] + 1, $tokens[$functionPointer]['scope_opener'] - 1 34 | ); 35 | 36 | if ($colonToken === FALSE) { 37 | return NULL; 38 | } 39 | $returnTypeHint = NULL; 40 | $nextToken = $colonToken; 41 | 42 | do { 43 | $nextToken = $isAbstract 44 | ? $codeSnifferFile->findNext([T_WHITESPACE, T_COMMENT, T_SEMICOLON], $nextToken + 1, NULL, TRUE, NULL, TRUE) 45 | : $codeSnifferFile->findNext( 46 | [T_WHITESPACE, T_COMMENT], $nextToken + 1, $tokens[$functionPointer]['scope_opener'] - 1, TRUE 47 | ); 48 | 49 | $isTypeHint = $nextToken !== FALSE; 50 | if ($isTypeHint) { 51 | $returnTypeHint .= $tokens[$nextToken]['content']; 52 | } 53 | } while ($isTypeHint); 54 | 55 | return $returnTypeHint; 56 | } 57 | 58 | 59 | public static function isAbstract(PHP_CodeSniffer_File $codeSnifferFile, int $functionPointer): bool 60 | { 61 | if ( ! isset($codeSnifferFile->getTokens()[$functionPointer]['scope_opener'])) { 62 | return TRUE; 63 | } 64 | 65 | if ($codeSnifferFile->getTokens()[$functionPointer]['scope_opener'] >= 39) { 66 | return TRUE; 67 | } 68 | 69 | return FALSE; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Commenting/MethodCommentReturnTagSniff.php: -------------------------------------------------------------------------------- 1 | getDeclarationName($position); 49 | $isGetterMethod = $this->guessIsGetterMethod($methodName); 50 | if ($isGetterMethod === FALSE) { 51 | return; 52 | } 53 | 54 | $returnTypeHint = FunctionHelper::findReturnTypeHint($file, $position); 55 | if ($returnTypeHint) { 56 | return; 57 | } 58 | 59 | $commentString = MethodDocBlock::getMethodDocBlock($file, $position); 60 | if (strpos($commentString, '@return') !== FALSE) { 61 | return; 62 | } 63 | 64 | $file->addError('Getters should have @return tag or return type.', $position); 65 | } 66 | 67 | 68 | private function guessIsGetterMethod(string $methodName) : bool 69 | { 70 | foreach ($this->getterMethodPrefixes as $getterMethodPrefix) { 71 | if (strpos($methodName, $getterMethodPrefix) === 0) { 72 | if (strlen($methodName) === strlen($getterMethodPrefix)) { 73 | return TRUE; 74 | } 75 | 76 | $endPosition = strlen($getterMethodPrefix); 77 | $firstLetterAfterGetterPrefix = $methodName[$endPosition]; 78 | 79 | if (ctype_upper($firstLetterAfterGetterPrefix)) { 80 | return TRUE; 81 | } 82 | } 83 | } 84 | 85 | return FALSE; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Commenting/MethodCommentSniff.php: -------------------------------------------------------------------------------- 1 | hasMethodDocblock($file, $position)) { 44 | return; 45 | } 46 | 47 | $parameters = $file->getMethodParameters($position); 48 | $parameterCount = count($parameters); 49 | 50 | // 1. method has no parameters 51 | if ($parameterCount === 0) { 52 | return; 53 | } 54 | 55 | // 2. all methods have typehints 56 | if ($parameterCount === $this->countParametersWithTypehint($parameters)) { 57 | return; 58 | } 59 | 60 | $file->addError('Method docblock is missing, due to some parameters without typehints.', $position); 61 | } 62 | 63 | 64 | private function hasMethodDocblock(PHP_CodeSniffer_File $file, int $position) : bool 65 | { 66 | $tokens = $file->getTokens(); 67 | $currentToken = $tokens[$position]; 68 | $docBlockClosePosition = $file->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $position); 69 | 70 | if ($docBlockClosePosition === FALSE) { 71 | return FALSE; 72 | } 73 | 74 | $docBlockCloseToken = $tokens[$docBlockClosePosition]; 75 | if ($docBlockCloseToken['line'] === ($currentToken['line'] - 1)) { 76 | return TRUE; 77 | } 78 | 79 | return FALSE; 80 | } 81 | 82 | 83 | private function countParametersWithTypehint(array $parameters) : int 84 | { 85 | $parameterWithTypehintCount = 0; 86 | foreach ($parameters as $parameter) { 87 | if ($parameter['type_hint']) { 88 | $parameterWithTypehintCount++; 89 | } 90 | } 91 | return $parameterWithTypehintCount; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/ControlStructures/YodaConditionSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 64 | $this->position = $position; 65 | 66 | $previousNonEmptyToken = $this->getPreviousNonEmptyToken(); 67 | 68 | if ( ! $previousNonEmptyToken) { 69 | return; 70 | } 71 | 72 | if ( ! $this->isExpressionToken($previousNonEmptyToken)) { 73 | return; 74 | } 75 | 76 | $file->addError('Yoda condition should not be used; switch expression order', $position); 77 | } 78 | 79 | 80 | /** 81 | * @return array|bool 82 | */ 83 | private function getPreviousNonEmptyToken() 84 | { 85 | $leftTokenPosition = $this->file->findPrevious(T_WHITESPACE, ($this->position - 1), NULL, TRUE); 86 | $tokens = $this->file->getTokens(); 87 | if ($leftTokenPosition) { 88 | return $tokens[$leftTokenPosition]; 89 | } 90 | 91 | return FALSE; 92 | } 93 | 94 | 95 | private function isExpressionToken(array $token) : bool 96 | { 97 | return in_array($token['code'], [T_MINUS, T_NULL, T_FALSE, T_TRUE, T_LNUMBER, T_CONSTANT_ENCAPSED_STRING]); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Commenting/VarPropertyCommentSniff.php: -------------------------------------------------------------------------------- 1 | getPropertyComment($file, $position); 38 | 39 | if (strpos($commentString, '@var') !== FALSE) { 40 | return; 41 | } 42 | 43 | $file->addError('Property should have docblock comment.', $position); 44 | } 45 | 46 | 47 | /** 48 | * @param PHP_CodeSniffer_File $file 49 | * @param int $position 50 | */ 51 | protected function processVariable(PHP_CodeSniffer_File $file, $position) 52 | { 53 | } 54 | 55 | 56 | /** 57 | * @param PHP_CodeSniffer_File $file 58 | * @param int $position 59 | */ 60 | protected function processVariableInString(PHP_CodeSniffer_File $file, $position) 61 | { 62 | } 63 | 64 | 65 | private function getPropertyComment(PHP_CodeSniffer_File $file, int $position) : string 66 | { 67 | $commentEnd = $file->findPrevious([T_DOC_COMMENT_CLOSE_TAG], $position); 68 | if ($commentEnd === FALSE) { 69 | return ''; 70 | } 71 | 72 | $tokens = $file->getTokens(); 73 | if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { 74 | return ''; 75 | 76 | } else { 77 | // Make sure the comment we have found belongs to us. 78 | $commentFor = $file->findNext(T_VARIABLE, $commentEnd + 1); 79 | if ($commentFor !== $position) { 80 | return ''; 81 | } 82 | } 83 | 84 | $commentStart = $file->findPrevious(T_DOC_COMMENT_OPEN_TAG, $position); 85 | return $file->getTokensAsString($commentStart, $commentEnd - $commentStart + 1); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/ControlStructures/NewClassSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 60 | $this->position = $position; 61 | 62 | if ( ! $this->hasEmptyParentheses()) { 63 | return; 64 | } 65 | 66 | $fix = $file->addFixableError('New class statement should not have empty parentheses', $position); 67 | if ($fix) { 68 | $this->removeParenthesesFromClassStatement($position); 69 | } 70 | } 71 | 72 | 73 | private function hasEmptyParentheses() : bool 74 | { 75 | $tokens = $this->file->getTokens(); 76 | $nextPosition = $this->position; 77 | 78 | do { 79 | $nextPosition++; 80 | } while ( ! $this->doesContentContains($tokens[$nextPosition]['content'], [';', '(', ',', ')'])); 81 | 82 | if ($tokens[$nextPosition]['content'] === '(') { 83 | if ($tokens[$nextPosition + 1]['content'] === ')') { 84 | $this->openParenthesisPosition = $nextPosition; 85 | return TRUE; 86 | } 87 | } 88 | 89 | return FALSE; 90 | } 91 | 92 | 93 | private function doesContentContains(string $content, array $chars) : bool 94 | { 95 | foreach ($chars as $char) { 96 | if ($content === $char) { 97 | return TRUE; 98 | } 99 | } 100 | return FALSE; 101 | } 102 | 103 | 104 | private function removeParenthesesFromClassStatement(int $position) 105 | { 106 | $this->file->fixer->replaceToken($this->openParenthesisPosition, ''); 107 | $this->file->fixer->replaceToken($this->openParenthesisPosition + 1, ''); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/WhiteSpace/DocBlockSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 60 | $this->position = $position; 61 | $this->tokens = $file->getTokens(); 62 | 63 | if ($this->isInlineComment()) { 64 | return; 65 | } 66 | 67 | if ( ! $this->isIndentationInFrontCorrect()) { 68 | $file->addError('DocBlock lines should start with space (except first one)', $position); 69 | } 70 | 71 | if ( ! $this->isIndentationInsideCorrect()) { 72 | $file->addError('Indentation in DocBlock should be one space followed by tabs (if necessary)', $position); 73 | } 74 | } 75 | 76 | 77 | private function isInlineComment() : bool 78 | { 79 | if ($this->tokens[$this->position - 1]['code'] !== T_DOC_COMMENT_WHITESPACE) { 80 | return TRUE; 81 | } 82 | return FALSE; 83 | } 84 | 85 | 86 | private function isIndentationInFrontCorrect() : bool 87 | { 88 | $tokens = $this->file->getTokens(); 89 | if ($tokens[$this->position - 1]['content'] === ' ') { 90 | return TRUE; 91 | } 92 | if ((strlen($tokens[$this->position - 1]['content']) % 2) === 0) { 93 | return TRUE; 94 | } 95 | return FALSE; 96 | } 97 | 98 | 99 | private function isIndentationInsideCorrect() : bool 100 | { 101 | $tokens = $this->file->getTokens(); 102 | if ($tokens[$this->position + 1]['code'] === 'PHPCS_T_DOC_COMMENT_WHITESPACE') { 103 | $content = $tokens[$this->position + 1]['content']; 104 | $content = rtrim($content, "\n"); 105 | if ( strlen($content) > 1 106 | && $content !== ' ' . str_repeat("\t", strlen($content) - 1) 107 | ) { 108 | return FALSE; 109 | } 110 | } 111 | return TRUE; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/WhiteSpace/IfElseTryCatchFinallySniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 67 | $this->position = $position; 68 | $this->tokens = $file->getTokens(); 69 | 70 | // Fix type 71 | $this->requiredEmptyLineCountBeforeStatement = (int) $this->requiredEmptyLineCountBeforeStatement; 72 | 73 | $emptyLineCountBeforeStatement = $this->getEmptyLinesCountBefore(); 74 | if ($emptyLineCountBeforeStatement === $this->requiredEmptyLineCountBeforeStatement) { 75 | return; 76 | } 77 | 78 | $error = sprintf( 79 | '%s statement should be preceded by %s empty line(s); %s found', 80 | ucfirst($this->tokens[$position]['content']), 81 | $this->requiredEmptyLineCountBeforeStatement, 82 | $emptyLineCountBeforeStatement 83 | ); 84 | $fix = $file->addFixableError($error, $position); 85 | if ($fix) { 86 | EmptyLinesResizer::resizeLines( 87 | $file, 88 | PositionFinder::findFirstPositionInCurrentLine($this->file, $position), 89 | $emptyLineCountBeforeStatement, 90 | $this->requiredEmptyLineCountBeforeStatement 91 | ); 92 | } 93 | } 94 | 95 | 96 | private function getEmptyLinesCountBefore() : int 97 | { 98 | $currentLine = $this->tokens[$this->position]['line']; 99 | $previousPosition = $this->position; 100 | 101 | do { 102 | $previousPosition--; 103 | } while ( 104 | $currentLine === $this->tokens[$previousPosition]['line'] 105 | || $this->tokens[$previousPosition]['code'] === T_WHITESPACE 106 | ); 107 | 108 | return $this->tokens[$this->position]['line'] - $this->tokens[$previousPosition]['line'] - 1; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Classes/FinalInterfaceSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 59 | $this->position = $position; 60 | 61 | if ($this->implementsInterface() === FALSE) { 62 | return; 63 | } 64 | 65 | if ($this->isFinalOrAbstractClass()) { 66 | return; 67 | } 68 | 69 | if ($this->isDoctrineEntity()) { 70 | return; 71 | } 72 | 73 | $fix = $file->addFixableError('Non-abstract class that implements interface should be final.', $position); 74 | 75 | if ($fix) { 76 | $this->addFinalToClass($position); 77 | } 78 | } 79 | 80 | 81 | /** 82 | * @return bool 83 | */ 84 | private function implementsInterface() 85 | { 86 | return (bool) $this->file->findNext(T_IMPLEMENTS, $this->position); 87 | } 88 | 89 | 90 | /** 91 | * @return bool 92 | */ 93 | private function isFinalOrAbstractClass() 94 | { 95 | $classProperties = $this->file->getClassProperties($this->position); 96 | return ($classProperties['is_abstract'] || $classProperties['is_final']); 97 | } 98 | 99 | 100 | /** 101 | * @return bool 102 | */ 103 | private function isDoctrineEntity() 104 | { 105 | $docCommentPosition = $this->file->findPrevious(T_DOC_COMMENT_OPEN_TAG, $this->position); 106 | if ($docCommentPosition === FALSE) { 107 | return FALSE; 108 | } 109 | 110 | $seekPosition = $docCommentPosition; 111 | 112 | do { 113 | $docCommentTokenContent = $this->file->getTokens()[$docCommentPosition]['content']; 114 | if (strpos($docCommentTokenContent, 'Entity') !== FALSE) { 115 | return TRUE; 116 | } 117 | $seekPosition++; 118 | 119 | } while ($docCommentPosition = $this->file->findNext(T_DOC_COMMENT_TAG, $seekPosition, $this->position)); 120 | 121 | return FALSE; 122 | } 123 | 124 | 125 | public function addFinalToClass(int $position) 126 | { 127 | $this->file->fixer->addContentBefore($position, 'final '); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Helper/Whitespace/ClassMetrics.php: -------------------------------------------------------------------------------- 1 | file = $file; 37 | $this->classPosition = $classPosition; 38 | $this->tokens = $file->getTokens(); 39 | } 40 | 41 | 42 | /** 43 | * @return FALSE|int 44 | */ 45 | public function getLineDistanceBetweenClassAndLastUseStatement() 46 | { 47 | $lastUseStatementPosition = $this->getLastUseStatementPosition(); 48 | if ( ! $lastUseStatementPosition) { 49 | return FALSE; 50 | } 51 | 52 | return (int) $this->tokens[$this->getClassPositionIncludingComment()]['line'] 53 | - $this->tokens[$lastUseStatementPosition]['line'] 54 | - 1; 55 | } 56 | 57 | 58 | /** 59 | * @return FALSE|int 60 | */ 61 | public function getLastUseStatementPosition() 62 | { 63 | return $this->file->findPrevious(T_USE, $this->classPosition); 64 | } 65 | 66 | 67 | /** 68 | * @return FALSE|int 69 | */ 70 | public function getLineDistanceBetweenNamespaceAndFirstUseStatement() 71 | { 72 | $namespacePosition = $this->file->findPrevious(T_NAMESPACE, $this->classPosition); 73 | 74 | $nextUseStatementPosition = $this->file->findNext(T_USE, $namespacePosition); 75 | if ( ! $nextUseStatementPosition) { 76 | return FALSE; 77 | } 78 | 79 | if ($this->tokens[$nextUseStatementPosition]['line'] === 1 || $this->isInsideClass($nextUseStatementPosition)) { 80 | return FALSE; 81 | } 82 | 83 | return $this->tokens[$nextUseStatementPosition]['line'] - $this->tokens[$namespacePosition]['line'] - 1; 84 | } 85 | 86 | 87 | /** 88 | * @return FALSE|int 89 | */ 90 | public function getLineDistanceBetweenClassAndNamespace() 91 | { 92 | $namespacePosition = $this->file->findPrevious(T_NAMESPACE, $this->classPosition); 93 | 94 | if ( ! $namespacePosition) { 95 | return FALSE; 96 | } 97 | 98 | $classStartPosition = $this->getClassPositionIncludingComment(); 99 | 100 | return $this->tokens[$classStartPosition]['line'] - $this->tokens[$namespacePosition]['line'] - 1; 101 | } 102 | 103 | 104 | /** 105 | * @return FALSE|int 106 | */ 107 | private function getClassPositionIncludingComment() 108 | { 109 | $classStartPosition = $this->file->findPrevious(T_DOC_COMMENT_OPEN_TAG, $this->classPosition); 110 | if ($classStartPosition) { 111 | return $classStartPosition; 112 | } 113 | 114 | return $this->classPosition; 115 | } 116 | 117 | 118 | private function isInsideClass(int $position) : bool 119 | { 120 | $prevClassPosition = $this->file->findPrevious(T_CLASS, $position, NULL, FALSE); 121 | if ($prevClassPosition) { 122 | return TRUE; 123 | } 124 | 125 | return FALSE; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Commenting/ComponentFactoryCommentSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 60 | $this->position = $position; 61 | $this->tokens = $file->getTokens(); 62 | 63 | if ( ! $this->isComponentFactoryMethod()) { 64 | return; 65 | } 66 | 67 | $returnTypeHint = FunctionHelper::findReturnTypeHint($file, $position); 68 | if ($returnTypeHint) { 69 | return; 70 | } 71 | 72 | $commentEnd = $this->getCommentEnd(); 73 | if ( ! $this->hasMethodComment($commentEnd)) { 74 | $file->addError('createComponent method should have a doc comment or return type.', $position); 75 | return; 76 | } 77 | 78 | $commentStart = $this->tokens[$commentEnd]['comment_opener']; 79 | $this->processReturnTag($commentStart); 80 | } 81 | 82 | 83 | private function isComponentFactoryMethod() : bool 84 | { 85 | $functionName = $this->file->getDeclarationName($this->position); 86 | return (strpos($functionName, 'createComponent') === 0); 87 | } 88 | 89 | 90 | /** 91 | * @return bool|int 92 | */ 93 | private function getCommentEnd() 94 | { 95 | return $this->file->findPrevious(T_WHITESPACE, ($this->position - 3), NULL, TRUE); 96 | } 97 | 98 | 99 | private function hasMethodComment(int $position) : bool 100 | { 101 | if ($this->tokens[$position]['code'] === T_DOC_COMMENT_CLOSE_TAG) { 102 | return TRUE; 103 | } 104 | return FALSE; 105 | } 106 | 107 | 108 | private function processReturnTag(int $commentStartPosition) 109 | { 110 | $return = NULL; 111 | foreach ($this->tokens[$commentStartPosition]['comment_tags'] as $tag) { 112 | if ($this->tokens[$tag]['content'] === '@return') { 113 | $return = $tag; 114 | } 115 | } 116 | if ($return !== NULL) { 117 | $content = $this->tokens[($return + 2)]['content']; 118 | if (empty($content) === TRUE || $this->tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { 119 | $error = 'Return tag should contain type'; 120 | $this->file->addError($error, $return); 121 | } 122 | 123 | } else { 124 | $error = 'CreateComponent* method should have a @return tag'; 125 | $this->file->addError($error, $this->tokens[$commentStartPosition]['comment_closer']); 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Zenify selection from default sniffs 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 3 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 0 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Commenting/BlockPropertyCommentSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 55 | $this->tokens = $file->getTokens(); 56 | 57 | $closeTagPosition = $file->findNext(T_DOC_COMMENT_CLOSE_TAG, $position + 1); 58 | if ($this->isPropertyOrMethodComment($closeTagPosition) === FALSE) { 59 | return; 60 | 61 | } elseif ($this->isSingleLineDoc($position, $closeTagPosition) === FALSE) { 62 | return; 63 | } 64 | 65 | $fix = $file->addFixableError('Block comment should be used instead of one liner', $position); 66 | 67 | if ($fix) { 68 | $this->changeSingleLineDocToDocBlock($position); 69 | } 70 | } 71 | 72 | 73 | private function isPropertyOrMethodComment(int $position) : bool 74 | { 75 | $nextPropertyOrMethodPosition = $this->file->findNext([T_VARIABLE, T_FUNCTION], $position + 1); 76 | 77 | if ($nextPropertyOrMethodPosition && $this->tokens[$nextPropertyOrMethodPosition]['code'] !== T_FUNCTION) { 78 | if ($this->isVariableOrPropertyUse($nextPropertyOrMethodPosition) === TRUE) { 79 | return FALSE; 80 | } 81 | 82 | if (($this->tokens[$position]['line'] + 1) === $this->tokens[$nextPropertyOrMethodPosition]['line']) { 83 | return TRUE; 84 | } 85 | } 86 | 87 | return FALSE; 88 | } 89 | 90 | 91 | private function isSingleLineDoc(int $openTagPosition, int $closeTagPosition) : bool 92 | { 93 | $lines = $this->tokens[$closeTagPosition]['line'] - $this->tokens[$openTagPosition]['line']; 94 | if ($lines < 2) { 95 | return TRUE; 96 | } 97 | return FALSE; 98 | } 99 | 100 | 101 | private function isVariableOrPropertyUse(int $position) : bool 102 | { 103 | $previous = $this->file->findPrevious(T_OPEN_CURLY_BRACKET, $position); 104 | if ($previous) { 105 | $previous = $this->file->findPrevious(T_OPEN_CURLY_BRACKET, $previous - 1); 106 | if ($this->tokens[$previous]['code'] === T_OPEN_CURLY_BRACKET) { // used in method 107 | return TRUE; 108 | } 109 | } 110 | return FALSE; 111 | } 112 | 113 | 114 | private function changeSingleLineDocToDocBlock(int $position) 115 | { 116 | $commentEndPosition = $this->tokens[$position]['comment_closer']; 117 | 118 | $empty = [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR]; 119 | $shortPosition = $this->file->findNext($empty, $position + 1, $commentEndPosition, TRUE); 120 | 121 | // indent content after /** to indented new line 122 | $this->file->fixer->addContentBefore($shortPosition, PHP_EOL . "\t" . ' * '); 123 | 124 | // remove spaces 125 | $this->file->fixer->replaceToken($position + 1, ''); 126 | $spacelessContent = trim($this->tokens[$commentEndPosition - 1]['content']); 127 | $this->file->fixer->replaceToken($commentEndPosition - 1, $spacelessContent); 128 | 129 | // indent end to indented newline 130 | $this->file->fixer->replaceToken($commentEndPosition, PHP_EOL . "\t" . ' */'); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/WhiteSpace/PropertiesMethodsMutualSpacingSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 67 | $this->position = $position; 68 | $this->tokens = $file->getTokens(); 69 | 70 | // Fix type 71 | $this->desiredBlankLinesInBetween = (int) $this->desiredBlankLinesInBetween; 72 | 73 | if ($this->isLastProperty() === FALSE) { 74 | return; 75 | } 76 | 77 | if ($this->areMethodsPresent() === FALSE) { 78 | return; 79 | } 80 | 81 | $next = $file->findNext([T_DOC_COMMENT_OPEN_TAG, T_FUNCTION], $position); 82 | 83 | $positionOfLastProperty = $this->getPositionOfLastProperty(); 84 | $blankLines = $this->tokens[$next]['line'] - $this->tokens[$positionOfLastProperty]['line'] - 1; 85 | if ($blankLines !== $this->desiredBlankLinesInBetween) { 86 | $error = sprintf( 87 | 'Between properties and methods should be %s empty line(s); %s found.', 88 | $this->desiredBlankLinesInBetween, 89 | $blankLines 90 | ); 91 | $fix = $file->addFixableError($error, $position); 92 | if ($fix) { 93 | $this->fixSpacingInBetween($blankLines); 94 | } 95 | } 96 | } 97 | 98 | 99 | private function isLastProperty() : bool 100 | { 101 | if ($this->isInsideMethod()) { 102 | return FALSE; 103 | } 104 | 105 | $next = $this->file->findNext([T_VARIABLE, T_FUNCTION], $this->position + 1); 106 | return $this->tokens[$next]['code'] !== T_VARIABLE; 107 | } 108 | 109 | 110 | private function isInsideMethod() : bool 111 | { 112 | $previousMethod = $this->file->findPrevious(T_FUNCTION, $this->position); 113 | return $this->tokens[$previousMethod]['code'] === T_FUNCTION; 114 | } 115 | 116 | 117 | private function areMethodsPresent() : bool 118 | { 119 | $next = $this->file->findNext(T_FUNCTION, $this->position + 1); 120 | return $this->tokens[$next]['code'] === T_FUNCTION; 121 | } 122 | 123 | 124 | private function getPositionOfLastProperty() : int 125 | { 126 | $arrayPosition = $this->file->findNext(T_ARRAY, $this->position); 127 | if ($this->tokens[$arrayPosition]['line'] === $this->tokens[$this->position]['line']) { 128 | if ($this->tokens[$arrayPosition]['parenthesis_closer']) { 129 | return $this->tokens[$arrayPosition]['parenthesis_closer']; 130 | } 131 | } 132 | 133 | $openShortArrayPosition = $this->file->findNext(T_OPEN_SHORT_ARRAY, $this->position); 134 | if ($this->tokens[$openShortArrayPosition]['line'] === $this->tokens[$this->position]['line']) { 135 | return $this->tokens[$openShortArrayPosition]['bracket_closer']; 136 | } 137 | 138 | return $this->position; 139 | } 140 | 141 | 142 | private function fixSpacingInBetween(int $blankLinesInBetween) 143 | { 144 | $position = PositionFinder::findLastPositionInCurrentLine($this->file, $this->getPositionOfLastProperty()); 145 | 146 | EmptyLinesResizer::resizeLines( 147 | $this->file, 148 | $position, 149 | $blankLinesInBetween, 150 | $this->desiredBlankLinesInBetween 151 | ); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Namespaces/UseDeclarationSniff.php: -------------------------------------------------------------------------------- 1 | blankLinesAfterUseStatement = (int) $this->blankLinesAfterUseStatement; 53 | 54 | if ($this->shouldIgnoreUse($file, $position) === TRUE) { 55 | return; 56 | } 57 | 58 | $this->checkIfSingleSpaceAfterUseKeyword($file, $position); 59 | $this->checkIfOneUseDeclarationPerStatement($file, $position); 60 | $this->checkIfUseComesAfterNamespaceDeclaration($file, $position); 61 | 62 | // Only interested in the last USE statement from here onwards. 63 | $nextUse = $file->findNext(T_USE, ($position + 1)); 64 | while ($this->shouldIgnoreUse($file, $nextUse) === TRUE) { 65 | $nextUse = $file->findNext(T_USE, ($nextUse + 1)); 66 | if ($nextUse === FALSE) { 67 | break; 68 | } 69 | } 70 | 71 | if ($nextUse !== FALSE) { 72 | return; 73 | } 74 | 75 | $this->checkBlankLineAfterLastUseStatement($file, $position); 76 | } 77 | 78 | 79 | /** 80 | * Check if this use statement is part of the namespace block. 81 | * @param PHP_CodeSniffer_File $file 82 | * @param int|bool $position 83 | */ 84 | private function shouldIgnoreUse(PHP_CodeSniffer_File $file, $position) : bool 85 | { 86 | $tokens = $file->getTokens(); 87 | 88 | // Ignore USE keywords inside closures. 89 | $next = $file->findNext(T_WHITESPACE, ($position + 1), NULL, TRUE); 90 | if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS) { 91 | return TRUE; 92 | } 93 | 94 | // Ignore USE keywords for traits. 95 | if ($file->hasCondition($position, [T_CLASS, T_TRAIT]) === TRUE) { 96 | return TRUE; 97 | } 98 | 99 | return FALSE; 100 | } 101 | 102 | 103 | private function checkIfSingleSpaceAfterUseKeyword(PHP_CodeSniffer_File $file, int $position) 104 | { 105 | $tokens = $file->getTokens(); 106 | if ($tokens[($position + 1)]['content'] !== ' ') { 107 | $error = 'There must be a single space after the USE keyword'; 108 | $file->addError($error, $position, 'SpaceAfterUse'); 109 | } 110 | } 111 | 112 | 113 | private function checkIfOneUseDeclarationPerStatement(PHP_CodeSniffer_File $file, int $position) 114 | { 115 | $tokens = $file->getTokens(); 116 | $next = $file->findNext([T_COMMA, T_SEMICOLON], ($position + 1)); 117 | if ($tokens[$next]['code'] === T_COMMA) { 118 | $error = 'There must be one USE keyword per declaration'; 119 | $file->addError($error, $position, 'MultipleDeclarations'); 120 | } 121 | } 122 | 123 | 124 | private function checkIfUseComesAfterNamespaceDeclaration(PHP_CodeSniffer_File $file, int $position) 125 | { 126 | $prev = $file->findPrevious(T_NAMESPACE, ($position - 1)); 127 | if ($prev !== FALSE) { 128 | $first = $file->findNext(T_NAMESPACE, 1); 129 | if ($prev !== $first) { 130 | $error = 'USE declarations must go after the first namespace declaration'; 131 | $file->addError($error, $position, 'UseAfterNamespace'); 132 | } 133 | } 134 | } 135 | 136 | 137 | private function checkBlankLineAfterLastUseStatement(PHP_CodeSniffer_File $file, int $position) 138 | { 139 | $tokens = $file->getTokens(); 140 | $end = $file->findNext(T_SEMICOLON, ($position + 1)); 141 | $next = $file->findNext(T_WHITESPACE, ($end + 1), NULL, TRUE); 142 | $diff = ($tokens[$next]['line'] - $tokens[$end]['line'] - 1); 143 | if ($diff !== (int) $this->blankLinesAfterUseStatement) { 144 | if ($diff < 0) { 145 | $diff = 0; 146 | } 147 | 148 | $error = 'There must be %s blank line(s) after the last USE statement; %s found.'; 149 | $data = [$this->blankLinesAfterUseStatement, $diff]; 150 | $file->addError($error, $position, 'SpaceAfterLastUse', $data); 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/WhiteSpace/InBetweenMethodSpacingSniff.php: -------------------------------------------------------------------------------- 1 | blankLinesBetweenMethods = (int) $this->blankLinesBetweenMethods; 71 | 72 | $this->file = $file; 73 | $this->position = $position; 74 | $this->tokens = $file->getTokens(); 75 | 76 | $blankLinesCountAfterFunction = $this->getBlankLineCountAfterFunction(); 77 | if ($blankLinesCountAfterFunction !== $this->blankLinesBetweenMethods) { 78 | if ($this->isLastMethod()) { 79 | return; 80 | 81 | } else { 82 | $error = sprintf( 83 | 'Method should have %s empty line(s) after itself, %s found.', 84 | $this->blankLinesBetweenMethods, 85 | $blankLinesCountAfterFunction 86 | ); 87 | $fix = $file->addFixableError($error, $position); 88 | if ($fix) { 89 | $this->fixSpacingAfterMethod($blankLinesCountAfterFunction); 90 | } 91 | } 92 | } 93 | } 94 | 95 | 96 | private function getBlankLineCountAfterFunction() : int 97 | { 98 | $closer = $this->getScopeCloser(); 99 | $nextLineToken = $this->getNextLineTokenByScopeCloser($closer); 100 | 101 | $nextContent = $this->getNextLineContent($nextLineToken); 102 | if ($nextContent !== FALSE) { 103 | $foundLines = ($this->tokens[$nextContent]['line'] - $this->tokens[$nextLineToken]['line']); 104 | 105 | } else { 106 | // We are at the end of the file. 107 | $foundLines = $this->blankLinesBetweenMethods; 108 | } 109 | 110 | return $foundLines; 111 | } 112 | 113 | 114 | private function isLastMethod() : bool 115 | { 116 | $closer = $this->getScopeCloser(); 117 | $nextLineToken = $this->getNextLineTokenByScopeCloser($closer); 118 | if ($this->tokens[$nextLineToken + 1]['code'] === T_CLOSE_CURLY_BRACKET) { 119 | return TRUE; 120 | } 121 | return FALSE; 122 | } 123 | 124 | 125 | /** 126 | * @return bool|int 127 | */ 128 | private function getScopeCloser() 129 | { 130 | if (isset($this->tokens[$this->position]['scope_closer']) === FALSE) { 131 | // Must be an interface method, so the closer is the semi-colon. 132 | return $this->file->findNext(T_SEMICOLON, $this->position); 133 | } 134 | 135 | return $this->tokens[$this->position]['scope_closer']; 136 | } 137 | 138 | 139 | /** 140 | * @return int|NULL 141 | */ 142 | private function getNextLineTokenByScopeCloser(int $closer) 143 | { 144 | $nextLineToken = NULL; 145 | for ($i = $closer; $i < $this->file->numTokens; $i++) { 146 | if (strpos($this->tokens[$i]['content'], $this->file->eolChar) === FALSE) { 147 | continue; 148 | 149 | } else { 150 | $nextLineToken = ($i + 1); 151 | if ( ! isset($this->tokens[$nextLineToken])) { 152 | $nextLineToken = NULL; 153 | } 154 | 155 | break; 156 | } 157 | } 158 | return $nextLineToken; 159 | } 160 | 161 | 162 | /** 163 | * @return FALSE|int 164 | */ 165 | private function getNextLineContent(int $nextLineToken) 166 | { 167 | if ($nextLineToken !== NULL) { 168 | return $this->file->findNext(T_WHITESPACE, ($nextLineToken + 1), NULL, TRUE); 169 | } 170 | return FALSE; 171 | } 172 | 173 | 174 | private function fixSpacingAfterMethod(int $blankLinesCountAfterFunction) 175 | { 176 | EmptyLinesResizer::resizeLines( 177 | $this->file, 178 | $this->getScopeCloser() + 1, 179 | $blankLinesCountAfterFunction, 180 | $this->blankLinesBetweenMethods 181 | ); 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Classes/ClassDeclarationSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 53 | 54 | // Fix type 55 | $this->emptyLinesAfterOpeningBrace = (int) $this->emptyLinesAfterOpeningBrace; 56 | $this->emptyLinesBeforeClosingBrace = (int) $this->emptyLinesBeforeClosingBrace; 57 | 58 | $this->processOpen($file, $position); 59 | $this->processClose($file, $position); 60 | } 61 | 62 | 63 | private function processOpen(PHP_CodeSniffer_File $file, int $position) 64 | { 65 | $tokens = $file->getTokens(); 66 | $openingBracePosition = $tokens[$position]['scope_opener']; 67 | $emptyLinesCount = $this->getEmptyLinesAfterOpeningBrace($file, $openingBracePosition); 68 | 69 | if ($emptyLinesCount !== $this->emptyLinesAfterOpeningBrace) { 70 | $error = 'Opening brace for the %s should be followed by %s empty line(s); %s found.'; 71 | $data = [ 72 | $tokens[$position]['content'], 73 | $this->emptyLinesAfterOpeningBrace, 74 | $emptyLinesCount, 75 | ]; 76 | $fix = $file->addFixableError($error, $openingBracePosition, 'OpenBraceFollowedByEmptyLines', $data); 77 | if ($fix) { 78 | $this->fixOpeningBraceSpaces($openingBracePosition, $emptyLinesCount); 79 | } 80 | } 81 | } 82 | 83 | 84 | private function processClose(PHP_CodeSniffer_File $file, int $position) 85 | { 86 | $tokens = $file->getTokens(); 87 | $closeBracePosition = $tokens[$position]['scope_closer']; 88 | $emptyLinesCount = $this->getEmptyLinesBeforeClosingBrace($file, $closeBracePosition); 89 | 90 | if ($emptyLinesCount !== $this->emptyLinesBeforeClosingBrace) { 91 | $error = 'Closing brace for the %s should be preceded by %s empty line(s); %s found.'; 92 | $data = [ 93 | $tokens[$position]['content'], 94 | $this->emptyLinesBeforeClosingBrace, 95 | $emptyLinesCount 96 | ]; 97 | $fix = $file->addFixableError($error, $closeBracePosition, 'CloseBracePrecededByEmptyLines', $data); 98 | if ($fix) { 99 | $this->fixClosingBraceSpaces($closeBracePosition, $emptyLinesCount); 100 | } 101 | } 102 | } 103 | 104 | 105 | private function getEmptyLinesBeforeClosingBrace(PHP_CodeSniffer_File $file, int $position) : int 106 | { 107 | $tokens = $file->getTokens(); 108 | $prevContent = $file->findPrevious(T_WHITESPACE, ($position - 1), NULL, TRUE); 109 | return $tokens[$position]['line'] - $tokens[$prevContent]['line'] - 1; 110 | } 111 | 112 | 113 | private function getEmptyLinesAfterOpeningBrace(PHP_CodeSniffer_File $file, int $position) : int 114 | { 115 | $tokens = $file->getTokens(); 116 | $nextContent = $file->findNext(T_WHITESPACE, ($position + 1), NULL, TRUE); 117 | return $tokens[$nextContent]['line'] - $tokens[$position]['line'] - 1; 118 | } 119 | 120 | 121 | private function fixOpeningBraceSpaces(int $position, int $numberOfSpaces) 122 | { 123 | if ($numberOfSpaces < $this->emptyLinesAfterOpeningBrace) { 124 | for ($i = $numberOfSpaces; $i < $this->emptyLinesAfterOpeningBrace; $i++) { 125 | $this->file->fixer->addContent($position, PHP_EOL); 126 | } 127 | 128 | } else { 129 | for ($i = $numberOfSpaces; $i > $this->emptyLinesAfterOpeningBrace; $i--) { 130 | $this->file->fixer->replaceToken($position + $i, ''); 131 | } 132 | } 133 | } 134 | 135 | 136 | private function fixClosingBraceSpaces(int $position, int $numberOfSpaces) 137 | { 138 | if ($numberOfSpaces < $this->emptyLinesBeforeClosingBrace) { 139 | for ($i = $numberOfSpaces; $i < $this->emptyLinesBeforeClosingBrace; $i++) { 140 | $this->file->fixer->addContentBefore($position, PHP_EOL); 141 | } 142 | 143 | } else { 144 | for ($i = $numberOfSpaces; $i > $this->emptyLinesBeforeClosingBrace; $i--) { 145 | $this->file->fixer->replaceToken($position - $i, ''); 146 | } 147 | } 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/Namespaces/NamespaceDeclarationSniff.php: -------------------------------------------------------------------------------- 1 | findNext([T_CLASS, T_TRAIT, T_INTERFACE], $position); 77 | if ( ! $classPosition) { 78 | // there is no class, nothing to see here 79 | return; 80 | } 81 | 82 | $this->file = $file; 83 | $this->position = $position; 84 | $this->tokens = $file->getTokens(); 85 | 86 | // Fix type 87 | $this->emptyLinesAfterNamespace = (int) $this->emptyLinesAfterNamespace; 88 | $this->emptyLinesBeforeUseStatement = (int) $this->emptyLinesBeforeUseStatement; 89 | 90 | // prepare class metrics class 91 | $this->classMetrics = new ClassMetrics($file, $classPosition); 92 | 93 | $lineDistanceBetweenNamespaceAndFirstUseStatement = 94 | $this->classMetrics->getLineDistanceBetweenNamespaceAndFirstUseStatement(); 95 | $lineDistanceBetweenClassAndNamespace = 96 | $this->classMetrics->getLineDistanceBetweenClassAndNamespace(); 97 | 98 | if ($lineDistanceBetweenNamespaceAndFirstUseStatement) { 99 | $this->processWithUseStatement($lineDistanceBetweenNamespaceAndFirstUseStatement); 100 | 101 | } else { 102 | $this->processWithoutUseStatement($lineDistanceBetweenClassAndNamespace); 103 | } 104 | } 105 | 106 | 107 | private function processWithoutUseStatement(int $linesToNextClass) 108 | { 109 | if ($linesToNextClass) { 110 | if ($linesToNextClass !== $this->emptyLinesAfterNamespace) { 111 | $errorMessage = sprintf( 112 | 'There should be %s empty line(s) after the namespace declaration; %s found', 113 | $this->emptyLinesAfterNamespace, 114 | $linesToNextClass 115 | ); 116 | 117 | $fix = $this->file->addFixableError($errorMessage, $this->position); 118 | if ($fix) { 119 | $this->fixSpacesFromNamespaceToClass($this->position, $linesToNextClass); 120 | } 121 | } 122 | } 123 | } 124 | 125 | 126 | private function processWithUseStatement(int $linesToNextUse) 127 | { 128 | if ($linesToNextUse !== $this->emptyLinesBeforeUseStatement) { 129 | $errorMessage = sprintf( 130 | 'There should be %s empty line(s) from namespace to use statement; %s found', 131 | $this->emptyLinesBeforeUseStatement, 132 | $linesToNextUse 133 | ); 134 | 135 | $fix = $this->file->addFixableError($errorMessage, $this->position); 136 | if ($fix) { 137 | $this->fixSpacesFromNamespaceToUseStatements($this->position, $linesToNextUse); 138 | } 139 | } 140 | 141 | $linesToNextClass = $this->classMetrics->getLineDistanceBetweenClassAndLastUseStatement(); 142 | if ($linesToNextClass !== $this->emptyLinesAfterNamespace) { 143 | $errorMessage = sprintf( 144 | 'There should be %s empty line(s) between last use and class; %s found', 145 | $this->emptyLinesAfterNamespace, 146 | $linesToNextClass 147 | ); 148 | 149 | $fix = $this->file->addFixableError($errorMessage, $this->position); 150 | if ($fix) { 151 | $this->fixSpacesFromUseStatementToClass( 152 | $this->classMetrics->getLastUseStatementPosition(), 153 | $linesToNextClass 154 | ); 155 | } 156 | } 157 | } 158 | 159 | 160 | private function fixSpacesFromNamespaceToUseStatements(int $position, int $linesToNextUse) 161 | { 162 | $nextLinePosition = WhitespaceFinder::findNextEmptyLinePosition($this->file, $position); 163 | 164 | if ($linesToNextUse < $this->emptyLinesBeforeUseStatement) { 165 | for ($i = $linesToNextUse; $i < $this->emptyLinesBeforeUseStatement; $i++) { 166 | $this->file->fixer->addContent($nextLinePosition, PHP_EOL); 167 | } 168 | 169 | } else { 170 | for ($i = $linesToNextUse; $i > $this->emptyLinesBeforeUseStatement; $i--) { 171 | $this->file->fixer->replaceToken($nextLinePosition, ''); 172 | $nextLinePosition = WhitespaceFinder::findNextEmptyLinePosition($this->file, $nextLinePosition); 173 | } 174 | } 175 | } 176 | 177 | 178 | private function fixSpacesFromNamespaceToClass(int $position, int $linesToClass) 179 | { 180 | $nextLinePosition = WhitespaceFinder::findNextEmptyLinePosition($this->file, $position); 181 | if ($linesToClass < $this->emptyLinesAfterNamespace) { 182 | for ($i = $linesToClass; $i < $this->emptyLinesAfterNamespace; $i++) { 183 | $this->file->fixer->addContent($nextLinePosition, PHP_EOL); 184 | } 185 | 186 | } else { 187 | for ($i = $linesToClass; $i > $this->emptyLinesAfterNamespace; $i--) { 188 | $this->file->fixer->replaceToken($nextLinePosition, ''); 189 | $nextLinePosition = WhitespaceFinder::findNextEmptyLinePosition($this->file, $nextLinePosition); 190 | } 191 | } 192 | } 193 | 194 | 195 | private function fixSpacesFromUseStatementToClass(int $position, int $linesToClass) 196 | { 197 | if ($linesToClass < $this->emptyLinesAfterNamespace) { 198 | for ($i = $linesToClass; $i < $this->emptyLinesAfterNamespace; $i++) { 199 | $this->file->fixer->addContent($position, PHP_EOL); 200 | } 201 | 202 | } else { 203 | $nextLinePosition = WhitespaceFinder::findNextEmptyLinePosition($this->file, $position); 204 | for ($i = $linesToClass; $i > $this->emptyLinesAfterNamespace; $i--) { 205 | $this->file->fixer->replaceToken($nextLinePosition, ''); 206 | $nextLinePosition = WhitespaceFinder::findNextEmptyLinePosition($this->file, $nextLinePosition); 207 | } 208 | } 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /src/ZenifyCodingStandard/Sniffs/ControlStructures/SwitchDeclarationSniff.php: -------------------------------------------------------------------------------- 1 | file = $file; 60 | $this->position = $position; 61 | 62 | $this->tokens = $tokens = $file->getTokens(); 63 | $this->token = $tokens[$position]; 64 | 65 | if ($this->areSwitchStartAndEndKnown() === FALSE) { 66 | return; 67 | } 68 | 69 | $switch = $tokens[$position]; 70 | $nextCase = $position; 71 | $caseAlignment = ($switch['column'] + $this->indent); 72 | $caseCount = 0; 73 | $foundDefault = FALSE; 74 | 75 | $lookFor = [T_CASE, T_DEFAULT, T_SWITCH]; 76 | while (($nextCase = $file->findNext($lookFor, ($nextCase + 1), $switch['scope_closer'])) !== FALSE) { 77 | // Skip nested SWITCH statements; they are handled on their own. 78 | if ($tokens[$nextCase]['code'] === T_SWITCH) { 79 | $nextCase = $tokens[$nextCase]['scope_closer']; 80 | continue; 81 | } 82 | if ($tokens[$nextCase]['code'] === T_DEFAULT) { 83 | $type = 'Default'; 84 | $foundDefault = TRUE; 85 | 86 | } else { 87 | $type = 'Case'; 88 | $caseCount++; 89 | } 90 | 91 | $this->checkIfKeywordIsIndented($file, $nextCase, $tokens, $type, $caseAlignment); 92 | $this->checkSpaceAfterKeyword($nextCase, $type); 93 | 94 | $opener = $tokens[$nextCase]['scope_opener']; 95 | 96 | $this->ensureNoSpaceBeforeColon($opener, $nextCase, $type); 97 | 98 | $nextBreak = $tokens[$nextCase]['scope_closer']; 99 | 100 | $allowedTokens = [T_BREAK, T_RETURN, T_CONTINUE, T_THROW, T_EXIT]; 101 | if (in_array($tokens[$nextBreak]['code'], $allowedTokens)) { 102 | $this->processSwitchStructureToken($nextBreak, $nextCase, $caseAlignment, $type, $opener); 103 | 104 | } elseif ($type === 'Default') { 105 | $error = 'DEFAULT case must have a breaking statement'; 106 | $file->addError($error, $nextCase, 'DefaultNoBreak'); 107 | } 108 | } 109 | 110 | $this->ensureDefaultIsPresent($foundDefault); 111 | $this->ensureClosingBraceAlignment($switch); 112 | } 113 | 114 | 115 | private function checkIfKeywordIsIndented( 116 | PHP_CodeSniffer_File $file, 117 | int $position, 118 | array $tokens, 119 | string $type, 120 | int $caseAlignment 121 | ) { 122 | if ($tokens[$position]['column'] !== $caseAlignment) { 123 | $error = strtoupper($type) . ' keyword must be indented ' . $this->indent . ' spaces from SWITCH keyword'; 124 | $file->addError($error, $position, $type . 'Indent'); 125 | } 126 | } 127 | 128 | 129 | private function checkBreak(int $nextCase, int $nextBreak, string $type) 130 | { 131 | if ($type === 'Case') { 132 | // Ensure empty CASE statements are not allowed. 133 | // They must have some code content in them. A comment is not enough. 134 | // But count RETURN statements as valid content if they also 135 | // happen to close the CASE statement. 136 | $foundContent = FALSE; 137 | for ($i = ($this->tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) { 138 | if ($this->tokens[$i]['code'] === T_CASE) { 139 | $i = $this->tokens[$i]['scope_opener']; 140 | continue; 141 | } 142 | 143 | $tokenCode = $this->tokens[$i]['code']; 144 | $emptyTokens = PHP_CodeSniffer_Tokens::$emptyTokens; 145 | if (in_array($tokenCode, $emptyTokens) === FALSE) { 146 | $foundContent = TRUE; 147 | break; 148 | } 149 | } 150 | if ($foundContent === FALSE) { 151 | $error = 'Empty CASE statements are not allowed'; 152 | $this->file->addError($error, $nextCase, 'EmptyCase'); 153 | } 154 | 155 | } else { 156 | // Ensure empty DEFAULT statements are not allowed. 157 | // They must (at least) have a comment describing why 158 | // the default case is being ignored. 159 | $foundContent = FALSE; 160 | for ($i = ($this->tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) { 161 | if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') { 162 | $foundContent = TRUE; 163 | break; 164 | } 165 | } 166 | if ($foundContent === FALSE) { 167 | $error = 'Comment required for empty DEFAULT case'; 168 | $this->file->addError($error, $nextCase, 'EmptyDefault'); 169 | } 170 | } 171 | } 172 | 173 | 174 | private function areSwitchStartAndEndKnown() : bool 175 | { 176 | if ( ! isset($this->tokens[$this->position]['scope_opener'])) { 177 | return FALSE; 178 | } 179 | 180 | if ( ! isset($this->tokens[$this->position]['scope_closer'])) { 181 | return FALSE; 182 | } 183 | 184 | return TRUE; 185 | } 186 | 187 | 188 | private function processSwitchStructureToken( 189 | int $nextBreak, 190 | int $nextCase, 191 | int $caseAlignment, 192 | string $type, 193 | int $opener 194 | ) { 195 | if ($this->tokens[$nextBreak]['scope_condition'] === $nextCase) { 196 | $this->ensureCaseIndention($nextBreak, $caseAlignment); 197 | 198 | $this->ensureNoBlankLinesBeforeBreak($nextBreak); 199 | 200 | $breakLine = $this->tokens[$nextBreak]['line']; 201 | $nextLine = $this->getNextLineFromNextBreak($nextBreak); 202 | if ($type !== 'Case') { 203 | $this->ensureBreakIsNotFollowedByBlankLine($nextLine, $breakLine, $nextBreak); 204 | } 205 | 206 | $this->ensureNoBlankLinesAfterStatement($nextCase, $nextBreak, $type, $opener); 207 | } 208 | 209 | if ($this->tokens[$nextBreak]['code'] === T_BREAK) { 210 | $this->checkBreak($nextCase, $nextBreak, $type); 211 | } 212 | } 213 | 214 | 215 | private function ensureBreakIsNotFollowedByBlankLine(int $nextLine, int $breakLine, int $nextBreak) 216 | { 217 | if ($nextLine !== ($breakLine + 1)) { 218 | $error = 'Blank lines are not allowed after the DEFAULT case\'s breaking statement'; 219 | $this->file->addError($error, $nextBreak, 'SpacingAfterDefaultBreak'); 220 | } 221 | } 222 | 223 | 224 | private function ensureNoBlankLinesBeforeBreak(int $nextBreak) 225 | { 226 | $prev = $this->file->findPrevious(T_WHITESPACE, ($nextBreak - 1), $this->position, TRUE); 227 | if ($this->tokens[$prev]['line'] !== ($this->tokens[$nextBreak]['line'] - 1)) { 228 | $error = 'Blank lines are not allowed before case breaking statements'; 229 | $this->file->addError($error, $nextBreak, 'SpacingBeforeBreak'); 230 | } 231 | } 232 | 233 | 234 | private function ensureNoBlankLinesAfterStatement(int $nextCase, int $nextBreak, string $type, int $opener) 235 | { 236 | $caseLine = $this->tokens[$nextCase]['line']; 237 | $nextLine = $this->tokens[$nextBreak]['line']; 238 | for ($i = ($opener + 1); $i < $nextBreak; $i++) { 239 | if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') { 240 | $nextLine = $this->tokens[$i]['line']; 241 | break; 242 | } 243 | } 244 | if ($nextLine !== ($caseLine + 1)) { 245 | $error = 'Blank lines are not allowed after ' . strtoupper($type) . ' statements'; 246 | $this->file->addError($error, $nextCase, 'SpacingAfter' . $type); 247 | } 248 | } 249 | 250 | 251 | private function getNextLineFromNextBreak(int $nextBreak) : int 252 | { 253 | $semicolon = $this->file->findNext(T_SEMICOLON, $nextBreak); 254 | for ($i = ($semicolon + 1); $i < $this->tokens[$this->position]['scope_closer']; $i++) { 255 | if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') { 256 | return $this->tokens[$i]['line']; 257 | } 258 | } 259 | 260 | return $this->tokens[$this->tokens[$this->position]['scope_closer']]['line']; 261 | } 262 | 263 | 264 | private function ensureCaseIndention(int $nextBreak, int $caseAlignment) 265 | { 266 | // Only need to check a couple of things once, even if the 267 | // break is shared between multiple case statements, or even 268 | // the default case. 269 | if (($this->tokens[$nextBreak]['column'] - 1) !== $caseAlignment) { 270 | $error = 'Case breaking statement must be indented ' . ($this->indent + 1) . ' tabs from SWITCH keyword'; 271 | $this->file->addError($error, $nextBreak, 'BreakIndent'); 272 | } 273 | } 274 | 275 | 276 | private function ensureDefaultIsPresent(bool $foundDefault) 277 | { 278 | if ($foundDefault === FALSE) { 279 | $error = 'All SWITCH statements must contain a DEFAULT case'; 280 | $this->file->addError($error, $this->position, 'MissingDefault'); 281 | } 282 | } 283 | 284 | 285 | private function ensureClosingBraceAlignment(array $switch) 286 | { 287 | if ($this->tokens[$switch['scope_closer']]['column'] !== $switch['column']) { 288 | $error = 'Closing brace of SWITCH statement must be aligned with SWITCH keyword'; 289 | $this->file->addError($error, $switch['scope_closer'], 'CloseBraceAlign'); 290 | } 291 | } 292 | 293 | 294 | private function ensureNoSpaceBeforeColon(int $opener, int $nextCase, string $type) 295 | { 296 | if ($this->tokens[($opener - 1)]['type'] === 'T_WHITESPACE') { 297 | $error = 'There must be no space before the colon in a ' . strtoupper($type) . ' statement'; 298 | $this->file->addError($error, $nextCase, 'SpaceBeforeColon' . $type); 299 | } 300 | } 301 | 302 | 303 | private function checkSpaceAfterKeyword(int $nextCase, string $type) 304 | { 305 | if ($type === 'Case' && ($this->tokens[($nextCase + 1)]['type'] !== 'T_WHITESPACE' 306 | || $this->tokens[($nextCase + 1)]['content'] !== ' ') 307 | ) { 308 | $error = 'CASE keyword must be followed by a single space'; 309 | $this->file->addError($error, $nextCase, 'SpacingAfterCase'); 310 | } 311 | } 312 | 313 | } 314 | --------------------------------------------------------------------------------