Skip to content

Commit 9748708

Browse files
committed
Added PSR12.Files.FileHeader to enforce order and formatting of file header blocks (ref #750)
1 parent 0260750 commit 9748708

File tree

8 files changed

+428
-2
lines changed

8 files changed

+428
-2
lines changed

package.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
6969
-- Enforce the use of a strict types declaration in PHP files
7070
- Added PSR12.Files.DeclareStatement sniff
7171
-- Enforces the formatting of declare statements within a file
72+
- Added PSR12.Files.FileHeader sniff
73+
-- Enforces the order and formatting of file header blocks
7274
- Added PSR12.Files.ImportStatement sniff
7375
-- Enforces the formatting of import statements within a file
7476
- Added PSR12.Functions.ReturnTypeDeclaration sniff
@@ -1114,6 +1116,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
11141116
</dir>
11151117
<dir name="Files">
11161118
<file baseinstalldir="PHP/CodeSniffer" name="DeclareStatementSniff.php" role="php" />
1119+
<file baseinstalldir="PHP/CodeSniffer" name="FileHeaderSniff.php" role="php" />
11171120
<file baseinstalldir="PHP/CodeSniffer" name="ImportStatementSniff.php" role="php" />
11181121
</dir>
11191122
<dir name="Functions">
@@ -1146,6 +1149,11 @@ http://pear.php.net/dtd/package-2.0.xsd">
11461149
<file baseinstalldir="PHP/CodeSniffer" name="DeclareStatementUnitTest.inc" role="test" />
11471150
<file baseinstalldir="PHP/CodeSniffer" name="DeclareStatementUnitTest.inc.fixed" role="test" />
11481151
<file baseinstalldir="PHP/CodeSniffer" name="DeclareStatementUnitTest.php" role="test" />
1152+
<file baseinstalldir="PHP/CodeSniffer" name="FileHeaderUnitTest.1.inc" role="test" />
1153+
<file baseinstalldir="PHP/CodeSniffer" name="FileHeaderUnitTest.2.inc" role="test" />
1154+
<file baseinstalldir="PHP/CodeSniffer" name="FileHeaderUnitTest.3.inc" role="test" />
1155+
<file baseinstalldir="PHP/CodeSniffer" name="FileHeaderUnitTest.4.inc" role="test" />
1156+
<file baseinstalldir="PHP/CodeSniffer" name="FileHeaderUnitTest.php" role="test" />
11491157
<file baseinstalldir="PHP/CodeSniffer" name="ImportStatementUnitTest.inc" role="test" />
11501158
<file baseinstalldir="PHP/CodeSniffer" name="ImportStatementUnitTest.php" role="test" />
11511159
</dir>
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/**
4+
* This file contains an example of coding styles.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Vendor\Package;
10+
11+
use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
12+
// Comments are probably ok; PSR-12 doesn't say.
13+
use Vendor\Package\SomeNamespace\ClassD as D;
14+
use Vendor\Package\AnotherNamespace\ClassE as E;
15+
16+
// Comments are probably ok; PSR-12 doesn't say.
17+
use function Vendor\Package\{functionA, functionB, functionC};
18+
use function Another\Vendor\functionD;
19+
20+
use const Vendor\Package\{CONSTANT_A, CONSTANT_B, CONSTANT_C};
21+
use const Another\Vendor\CONSTANT_D;
22+
23+
/**
24+
* FooBar is an example class.
25+
*/
26+
class FooBar
27+
{
28+
// ... additional PHP code ...
29+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
4+
/**
5+
* This file contains an example of coding styles.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
11+
namespace Vendor\Package;
12+
13+
use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
14+
// Comments are probably ok; PSR-12 doesn't say.
15+
use Vendor\Package\SomeNamespace\ClassD as D;
16+
use Vendor\Package\AnotherNamespace\ClassE as E;
17+
18+
// Comments are probably ok; PSR-12 doesn't say.
19+
use function Vendor\Package\{functionA, functionB, functionC};
20+
21+
use function Another\Vendor\functionD;
22+
23+
24+
use const Vendor\Package\{CONSTANT_A, CONSTANT_B, CONSTANT_C};
25+
use const Another\Vendor\CONSTANT_D;
26+
27+
28+
/**
29+
* FooBar is an example class.
30+
*/
31+
class FooBar
32+
{
33+
// ... additional PHP code ...
34+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/**
4+
* This file contains an example of coding styles.
5+
*/
6+
7+
namespace Vendor\Package;
8+
9+
declare(strict_types=1);
10+
11+
use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
12+
use Vendor\Package\SomeNamespace\ClassD as D;
13+
use Vendor\Package\AnotherNamespace\ClassE as E;
14+
15+
use const Vendor\Package\{CONSTANT_A, CONSTANT_B, CONSTANT_C};
16+
use const Another\Vendor\CONSTANT_D;
17+
18+
use function Vendor\Package\{functionA, functionB, functionC};
19+
use function Another\Vendor\functionD;
20+
21+
/**
22+
* FooBar is an example class.
23+
*/
24+
class FooBar
25+
{
26+
// ... additional PHP code ...
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
namespace Vendor\Package;
3+
declare(strict_types=1);
4+
5+
use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
6+
use Vendor\Package\SomeNamespace\ClassD as D;
7+
use Vendor\Package\AnotherNamespace\ClassE as E;
8+
9+
/**
10+
* FooBar is an example class.
11+
*/
12+
class FooBar
13+
{
14+
// ... additional PHP code ...
15+
}

0 commit comments

Comments
 (0)