Skip to content

Commit a436b1b

Browse files
committed
WIP 392 / SingleAttributeInMultilineBlockSniff
1 parent d5f7562 commit a436b1b

6 files changed

+409
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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="Single Attribute In Multi-line Block"
5+
>
6+
<standard>
7+
<![CDATA[
8+
Require for an attribute block to only contain the attribute instantiation for a single attribute if the block is multi-line.
9+
]]>
10+
</standard>
11+
<code_comparison>
12+
<code title="Valid: Single line attribute block containing multiple attributes.">
13+
<![CDATA[
14+
#[<em>AttributeOne(10)</em>, <em>AttributeTwo</em>]
15+
class Foo {}
16+
]]>
17+
</code>
18+
<code title="Invalid: Multi-line attribute block containing multiple attributes.">
19+
<![CDATA[
20+
#[
21+
<em>AttributeOne(10)</em>,
22+
<em>AttributeTwo</em>
23+
]
24+
class Foo {}
25+
]]>
26+
</code>
27+
</code_comparison>
28+
<code_comparison>
29+
<code title="Valid: Multi-line attribute block containing a single attribute.">
30+
<![CDATA[
31+
#[<em>MyAttribute(
32+
param: 10,
33+
)</em>]
34+
class Foo {}
35+
]]>
36+
</code>
37+
<code title="Invalid: Multi-line attribute block containing multiple attributes.">
38+
<![CDATA[
39+
#[<em>AttributeOne(10)</em>,
40+
<em>AttributeTwo</em>]
41+
class Foo {}
42+
]]>
43+
</code>
44+
</code_comparison>
45+
</documentation>
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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 PHP_CodeSniffer\Util\Tokens;
16+
use PHPCSUtils\Utils\AttributeBlock;
17+
18+
/**
19+
* Requires that when an attribute block is multi-line, it only contains a single attribute instantiation.
20+
*
21+
* @since 1.5.0
22+
*/
23+
final class SingleAttributeInMultilineBlockSniff implements Sniff
24+
{
25+
26+
/**
27+
* Returns an array of tokens this test wants to listen for.
28+
*
29+
* @since 1.5.0
30+
*
31+
* @return array<int|string>
32+
*/
33+
public function register()
34+
{
35+
return [\T_ATTRIBUTE];
36+
}
37+
38+
/**
39+
* Processes this test, when one of its tokens is encountered.
40+
*
41+
* @since 1.5.0
42+
*
43+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
44+
* @param int $stackPtr The position of the current token
45+
* in the stack passed in $tokens.
46+
*
47+
* @return void
48+
*/
49+
public function process(File $phpcsFile, $stackPtr)
50+
{
51+
// TODO
52+
53+
/*
54+
This needs clarification from FIG
55+
- if an attribute is multiline because of params, that's one thing, but what if an attribute is multiline because there are a lot of attributes ?
56+
=> should those be split and made single-line attributes for each ?
57+
=> Actually, that will probably sort itself out automatically once the bracket spacing sniff kicks in
58+
*/
59+
60+
$tokens = $phpcsFile->getTokens();
61+
62+
if (isset($tokens[$stackPtr]['attribute_closer']) === false) {
63+
// Live coding/parse error. Ignore.
64+
return;
65+
}
66+
67+
if ($tokens[$stackPtr]['line'] === $tokens[$tokens[$stackPtr]['attribute_closer']]['line']) {
68+
// Single line, nothing to do.
69+
return;
70+
}
71+
72+
$nrOfAttributes = AttributeBlock::countAttributes($phpcsFile, $stackPtr);
73+
if ($nrOfAttributes <= 1) {
74+
// Single attribute, nothing to do.
75+
return;
76+
}
77+
78+
$fix = $phpcsFile->addFixableError(
79+
'A multi-line attribute block must only contain a single attribute instantiation. Found %d instantiations',
80+
$stackPtr,
81+
'MultipleFound',
82+
[$nrOfAttributes]
83+
);
84+
85+
if ($fix === true) {
86+
// TODO: Figure out _original_ indent of opener
87+
$instantiations = AttributeBlock::getAttributes($phpcsFile, $stackPtr);
88+
// Remove last one of the attributes ??? (as that one doesn't need action _after_, though might be easier to skip the first one and make the fix at the start of the attribute ?
89+
// Should probably also remove leading whitespace, unless it is new line/indent
90+
// Should we also figure out if closer is on own line and perpetuate that if that's the case ?
91+
92+
$phpcsFile->fixer->beginChangeset();
93+
foreach ($instantiations as $instantiation) {
94+
// If start and end of instantiation is on the same line -> add attribute closer on same line + new line char + indent + attribute opener
95+
// If start and end of instantiation are not on the same line -> ???
96+
}
97+
$phpcsFile->fixer->endChangeset();
98+
}
99+
/*
100+
if ($fix === true) {
101+
$phpcsFile->fixer->beginChangeset();
102+
103+
for ($i = $opener; $i <= $closer; $i++) {
104+
if (isset(Tokens::$commentTokens[$tokens[$i]['code']]) === true) {
105+
continue;
106+
}
107+
108+
$phpcsFile->fixer->replaceToken($i, '');
109+
}
110+
111+
$phpcsFile->fixer->endChangeset();
112+
}
113+
114+
$opener = $stackPtr;
115+
$closer = $tokens[$stackPtr]['attribute_closer'];
116+
117+
$instantiations = AttributeBlock::getAttributes($phpcsFile, $stackPtr);
118+
119+
foreach ($instantiations as $attribute) {
120+
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($attribute['name_token'] + 1), null, true);
121+
122+
// Note: no need to check for `false` as there will always be something after, if only the attribute closer.
123+
if ($tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS) {
124+
// No parentheses found.
125+
$phpcsFile->recordMetric($attribute['name_token'], self::METRIC_NAME, 'no');
126+
continue;
127+
}
128+
129+
if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) {
130+
/*
131+
* Incomplete set of parentheses. Ignore.
132+
* Shouldn't be possible as PHPCS won't have matched the attribute opener with the closer in that case.
133+
* /
134+
// @codeCoverageIgnoreStart
135+
$phpcsFile->recordMetric($attribute['name_token'], self::METRIC_NAME, 'yes');
136+
continue;
137+
// @codeCoverageIgnoreEnd
138+
}
139+
140+
$opener = $nextNonEmpty;
141+
$closer = $tokens[$opener]['parenthesis_closer'];
142+
$hasParams = $phpcsFile->findNext(Tokens::$emptyTokens, ($opener + 1), $closer, true);
143+
if ($hasParams !== false) {
144+
// There is something between the parentheses. Ignore.
145+
$phpcsFile->recordMetric($attribute['name_token'], self::METRIC_NAME, 'yes, with parameter(s)');
146+
continue;
147+
}
148+
149+
$phpcsFile->recordMetric($attribute['name_token'], self::METRIC_NAME, 'yes');
150+
151+
$fix = $phpcsFile->addFixableError(
152+
'Parenthesis not allowed when instantiating an attribute class without passing parameters',
153+
$opener,
154+
'Found'
155+
);
156+
157+
if ($fix === true) {
158+
$phpcsFile->fixer->beginChangeset();
159+
160+
for ($i = $opener; $i <= $closer; $i++) {
161+
if (isset(Tokens::$commentTokens[$tokens[$i]['code']]) === true) {
162+
continue;
163+
}
164+
165+
$phpcsFile->fixer->replaceToken($i, '');
166+
}
167+
168+
$phpcsFile->fixer->endChangeset();
169+
}
170+
}
171+
*/
172+
}
173+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* OK.
5+
*/
6+
#[] // Nothing to do.
7+
8+
#[SingleAttributeSingleLine()]
9+
#[SingleAttributeMultiLineNoParams(
10+
)]
11+
#[SingleAttributeMultiLineWithParams(
12+
10,
13+
[],
14+
)]
15+
#[
16+
SingleAttributeMultiLineWithTrailingCommaOnAttribute(
17+
10,
18+
[],
19+
),
20+
]
21+
22+
#[\MultipleAttributes(10), On /*comment*/, SingleLine]
23+
#[\Fully\Qualified\MultipleAttributes(10), Partial\On (), namespace\SingleLineTrailingComma ('foo'), ]
24+
function foo() {}
25+
26+
27+
/*
28+
* Bad.
29+
*/
30+
#[
31+
AttributeOne,
32+
AttributeTwo,
33+
AttributeThree,
34+
]
35+
36+
#[\Fully\AttributeOneIndented,
37+
Partially\AttributeTwoIndented,
38+
namespace\AttributeThreeIndented]
39+
40+
#[
41+
42+
AttributeOneWithBlankLines(),
43+
44+
AttributeTwoWithBlankLines,
45+
46+
AttributeThreeWithBlankLines(),
47+
48+
]
49+
50+
#[AttributeOneMultiline(
51+
10,
52+
[],
53+
), AttributeTwo()]
54+
#[AttributeOne(), AttributeTwoMultiline(
55+
10,
56+
[],
57+
),]
58+
function foo() {}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* OK.
5+
*/
6+
#[] // Nothing to do.
7+
8+
#[SingleAttributeSingleLine()]
9+
#[SingleAttributeMultiLineNoParams(
10+
)]
11+
#[SingleAttributeMultiLineWithParams(
12+
10,
13+
[],
14+
)]
15+
#[
16+
SingleAttributeMultiLineWithTrailingCommaOnAttribute(
17+
10,
18+
[],
19+
),
20+
]
21+
22+
#[\MultipleAttributes(10), On /*comment*/, SingleLine]
23+
#[\Fully\Qualified\MultipleAttributes(10), Partial\On (), namespace\SingleLineTrailingComma ('foo'), ]
24+
function foo() {}
25+
26+
27+
/*
28+
* Bad.
29+
*/
30+
#[
31+
AttributeOne
32+
]
33+
#[
34+
AttributeTwo
35+
]
36+
#[
37+
AttributeThree,
38+
]
39+
40+
#[\Fully\AttributeOneIndented]
41+
#[
42+
Partially\AttributeTwoIndented
43+
]
44+
#[namespace\AttributeThreeIndented]
45+
46+
#[
47+
48+
AttributeOneWithBlankLines()
49+
]
50+
#[
51+
52+
AttributeTwoWithBlankLines
53+
]
54+
#[
55+
56+
AttributeThreeWithBlankLines(),
57+
58+
]
59+
60+
#[AttributeOneMultiline(
61+
10,
62+
[],
63+
)]
64+
#[ AttributeTwo()]
65+
#[AttributeOne()]
66+
#[AttributeTwoMultiline(
67+
10,
68+
[],
69+
),]
70+
function foo() {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
// Intentional parse error. Specifically testing the handling of this and safeguarding it for the future.
4+
#[
5+
function foo() {}

0 commit comments

Comments
 (0)