Skip to content

Commit 4df873f

Browse files
committed
Added FunctionBodyStartSniff
to deal with blank lines on top of functions body.
1 parent 4298073 commit 4df873f

File tree

7 files changed

+358
-16
lines changed

7 files changed

+358
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Fix false positive in `ReturnTypeDeclarationSniff` with nullable types.
55
- Relax check for missing return type when `{aType}|null` doc bloc is present.
66
- Add `is` to the list of allowed short names.
7+
- Added `FunctionBodyStartSniff` to deal with blank lines on top of functions body.
78

89
## 0.10.0
910
- Renamed sniffs namespace (**breaking change**).
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php declare(strict_types=1); # -*- coding: utf-8 -*-
2+
/*
3+
* This file is part of the php-coding-standards package.
4+
*
5+
* (c) Inpsyde GmbH
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace Inpsyde\Sniffs\CodeQuality;
12+
13+
use PHP_CodeSniffer\Files\File;
14+
use PHP_CodeSniffer\Sniffs\Sniff;
15+
use PHP_CodeSniffer\Util\Tokens;
16+
17+
class FunctionBodyStartSniff implements Sniff
18+
{
19+
/**
20+
* @return int[]
21+
*/
22+
public function register()
23+
{
24+
return [T_FUNCTION];
25+
}
26+
27+
/**
28+
* @param File $phpcsFile
29+
* @param int $stackPtr
30+
* @return void
31+
*/
32+
public function process(File $phpcsFile, $stackPtr)
33+
{
34+
$tokens = $phpcsFile->getTokens();
35+
$token = $tokens[$stackPtr] ?? [];
36+
37+
/** @var int $scopeOpener */
38+
$scopeOpener = $token['scope_opener'] ?? -1;
39+
$scopeCloser = $token['scope_closer'] ?? -1;
40+
41+
if ($scopeOpener < 0 || $scopeCloser < 0 || $scopeCloser <= $scopeOpener) {
42+
return;
43+
}
44+
45+
$bodyStart = $phpcsFile->findNext([T_WHITESPACE], $scopeOpener + 1, null, true);
46+
if (!$bodyStart
47+
|| !array_key_exists($bodyStart, $tokens)
48+
|| $bodyStart <= $scopeOpener
49+
|| $bodyStart >= $scopeCloser
50+
) {
51+
return;
52+
}
53+
54+
list($code, $message, $expectedLine) = $this->checkBodyStart(
55+
$bodyStart,
56+
$tokens[$scopeOpener]['line'] ?? -1,
57+
$token['line'] ?? -1,
58+
$phpcsFile
59+
);
60+
61+
if ($code && $message && $phpcsFile->addFixableWarning($message, $stackPtr, $code)) {
62+
$this->fix($bodyStart, $expectedLine, $scopeOpener, $phpcsFile);
63+
}
64+
}
65+
66+
/**
67+
* @param int $bodyStart
68+
* @param int $openerLine
69+
* @param int $functionLine
70+
* @param File $phpcsFile
71+
* @return array
72+
*/
73+
private function checkBodyStart(
74+
int $bodyStart,
75+
int $openerLine,
76+
int $functionLine,
77+
File $phpcsFile
78+
): array {
79+
80+
$tokens = $phpcsFile->getTokens();
81+
$bodyLine = $tokens[$bodyStart]['line'] ?? -1;
82+
83+
$isMultiLineDeclare = ($openerLine - $functionLine) > 1;
84+
$isSingleLineDeclare = $openerLine === ($functionLine + 1);
85+
$isSingleLineSignature = $openerLine && $openerLine === $functionLine;
86+
87+
$error =
88+
($isMultiLineDeclare || $isSingleLineSignature) && $bodyLine !== ($openerLine + 2)
89+
|| $isSingleLineDeclare && $bodyLine !== ($openerLine + 1);
90+
91+
if (!$error) {
92+
return [null, null, null];
93+
}
94+
95+
$startWithComment = in_array($tokens[$bodyStart]['code'], Tokens::$emptyTokens, true);
96+
97+
if (!$startWithComment && ($isMultiLineDeclare || $isSingleLineSignature)) {
98+
$where = $isSingleLineSignature === 'SingleLineSignature'
99+
? 'with single-line signature and open curly bracket on same line'
100+
: 'where arguments declaration spans across multiple lines';
101+
$code = $isSingleLineSignature
102+
? 'WrongForSingleLineSignature'
103+
: 'WrongForMultiLineDeclaration';
104+
105+
return [
106+
$code,
107+
"In functions {$where}, function body should start with a blank line.",
108+
$openerLine + 2
109+
];
110+
}
111+
112+
if (!$isSingleLineDeclare) {
113+
return [null, null, null];
114+
}
115+
116+
$message = 'In functions where arguments declaration is in a single line and curly bracket '
117+
. 'is on next line, function body should start in the line below opened curly bracket.';
118+
119+
return [
120+
'WrongForSingleLineDeclaration',
121+
$message,
122+
$openerLine + 1
123+
];
124+
}
125+
126+
/**
127+
* @param int $bodyStart
128+
* @param int $expectedLine
129+
* @param int $scopeOpener
130+
* @param File $file
131+
*/
132+
private function fix(int $bodyStart, int $expectedLine, int $scopeOpener, File $file)
133+
{
134+
$tokens = $file->getTokens();
135+
$currentLine = $tokens[$bodyStart]['line'] ?? -1;
136+
137+
if ($currentLine === $expectedLine) {
138+
return;
139+
}
140+
141+
$fixer = $file->fixer;
142+
$fixer->beginChangeset();
143+
144+
if ($currentLine < $expectedLine) {
145+
for ($i = ($expectedLine - $currentLine); $i > 0; $i--) {
146+
$fixer->addNewline($scopeOpener);
147+
}
148+
$fixer->endChangeset();
149+
150+
return;
151+
}
152+
153+
for ($i = $bodyStart - 1; $i > 0; $i--) {
154+
$line = $tokens[$i]['line'];
155+
if ($line === $currentLine) {
156+
continue;
157+
}
158+
if ($line < $expectedLine) {
159+
break;
160+
}
161+
162+
$fixer->replaceToken($i, '');
163+
}
164+
165+
$fixer->endChangeset();
166+
}
167+
}

README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,22 +138,23 @@ The tree of used rules are listed in the `/docs/rules-list/generic.md` file in t
138138

139139
Some custom rules are also in use. They are:
140140

141-
| Sniff name | Description | Has Config | Has Notes |
142-
|:-----------|:------------|:----------:|:---------:|
143-
| `ArgumentTypeDeclarationSniff`|Enforce argument type declaration, with few exception (e.g. hook callbacks or `ArrayAccess` methods)|||
144-
| `AssignmentInsideConditionSniff`|Ensure that any assignment inside conditions in wrapped in parenthesis|||
145-
| `DisallowShortOpenTagSniff`|Disallow short open PHP tag (short echo tag allowed).|||
146-
| `ElementNameMinimalLengthSniff`|Use minimum 3 chars for names (with a few exclusions)|||
147-
| `ForbiddenPublicPropertySniff`|No public class properties|||
148-
| `FunctionLengthSniff`|Max 50 lines per function/method, excluding blank lines and comments-only lines.|||
149-
| `HookClosureReturnSniff`|Ensure that actions callbacks do not return anything, while filter callbacks return something.|||
150-
| `LineLengthSniff`|Max 100 chars per line, excluding leading indent space and long string in WP translation functions|||
151-
| `NoAccessorsSniff`|Discourage usage of getters and setters.|||
152-
| `NoElseSniff`|Discourage usage of `else`.|||
153-
| `NoTopLevelDefineSniff`|Discourage usage of `define` where `const` is preferable.|||
154-
| `PropertyPerClassLimitSniff`|Discourage usage of more than 10 properties per class.|||
155-
| `Psr4Sniff`|Check PSR-4 compliance|||
156-
| `ReturnTypeDeclarationSniff`|Enforce return type declaration, with few exceptions (e.g. hook callbacks or `ArrayAccess` methods)|||
141+
| Sniff name | Description | Has Config | Has Notes | Auto-Fixable |
142+
|:-----------|:------------|:----------:|:---------:|:------------:|
143+
| `ArgumentTypeDeclarationSniff`|Enforce argument type declaration, with few exception (e.g. hook callbacks or `ArrayAccess` methods)||||
144+
| `AssignmentInsideConditionSniff`|Ensure that any assignment inside conditions in wrapped in parenthesis||||
145+
| `DisallowShortOpenTagSniff`|Disallow short open PHP tag (short echo tag allowed).||||
146+
| `ElementNameMinimalLengthSniff`|Use minimum 3 chars for names (with a few exclusions)||||
147+
| `ForbiddenPublicPropertySniff`|No public class properties||||
148+
| `FunctionBodyStartSniff`|Handle blank line at start of function body when necessary.||||
149+
| `FunctionLengthSniff`|Max 50 lines per function/method, excluding blank lines and comments-only lines.||||
150+
| `HookClosureReturnSniff`|Ensure that actions callbacks do not return anything, while filter callbacks return something.||||
151+
| `LineLengthSniff`|Max 100 chars per line, excluding leading indent space and long string in WP translation functions||||
152+
| `NoAccessorsSniff`|Discourage usage of getters and setters.||||
153+
| `NoElseSniff`|Discourage usage of `else`.||||
154+
| `NoTopLevelDefineSniff`|Discourage usage of `define` where `const` is preferable.||||
155+
| `PropertyPerClassLimitSniff`|Discourage usage of more than 10 properties per class.||||
156+
| `Psr4Sniff`|Check PSR-4 compliance||||
157+
| `ReturnTypeDeclarationSniff`|Enforce return type declaration, with few exceptions (e.g. hook callbacks or `ArrayAccess` methods)||||
157158

158159
For **notes and configuration** see `/docs/rules-list/inpsyde-rules-configuration.md` file in this repo.
159160

docs/inpsyde-rules-configuration.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,63 @@ By default the whitelisted names are:
3838
</rule>
3939
```
4040

41+
-----
42+
43+
44+
## Inpsyde.CodeQuality.FunctionBodyStart
45+
46+
This sniff enforces a blank line on top of function body when the function declaration spans
47+
across multiple lines, e.g.:
48+
49+
```php
50+
function foo(
51+
string $foo,
52+
string $bar
53+
): bool {
54+
55+
echo $foo . $bar;
56+
57+
return true;
58+
}
59+
```
60+
61+
while blank line is forbidden for functions whose argument declaration is in one line and the
62+
opened curly bracket is on next line, e.g.:
63+
64+
```php
65+
function foo(string $foo, string $bar, string $baz): bool
66+
{
67+
echo $foo . $bar;
68+
69+
return true;
70+
}
71+
```
72+
73+
Blank line is also required if the opened curly bracket is on the same line (not PSR 1/2 compliant):
74+
75+
```php
76+
function foo(string $foo, string $bar, string $baz): bool {
77+
78+
echo $foo . $bar;
79+
80+
return true;
81+
}
82+
```
83+
84+
A special case is when the first line of body contains a comment, in that case no blank line is required
85+
before the comment, e.g.:
86+
87+
```php
88+
function foo(
89+
string $foo,
90+
string $bar
91+
): bool {
92+
// This is ok.
93+
echo $foo . $bar;
94+
95+
return true;
96+
}
97+
```
4198

4299
-----
43100

docs/rules-list/custom.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
- Inpsyde.CodeQuality.ElementNameMinimalLength.TooShort
99
- Inpsyde.CodeQuality.ForbiddenPublicProperty
1010
- Inpsyde.CodeQuality.ForbiddenPublicProperty.Found
11+
- Inpsyde.CodeQuality.FunctionBodyStart
12+
- Inpsyde.CodeQuality.FunctionBodyStart.WrongForSingleLineSignature
13+
- Inpsyde.CodeQuality.FunctionBodyStart.WrongForMultiLineDeclaration
14+
- Inpsyde.CodeQuality.FunctionBodyStart.WrongForSingleLineDeclaration
1115
- Inpsyde.CodeQuality.FunctionLength
1216
- Inpsyde.CodeQuality.FunctionLength.TooLong
1317
- Inpsyde.CodeQuality.HookClosureReturn

phpcs.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
<rule ref="./Inpsyde/Sniffs/CodeQuality/DisallowShortOpenTagSniff.php"/>
9797
<rule ref="./Inpsyde/Sniffs/CodeQuality/ElementNameMinimalLengthSniff.php"/>
9898
<rule ref="./Inpsyde/Sniffs/CodeQuality/ForbiddenPublicPropertySniff.php"/>
99+
<rule ref="./Inpsyde/Sniffs/CodeQuality/FunctionBodyStartSniff.php"/>
99100
<rule ref="./Inpsyde/Sniffs/CodeQuality/FunctionLengthSniff.php"/>
100101
<rule ref="./Inpsyde/Sniffs/CodeQuality/LineLengthSniff.php"/>
101102
<rule ref="./Inpsyde/Sniffs/CodeQuality/NoAccessorsSniff.php"/>

0 commit comments

Comments
 (0)