Skip to content

Commit 6905d66

Browse files
committed
Check leading and trailing file whitespace and BOM
1 parent 37e65a4 commit 6905d66

File tree

9 files changed

+176
-1
lines changed

9 files changed

+176
-1
lines changed

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ parameters:
33
closureUsesThis: true
44
randomIntParameters: true
55
nullCoalesce: true
6+
fileWhitespace: true

conf/config.level0.neon

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ conditionalTags:
77
phpstan.rules.rule: %featureToggles.closureUsesThis%
88
PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule:
99
phpstan.rules.rule: %checkMissingClosureNativeReturnTypehintRule%
10+
PHPStan\Rules\Whitespace\FileWhitespaceRule:
11+
phpstan.rules.rule: %featureToggles.fileWhitespace%
1012

1113
parametersSchema:
1214
missingClosureNativeReturnCheckObjectTypehint: bool()
@@ -178,3 +180,6 @@ services:
178180
class: PHPStan\Rules\Regexp\RegularExpressionPatternRule
179181
tags:
180182
- phpstan.rules.rule
183+
184+
-
185+
class: PHPStan\Rules\Whitespace\FileWhitespaceRule

conf/config.neon

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ parameters:
1111
closureUsesThis: false
1212
randomIntParameters: false
1313
nullCoalesce: false
14+
fileWhitespace: false
1415
fileExtensions:
1516
- php
1617
checkAlwaysTrueCheckTypeFunctionCall: false
@@ -145,7 +146,8 @@ parametersSchema:
145146
disableRuntimeReflectionProvider: bool(),
146147
closureUsesThis: bool(),
147148
randomIntParameters: bool(),
148-
nullCoalesce: bool()
149+
nullCoalesce: bool(),
150+
fileWhitespace: bool()
149151
])
150152
fileExtensions: listOf(string())
151153
checkAlwaysTrueCheckTypeFunctionCall: bool()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Whitespace;
4+
5+
use Nette\Utils\Strings;
6+
use PhpParser\Node;
7+
use PhpParser\NodeTraverser;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Node\FileNode;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
13+
/**
14+
* @implements Rule<FileNode>
15+
*/
16+
class FileWhitespaceRule implements Rule
17+
{
18+
19+
public function getNodeType(): string
20+
{
21+
return FileNode::class;
22+
}
23+
24+
public function processNode(Node $node, Scope $scope): array
25+
{
26+
$nodes = $node->getNodes();
27+
if (count($nodes) === 0) {
28+
return [];
29+
}
30+
31+
$firstNode = $nodes[0];
32+
$messages = [];
33+
if ($firstNode instanceof Node\Stmt\InlineHTML && $firstNode->value === "\xef\xbb\xbf") {
34+
$messages[] = RuleErrorBuilder::message('File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.')->build();
35+
}
36+
37+
$nodeTraverser = new NodeTraverser();
38+
$visitor = new class () extends \PhpParser\NodeVisitorAbstract {
39+
40+
/** @var \PhpParser\Node[] */
41+
private $lastNodes = [];
42+
43+
/**
44+
* @param Node $node
45+
* @return int|Node|null
46+
*/
47+
public function enterNode(Node $node)
48+
{
49+
if ($node instanceof Node\Stmt\Declare_) {
50+
if ($node->stmts !== null && count($node->stmts) > 0) {
51+
$this->lastNodes[] = $node->stmts[count($node->stmts) - 1];
52+
}
53+
return null;
54+
}
55+
if ($node instanceof Node\Stmt\Namespace_) {
56+
if (count($node->stmts) > 0) {
57+
$this->lastNodes[] = $node->stmts[count($node->stmts) - 1];
58+
}
59+
return null;
60+
}
61+
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
62+
}
63+
64+
/**
65+
* @return Node[]
66+
*/
67+
public function getLastNodes(): array
68+
{
69+
return $this->lastNodes;
70+
}
71+
72+
};
73+
$nodeTraverser->addVisitor($visitor);
74+
$nodeTraverser->traverse($nodes);
75+
76+
$lastNodes = $visitor->getLastNodes();
77+
if (count($nodes) > 0) {
78+
$lastNodes[] = $nodes[count($nodes) - 1];
79+
}
80+
foreach ($lastNodes as $lastNode) {
81+
if (!$lastNode instanceof Node\Stmt\InlineHTML || Strings::match($lastNode->value, '#(\s+)#') === null) {
82+
continue;
83+
}
84+
85+
$messages[] = RuleErrorBuilder::message('File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.')->line($lastNode->getStartLine())->build();
86+
}
87+
88+
return $messages;
89+
}
90+
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Whitespace;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<FileWhitespaceRule>
10+
*/
11+
class FileWhitespaceRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new FileWhitespaceRule();
17+
}
18+
19+
public function testBom(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/bom.php'], [
22+
[
23+
'File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.',
24+
1,
25+
],
26+
]);
27+
}
28+
29+
public function testCorrectFile(): void
30+
{
31+
$this->analyse([__DIR__ . '/data/correct.php'], []);
32+
}
33+
34+
public function testTrailingWhitespaceWithoutNamespace(): void
35+
{
36+
$this->analyse([__DIR__ . '/data/trailing.php'], [
37+
[
38+
'File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.',
39+
6,
40+
],
41+
]);
42+
}
43+
44+
public function testTrailingWhitespace(): void
45+
{
46+
$this->analyse([__DIR__ . '/data/trailing-namespace.php'], [
47+
[
48+
'File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.',
49+
8,
50+
],
51+
]);
52+
}
53+
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
echo 'test';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace Test;
4+
5+
echo 'foo';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Test;
4+
5+
echo 'foo';
6+
7+
?>
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
echo 'foo';
4+
5+
?>
6+

0 commit comments

Comments
 (0)