Skip to content

Commit a668bf2

Browse files
committed
✨ New Universal.Attributes.BracketSpacing sniff
New sniff to enforce a fixed number of spaces on the inside of attribute block brackets. By default, the sniff expects `0` spaces, but this is configurable via a custom ruleset using the `spaces` property. Also by default, new lines will not be allowed. This setting can also be toggled by changing the `ignoreNewlines` property in a custom ruleset. Additionally, when `ignoreNewlines` is set to `true`, the sniff will also check there are no blank lines at the start or end of the attribute block. Note: the `$spacing` and `$ignoreNewlines` property names are chosen to be in line with commonly used property names as used in PHPCS itself. This sniff can be used with the default settings to address the following rule from PER-CS: > 12.1 Basics > ... > Attribute names MUST immediately follow the opening attribute block indicator `#[` with no space. > > The closing attribute block indicator `]` MUST follow the last character of the attribute name or the closing `)` of its argument list, with no preceding space. Ref: https://www.php-fig.org/per/coding-style/#121-basics Includes fixers. Includes unit tests. Includes documentation. Fixes 386
1 parent 02052c7 commit a668bf2

File tree

6 files changed

+685
-0
lines changed

6 files changed

+685
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?xml version="1.0"?>
2+
<documentation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://phpcsstandards.github.io/PHPCSDevTools/phpcsdocs.xsd"
4+
title="Attribute Bracket Spacing"
5+
>
6+
<standard>
7+
<![CDATA[
8+
Enforce a fixed number of spaces on the inside of attribute block brackets. By default, 0 (no) spaces are allowed.
9+
10+
Optionally allows for new lines.
11+
]]>
12+
</standard>
13+
<code_comparison>
14+
<code title="Valid: No whitespace on the inside of attribute block brackets.">
15+
<![CDATA[
16+
<em>#[</em>MyAttribute()<em>]<em>
17+
class Foo {}
18+
]]>
19+
</code>
20+
<code title="Invalid: Whitespace on the inside of attribute block brackets.">
21+
<![CDATA[
22+
<em>#[ </em>MyAttribute()<em> ]<em>
23+
class Foo {}
24+
]]>
25+
</code>
26+
</code_comparison>
27+
28+
<standard>
29+
<![CDATA[
30+
When the option to allow a new line is turned on, the sniff will verify that there are no blank lines at the start/end of the attribute block.
31+
]]>
32+
</standard>
33+
<code_comparison>
34+
<code title="Valid: No blank lines at the start/end of an attribute block.">
35+
<![CDATA[
36+
#[<em>
37+
</em>MyAttribute()<em>
38+
<em>]
39+
class Foo {}
40+
]]>
41+
</code>
42+
<code title="Invalid: Blank lines at the start/end of an attribute block.">
43+
<![CDATA[
44+
#[<em>
45+
46+
47+
</em>MyAttribute()<em>
48+
49+
<em>]
50+
class Foo {}
51+
]]>
52+
</code>
53+
</code_comparison>
54+
</documentation>
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
/**
3+
* PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer.
4+
*
5+
* @package PHPCSExtra
6+
* @copyright 2020 PHPCSExtra Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSExtra
9+
*/
10+
11+
namespace PHPCSExtra\Universal\Sniffs\Attributes;
12+
13+
use PHP_CodeSniffer\Files\File;
14+
use PHP_CodeSniffer\Sniffs\Sniff;
15+
use PHPCSUtils\Fixers\SpacesFixer;
16+
17+
/**
18+
* Requires a configurable number of spaces on the inside of attribute block brackets.
19+
*
20+
* When newlines are allowed, will also safeguard against blank lines at the start/end of the attribute block.
21+
*
22+
* @since 1.5.0
23+
*/
24+
final class BracketSpacingSniff implements Sniff
25+
{
26+
27+
/**
28+
* Name of the metric.
29+
*
30+
* @since 1.5.0
31+
*
32+
* @var string
33+
*/
34+
const METRIC_NAME = 'Spaces on the inside of attribute brackets';
35+
36+
/**
37+
* The amount of spacing to demand on the inside of attribute brackets.
38+
*
39+
* @since 1.5.0
40+
*
41+
* @var int
42+
*/
43+
public $spacing = 0;
44+
45+
/**
46+
* Allow newlines instead of spaces.
47+
*
48+
* @since 1.5.0
49+
*
50+
* @var bool
51+
*/
52+
public $ignoreNewlines = false;
53+
54+
/**
55+
* Returns an array of tokens this test wants to listen for.
56+
*
57+
* @since 1.5.0
58+
*
59+
* @return array<int|string>
60+
*/
61+
public function register()
62+
{
63+
return [\T_ATTRIBUTE];
64+
}
65+
66+
/**
67+
* Processes this test, when one of its tokens is encountered.
68+
*
69+
* @since 1.5.0
70+
*
71+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
72+
* @param int $stackPtr The position of the current token
73+
* in the stack passed in $tokens.
74+
*
75+
* @return void
76+
*/
77+
public function process(File $phpcsFile, $stackPtr)
78+
{
79+
$tokens = $phpcsFile->getTokens();
80+
81+
if (isset($tokens[$stackPtr]['attribute_closer']) === false) {
82+
// Live coding/parse error. Ignore.
83+
return;
84+
}
85+
86+
if ($tokens[$stackPtr]['attribute_closer'] === ($stackPtr + 1)) {
87+
// Empty attribute block. Ignore.
88+
return;
89+
}
90+
91+
$this->spacing = (int) $this->spacing;
92+
93+
$this->processOpener($phpcsFile, $stackPtr);
94+
$this->processCloser($phpcsFile, $tokens[$stackPtr]['attribute_closer']);
95+
}
96+
97+
/**
98+
* Processes the attribute block opener bracket.
99+
*
100+
* @since 1.5.0
101+
*
102+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
103+
* @param int $stackPtr The position of the attribute block opener
104+
* in the stack passed in $tokens.
105+
*
106+
* @return void
107+
*/
108+
public function processOpener(File $phpcsFile, $stackPtr)
109+
{
110+
$tokens = $phpcsFile->getTokens();
111+
112+
$nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($stackPtr + 1), null, true);
113+
if ($this->ignoreNewlines === true
114+
&& $tokens[$stackPtr]['line'] !== $tokens[$nextNonWhitespace]['line']
115+
) {
116+
if (($tokens[$stackPtr]['line'] + 1) === $tokens[$nextNonWhitespace]['line']) {
117+
// Single new line.
118+
$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'a new line');
119+
return;
120+
}
121+
122+
$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'multiple new lines');
123+
124+
$error = 'Blank line(s) found at the start of an attribute block';
125+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'BlankLineAtStart');
126+
127+
if ($fix === true) {
128+
$phpcsFile->fixer->beginChangeset();
129+
$phpcsFile->fixer->addNewline($stackPtr);
130+
131+
// Remove all blank lines, but don't remove the indentation of the line containing the next bit of code.
132+
for ($i = ($stackPtr + 1); $i < $nextNonWhitespace; $i++) {
133+
if ($tokens[$i]['line'] === $tokens[$nextNonWhitespace]['line']) {
134+
break;
135+
}
136+
137+
$phpcsFile->fixer->replaceToken($i, '');
138+
}
139+
$phpcsFile->fixer->endChangeset();
140+
}
141+
return;
142+
}
143+
144+
SpacesFixer::checkAndFix(
145+
$phpcsFile,
146+
$stackPtr,
147+
$nextNonWhitespace,
148+
$this->spacing,
149+
'Expected %s after the attribute block opener. Found: %s.',
150+
'SpaceAfterOpener',
151+
'error',
152+
0,
153+
self::METRIC_NAME
154+
);
155+
}
156+
157+
/**
158+
* Processes the attribute block closer bracket.
159+
*
160+
* @since 1.5.0
161+
*
162+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
163+
* @param int $stackPtr The position of the attribute block closer
164+
* in the stack passed in $tokens.
165+
*
166+
* @return void
167+
*/
168+
public function processCloser(File $phpcsFile, $stackPtr)
169+
{
170+
$tokens = $phpcsFile->getTokens();
171+
172+
$previousNonWhitespace = $phpcsFile->findPrevious(\T_WHITESPACE, ($stackPtr - 1), null, true);
173+
if ($this->ignoreNewlines === true
174+
&& $tokens[$stackPtr]['line'] !== $tokens[$previousNonWhitespace]['line']
175+
) {
176+
if (($tokens[$stackPtr]['line'] - 1) === $tokens[$previousNonWhitespace]['line']) {
177+
// Single new line.
178+
$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'a new line');
179+
return;
180+
}
181+
182+
$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'multiple new lines');
183+
184+
$error = 'Blank line(s) found at the end of an attribute block';
185+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'BlankLineAtEnd');
186+
187+
if ($fix === true) {
188+
$phpcsFile->fixer->beginChangeset();
189+
$phpcsFile->fixer->addNewline($previousNonWhitespace);
190+
191+
// Remove all blank lines, but don't remove the indentation of the line containing the next bit of code.
192+
for ($i = ($previousNonWhitespace + 1); $i < $stackPtr; $i++) {
193+
if ($tokens[$i]['line'] === $tokens[$stackPtr]['line']) {
194+
break;
195+
}
196+
197+
$phpcsFile->fixer->replaceToken($i, '');
198+
}
199+
$phpcsFile->fixer->endChangeset();
200+
}
201+
return;
202+
}
203+
204+
SpacesFixer::checkAndFix(
205+
$phpcsFile,
206+
$previousNonWhitespace,
207+
$stackPtr,
208+
$this->spacing,
209+
'Expected %s before the attribute block closer. Found: %s.',
210+
'SpaceBeforeCloser',
211+
'error',
212+
0,
213+
self::METRIC_NAME
214+
);
215+
}
216+
}

0 commit comments

Comments
 (0)