├── CakePHP ├── Sniffs │ ├── Classes │ │ └── ReturnTypeHintSniff.php │ ├── Commenting │ │ ├── DocBlockAlignmentSniff.php │ │ ├── FunctionCommentSniff.php │ │ ├── InheritDocSniff.php │ │ └── TypeHintSniff.php │ ├── ControlStructures │ │ ├── ControlStructuresSniff.php │ │ ├── ElseIfDeclarationSniff.php │ │ └── WhileStructuresSniff.php │ ├── Formatting │ │ └── BlankLineBeforeReturnSniff.php │ ├── NamingConventions │ │ ├── ValidFunctionNameSniff.php │ │ └── ValidTraitNameSniff.php │ ├── PHP │ │ ├── DisallowShortOpenTagSniff.php │ │ └── SingleQuoteSniff.php │ └── WhiteSpace │ │ ├── EmptyLinesSniff.php │ │ ├── FunctionCallSpacingSniff.php │ │ ├── FunctionClosingBraceSpaceSniff.php │ │ ├── FunctionOpeningBraceSpaceSniff.php │ │ ├── FunctionSpacingSniff.php │ │ └── TabAndSpaceSniff.php └── ruleset.xml ├── LICENSE ├── README.md ├── composer.json ├── docs ├── README.md └── generate.php └── tests └── phpstan_bootstrap.php /CakePHP/Sniffs/Classes/ReturnTypeHintSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 39 | 40 | $openParenthesisIndex = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $stackPtr + 1); 41 | $closeParenthesisIndex = $tokens[$openParenthesisIndex]['parenthesis_closer']; 42 | 43 | $colonIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenthesisIndex + 1, null, true); 44 | if (!$colonIndex) { 45 | return; 46 | } 47 | 48 | $startIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $colonIndex + 1, $colonIndex + 3, true); 49 | if (!$startIndex) { 50 | return; 51 | } 52 | 53 | if (!$this->isChainingMethod($phpcsFile, $stackPtr)) { 54 | $this->assertNotThisOrStatic($phpcsFile, $stackPtr); 55 | 56 | return; 57 | } 58 | 59 | // We skip for interface methods 60 | if (empty($tokens[$stackPtr]['scope_opener']) || empty($tokens[$stackPtr]['scope_closer'])) { 61 | return; 62 | } 63 | 64 | $returnTokenCode = $tokens[$startIndex]['code']; 65 | if ($returnTokenCode !== T_SELF) { 66 | // Then we can only warn, but not auto-fix 67 | $phpcsFile->addError( 68 | 'Chaining methods (@return $this) should not have any return-type-hint.', 69 | $startIndex, 70 | 'InvalidSelf', 71 | ); 72 | 73 | return; 74 | } 75 | 76 | $fix = $phpcsFile->addFixableError( 77 | 'Chaining methods (@return $this) should not have any return-type-hint (Remove "self").', 78 | $startIndex, 79 | 'InvalidSelf', 80 | ); 81 | if (!$fix) { 82 | return; 83 | } 84 | 85 | $phpcsFile->fixer->beginChangeset(); 86 | for ($i = $colonIndex; $i <= $startIndex; $i++) { 87 | $phpcsFile->fixer->replaceToken($i, ''); 88 | } 89 | $phpcsFile->fixer->endChangeset(); 90 | } 91 | 92 | /** 93 | * @param \PHP_CodeSniffer\Files\File $phpCsFile File 94 | * @param int $stackPointer Stack pointer 95 | * @return bool 96 | */ 97 | protected function isChainingMethod(File $phpCsFile, int $stackPointer): bool 98 | { 99 | $docBlockEndIndex = $this->findRelatedDocBlock($phpCsFile, $stackPointer); 100 | 101 | if (!$docBlockEndIndex) { 102 | return false; 103 | } 104 | 105 | $tokens = $phpCsFile->getTokens(); 106 | 107 | $docBlockStartIndex = $tokens[$docBlockEndIndex]['comment_opener']; 108 | 109 | for ($i = $docBlockStartIndex + 1; $i < $docBlockEndIndex; $i++) { 110 | if ($tokens[$i]['code'] !== T_DOC_COMMENT_TAG) { 111 | continue; 112 | } 113 | if ($tokens[$i]['content'] !== '@return') { 114 | continue; 115 | } 116 | 117 | $classNameIndex = $i + 2; 118 | 119 | if ($tokens[$classNameIndex]['code'] !== T_DOC_COMMENT_STRING) { 120 | continue; 121 | } 122 | 123 | $content = $tokens[$classNameIndex]['content']; 124 | if (!$content) { 125 | continue; 126 | } 127 | 128 | return $content === '$this'; 129 | } 130 | 131 | return false; 132 | } 133 | 134 | /** 135 | * @param \PHP_CodeSniffer\Files\File $phpCsFile File 136 | * @param int $stackPointer Stack pointer 137 | * @return void 138 | */ 139 | protected function assertNotThisOrStatic(File $phpCsFile, int $stackPointer): void 140 | { 141 | $docBlockEndIndex = $this->findRelatedDocBlock($phpCsFile, $stackPointer); 142 | 143 | if (!$docBlockEndIndex) { 144 | return; 145 | } 146 | 147 | $tokens = $phpCsFile->getTokens(); 148 | 149 | $docBlockStartIndex = $tokens[$docBlockEndIndex]['comment_opener']; 150 | 151 | for ($i = $docBlockStartIndex + 1; $i < $docBlockEndIndex; $i++) { 152 | if ($tokens[$i]['code'] !== T_DOC_COMMENT_TAG) { 153 | continue; 154 | } 155 | if ($tokens[$i]['content'] !== '@return') { 156 | continue; 157 | } 158 | 159 | $classNameIndex = $i + 2; 160 | 161 | if ($tokens[$classNameIndex]['code'] !== T_DOC_COMMENT_STRING) { 162 | continue; 163 | } 164 | 165 | $content = $tokens[$classNameIndex]['content']; 166 | if (!$content || strpos($content, '\\') !== 0) { 167 | continue; 168 | } 169 | 170 | $classNameWithNamespace = $this->getClassNameWithNamespace($phpCsFile); 171 | if ($content !== $classNameWithNamespace) { 172 | continue; 173 | } 174 | 175 | $phpCsFile->addError( 176 | 'Class name repeated, expected `self` or `$this`.', 177 | $classNameIndex, 178 | 'InvalidClass', 179 | ); 180 | } 181 | } 182 | 183 | /** 184 | * @param \PHP_CodeSniffer\Files\File $phpCsFile File 185 | * @param int $stackPointer Stack pointer 186 | * @return int|null Stackpointer value of docblock end tag, or null if cannot be found 187 | */ 188 | protected function findRelatedDocBlock(File $phpCsFile, int $stackPointer): ?int 189 | { 190 | $tokens = $phpCsFile->getTokens(); 191 | 192 | $line = $tokens[$stackPointer]['line']; 193 | $beginningOfLine = $stackPointer; 194 | while (!empty($tokens[$beginningOfLine - 1]) && $tokens[$beginningOfLine - 1]['line'] === $line) { 195 | $beginningOfLine--; 196 | } 197 | 198 | if ( 199 | !empty($tokens[$beginningOfLine - 2]) 200 | && $tokens[$beginningOfLine - 2]['code'] === T_DOC_COMMENT_CLOSE_TAG 201 | ) { 202 | return $beginningOfLine - 2; 203 | } 204 | 205 | if ( 206 | !empty($tokens[$beginningOfLine - 3]) 207 | && $tokens[$beginningOfLine - 3]['code'] === T_DOC_COMMENT_CLOSE_TAG 208 | ) { 209 | return $beginningOfLine - 3; 210 | } 211 | 212 | return null; 213 | } 214 | 215 | /** 216 | * @param \PHP_CodeSniffer\Files\File $phpCsFile File 217 | * @return string|null 218 | */ 219 | protected function getClassNameWithNamespace(File $phpCsFile): ?string 220 | { 221 | try { 222 | $lastToken = TokenHelper::getLastTokenPointer($phpCsFile); 223 | } catch (EmptyFileException $e) { 224 | return null; 225 | } 226 | 227 | if (!NamespaceHelper::findCurrentNamespaceName($phpCsFile, $lastToken)) { 228 | return null; 229 | } 230 | 231 | $classPointer = $phpCsFile->findPrevious( 232 | [T_CLASS, T_TRAIT, T_INTERFACE, T_ENUM], 233 | $lastToken, 234 | ); 235 | if (!$classPointer) { 236 | return null; 237 | } 238 | 239 | return ClassHelper::getFullyQualifiedName( 240 | $phpCsFile, 241 | $classPointer, 242 | ); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 40 | $commentClose = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, $stackPtr); 41 | $afterComment = $phpcsFile->findNext(T_WHITESPACE, $commentClose + 1, null, true); 42 | $commentIndentation = $tokens[$stackPtr]['column'] - 1; 43 | $codeIndentation = $tokens[$afterComment]['column'] - 1; 44 | 45 | // Check for doc block opening being misaligned 46 | if ($commentIndentation != $codeIndentation) { 47 | $msg = 'Doc block not aligned with code; expected indentation of %s but found %s'; 48 | $data = [$codeIndentation, $commentIndentation]; 49 | $fix = $phpcsFile->addFixableError($msg, $stackPtr, 'DocBlockMisaligned', $data); 50 | if ($fix === true) { 51 | // Collect tokens to change indentation of 52 | $tokensToIndent = [ 53 | $stackPtr => $codeIndentation, 54 | ]; 55 | $commentOpenLine = $tokens[$stackPtr]['line']; 56 | $commentCloseLine = $tokens[$commentClose]['line']; 57 | $lineBreaksInComment = $commentCloseLine - $commentOpenLine; 58 | if ($lineBreaksInComment !== 0) { 59 | $searchToken = $stackPtr + 1; 60 | do { 61 | $commentBorder = $phpcsFile->findNext( 62 | [T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG], 63 | $searchToken, 64 | $commentClose + 1, 65 | ); 66 | if ($commentBorder !== false) { 67 | $tokensToIndent[$commentBorder] = $codeIndentation + 1; 68 | $searchToken = $commentBorder + 1; 69 | } 70 | } while ($commentBorder !== false); 71 | } 72 | 73 | // Update indentation 74 | $phpcsFile->fixer->beginChangeset(); 75 | foreach ($tokensToIndent as $searchToken => $indent) { 76 | $indentString = str_repeat(' ', $indent); 77 | $isOpenTag = $tokens[$searchToken]['code'] === T_DOC_COMMENT_OPEN_TAG; 78 | if ($isOpenTag && $commentIndentation === 0) { 79 | $phpcsFile->fixer->addContentBefore($searchToken, $indentString); 80 | } else { 81 | $phpcsFile->fixer->replaceToken($searchToken - 1, $indentString); 82 | } 83 | } 84 | $phpcsFile->fixer->endChangeset(); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/Commenting/FunctionCommentSniff.php: -------------------------------------------------------------------------------- 1 | 25 | *
  • A comment exists
  • 26 | *
  • No spacing between doc comment and function
  • 27 | *
  • Any throw tag must have a comment
  • 28 | * 29 | * 30 | * @category PHP 31 | * @package PHP_CodeSniffer 32 | * @author Greg Sherwood 33 | * @author Marc McIntyre 34 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 35 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 36 | * @version Release: @package_version@ 37 | * @link https://pear.php.net/package/PHP_CodeSniffer 38 | */ 39 | class FunctionCommentSniff implements Sniff 40 | { 41 | /** 42 | * Returns an array of tokens this test wants to listen for. 43 | * 44 | * @return array 45 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint 46 | */ 47 | public function register() 48 | { 49 | return [T_FUNCTION]; 50 | } 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | public function process(File $phpcsFile, $stackPtr) 56 | { 57 | $tokens = $phpcsFile->getTokens(); 58 | 59 | $docCommentEnd = $phpcsFile->findPrevious( 60 | [T_DOC_COMMENT_CLOSE_TAG, T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET], 61 | $stackPtr - 1, 62 | null, 63 | ); 64 | if ($docCommentEnd === false || $tokens[$docCommentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { 65 | $phpcsFile->addError( 66 | 'Missing doc comment for function %s()', 67 | $stackPtr, 68 | 'Missing', 69 | [$phpcsFile->getDeclarationName($stackPtr)], 70 | ); 71 | 72 | return; 73 | } 74 | 75 | $lastEndToken = $docCommentEnd; 76 | do { 77 | $attribute = $phpcsFile->findNext( 78 | [T_ATTRIBUTE], 79 | $lastEndToken + 1, 80 | $stackPtr, 81 | ); 82 | if ($attribute !== false) { 83 | if ($tokens[$lastEndToken]['line'] !== $tokens[$attribute]['line'] - 1) { 84 | $phpcsFile->addError( 85 | 'There must be no blank lines after the function comment or attribute', 86 | $lastEndToken, 87 | 'SpacingAfter', 88 | ); 89 | 90 | return; 91 | } 92 | 93 | $lastEndToken = $tokens[$attribute]['attribute_closer']; 94 | } 95 | } while ($attribute !== false); 96 | 97 | if ($tokens[$lastEndToken]['line'] !== $tokens[$stackPtr]['line'] - 1) { 98 | $phpcsFile->addError( 99 | 'There must be no blank lines after the function comment or attribute', 100 | $lastEndToken, 101 | 'SpacingAfter', 102 | ); 103 | } 104 | 105 | $commentStart = $tokens[$docCommentEnd]['comment_opener']; 106 | $this->processTagSpacing($phpcsFile, $stackPtr, $commentStart); 107 | $this->processThrows($phpcsFile, $stackPtr, $commentStart); 108 | } 109 | 110 | /** 111 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 112 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 113 | * @return void 114 | */ 115 | protected function processTagSpacing(File $phpcsFile, int $stackPtr, int $commentStart): void 116 | { 117 | $tokens = $phpcsFile->getTokens(); 118 | $tags = $tokens[$commentStart]['comment_tags']; 119 | foreach ($tags as $tag) { 120 | if ( 121 | $tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING && 122 | $phpcsFile->fixer->getTokenContent($tag + 1) !== ' ' 123 | ) { 124 | $fix = $phpcsFile->addFixableWarning('Should be only one space after tag', $tag, 'TagAlignment'); 125 | if ($fix) { 126 | $phpcsFile->fixer->beginChangeset(); 127 | $phpcsFile->fixer->replaceToken($tag + 1, ' '); 128 | $phpcsFile->fixer->endChangeset(); 129 | } 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * Process any throw tags that this function comment has. 136 | * 137 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 138 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 139 | * @param int $commentStart The position in the stack where the comment started. 140 | * @return void 141 | */ 142 | protected function processThrows(File $phpcsFile, int $stackPtr, int $commentStart): void 143 | { 144 | $tokens = $phpcsFile->getTokens(); 145 | 146 | foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 147 | if ($tokens[$tag]['content'] !== '@throws') { 148 | continue; 149 | } 150 | 151 | $exception = null; 152 | if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) { 153 | $matches = []; 154 | preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[$tag + 2]['content'], $matches); 155 | $exception = $matches[1] ?? null; 156 | } 157 | 158 | if ($exception === null) { 159 | $error = 'Exception type and comment missing for @throws tag in function comment'; 160 | $phpcsFile->addWarning($error, $tag, 'InvalidThrows'); 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/Commenting/InheritDocSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 40 | 41 | $commentEnd = $tokens[$stackPtr]['comment_closer']; 42 | 43 | $empty = [ 44 | T_DOC_COMMENT_WHITESPACE, 45 | T_DOC_COMMENT_STAR, 46 | ]; 47 | 48 | $inheritDoc = $phpcsFile->findNext($empty, $stackPtr + 1, $commentEnd, true); 49 | if ($inheritDoc === false) { 50 | // Ignore empty comments 51 | return; 52 | } 53 | 54 | if ( 55 | preg_match('/@inheritDoc/i', $tokens[$inheritDoc]['content']) === 1 && 56 | preg_match('/@inheritDoc/', $tokens[$inheritDoc]['content']) === 0 57 | ) { 58 | $msg = 'inheritDoc is not capitalized correctly'; 59 | $fix = $phpcsFile->addFixableWarning($msg, $inheritDoc, 'BadSpelling'); 60 | if ($fix === true) { 61 | $fixed = preg_replace('/inheritDoc/i', 'inheritDoc', $tokens[$inheritDoc]['content']); 62 | $phpcsFile->fixer->replaceToken($inheritDoc, $fixed); 63 | } 64 | } 65 | 66 | if ( 67 | preg_match('/^@inheritDoc/i', $tokens[$inheritDoc]['content']) === 1 && 68 | ( preg_match('/^@inheritDoc$/i', $tokens[$inheritDoc]['content']) !== 1 || 69 | $phpcsFile->findNext($empty, $inheritDoc + 1, $commentEnd, true) !== false 70 | ) 71 | ) { 72 | $msg = 'When using @inheritDoc, it must be the only doc comment.'; 73 | $phpcsFile->addWarning($msg, $inheritDoc, 'NotEmpty'); 74 | } 75 | 76 | if ( 77 | preg_match('/^{@inheritDoc}$/i', $tokens[$inheritDoc]['content']) === 1 && 78 | $phpcsFile->findNext($empty, $inheritDoc + 1, $commentEnd, true) === false 79 | ) { 80 | $msg = 'When inheriting entire doc comment, @inheritDoc must be used instead of {@inheritDoc}.'; 81 | $fix = $phpcsFile->addFixableWarning($msg, $inheritDoc, 'ShouldNotWrap'); 82 | if ($fix === true) { 83 | $phpcsFile->fixer->replaceToken($inheritDoc, '@inheritDoc'); 84 | } 85 | } 86 | 87 | if ( 88 | preg_match('/^{@inheritDoc}/i', $tokens[$inheritDoc]['content']) === 1 && 89 | preg_match('/^{@inheritDoc}$/i', $tokens[$inheritDoc]['content']) !== 1 90 | ) { 91 | $msg = 'If using {@inheritDoc} to copy description, it must be the first line in doc comment.'; 92 | $phpcsFile->addWarning($msg, $inheritDoc, 'FirstLine'); 93 | } 94 | 95 | $nextComment = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $inheritDoc + 1, $commentEnd); 96 | while ($nextComment !== false) { 97 | if (preg_match('/^{@inheritDoc}$/i', $tokens[$nextComment]['content']) === 1) { 98 | $msg = 'If using {@inheritDoc} to copy description, it must be the first line in doc comment.'; 99 | $phpcsFile->addWarning($msg, $nextComment, 'FirstLine'); 100 | } 101 | $nextComment = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $nextComment + 1, $commentEnd); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/Commenting/TypeHintSniff.php: -------------------------------------------------------------------------------- 1 | 56 | */ 57 | protected static array $typeHintTags = [ 58 | '@var', 59 | '@psalm-var', 60 | '@phpstan-var', 61 | '@param', 62 | '@psalm-param', 63 | '@phpstan-param', 64 | '@return', 65 | '@psalm-return', 66 | '@phpstan-return', 67 | ]; 68 | 69 | /** 70 | * Returns an array of tokens this test wants to listen for. 71 | * 72 | * @return array 73 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint 74 | */ 75 | public function register() 76 | { 77 | return [T_DOC_COMMENT_OPEN_TAG]; 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | public function process(File $phpcsFile, $stackPtr) 84 | { 85 | $tokens = $phpcsFile->getTokens(); 86 | 87 | if (!isset($tokens[$stackPtr]['comment_closer'])) { 88 | return; 89 | } 90 | 91 | foreach ($tokens[$stackPtr]['comment_tags'] as $tag) { 92 | if ( 93 | $tokens[$tag + 2]['code'] !== T_DOC_COMMENT_STRING || 94 | !in_array($tokens[$tag]['content'], self::$typeHintTags, true) 95 | ) { 96 | continue; 97 | } 98 | 99 | $tagComment = $phpcsFile->fixer->getTokenContent($tag + 2); 100 | $valueNode = self::getValueNode($tokens[$tag]['content'], $tagComment); 101 | if ($valueNode instanceof InvalidTagValueNode) { 102 | continue; 103 | } 104 | 105 | if (isset($valueNode->type)) { 106 | if ($valueNode->type instanceof UnionTypeNode) { 107 | $types = $valueNode->type->types; 108 | } elseif ($valueNode->type instanceof ArrayTypeNode) { 109 | $types = [$valueNode->type]; 110 | } else { 111 | continue; 112 | } 113 | } else { 114 | $phpcsFile->addWarning('@param type hint is missing', $tag, 'MissingParamType'); 115 | continue; 116 | } 117 | 118 | if ($this->ignorePhpStormGenerics && $this->isPhpStormGenericType($types)) { 119 | continue; 120 | } 121 | 122 | $originalTypeHint = $this->renderUnionTypes($types); 123 | $sortedTypeHint = $this->getSortedTypeHint($types); 124 | if ($sortedTypeHint === $originalTypeHint) { 125 | continue; 126 | } 127 | 128 | $fix = $phpcsFile->addFixableWarning( 129 | '%s type hint is not formatted properly, expected "%s"', 130 | $tag, 131 | 'IncorrectFormat', 132 | [$tokens[$tag]['content'], $sortedTypeHint], 133 | ); 134 | if (!$fix) { 135 | continue; 136 | } 137 | 138 | $newComment = $tagComment; 139 | if ($valueNode instanceof VarTagValueNode) { 140 | $newComment = trim(sprintf( 141 | '%s %s %s', 142 | $sortedTypeHint, 143 | $valueNode->variableName, 144 | $valueNode->description, 145 | )); 146 | if ($tagComment[-1] === ' ') { 147 | // tags above variables in code have a trailing space 148 | $newComment .= ' '; 149 | } 150 | } elseif ($valueNode instanceof ParamTagValueNode) { 151 | $newComment = trim(sprintf( 152 | '%s %s%s %s', 153 | $sortedTypeHint, 154 | $valueNode->isVariadic ? '...' : '', 155 | $valueNode->parameterName, 156 | $valueNode->description, 157 | )); 158 | } elseif ($valueNode instanceof ReturnTagValueNode) { 159 | $newComment = trim(sprintf( 160 | '%s %s', 161 | $sortedTypeHint, 162 | $valueNode->description, 163 | )); 164 | } 165 | 166 | $phpcsFile->fixer->beginChangeset(); 167 | $phpcsFile->fixer->replaceToken($tag + 2, $newComment); 168 | $phpcsFile->fixer->endChangeset(); 169 | } 170 | } 171 | 172 | /** 173 | * @param array $types node types 174 | * @return bool 175 | */ 176 | protected function isPhpStormGenericType(array $types): bool 177 | { 178 | if (count($types) != 2) { 179 | return false; 180 | } 181 | 182 | return $types[0] instanceof IdentifierTypeNode && 183 | $types[1] instanceof ArrayTypeNode && 184 | $types[0]->name[0] === '\\'; 185 | } 186 | 187 | /** 188 | * @param array $types node types 189 | * @return string 190 | */ 191 | protected function getSortedTypeHint(array $types): string 192 | { 193 | static $shouldSort = [ 194 | '\\Closure', 195 | '\\Generator', 196 | '\\ArrayObject', 197 | '\\ArrayAccess', 198 | '\\Traversable', 199 | '\\Stringable', 200 | '\\Countable', 201 | '$this', 202 | 'self', 203 | 'mixed', 204 | 'callable', 205 | 'resource', 206 | 'object', 207 | 'iterable', 208 | 'list', 209 | 'array', 210 | 'callable-string', 211 | 'class-string', 212 | 'interface-string', 213 | 'scalar', 214 | 'string', 215 | 'float', 216 | 'int', 217 | 'bool', 218 | 'true', 219 | 'false', 220 | 'null', 221 | 'void', 222 | ]; 223 | 224 | $sortable = array_fill_keys($shouldSort, []); 225 | $unsortable = []; 226 | foreach ($types as $type) { 227 | $sortName = null; 228 | if ($type instanceof IdentifierTypeNode) { 229 | $sortName = $type->name; 230 | } elseif ($type instanceof NullableTypeNode) { 231 | if ($type->type instanceof IdentifierTypeNode) { 232 | $sortName = $type->type->name; 233 | } 234 | } elseif ($type instanceof ArrayTypeNode) { 235 | if ($this->convertArraysToGenerics) { 236 | $type = new GenericTypeNode(new IdentifierTypeNode('array'), [$type->type]); 237 | $sortName = 'array'; 238 | } elseif ($type->type instanceof IdentifierTypeNode) { 239 | $sortName = $type->type->name; 240 | } else { 241 | $sortName = 'array'; 242 | } 243 | } elseif ($type instanceof ArrayShapeNode) { 244 | $sortName = 'array'; 245 | } elseif ($type instanceof GenericTypeNode) { 246 | if (in_array($type->type->name, $shouldSort)) { 247 | $sortName = $type->type->name; 248 | } 249 | } 250 | 251 | if (!$sortName) { 252 | $unsortable[] = $type; 253 | continue; 254 | } 255 | 256 | if (in_array($sortName, $shouldSort, true)) { 257 | if ($type instanceof ArrayTypeNode) { 258 | array_unshift($sortable[$sortName], $type); 259 | } else { 260 | $sortable[$sortName][] = $type; 261 | } 262 | } else { 263 | $unsortable[] = $type; 264 | } 265 | } 266 | 267 | $sorted = []; 268 | array_walk($sortable, function ($types) use (&$sorted): void { 269 | $sorted = array_merge($sorted, $types); 270 | }); 271 | 272 | return $this->renderUnionTypes(array_merge($unsortable, $sorted)); 273 | } 274 | 275 | /** 276 | * @param array<\PHPStan\PhpDocParser\Ast\Type\TypeNode> $typeNodes type nodes 277 | * @return string 278 | */ 279 | protected function renderUnionTypes(array $typeNodes): string 280 | { 281 | // Remove parenthesis added by phpstan around union and intersection types 282 | return (string)preg_replace( 283 | ['/ ([\|&]) /', '/<\(/', '/\)>/', '/\), /', '/, \(/'], 284 | ['${1}', '<', '>', ', ', ', '], 285 | implode('|', $typeNodes), 286 | ); 287 | } 288 | 289 | /** 290 | * @param string $tagName tag name 291 | * @param string $tagComment tag comment 292 | * @return \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode 293 | */ 294 | protected static function getValueNode(string $tagName, string $tagComment): PhpDocTagValueNode 295 | { 296 | static $phpDocParser; 297 | if (!$phpDocParser) { 298 | $config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]); 299 | 300 | $constExprParser = new ConstExprParser($config); 301 | $phpDocParser = new PhpDocParser($config, new TypeParser($config, $constExprParser), $constExprParser); 302 | } 303 | 304 | static $phpDocLexer; 305 | if (!$phpDocLexer) { 306 | $config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]); 307 | $phpDocLexer = new Lexer($config); 308 | } 309 | 310 | return $phpDocParser->parseTagValue(new TokenIterator($phpDocLexer->tokenize($tagComment)), $tagName); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/ControlStructures/ControlStructuresSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 42 | 43 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); 44 | if ($tokens[$nextToken]['code'] === T_OPEN_PARENTHESIS) { 45 | $closer = $tokens[$nextToken]['parenthesis_closer']; 46 | $diff = $closer - $stackPtr; 47 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + $diff + 1), null, true); 48 | } 49 | if ($tokens[$nextToken]['code'] === T_IF) { 50 | // "else if" is not checked by this sniff, another sniff takes care of that. 51 | return; 52 | } 53 | if ($tokens[$nextToken]['code'] !== T_OPEN_CURLY_BRACKET && $tokens[$nextToken]['code'] !== T_COLON) { 54 | $error = 'Curly brackets required for if/elseif/else.'; 55 | $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/ControlStructures/ElseIfDeclarationSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 40 | 41 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); 42 | if ($tokens[$nextToken]['code'] !== T_IF) { 43 | return; 44 | } 45 | 46 | $error = 'Usage of ELSE IF not allowed; use ELSEIF instead'; 47 | $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/ControlStructures/WhileStructuresSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 40 | 41 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); 42 | if ($tokens[$nextToken]['code'] === T_OPEN_PARENTHESIS) { 43 | $closer = $tokens[$nextToken]['parenthesis_closer']; 44 | $diff = $closer - $stackPtr; 45 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + $diff + 1), null, true); 46 | } 47 | 48 | if ($tokens[$stackPtr]['code'] === T_WHILE && $tokens[$nextToken]['code'] === T_SEMICOLON) { 49 | /* This while is probably part of a do-while construction, skip it .. */ 50 | return; 51 | } 52 | if ($tokens[$nextToken]['code'] !== T_OPEN_CURLY_BRACKET && $tokens[$nextToken]['code'] !== T_COLON) { 53 | $error = 'Curly brackets required in a do-while or while loop'; 54 | $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php: -------------------------------------------------------------------------------- 1 | 25 | * @license https://spdx.org/licenses/MIT MIT License 26 | * @link https://github.com/escapestudios/Symfony2-coding-standard 27 | */ 28 | class BlankLineBeforeReturnSniff implements Sniff 29 | { 30 | /** 31 | * @inheritDoc 32 | */ 33 | public function register() 34 | { 35 | return [T_RETURN]; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function process(File $phpcsFile, $stackPtr) 42 | { 43 | $tokens = $phpcsFile->getTokens(); 44 | $current = $stackPtr; 45 | $previousLine = $tokens[$stackPtr]['line'] - 1; 46 | $prevLineTokens = []; 47 | 48 | while ($current >= 0 && $tokens[$current]['line'] >= $previousLine) { 49 | if ( 50 | $tokens[$current]['line'] == $previousLine 51 | && $tokens[$current]['code'] !== T_WHITESPACE 52 | && $tokens[$current]['code'] !== T_COMMENT 53 | && $tokens[$current]['code'] !== T_DOC_COMMENT_OPEN_TAG 54 | && $tokens[$current]['code'] !== T_DOC_COMMENT_TAG 55 | && $tokens[$current]['code'] !== T_DOC_COMMENT_STRING 56 | && $tokens[$current]['code'] !== T_DOC_COMMENT_CLOSE_TAG 57 | && $tokens[$current]['code'] !== T_DOC_COMMENT_WHITESPACE 58 | ) { 59 | $prevLineTokens[] = $tokens[$current]['code']; 60 | } 61 | $current--; 62 | } 63 | 64 | if ( 65 | isset($prevLineTokens[0]) 66 | && ($prevLineTokens[0] === T_OPEN_CURLY_BRACKET 67 | || $prevLineTokens[0] === T_COLON 68 | || $prevLineTokens[0] === T_OPEN_TAG) 69 | ) { 70 | return; 71 | } elseif (count($prevLineTokens) > 0) { 72 | $fix = $phpcsFile->addFixableError( 73 | 'Missing blank line before return statement', 74 | $stackPtr, 75 | 'BlankLineBeforeReturn', 76 | ); 77 | if ($fix === true) { 78 | $phpcsFile->fixer->beginChangeset(); 79 | $phpcsFile->fixer->addNewlineBefore($stackPtr - 1); 80 | $phpcsFile->fixer->endChangeset(); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php: -------------------------------------------------------------------------------- 1 | getDeclarationName($stackPtr); 66 | if ($methodName === null) { 67 | return; 68 | } 69 | 70 | $className = $phpcsFile->getDeclarationName($currScope); 71 | $errorData = [$className . '::' . $methodName]; 72 | 73 | // Ignore magic methods 74 | if (preg_match('/^__(' . implode('|', $this->_magicMethods) . ')$/', $methodName)) { 75 | return; 76 | } 77 | 78 | $methodProps = $phpcsFile->getMethodProperties($stackPtr); 79 | if ($methodProps['scope_specified'] === false) { 80 | // Let another sniffer take care of that 81 | return; 82 | } 83 | 84 | $isPublic = $methodProps['scope'] === 'public'; 85 | 86 | if ($isPublic === true && $methodName[0] === '_') { 87 | $error = 'Public method name "%s" must not be prefixed with underscore'; 88 | $phpcsFile->addError($error, $stackPtr, 'PublicWithUnderscore', $errorData); 89 | 90 | return; 91 | } 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | */ 97 | protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) 98 | { 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 40 | $traitName = $tokens[$stackPtr + 2]['content']; 41 | 42 | if (substr($traitName, -5) !== 'Trait') { 43 | $error = 'Traits must have a "Trait" suffix.'; 44 | $phpcsFile->addError($error, $stackPtr, 'InvalidTraitName'); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/PHP/DisallowShortOpenTagSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 51 | $openTag = $tokens[$stackPtr]; 52 | 53 | if (trim($openTag['content']) === 'addError($error, $stackPtr, 'Found', $data); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/PHP/SingleQuoteSniff.php: -------------------------------------------------------------------------------- 1 | 7 | * Dariusz Rumiński 8 | * 9 | * This source file is subject to the MIT license that is bundled 10 | * with this source code in the file LICENSE. 11 | */ 12 | 13 | namespace CakePHP\Sniffs\PHP; 14 | 15 | use PHP_CodeSniffer\Files\File; 16 | use PHP_CodeSniffer\Sniffs\Sniff; 17 | use PHP_CodeSniffer\Util\Tokens; 18 | 19 | /** 20 | * Converts double quotes to single quotes for simple strings. 21 | * 22 | * @author Gregor Harlan 23 | * @author Mark Scherer 24 | */ 25 | class SingleQuoteSniff implements Sniff 26 | { 27 | /** 28 | * @inheritDoc 29 | */ 30 | public function register() 31 | { 32 | return [T_CONSTANT_ENCAPSED_STRING]; 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function process(File $phpcsFile, $stackPtr) 39 | { 40 | $tokens = $phpcsFile->getTokens(); 41 | 42 | // Skip for complex multiline 43 | $prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); 44 | if ($prevIndex && $tokens[$prevIndex]['code'] === T_CONSTANT_ENCAPSED_STRING) { 45 | return; 46 | } 47 | 48 | $content = $tokens[$stackPtr]['content']; 49 | if ( 50 | $content[0] === '"' 51 | && strpos($content, "'") === false 52 | && strpos($content, "\n") === false 53 | // regex: odd number of backslashes, not followed by double quote or dollar 54 | && !preg_match('/(?addFixableError( 57 | 'Use single instead of double quotes for simple strings.', 58 | $stackPtr, 59 | 'UseSingleQuote', 60 | ); 61 | if ($fix) { 62 | $content = substr($content, 1, -1); 63 | $content = str_replace(['\\"', '\\$'], ['"', '$'], $content); 64 | $phpcsFile->fixer->replaceToken($stackPtr, '\'' . $content . '\''); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/WhiteSpace/EmptyLinesSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 44 | // If the current and next two tokens are newlines 45 | // We can remove the next token (the first newline) 46 | if ( 47 | $tokens[$stackPtr]['content'] === $phpcsFile->eolChar 48 | && isset($tokens[$stackPtr + 1]) 49 | && $tokens[$stackPtr + 1]['content'] === $phpcsFile->eolChar 50 | && isset($tokens[$stackPtr + 2]) 51 | && $tokens[$stackPtr + 2]['content'] === $phpcsFile->eolChar 52 | ) { 53 | $error = 'Found more than a single empty line between content'; 54 | $fix = $phpcsFile->addFixableError($error, $stackPtr + 2, 'EmptyLines'); 55 | if ($fix) { 56 | $phpcsFile->fixer->replaceToken($stackPtr + 2, ''); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/WhiteSpace/FunctionCallSpacingSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 49 | 50 | // Find the next non-empty token. 51 | $openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); 52 | 53 | if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) { 54 | // Not a function call. 55 | return; 56 | } 57 | 58 | // Look for funcName ( 59 | if ($stackPtr + 1 !== $openBracket) { 60 | $error = 'Space before opening parenthesis of function call not allowed'; 61 | $phpcsFile->addError($error, $stackPtr, 'SpaceBeforeOpenBracket'); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/WhiteSpace/FunctionClosingBraceSpaceSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 39 | 40 | if (isset($tokens[$stackPtr]['scope_closer']) === false) { 41 | // Probably an interface method. 42 | return; 43 | } 44 | 45 | $closeBrace = $tokens[$stackPtr]['scope_closer']; 46 | $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, $closeBrace - 1, null, true); 47 | 48 | $braceLine = $tokens[$closeBrace]['line']; 49 | $prevLine = $tokens[$prevContent]['line']; 50 | 51 | $found = $braceLine - $prevLine - 1; 52 | if ( 53 | $phpcsFile->hasCondition($stackPtr, T_FUNCTION) === true 54 | || isset($tokens[$stackPtr]['nested_parenthesis']) === true 55 | ) { 56 | // Nested function. 57 | if ($found < 0) { 58 | $error = 'Closing brace of nested function must be on a new line'; 59 | $phpcsFile->addError($error, $closeBrace, 'ContentBeforeClose'); 60 | } elseif ($found > 0) { 61 | $error = 'Expected 0 blank lines before closing brace of nested function; %s found'; 62 | $data = [$found]; 63 | $phpcsFile->addError($error, $closeBrace, 'SpacingBeforeNestedClose', $data); 64 | } 65 | } else { 66 | if ($found !== 0) { 67 | $error = 'Expected 0 blank lines before closing function brace; %s found'; 68 | $data = [$found]; 69 | $phpcsFile->addError($error, $closeBrace, 'SpacingBeforeClose', $data); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/WhiteSpace/FunctionOpeningBraceSpaceSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 39 | 40 | if (isset($tokens[$stackPtr]['scope_opener']) === false) { 41 | // Probably an interface method. 42 | return; 43 | } 44 | 45 | $openBrace = $tokens[$stackPtr]['scope_opener']; 46 | $nextContent = $phpcsFile->findNext(T_WHITESPACE, $openBrace + 1, null, true); 47 | 48 | if ($nextContent === $tokens[$stackPtr]['scope_closer']) { 49 | // The next bit of content is the closing brace, so this 50 | // is an empty function and should have a blank line 51 | // between the opening and closing braces. 52 | return; 53 | } 54 | 55 | $braceLine = $tokens[$openBrace]['line']; 56 | $nextLine = $tokens[$nextContent]['line']; 57 | 58 | $found = $nextLine - $braceLine - 1; 59 | if ($found > 0) { 60 | $error = 'Expected 0 blank lines after opening function brace; %s found'; 61 | $data = [$found]; 62 | $phpcsFile->addError($error, $openBrace, 'SpacingAfter', $data); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 43 | 44 | $level = $tokens[$stackPointer]['level']; 45 | if ($level < 1) { 46 | return; 47 | } 48 | 49 | $openingBraceIndex = $phpCsFile->findNext(T_OPEN_CURLY_BRACKET, $stackPointer + 1); 50 | // Fix interface methods 51 | if (!$openingBraceIndex) { 52 | $openingParenthesisIndex = $phpCsFile->findNext(T_OPEN_PARENTHESIS, $stackPointer + 1); 53 | $closingParenthesisIndex = $tokens[$openingParenthesisIndex]['parenthesis_closer']; 54 | 55 | $semicolonIndex = $phpCsFile->findNext(T_SEMICOLON, $closingParenthesisIndex + 1); 56 | if (!$semicolonIndex) { 57 | return; 58 | } 59 | 60 | $nextContentIndex = $phpCsFile->findNext(T_WHITESPACE, $semicolonIndex + 1, null, true); 61 | 62 | // Do not mess with the end of the class 63 | if ($tokens[$nextContentIndex]['code'] === T_CLOSE_CURLY_BRACKET) { 64 | return; 65 | } 66 | 67 | if ($tokens[$nextContentIndex]['line'] - $tokens[$semicolonIndex]['line'] <= 1) { 68 | $fix = $phpCsFile->addFixableError( 69 | 'Every function/method needs a newline afterwards', 70 | $closingParenthesisIndex, 71 | 'Abstract', 72 | ); 73 | if ($fix) { 74 | $phpCsFile->fixer->addNewline($semicolonIndex); 75 | } 76 | } 77 | 78 | return; 79 | } 80 | 81 | $closingBraceIndex = $tokens[$openingBraceIndex]['scope_closer']; 82 | 83 | // Ignore closures 84 | $nextIndex = $phpCsFile->findNext(Tokens::$emptyTokens, $closingBraceIndex + 1, null, true); 85 | if (in_array($tokens[$nextIndex]['content'], [';', ',', ')'])) { 86 | return; 87 | } 88 | 89 | $nextContentIndex = $phpCsFile->findNext(T_WHITESPACE, $closingBraceIndex + 1, null, true); 90 | if (!$nextContentIndex) { 91 | return; 92 | } 93 | 94 | // Do not mess with the end of the class 95 | if ($tokens[$nextContentIndex]['code'] === T_CLOSE_CURLY_BRACKET) { 96 | return; 97 | } 98 | 99 | $this->assertNewLineAtTheEnd($phpCsFile, $closingBraceIndex, $nextContentIndex); 100 | $this->assertNewLineAtTheBeginning($phpCsFile, $stackPointer); 101 | } 102 | 103 | /** 104 | * @param \PHP_CodeSniffer\Files\File $phpCsFile File 105 | * @param int $closingBraceIndex Index 106 | * @param int|null $nextContentIndex Index 107 | * @return void 108 | */ 109 | protected function assertNewLineAtTheEnd(File $phpCsFile, int $closingBraceIndex, ?int $nextContentIndex): void 110 | { 111 | $tokens = $phpCsFile->getTokens(); 112 | 113 | if (!$nextContentIndex || $tokens[$nextContentIndex]['line'] - $tokens[$closingBraceIndex]['line'] <= 1) { 114 | $fix = $phpCsFile->addFixableError( 115 | 'Every function/method needs a newline afterwards', 116 | $closingBraceIndex, 117 | 'Concrete', 118 | ); 119 | if ($fix) { 120 | $phpCsFile->fixer->addNewline($closingBraceIndex); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Asserts newline at the beginning, including the doc block. 127 | * 128 | * @param \PHP_CodeSniffer\Files\File $phpCsFile File 129 | * @param int $stackPointer Stack pointer 130 | * @return void 131 | */ 132 | protected function assertNewLineAtTheBeginning(File $phpCsFile, int $stackPointer): void 133 | { 134 | $tokens = $phpCsFile->getTokens(); 135 | 136 | $line = $tokens[$stackPointer]['line']; 137 | $firstTokenInLineIndex = $stackPointer; 138 | while ($tokens[$firstTokenInLineIndex - 1]['line'] === $line) { 139 | $firstTokenInLineIndex--; 140 | } 141 | 142 | $prevContentIndex = $phpCsFile->findPrevious(T_WHITESPACE, $firstTokenInLineIndex - 1, null, true); 143 | 144 | if ($tokens[$prevContentIndex]['code'] === T_ATTRIBUTE_END) { 145 | return; 146 | } 147 | 148 | if ($tokens[$prevContentIndex]['code'] === T_DOC_COMMENT_CLOSE_TAG) { 149 | $firstTokenInLineIndex = $tokens[$prevContentIndex]['comment_opener']; 150 | while ($tokens[$firstTokenInLineIndex - 1]['line'] === $line) { 151 | $firstTokenInLineIndex--; 152 | } 153 | } 154 | 155 | $prevContentIndex = $phpCsFile->findPrevious(T_WHITESPACE, $firstTokenInLineIndex - 1, null, true); 156 | if (!$prevContentIndex) { 157 | return; 158 | } 159 | 160 | // Do not mess with the start of the class 161 | if ($tokens[$prevContentIndex]['code'] === T_OPEN_CURLY_BRACKET) { 162 | return; 163 | } 164 | 165 | if ($tokens[$prevContentIndex]['line'] < $tokens[$firstTokenInLineIndex]['line'] - 1) { 166 | return; 167 | } 168 | 169 | $fix = $phpCsFile->addFixableError( 170 | 'Every function/method needs a newline before', 171 | $firstTokenInLineIndex, 172 | 'Concrete', 173 | ); 174 | if ($fix) { 175 | $phpCsFile->fixer->addNewline($prevContentIndex); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /CakePHP/Sniffs/WhiteSpace/TabAndSpaceSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 41 | 42 | $line = $tokens[$stackPtr]['line']; 43 | if ($stackPtr > 0 && $tokens[$stackPtr - 1]['line'] !== $line) { 44 | return; 45 | } 46 | 47 | if (strpos($tokens[$stackPtr]['content'], ' ') !== false) { 48 | $error = 'Double space found'; 49 | $phpcsFile->addError($error, $stackPtr, 'DoubleSpace'); 50 | } 51 | if (strpos($tokens[$stackPtr]['content'], " \t") !== false) { 52 | $error = 'Space and tab found'; 53 | $phpcsFile->addError($error, $stackPtr, 'SpaceAndTab'); 54 | } 55 | if (strpos($tokens[$stackPtr]['content'], "\t ") !== false) { 56 | $error = 'Tab and space found'; 57 | $phpcsFile->addError($error, $stackPtr, 'TabAndSpace'); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CakePHP/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | CakePHP coding standard 4 | 5 | 6 | 7 | \.git 8 | /*/tmp/ 9 | tests/*/templates/* 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | */config/Migrations/* 26 | */config/Seeds/* 27 | 28 | 29 | */config/* 30 | */tests/* 31 | 32 | 33 | */src/Controller/* 34 | */src/Command/* 35 | */src/Shell/* 36 | */tests/* 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | */tests/* 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | */tests/* 67 | 68 | 69 | */tests/* 70 | 71 | 72 | 73 | 74 | 75 | 76 | */config/Migrations/* 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | */tests/* 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | */templates/* 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | */config/* 192 | */templates/* 193 | */tests/Fixture/* 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | */tests/* 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | */tests/* 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | */tests/* 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | */tests/* 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-present, Cake Software Foundation, Inc. (https://cakefoundation.org) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP Code Sniffer 2 | 3 | ![Build Status](https://github.com/cakephp/cakephp-codesniffer/actions/workflows/ci.yml/badge.svg?branch=5.next) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/cakephp/cakephp-codesniffer.svg?style=flat-square)](https://packagist.org/packages/cakephp/cakephp-codesniffer) 5 | [![Latest Stable Version](https://img.shields.io/packagist/v/cakephp/cakephp-codesniffer.svg?style=flat-square)](https://packagist.org/packages/cakephp/cakephp-codesniffer) 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 7 | 8 | This code works with [squizlabs/php_codesniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) 9 | and checks code against the coding standards used in CakePHP. 10 | 11 | This sniffer package follows [PSR-12](https://www.php-fig.org/psr/psr-12/) completely and ships with a lot of additional fixers on top. 12 | 13 | [List of included sniffs](/docs) 14 | 15 | ## Which version should I use? 16 | See [version map](https://github.com/cakephp/cakephp-codesniffer/wiki). 17 | 18 | ## Installation 19 | 20 | You should install this codesniffer with composer: 21 | 22 | composer require --dev cakephp/cakephp-codesniffer 23 | vendor/bin/phpcs --config-set installed_paths /path/to/your/app/vendor/cakephp/cakephp-codesniffer 24 | 25 | The second command lets `phpcs` know where to find your new sniffs. Ensure that 26 | you do not overwrite any existing `installed_paths` value. Alternatively, install 27 | the [`dealerdirect/phpcodesniffer-composer-installer`](https://github.com/Dealerdirect/phpcodesniffer-composer-installer) 28 | composer package which will handle configuring the `phpcs` `installed_paths` for you. 29 | 30 | ## Usage 31 | 32 | :warning: Warning when these sniffs are installed with composer, ensure that 33 | you have configured the CodeSniffer `installed_paths` setting. 34 | 35 | Depending on how you installed the code sniffer changes how you run it. If you have 36 | installed phpcs, and this package with PEAR, you can do the following: 37 | 38 | vendor/bin/phpcs --colors -p -s --standard=CakePHP /path/to/code/ 39 | 40 | You can also copy the `phpcs.xml.dist` file to your project's root folder as `phpcs.xml`. 41 | This file will import the CakePHP Coding Standard. From there you can edit it to 42 | include/exclude as needed. With this file in place, you can run: 43 | 44 | vendor/bin/phpcs --colors -p -s /path/to/code/ 45 | 46 | If you are using Composer to manage your CakePHP project, you can also add the below to your composer.json file: 47 | 48 | ```json 49 | { 50 | "scripts": { 51 | "cs-check": "vendor/bin/phpcs --colors -p -s --extensions=php src/ tests/" 52 | } 53 | } 54 | ``` 55 | 56 | ## Running Tests 57 | 58 | You can run tests with composer. Because of how PHPCS test suites work, there is 59 | additional configuration state in `phpcs` that is required. 60 | 61 | ```bash 62 | composer test 63 | ``` 64 | 65 | Once this has been done once, you can use `composer phpunit` to run the 66 | tests for the rules in this repository. 67 | 68 | The tests are present inside the `CakePHP/Tests/` folder. 69 | 70 | ## Contributing 71 | 72 | If you'd like to contribute to the Code Sniffer, you can fork the project add 73 | features and send pull requests. 74 | 75 | > [!NOTE] 76 | > Please make sure to run `composer docs` if you change the ruleset.xml file. 77 | 78 | ## Releasing CakePHP Code Sniffer 79 | 80 | * Create a signed tag 81 | * Write the changelog in the tag commit 82 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/cakephp-codesniffer", 3 | "description": "CakePHP CodeSniffer Standards", 4 | "type": "phpcodesniffer-standard", 5 | "keywords": ["framework", "codesniffer"], 6 | "homepage": "https://cakephp.org", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "CakePHP Community", 11 | "homepage": "https://github.com/cakephp/cakephp-codesniffer/graphs/contributors" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/cakephp/cakephp-codesniffer/issues", 16 | "forum": "https://stackoverflow.com/tags/cakephp", 17 | "irc": "irc://irc.freenode.org/cakephp", 18 | "source": "https://github.com/cakephp/cakephp-codesniffer" 19 | }, 20 | "require": { 21 | "php": ">=8.1.0", 22 | "phpstan/phpdoc-parser": "^2.1.0", 23 | "slevomat/coding-standard": "^8.16", 24 | "squizlabs/php_codesniffer": "^3.9" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^9.3.4" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "CakePHP\\": "CakePHP/" 32 | } 33 | }, 34 | "config": { 35 | "allow-plugins": { 36 | "dealerdirect/phpcodesniffer-composer-installer": true 37 | } 38 | }, 39 | "scripts": { 40 | "add-standard" : "phpcs --config-set installed_paths $(pwd)", 41 | "phpunit": "phpunit vendor/squizlabs/php_codesniffer/tests/AllTests.php --filter CakePHP --no-configuration --bootstrap=vendor/squizlabs/php_codesniffer/tests/bootstrap.php --dont-report-useless-tests", 42 | "test": [ 43 | "@add-standard", 44 | "@phpunit" 45 | ], 46 | "cs-check": "phpcs --colors --parallel=16 -p -s CakePHP/", 47 | "cs-fix": "phpcbf --colors --parallel=16 -p CakePHP/", 48 | "stan": "tools/phpstan analyse", 49 | "docs": "php docs/generate.php", 50 | "explain": "phpcs -e --standard=CakePHP" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # CakePHP ruleset 2 | 3 | The CakePHP standard contains 155 sniffs 4 | 5 | CakePHP (19 sniffs) 6 | ------------------- 7 | - CakePHP.Classes.ReturnTypeHint 8 | - CakePHP.Commenting.DocBlockAlignment 9 | - CakePHP.Commenting.FunctionComment 10 | - CakePHP.Commenting.InheritDoc 11 | - CakePHP.Commenting.TypeHint 12 | - CakePHP.ControlStructures.ControlStructures 13 | - CakePHP.ControlStructures.ElseIfDeclaration 14 | - CakePHP.ControlStructures.WhileStructures 15 | - CakePHP.Formatting.BlankLineBeforeReturn 16 | - CakePHP.NamingConventions.ValidFunctionName 17 | - CakePHP.NamingConventions.ValidTraitName 18 | - CakePHP.PHP.DisallowShortOpenTag 19 | - CakePHP.PHP.SingleQuote 20 | - CakePHP.WhiteSpace.EmptyLines 21 | - CakePHP.WhiteSpace.FunctionCallSpacing 22 | - CakePHP.WhiteSpace.FunctionClosingBraceSpace 23 | - CakePHP.WhiteSpace.FunctionOpeningBraceSpace 24 | - CakePHP.WhiteSpace.FunctionSpacing 25 | - CakePHP.WhiteSpace.TabAndSpace 26 | 27 | Generic (26 sniffs) 28 | ------------------- 29 | - Generic.Arrays.DisallowLongArraySyntax 30 | - Generic.CodeAnalysis.ForLoopShouldBeWhileLoop 31 | - Generic.CodeAnalysis.ForLoopWithTestFunctionCall 32 | - Generic.CodeAnalysis.JumbledIncrementer 33 | - Generic.CodeAnalysis.UnconditionalIfStatement 34 | - Generic.CodeAnalysis.UnnecessaryFinalModifier 35 | - Generic.ControlStructures.InlineControlStructure 36 | - Generic.Files.ByteOrderMark 37 | - Generic.Files.LineEndings 38 | - Generic.Files.LineLength 39 | - Generic.Formatting.DisallowMultipleStatements 40 | - Generic.Formatting.SpaceAfterCast 41 | - Generic.Functions.FunctionCallArgumentSpacing 42 | - Generic.NamingConventions.UpperCaseConstantName 43 | - Generic.PHP.DeprecatedFunctions 44 | - Generic.PHP.DisallowAlternativePHPTags 45 | - Generic.PHP.DisallowShortOpenTag 46 | - Generic.PHP.ForbiddenFunctions 47 | - Generic.PHP.LowerCaseConstant 48 | - Generic.PHP.LowerCaseKeyword 49 | - Generic.PHP.LowerCaseType 50 | - Generic.PHP.NoSilencedErrors 51 | - Generic.WhiteSpace.DisallowTabIndent 52 | - Generic.WhiteSpace.IncrementDecrementSpacing 53 | - Generic.WhiteSpace.LanguageConstructSpacing 54 | - Generic.WhiteSpace.ScopeIndent 55 | 56 | PEAR (1 sniff) 57 | -------------- 58 | - PEAR.Functions.ValidDefaultValue 59 | 60 | PSR1 (3 sniffs) 61 | --------------- 62 | - PSR1.Classes.ClassDeclaration 63 | - PSR1.Files.SideEffects 64 | - PSR1.Methods.CamelCapsMethodName 65 | 66 | PSR2 (9 sniffs) 67 | --------------- 68 | - PSR2.Classes.ClassDeclaration 69 | - PSR2.Classes.PropertyDeclaration 70 | - PSR2.ControlStructures.ElseIfDeclaration 71 | - PSR2.ControlStructures.SwitchDeclaration 72 | - PSR2.Files.ClosingTag 73 | - PSR2.Files.EndFileNewline 74 | - PSR2.Methods.FunctionCallSignature 75 | - PSR2.Methods.FunctionClosingBrace 76 | - PSR2.Methods.MethodDeclaration 77 | 78 | PSR12 (17 sniffs) 79 | ----------------- 80 | - PSR12.Classes.AnonClassDeclaration 81 | - PSR12.Classes.ClassInstantiation 82 | - PSR12.Classes.ClosingBrace 83 | - PSR12.Classes.OpeningBraceSpace 84 | - PSR12.ControlStructures.BooleanOperatorPlacement 85 | - PSR12.ControlStructures.ControlStructureSpacing 86 | - PSR12.Files.DeclareStatement 87 | - PSR12.Files.FileHeader 88 | - PSR12.Files.ImportStatement 89 | - PSR12.Files.OpenTag 90 | - PSR12.Functions.NullableTypeDeclaration 91 | - PSR12.Functions.ReturnTypeDeclaration 92 | - PSR12.Keywords.ShortFormTypeKeywords 93 | - PSR12.Namespaces.CompoundNamespaceDepth 94 | - PSR12.Operators.OperatorSpacing 95 | - PSR12.Properties.ConstantVisibility 96 | - PSR12.Traits.UseDeclaration 97 | 98 | SlevomatCodingStandard (52 sniffs) 99 | ---------------------------------- 100 | - SlevomatCodingStandard.Arrays.TrailingArrayComma 101 | - SlevomatCodingStandard.Attributes.AttributeAndTargetSpacing 102 | - SlevomatCodingStandard.Attributes.RequireAttributeAfterDocComment 103 | - SlevomatCodingStandard.Classes.BackedEnumTypeSpacing 104 | - SlevomatCodingStandard.Classes.ClassConstantVisibility 105 | - SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces 106 | - SlevomatCodingStandard.Classes.ModernClassNameReference 107 | - SlevomatCodingStandard.Classes.PropertyDeclaration 108 | - SlevomatCodingStandard.Commenting.DisallowOneLinePropertyDocComment 109 | - SlevomatCodingStandard.Commenting.DocCommentSpacing 110 | - SlevomatCodingStandard.Commenting.EmptyComment 111 | - SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration 112 | - SlevomatCodingStandard.ControlStructures.AssignmentInCondition 113 | - SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch 114 | - SlevomatCodingStandard.ControlStructures.DisallowYodaComparison 115 | - SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses 116 | - SlevomatCodingStandard.ControlStructures.NewWithParentheses 117 | - SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator 118 | - SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator 119 | - SlevomatCodingStandard.Exceptions.DeadCatch 120 | - SlevomatCodingStandard.Functions.ArrowFunctionDeclaration 121 | - SlevomatCodingStandard.Functions.DisallowTrailingCommaInCall 122 | - SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse 123 | - SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration 124 | - SlevomatCodingStandard.Functions.NamedArgumentSpacing 125 | - SlevomatCodingStandard.Functions.RequireTrailingCommaInCall 126 | - SlevomatCodingStandard.Functions.RequireTrailingCommaInClosureUse 127 | - SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration 128 | - SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses 129 | - SlevomatCodingStandard.Namespaces.DisallowGroupUse 130 | - SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation 131 | - SlevomatCodingStandard.Namespaces.NamespaceDeclaration 132 | - SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly 133 | - SlevomatCodingStandard.Namespaces.UnusedUses 134 | - SlevomatCodingStandard.Namespaces.UseDoesNotStartWithBackslash 135 | - SlevomatCodingStandard.Namespaces.UseFromSameNamespace 136 | - SlevomatCodingStandard.Namespaces.UseSpacing 137 | - SlevomatCodingStandard.PHP.ShortList 138 | - SlevomatCodingStandard.PHP.TypeCast 139 | - SlevomatCodingStandard.PHP.UselessParentheses 140 | - SlevomatCodingStandard.PHP.UselessSemicolon 141 | - SlevomatCodingStandard.TypeHints.DeclareStrictTypes 142 | - SlevomatCodingStandard.TypeHints.DNFTypeHintFormat 143 | - SlevomatCodingStandard.TypeHints.LongTypeHints 144 | - SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue 145 | - SlevomatCodingStandard.TypeHints.ParameterTypeHint 146 | - SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing 147 | - SlevomatCodingStandard.TypeHints.PropertyTypeHint 148 | - SlevomatCodingStandard.TypeHints.ReturnTypeHint 149 | - SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing 150 | - SlevomatCodingStandard.Variables.DuplicateAssignmentToVariable 151 | - SlevomatCodingStandard.Variables.UnusedVariable 152 | 153 | Squiz (27 sniffs) 154 | ----------------- 155 | - Squiz.Arrays.ArrayBracketSpacing 156 | - Squiz.Classes.ClassFileName 157 | - Squiz.Classes.LowercaseClassKeywords 158 | - Squiz.Classes.ValidClassName 159 | - Squiz.Commenting.DocCommentAlignment 160 | - Squiz.ControlStructures.ControlSignature 161 | - Squiz.ControlStructures.ForEachLoopDeclaration 162 | - Squiz.ControlStructures.ForLoopDeclaration 163 | - Squiz.ControlStructures.LowercaseDeclaration 164 | - Squiz.Functions.FunctionDeclaration 165 | - Squiz.Functions.FunctionDeclarationArgumentSpacing 166 | - Squiz.Functions.LowercaseFunctionKeywords 167 | - Squiz.Functions.MultiLineFunctionDeclaration 168 | - Squiz.Operators.ValidLogicalOperators 169 | - Squiz.PHP.DisallowSizeFunctionsInLoops 170 | - Squiz.PHP.Eval 171 | - Squiz.PHP.NonExecutableCode 172 | - Squiz.Scope.MemberVarScope 173 | - Squiz.Scope.MethodScope 174 | - Squiz.Scope.StaticThisUsage 175 | - Squiz.WhiteSpace.CastSpacing 176 | - Squiz.WhiteSpace.ControlStructureSpacing 177 | - Squiz.WhiteSpace.LogicalOperatorSpacing 178 | - Squiz.WhiteSpace.ScopeClosingBrace 179 | - Squiz.WhiteSpace.ScopeKeywordSpacing 180 | - Squiz.WhiteSpace.SemicolonSpacing 181 | - Squiz.WhiteSpace.SuperfluousWhitespace 182 | 183 | Zend (1 sniff) 184 | -------------- 185 | - Zend.NamingConventions.ValidVariableName -------------------------------------------------------------------------------- /docs/generate.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 |