|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * Checks the format of the file header. |
| 4 | + * |
| 5 | + * @author Greg Sherwood <gsherwood@squiz.net> |
| 6 | + * @copyright 2006-2019 Squiz Pty Ltd (ABN 77 084 670 600) |
| 7 | + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence |
| 8 | + */ |
| 9 | + |
| 10 | +namespace PHP_CodeSniffer\Standards\PSR12\Sniffs\Files; |
| 11 | + |
| 12 | +use PHP_CodeSniffer\Sniffs\Sniff; |
| 13 | +use PHP_CodeSniffer\Files\File; |
| 14 | +use PHP_CodeSniffer\Util\Tokens; |
| 15 | + |
| 16 | +class FileHeaderSniff implements Sniff |
| 17 | +{ |
| 18 | + |
| 19 | + |
| 20 | + /** |
| 21 | + * Returns an array of tokens this test wants to listen for. |
| 22 | + * |
| 23 | + * @return array |
| 24 | + */ |
| 25 | + public function register() |
| 26 | + { |
| 27 | + return [T_OPEN_TAG]; |
| 28 | + |
| 29 | + }//end register() |
| 30 | + |
| 31 | + |
| 32 | + /** |
| 33 | + * Processes this sniff when one of its tokens is encountered. |
| 34 | + * |
| 35 | + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. |
| 36 | + * @param int $stackPtr The position of the current |
| 37 | + * token in the stack. |
| 38 | + * |
| 39 | + * @return void |
| 40 | + */ |
| 41 | + public function process(File $phpcsFile, $stackPtr) |
| 42 | + { |
| 43 | + $tokens = $phpcsFile->getTokens(); |
| 44 | + |
| 45 | + /* |
| 46 | + First, gather information about the statements inside |
| 47 | + the file header. |
| 48 | + */ |
| 49 | + |
| 50 | + $headerLines = []; |
| 51 | + $headerLines[] = [ |
| 52 | + 'type' => 'tag', |
| 53 | + 'start' => $stackPtr, |
| 54 | + 'end' => $stackPtr, |
| 55 | + ]; |
| 56 | + |
| 57 | + $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); |
| 58 | + if ($next === false) { |
| 59 | + return; |
| 60 | + } |
| 61 | + |
| 62 | + $foundDocblock = false; |
| 63 | + |
| 64 | + do { |
| 65 | + switch ($tokens[$next]['code']) { |
| 66 | + case T_DOC_COMMENT_OPEN_TAG: |
| 67 | + if ($foundDocblock === true) { |
| 68 | + // Found a second docblock, so start of code. |
| 69 | + break(2); |
| 70 | + } |
| 71 | + |
| 72 | + // Make sure this is not a code-level docblock. |
| 73 | + $end = $tokens[$next]['comment_closer']; |
| 74 | + $docToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true); |
| 75 | + if (isset(Tokens::$scopeOpeners[$tokens[$docToken]['code']]) === false) { |
| 76 | + $foundDocblock = true; |
| 77 | + $headerLines[] = [ |
| 78 | + 'type' => 'docblock', |
| 79 | + 'start' => $next, |
| 80 | + 'end' => $end, |
| 81 | + ]; |
| 82 | + } |
| 83 | + |
| 84 | + $next = $end; |
| 85 | + break; |
| 86 | + case T_DECLARE: |
| 87 | + case T_NAMESPACE: |
| 88 | + $end = $phpcsFile->findEndOfStatement($next); |
| 89 | + |
| 90 | + $headerLines[] = [ |
| 91 | + 'type' => substr(strtolower($tokens[$next]['type']), 2), |
| 92 | + 'start' => $next, |
| 93 | + 'end' => $end, |
| 94 | + ]; |
| 95 | + |
| 96 | + $next = $end; |
| 97 | + break; |
| 98 | + case T_USE: |
| 99 | + $type = 'use'; |
| 100 | + $useType = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true); |
| 101 | + if ($useType !== false && $tokens[$useType]['code'] === T_STRING) { |
| 102 | + $content = strtolower($tokens[$useType]['content']); |
| 103 | + if ($content === 'function' || $content === 'const') { |
| 104 | + $type .= ' '.$content; |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + $end = $phpcsFile->findEndOfStatement($next); |
| 109 | + |
| 110 | + $headerLines[] = [ |
| 111 | + 'type' => $type, |
| 112 | + 'start' => $next, |
| 113 | + 'end' => $end, |
| 114 | + ]; |
| 115 | + |
| 116 | + $next = $end; |
| 117 | + break; |
| 118 | + default: |
| 119 | + // Skip comments as PSR-12 doesn't say if these are allowed or not. |
| 120 | + if (isset(Tokens::$commentTokens[$tokens[$next]['code']]) === true) { |
| 121 | + $next = $phpcsFile->findNext(Tokens::$commentTokens, ($next + 1), null, true); |
| 122 | + $next--; |
| 123 | + break; |
| 124 | + } |
| 125 | + |
| 126 | + // We found the start of the main code block. |
| 127 | + break(2); |
| 128 | + }//end switch |
| 129 | + |
| 130 | + $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true); |
| 131 | + } while ($next !== false); |
| 132 | + |
| 133 | + /* |
| 134 | + Next, check the spacing and grouping of the statements |
| 135 | + inside each header block. |
| 136 | + */ |
| 137 | + |
| 138 | + $found = []; |
| 139 | + |
| 140 | + foreach ($headerLines as $i => $line) { |
| 141 | + if (isset($headerLines[($i + 1)]) === false |
| 142 | + || $headerLines[($i + 1)]['type'] !== $line['type'] |
| 143 | + ) { |
| 144 | + // We're at the end of the current header block. |
| 145 | + // Make sure there is a single blank line after |
| 146 | + // this block. |
| 147 | + $next = $phpcsFile->findNext(T_WHITESPACE, ($line['end'] + 1), null, true); |
| 148 | + if ($tokens[$next]['line'] !== ($tokens[$line['end']]['line'] + 2)) { |
| 149 | + $error = 'Header blocks must be followed by a single blank line'; |
| 150 | + $phpcsFile->addError($error, $line['end'], 'SpacingAfterBlock'); |
| 151 | + } |
| 152 | + |
| 153 | + // Make sure we haven't seen this next block before. |
| 154 | + if (isset($headerLines[($i + 1)]) === true |
| 155 | + && isset($found[$headerLines[($i + 1)]['type']]) === true |
| 156 | + ) { |
| 157 | + $error = 'Similar statements must be grouped together inside header blocks; '; |
| 158 | + $error .= 'the first "%s" statement was found on line %s'; |
| 159 | + $data = [ |
| 160 | + $headerLines[($i + 1)]['type'], |
| 161 | + $tokens[$found[$headerLines[($i + 1)]['type']]['start']]['line'], |
| 162 | + ]; |
| 163 | + $phpcsFile->addError($error, $headerLines[($i + 1)]['start'], 'IncorrectGrouping', $data); |
| 164 | + } |
| 165 | + } else if ($headerLines[($i + 1)]['type'] === $line['type']) { |
| 166 | + // Still in the same block, so make sure there is no |
| 167 | + // blank line after this statement. |
| 168 | + $next = $phpcsFile->findNext(T_WHITESPACE, ($line['end'] + 1), null, true); |
| 169 | + if ($tokens[$next]['line'] > ($tokens[$line['end']]['line'] + 1)) { |
| 170 | + $error = 'Header blocks must not contain blank lines'; |
| 171 | + $phpcsFile->addError($error, $line['end'], 'SpacingInsideBlock'); |
| 172 | + } |
| 173 | + }//end if |
| 174 | + |
| 175 | + if (isset($found[$line['type']]) === false) { |
| 176 | + $found[$line['type']] = $line; |
| 177 | + } |
| 178 | + }//end foreach |
| 179 | + |
| 180 | + /* |
| 181 | + Finally, check that the order of the header blocks |
| 182 | + is correct: |
| 183 | + Opening php tag. |
| 184 | + File-level docblock. |
| 185 | + One or more declare statements. |
| 186 | + The namespace declaration of the file. |
| 187 | + One or more class-based use import statements. |
| 188 | + One or more function-based use import statements. |
| 189 | + One or more constant-based use import statements. |
| 190 | + */ |
| 191 | + |
| 192 | + $blockOrder = [ |
| 193 | + 'tag' => 'opening PHP tag', |
| 194 | + 'docblock' => 'file-level docblock', |
| 195 | + 'declare' => 'declare statements', |
| 196 | + 'namespace' => 'namespace declaration', |
| 197 | + 'use' => 'class-based use imports', |
| 198 | + 'use function' => 'function-based use imports', |
| 199 | + 'use const' => 'constant-based use imports', |
| 200 | + ]; |
| 201 | + |
| 202 | + foreach (array_keys($found) as $type) { |
| 203 | + if ($type === 'tag') { |
| 204 | + // The opening tag is always in the correct spot. |
| 205 | + continue; |
| 206 | + } |
| 207 | + |
| 208 | + do { |
| 209 | + $orderedType = next($blockOrder); |
| 210 | + } while ($orderedType !== false && key($blockOrder) !== $type); |
| 211 | + |
| 212 | + if ($orderedType === false) { |
| 213 | + // We didn't find the block type in the rest of the |
| 214 | + // ordered array, so it is out of place. |
| 215 | + // Error and reset the array to the correct position |
| 216 | + // so we can check the next block. |
| 217 | + reset($blockOrder); |
| 218 | + $prevValidType = 'tag'; |
| 219 | + do { |
| 220 | + $orderedType = next($blockOrder); |
| 221 | + if (isset($found[key($blockOrder)]) === true |
| 222 | + && key($blockOrder) !== $type |
| 223 | + ) { |
| 224 | + $prevValidType = key($blockOrder); |
| 225 | + } |
| 226 | + } while ($orderedType !== false && key($blockOrder) !== $type); |
| 227 | + |
| 228 | + $error = 'The %s must follow the %s in the file header'; |
| 229 | + $data = [ |
| 230 | + $blockOrder[$type], |
| 231 | + $blockOrder[$prevValidType], |
| 232 | + ]; |
| 233 | + $phpcsFile->addError($error, $found[$type]['start'], 'IncorrectOrder', $data); |
| 234 | + }//end if |
| 235 | + }//end foreach |
| 236 | + |
| 237 | + }//end process() |
| 238 | + |
| 239 | + |
| 240 | +}//end class |
0 commit comments