diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index f3f31c8c80..d43538fa05 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -3,3 +3,4 @@ parameters: closureUsesThis: true randomIntParameters: true nullCoalesce: true + fileWhitespace: true diff --git a/conf/config.level0.neon b/conf/config.level0.neon index e7396651e3..97250fcde9 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -7,6 +7,8 @@ conditionalTags: phpstan.rules.rule: %featureToggles.closureUsesThis% PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule: phpstan.rules.rule: %checkMissingClosureNativeReturnTypehintRule% + PHPStan\Rules\Whitespace\FileWhitespaceRule: + phpstan.rules.rule: %featureToggles.fileWhitespace% parametersSchema: missingClosureNativeReturnCheckObjectTypehint: bool() @@ -178,3 +180,6 @@ services: class: PHPStan\Rules\Regexp\RegularExpressionPatternRule tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\Whitespace\FileWhitespaceRule diff --git a/conf/config.neon b/conf/config.neon index fdb6fc70a7..9ad402094f 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -11,6 +11,7 @@ parameters: closureUsesThis: false randomIntParameters: false nullCoalesce: false + fileWhitespace: false fileExtensions: - php checkAlwaysTrueCheckTypeFunctionCall: false @@ -145,7 +146,8 @@ parametersSchema: disableRuntimeReflectionProvider: bool(), closureUsesThis: bool(), randomIntParameters: bool(), - nullCoalesce: bool() + nullCoalesce: bool(), + fileWhitespace: bool() ]) fileExtensions: listOf(string()) checkAlwaysTrueCheckTypeFunctionCall: bool() diff --git a/src/Rules/Whitespace/FileWhitespaceRule.php b/src/Rules/Whitespace/FileWhitespaceRule.php new file mode 100644 index 0000000000..49f1ea4128 --- /dev/null +++ b/src/Rules/Whitespace/FileWhitespaceRule.php @@ -0,0 +1,91 @@ + + */ +class FileWhitespaceRule implements Rule +{ + + public function getNodeType(): string + { + return FileNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $nodes = $node->getNodes(); + if (count($nodes) === 0) { + return []; + } + + $firstNode = $nodes[0]; + $messages = []; + if ($firstNode instanceof Node\Stmt\InlineHTML && $firstNode->value === "\xef\xbb\xbf") { + $messages[] = RuleErrorBuilder::message('File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.')->build(); + } + + $nodeTraverser = new NodeTraverser(); + $visitor = new class () extends \PhpParser\NodeVisitorAbstract { + + /** @var \PhpParser\Node[] */ + private $lastNodes = []; + + /** + * @param Node $node + * @return int|Node|null + */ + public function enterNode(Node $node) + { + if ($node instanceof Node\Stmt\Declare_) { + if ($node->stmts !== null && count($node->stmts) > 0) { + $this->lastNodes[] = $node->stmts[count($node->stmts) - 1]; + } + return null; + } + if ($node instanceof Node\Stmt\Namespace_) { + if (count($node->stmts) > 0) { + $this->lastNodes[] = $node->stmts[count($node->stmts) - 1]; + } + return null; + } + return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + } + + /** + * @return Node[] + */ + public function getLastNodes(): array + { + return $this->lastNodes; + } + + }; + $nodeTraverser->addVisitor($visitor); + $nodeTraverser->traverse($nodes); + + $lastNodes = $visitor->getLastNodes(); + if (count($nodes) > 0) { + $lastNodes[] = $nodes[count($nodes) - 1]; + } + foreach ($lastNodes as $lastNode) { + if (!$lastNode instanceof Node\Stmt\InlineHTML || Strings::match($lastNode->value, '#(\s+)#') === null) { + continue; + } + + $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(); + } + + return $messages; + } + +} diff --git a/tests/PHPStan/Rules/Whitespace/FileWhitespaceRuleTest.php b/tests/PHPStan/Rules/Whitespace/FileWhitespaceRuleTest.php new file mode 100644 index 0000000000..7004e69611 --- /dev/null +++ b/tests/PHPStan/Rules/Whitespace/FileWhitespaceRuleTest.php @@ -0,0 +1,54 @@ + + */ +class FileWhitespaceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FileWhitespaceRule(); + } + + public function testBom(): void + { + $this->analyse([__DIR__ . '/data/bom.php'], [ + [ + 'File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.', + 1, + ], + ]); + } + + public function testCorrectFile(): void + { + $this->analyse([__DIR__ . '/data/correct.php'], []); + } + + public function testTrailingWhitespaceWithoutNamespace(): void + { + $this->analyse([__DIR__ . '/data/trailing.php'], [ + [ + '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.', + 6, + ], + ]); + } + + public function testTrailingWhitespace(): void + { + $this->analyse([__DIR__ . '/data/trailing-namespace.php'], [ + [ + '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.', + 8, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Whitespace/data/bom.php b/tests/PHPStan/Rules/Whitespace/data/bom.php new file mode 100644 index 0000000000..d17f0c1edf --- /dev/null +++ b/tests/PHPStan/Rules/Whitespace/data/bom.php @@ -0,0 +1,3 @@ + + diff --git a/tests/PHPStan/Rules/Whitespace/data/trailing.php b/tests/PHPStan/Rules/Whitespace/data/trailing.php new file mode 100644 index 0000000000..37f79ec490 --- /dev/null +++ b/tests/PHPStan/Rules/Whitespace/data/trailing.php @@ -0,0 +1,6 @@ + +