• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHP_CodeSniffer / 15253296250

26 May 2025 11:55AM UTC coverage: 78.632% (+0.3%) from 78.375%
15253296250

Pull #1105

github

web-flow
Merge d9441d98f into caf806050
Pull Request #1105: Skip tests when 'git' command is not available

19665 of 25009 relevant lines covered (78.63%)

88.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.63
/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php
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/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer\Standards\PSR12\Sniffs\Files;
11

12
use PHP_CodeSniffer\Files\File;
13
use PHP_CodeSniffer\Sniffs\Sniff;
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<int|string>
24
     */
25
    public function register()
3✔
26
    {
27
        return [T_OPEN_TAG];
3✔
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 int|void
40
     */
41
    public function process(File $phpcsFile, $stackPtr)
3✔
42
    {
43
        $tokens = $phpcsFile->getTokens();
3✔
44

45
        $possibleHeaders = [];
3✔
46

47
        $searchFor = Tokens::OO_SCOPE_TOKENS;
3✔
48
        $searchFor[T_OPEN_TAG] = T_OPEN_TAG;
3✔
49

50
        $openTag = $stackPtr;
3✔
51
        do {
52
            $headerLines = $this->getHeaderLines($phpcsFile, $openTag);
3✔
53
            if (empty($headerLines) === true && $openTag === $stackPtr) {
3✔
54
                // No content in the file.
55
                return;
×
56
            }
57

58
            $possibleHeaders[$openTag] = $headerLines;
3✔
59
            if (count($headerLines) > 1) {
3✔
60
                break;
3✔
61
            }
62

63
            $next = $phpcsFile->findNext($searchFor, ($openTag + 1));
3✔
64
            if (isset(Tokens::OO_SCOPE_TOKENS[$tokens[$next]['code']]) === true) {
3✔
65
                // Once we find an OO token, the file content has
66
                // definitely started.
67
                break;
3✔
68
            }
69

70
            $openTag = $next;
3✔
71
        } while ($openTag !== false);
3✔
72

73
        if ($openTag === false) {
3✔
74
            // We never found a proper file header.
75
            // If the file has multiple PHP open tags, we know
76
            // that it must be a mix of PHP and HTML (or similar)
77
            // so the header rules do not apply.
78
            if (count($possibleHeaders) > 1) {
3✔
79
                return $phpcsFile->numTokens;
3✔
80
            }
81

82
            // There is only one possible header.
83
            // If it is the first content in the file, it technically
84
            // serves as the file header, and the open tag needs to
85
            // have a newline after it. Otherwise, ignore it.
86
            if ($stackPtr > 0) {
3✔
87
                return $phpcsFile->numTokens;
3✔
88
            }
89

90
            $openTag = $stackPtr;
3✔
91
        } else if (count($possibleHeaders) > 1) {
3✔
92
            // There are other PHP blocks before the file header.
93
            $error = 'The file header must be the first content in the file';
3✔
94
            $phpcsFile->addError($error, $openTag, 'HeaderPosition');
3✔
95
        } else {
96
            // The first possible header was the file header block,
97
            // so make sure it is the first content in the file.
98
            if ($openTag !== 0) {
3✔
99
                // Allow for hashbang lines.
100
                $hashbang = false;
3✔
101
                if ($tokens[($openTag - 1)]['code'] === T_INLINE_HTML) {
3✔
102
                    $content = trim($tokens[($openTag - 1)]['content']);
3✔
103
                    if (substr($content, 0, 2) === '#!') {
3✔
104
                        $hashbang = true;
3✔
105
                    }
106
                }
107

108
                if ($hashbang === false) {
3✔
109
                    $error = 'The file header must be the first content in the file';
×
110
                    $phpcsFile->addError($error, $openTag, 'HeaderPosition');
×
111
                }
112
            }
113
        }//end if
114

115
        $this->processHeaderLines($phpcsFile, $possibleHeaders[$openTag]);
3✔
116

117
        return $phpcsFile->numTokens;
3✔
118

119
    }//end process()
120

121

122
    /**
123
     * Gather information about the statements inside a possible file header.
124
     *
125
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
126
     * @param int                         $stackPtr  The position of the current
127
     *                                               token in the stack.
128
     *
129
     * @return array
130
     */
131
    public function getHeaderLines(File $phpcsFile, $stackPtr)
3✔
132
    {
133
        $tokens = $phpcsFile->getTokens();
3✔
134

135
        $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
3✔
136
        if ($next === false) {
3✔
137
            return [];
×
138
        }
139

140
        $headerLines   = [];
3✔
141
        $headerLines[] = [
3✔
142
            'type'  => 'tag',
3✔
143
            'start' => $stackPtr,
3✔
144
            'end'   => $stackPtr,
3✔
145
        ];
2✔
146

147
        $foundDocblock = false;
3✔
148

149
        $commentOpeners = Tokens::SCOPE_OPENERS;
3✔
150
        unset($commentOpeners[T_NAMESPACE]);
3✔
151
        unset($commentOpeners[T_DECLARE]);
3✔
152
        unset($commentOpeners[T_USE]);
3✔
153
        unset($commentOpeners[T_IF]);
3✔
154
        unset($commentOpeners[T_WHILE]);
3✔
155
        unset($commentOpeners[T_FOR]);
3✔
156
        unset($commentOpeners[T_FOREACH]);
3✔
157
        unset($commentOpeners[T_DO]);
3✔
158
        unset($commentOpeners[T_TRY]);
3✔
159

160
        do {
161
            switch ($tokens[$next]['code']) {
3✔
162
            case T_DOC_COMMENT_OPEN_TAG:
3✔
163
                if ($foundDocblock === true) {
3✔
164
                    // Found a second docblock, so start of code.
165
                    break(2);
3✔
166
                }
167

168
                // Make sure this is not a code-level docblock.
169
                $end = $tokens[$next]['comment_closer'];
3✔
170
                for ($docToken = ($end + 1); $docToken < $phpcsFile->numTokens; $docToken++) {
3✔
171
                    if (isset(Tokens::EMPTY_TOKENS[$tokens[$docToken]['code']]) === true) {
3✔
172
                        continue;
3✔
173
                    }
174

175
                    if ($tokens[$docToken]['code'] === T_ATTRIBUTE
3✔
176
                        && isset($tokens[$docToken]['attribute_closer']) === true
3✔
177
                    ) {
178
                        $docToken = $tokens[$docToken]['attribute_closer'];
3✔
179
                        continue;
3✔
180
                    }
181

182
                    break;
3✔
183
                }
184

185
                if ($docToken === $phpcsFile->numTokens) {
3✔
186
                    $docToken--;
3✔
187
                }
188

189
                if (isset($commentOpeners[$tokens[$docToken]['code']]) === false
3✔
190
                    && isset(Tokens::METHOD_MODIFIERS[$tokens[$docToken]['code']]) === false
3✔
191
                    && $tokens[$docToken]['code'] !== T_READONLY
3✔
192
                ) {
193
                    // Check for an @var annotation.
194
                    $annotation = false;
3✔
195
                    for ($i = $next; $i < $end; $i++) {
3✔
196
                        if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG
3✔
197
                            && strtolower($tokens[$i]['content']) === '@var'
3✔
198
                        ) {
199
                            $annotation = true;
3✔
200
                            break;
3✔
201
                        }
202
                    }
203

204
                    if ($annotation === false) {
3✔
205
                        $foundDocblock = true;
3✔
206
                        $headerLines[] = [
3✔
207
                            'type'  => 'docblock',
3✔
208
                            'start' => $next,
3✔
209
                            'end'   => $end,
3✔
210
                        ];
2✔
211
                    }
212
                }//end if
213

214
                $next = $end;
3✔
215
                break;
3✔
216
            case T_DECLARE:
3✔
217
            case T_NAMESPACE:
3✔
218
                if (isset($tokens[$next]['scope_opener']) === true) {
3✔
219
                    // If this statement is using bracketed syntax, it doesn't
220
                    // apply to the entire files and so is not part of header.
221
                    // The header has now ended and the main code block begins.
222
                    break(2);
3✔
223
                }
224

225
                $end = $phpcsFile->findEndOfStatement($next);
3✔
226

227
                $headerLines[] = [
3✔
228
                    'type'  => substr(strtolower($tokens[$next]['type']), 2),
3✔
229
                    'start' => $next,
3✔
230
                    'end'   => $end,
3✔
231
                ];
2✔
232

233
                $next = $end;
3✔
234
                break;
3✔
235
            case T_USE:
3✔
236
                $type    = 'use';
3✔
237
                $useType = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($next + 1), null, true);
3✔
238
                if ($useType !== false && $tokens[$useType]['code'] === T_STRING) {
3✔
239
                    $content = strtolower($tokens[$useType]['content']);
3✔
240
                    if ($content === 'function' || $content === 'const') {
3✔
241
                        $type .= ' '.$content;
3✔
242
                    }
243
                }
244

245
                $end = $phpcsFile->findEndOfStatement($next);
3✔
246

247
                $headerLines[] = [
3✔
248
                    'type'  => $type,
3✔
249
                    'start' => $next,
3✔
250
                    'end'   => $end,
3✔
251
                ];
2✔
252

253
                $next = $end;
3✔
254
                break;
3✔
255
            default:
256
                // Skip comments as PSR-12 doesn't say if these are allowed or not.
257
                if (isset(Tokens::COMMENT_TOKENS[$tokens[$next]['code']]) === true) {
3✔
258
                    $next = $phpcsFile->findNext(Tokens::COMMENT_TOKENS, ($next + 1), null, true);
3✔
259
                    if ($next === false) {
3✔
260
                        // We reached the end of the file.
261
                        break(2);
3✔
262
                    }
263

264
                    $next--;
3✔
265
                    break;
3✔
266
                }
267

268
                // We found the start of the main code block.
269
                break(2);
3✔
270
            }//end switch
271

272
            $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true);
3✔
273
        } while ($next !== false);
3✔
274

275
        return $headerLines;
3✔
276

277
    }//end getHeaderLines()
278

279

280
    /**
281
     * Check the spacing and grouping of the statements inside each header block.
282
     *
283
     * @param \PHP_CodeSniffer\Files\File $phpcsFile   The file being scanned.
284
     * @param array                       $headerLines Header information, as sourced
285
     *                                                 from getHeaderLines().
286
     *
287
     * @return void
288
     */
289
    public function processHeaderLines(File $phpcsFile, $headerLines)
3✔
290
    {
291
        $tokens = $phpcsFile->getTokens();
3✔
292

293
        $found = [];
3✔
294

295
        foreach ($headerLines as $i => $line) {
3✔
296
            if (isset($headerLines[($i + 1)]) === false
3✔
297
                || $headerLines[($i + 1)]['type'] !== $line['type']
3✔
298
            ) {
299
                // We're at the end of the current header block.
300
                // Make sure there is a single blank line after
301
                // this block.
302
                $next = $phpcsFile->findNext(T_WHITESPACE, ($line['end'] + 1), null, true);
3✔
303
                if ($next !== false && $tokens[$next]['line'] !== ($tokens[$line['end']]['line'] + 2)) {
3✔
304
                    $error     = 'Header blocks must be separated by a single blank line';
3✔
305
                    $errorCode = 'SpacingAfter'.str_replace(' ', '', ucwords($line['type'])).'Block';
3✔
306
                    $fix       = $phpcsFile->addFixableError($error, $line['end'], $errorCode);
3✔
307
                    if ($fix === true) {
3✔
308
                        if ($tokens[$next]['line'] === $tokens[$line['end']]['line']) {
3✔
309
                            $phpcsFile->fixer->addContentBefore($next, $phpcsFile->eolChar.$phpcsFile->eolChar);
3✔
310
                        } else if ($tokens[$next]['line'] === ($tokens[$line['end']]['line'] + 1)) {
3✔
311
                            $phpcsFile->fixer->addNewline($line['end']);
3✔
312
                        } else {
313
                            $phpcsFile->fixer->beginChangeset();
3✔
314
                            for ($i = ($line['end'] + 1); $i < $next; $i++) {
3✔
315
                                if ($tokens[$i]['line'] === ($tokens[$line['end']]['line'] + 2)) {
3✔
316
                                    break;
3✔
317
                                }
318

319
                                $phpcsFile->fixer->replaceToken($i, '');
3✔
320
                            }
321

322
                            $phpcsFile->fixer->endChangeset();
3✔
323
                        }
324
                    }//end if
325
                }//end if
326

327
                // Make sure we haven't seen this next block before.
328
                if (isset($headerLines[($i + 1)]) === true
3✔
329
                    && isset($found[$headerLines[($i + 1)]['type']]) === true
3✔
330
                ) {
331
                    $error  = 'Similar statements must be grouped together inside header blocks; ';
×
332
                    $error .= 'the first "%s" statement was found on line %s';
×
333
                    $data   = [
334
                        $headerLines[($i + 1)]['type'],
×
335
                        $tokens[$found[$headerLines[($i + 1)]['type']]['start']]['line'],
×
336
                    ];
337
                    $phpcsFile->addError($error, $headerLines[($i + 1)]['start'], 'IncorrectGrouping', $data);
1✔
338
                }
339
            } else if ($headerLines[($i + 1)]['type'] === $line['type']) {
3✔
340
                // Still in the same block, so make sure there is no
341
                // blank line after this statement.
342
                $next = $phpcsFile->findNext(T_WHITESPACE, ($line['end'] + 1), null, true);
3✔
343
                if ($tokens[$next]['line'] > ($tokens[$line['end']]['line'] + 1)) {
3✔
344
                    $error     = 'Header blocks must not contain blank lines';
3✔
345
                    $errorCode = 'SpacingInside'.str_replace(' ', '', ucwords($line['type'])).'Block';
3✔
346
                    $fix       = $phpcsFile->addFixableError($error, $line['end'], $errorCode);
3✔
347
                    if ($fix === true) {
3✔
348
                        $phpcsFile->fixer->beginChangeset();
3✔
349
                        for ($i = ($line['end'] + 1); $i < $next; $i++) {
3✔
350
                            if ($tokens[$i]['line'] === $tokens[$line['end']]['line']) {
3✔
351
                                continue;
3✔
352
                            }
353

354
                            if ($tokens[$i]['line'] === $tokens[$next]['line']) {
3✔
355
                                break;
×
356
                            }
357

358
                            $phpcsFile->fixer->replaceToken($i, '');
3✔
359
                        }
360

361
                        $phpcsFile->fixer->endChangeset();
3✔
362
                    }
363
                }//end if
364
            }//end if
365

366
            if (isset($found[$line['type']]) === false) {
3✔
367
                $found[$line['type']] = $line;
3✔
368
            }
369
        }//end foreach
370

371
        /*
372
            Next, check that the order of the header blocks
373
            is correct:
374
                Opening php tag.
375
                File-level docblock.
376
                One or more declare statements.
377
                The namespace declaration of the file.
378
                One or more class-based use import statements.
379
                One or more function-based use import statements.
380
                One or more constant-based use import statements.
381
        */
382

383
        $blockOrder = [
2✔
384
            'tag'          => 'opening PHP tag',
3✔
385
            'docblock'     => 'file-level docblock',
2✔
386
            'declare'      => 'declare statements',
2✔
387
            'namespace'    => 'namespace declaration',
2✔
388
            'use'          => 'class-based use imports',
2✔
389
            'use function' => 'function-based use imports',
2✔
390
            'use const'    => 'constant-based use imports',
2✔
391
        ];
2✔
392

393
        foreach (array_keys($found) as $type) {
3✔
394
            if ($type === 'tag') {
3✔
395
                // The opening tag is always in the correct spot.
396
                continue;
3✔
397
            }
398

399
            do {
400
                $orderedType = next($blockOrder);
3✔
401
            } while ($orderedType !== false && key($blockOrder) !== $type);
3✔
402

403
            if ($orderedType === false) {
3✔
404
                // We didn't find the block type in the rest of the
405
                // ordered array, so it is out of place.
406
                // Error and reset the array to the correct position
407
                // so we can check the next block.
408
                reset($blockOrder);
3✔
409
                $prevValidType = 'tag';
3✔
410
                do {
411
                    $orderedType = next($blockOrder);
3✔
412
                    if (isset($found[key($blockOrder)]) === true
3✔
413
                        && key($blockOrder) !== $type
3✔
414
                    ) {
415
                        $prevValidType = key($blockOrder);
3✔
416
                    }
417
                } while ($orderedType !== false && key($blockOrder) !== $type);
3✔
418

419
                $error = 'The %s must follow the %s in the file header';
3✔
420
                $data  = [
2✔
421
                    $blockOrder[$type],
3✔
422
                    $blockOrder[$prevValidType],
3✔
423
                ];
2✔
424
                $phpcsFile->addError($error, $found[$type]['start'], 'IncorrectOrder', $data);
3✔
425
            }//end if
426
        }//end foreach
427

428
    }//end processHeaderLines()
1✔
429

430

431
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc