Skip to content

Commit b031559

Browse files
committed
Universal/TypeSeparatorSpacing: add support for PHP 8.2 DNF types
PHP 8.2 introduced DNF types, which use parenthesis. This commit adds support to the `Universal.Operators.TypeSeparatorSpacing` sniff to also check the spacing around the parenthesis for DNF types. Notes: * Allows for a new line before the start of a type for function parameters. * Expects one space before the type operator if the type starts with a DNF open parenthesis. * Expects one space after the type operator if the type ends on a DNF close parenthesis. * Includes protection against throwing two errors for the same issue, like when a DNF close parenthesis if followed by a union type operator and there is whitespace between these. Includes tests. Includes updated documentation.
1 parent 4ddfa44 commit b031559

File tree

5 files changed

+167
-27
lines changed

5 files changed

+167
-27
lines changed

Universal/Docs/Operators/TypeSeparatorSpacingStandard.xml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,31 @@
55
>
66
<standard>
77
<![CDATA[
8-
There should be no spacing around the union type separator or the intersection type separator.
8+
Enforce spacing rules around the union, intersection and DNF type operators.
9+
- No space on either side of a union or intersection type operator.
10+
- No space on the inside of DNF type parenthesis or before/after if the previous/next "thing" is part of the type.
11+
- One space before a DNF open parenthesis when it is at the start of a type.
12+
- One space after a DNF close parenthesis when it is at the end of a type.
913
10-
This applies to all locations where type declarations can be used, i.e. property types, parameter types and return types.
14+
This applies to all locations where type declarations can be used, i.e. property types, constant types, parameter types and return types.
1115
]]>
1216
</standard>
1317
<code_comparison>
14-
<code title="Valid: No space around the separators.">
18+
<code title="Valid: Correct spacing around the separators.">
1519
<![CDATA[
1620
function foo(
1721
int<em>|</em>string $paramA,
18-
TypeA<em>&</em>TypeB $paramB
22+
TypeA<em>&</em>TypeB $paramB,
23+
<em>(</em>TypeA&TypeB<em>)</em>|null $paramC
1924
): int<em>|</em>false {}
2025
]]>
2126
</code>
22-
<code title="Invalid: Space around the separators.">
27+
<code title="Invalid: Incorrect spacing around the separators.">
2328
<![CDATA[
2429
function foo(
2530
int<em> | </em>string $paramA,
26-
TypeA<em> & </em>TypeB $paramB
31+
TypeA<em> & </em>TypeB $paramB,
32+
<em>( </em>TypeA&TypeB<em> ) </em>|null $paramC
2733
): int<em>
2834
|
2935
</em>false {}

Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,31 @@
1414
use PHP_CodeSniffer\Sniffs\Sniff;
1515
use PHP_CodeSniffer\Util\Tokens;
1616
use PHPCSUtils\Fixers\SpacesFixer;
17+
use PHPCSUtils\Tokens\Collections;
1718

1819
/**
19-
* Enforce no space around union type and intersection type separators.
20+
* Enforce spacing rules around union, intersection and DNF type separators.
2021
*
2122
* @since 1.0.0
23+
* @since 1.3.0 Support for DNF types.
2224
*/
2325
final class TypeSeparatorSpacingSniff implements Sniff
2426
{
2527

28+
/**
29+
* Tokens this sniff targets.
30+
*
31+
* @since 1.3.0
32+
*
33+
* @var array<int|string, int|string>
34+
*/
35+
private $targetTokens = [
36+
\T_TYPE_UNION => \T_TYPE_UNION,
37+
\T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION,
38+
\T_TYPE_OPEN_PARENTHESIS => \T_TYPE_OPEN_PARENTHESIS,
39+
\T_TYPE_CLOSE_PARENTHESIS => \T_TYPE_CLOSE_PARENTHESIS,
40+
];
41+
2642
/**
2743
* Returns an array of tokens this test wants to listen for.
2844
*
@@ -32,10 +48,7 @@ final class TypeSeparatorSpacingSniff implements Sniff
3248
*/
3349
public function register()
3450
{
35-
return [
36-
\T_TYPE_UNION,
37-
\T_TYPE_INTERSECTION,
38-
];
51+
return $this->targetTokens;
3952
}
4053

4154
/**
@@ -53,28 +66,78 @@ public function process(File $phpcsFile, $stackPtr)
5366
{
5467
$tokens = $phpcsFile->getTokens();
5568

56-
$type = ($tokens[$stackPtr]['code'] === \T_TYPE_UNION) ? 'union' : 'intersection';
57-
$code = \ucfirst($type) . 'Type';
69+
$type = 'union';
70+
$code = 'UnionType';
71+
if ($tokens[$stackPtr]['code'] === \T_TYPE_INTERSECTION) {
72+
$type = 'intersection';
73+
$code = 'IntersectionType';
74+
} elseif ($tokens[$stackPtr]['code'] === \T_TYPE_OPEN_PARENTHESIS) {
75+
$type = 'DNF parenthesis open';
76+
$code = 'DNFOpen';
77+
} elseif ($tokens[$stackPtr]['code'] === \T_TYPE_CLOSE_PARENTHESIS) {
78+
$type = 'DNF parenthesis close';
79+
$code = 'DNFClose';
80+
}
5881

59-
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
60-
SpacesFixer::checkAndFix(
61-
$phpcsFile,
62-
$stackPtr,
63-
$prevNonEmpty,
64-
0, // Expected spaces.
65-
'Expected %s before the ' . $type . ' type separator. Found: %s',
66-
$code . 'SpacesBefore',
67-
'error',
68-
0, // Severity.
69-
'Space before ' . $type . ' type separator'
70-
);
82+
$expectedSpaces = 0;
83+
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
84+
if ($tokens[$stackPtr]['code'] === \T_TYPE_OPEN_PARENTHESIS) {
85+
if ($tokens[$prevNonEmpty]['code'] === \T_COLON
86+
|| $tokens[$prevNonEmpty]['code'] === \T_CONST
87+
|| isset(Collections::propertyModifierKeywords()[$tokens[$prevNonEmpty]['code']]) === true
88+
) {
89+
// Start of return type or property/const type. Always demand 1 space.
90+
$expectedSpaces = 1;
91+
}
92+
93+
if ($tokens[$prevNonEmpty]['code'] === \T_OPEN_PARENTHESIS
94+
|| $tokens[$prevNonEmpty]['code'] === \T_COMMA
95+
) {
96+
// Start of parameter type. Allow new line/indent before.
97+
if ($tokens[$prevNonEmpty]['line'] === $tokens[$stackPtr]['line']) {
98+
$expectedSpaces = 1;
99+
} else {
100+
$expectedSpaces = 'skip';
101+
}
102+
}
103+
}
104+
105+
if (isset($this->targetTokens[$tokens[$prevNonEmpty]['code']]) === true) {
106+
// Prevent duplicate errors when there are two adjacent operators.
107+
$expectedSpaces = 'skip';
108+
}
109+
110+
if ($expectedSpaces !== 'skip') {
111+
SpacesFixer::checkAndFix(
112+
$phpcsFile,
113+
$stackPtr,
114+
$prevNonEmpty,
115+
$expectedSpaces,
116+
'Expected %s before the ' . $type . ' type separator. Found: %s',
117+
$code . 'SpacesBefore',
118+
'error',
119+
0, // Severity.
120+
'Space before ' . $type . ' type separator'
121+
);
122+
}
123+
124+
$expectedSpaces = 0;
125+
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
126+
if ($tokens[$stackPtr]['code'] === \T_TYPE_CLOSE_PARENTHESIS) {
127+
if ($tokens[$nextNonEmpty]['code'] === \T_OPEN_CURLY_BRACKET
128+
|| $tokens[$nextNonEmpty]['code'] === \T_VARIABLE
129+
|| $tokens[$nextNonEmpty]['code'] === \T_STRING
130+
) {
131+
// End of return type, parameter or property/const type. Always demand 1 space.
132+
$expectedSpaces = 1;
133+
}
134+
}
71135

72-
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
73136
SpacesFixer::checkAndFix(
74137
$phpcsFile,
75138
$stackPtr,
76139
$nextNonEmpty,
77-
0, // Expected spaces.
140+
$expectedSpaces,
78141
'Expected %s after the ' . $type . ' type separator. Found: %s',
79142
$code . 'SpacesAfter',
80143
'error',

Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,38 @@ $closure = function (TypeA | TypeB $p, TypeA &namespace\TypeB $q): \
3636

3737
$arrow = fn (TypeA | TypeB $p, TypeA & namespace\TypeB $q): string | int => $p * $q;
3838

39+
/*
40+
* PHP 8.2 DNF types.
41+
*/
42+
class DNFTypes {
43+
private const (A&B)|false DNF_CORRECT_START = false;
44+
protected const false|(A&B) DNF_CORRECT_END = false;
45+
public const ( A&B ) |false DNF_INCORRECT_START = false;
46+
final const false| ( A&B ) DNF_INCORRECT_END = false;
47+
48+
private (A&B)|false $dnfCorrectStart = false;
49+
readonly false|(A&B) $dnfCorrectEnd = false;
50+
static ( A&B ) |false $dnfIncorrectStart = false;
51+
final false| ( A&B ) $dnfIncorrectEnd = false;
52+
53+
public function DNFCorrect(
54+
(A&B)|false $paramA, (A&B)|false $paramB,
55+
null|(\FQN&\Partially\Qualified) $paramC,
56+
): (A&B)|(C&D) {
57+
}
58+
59+
public function DNFIncorrect(
60+
( A&B ) | false $paramA, (A&B) |false $paramB,
61+
null| ( \FQN&\Partially\Qualified )
62+
$paramC,
63+
): ( A &B )
64+
|
65+
( C&D ) {
66+
}
67+
68+
public function DNFIncorrectReturnTypeNoSpace():(A&B)|(C&D){}
69+
}
70+
3971
// Live coding.
4072
// This test should be the last test in the file.
4173
function foo(int|

Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc.fixed

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,35 @@ $closure = function (TypeA|TypeB $p, TypeA&namespace\TypeB $q): \Fully\Qualified
3232

3333
$arrow = fn (TypeA|TypeB $p, TypeA&namespace\TypeB $q): string|int => $p * $q;
3434

35+
/*
36+
* PHP 8.2 DNF types.
37+
*/
38+
class DNFTypes {
39+
private const (A&B)|false DNF_CORRECT_START = false;
40+
protected const false|(A&B) DNF_CORRECT_END = false;
41+
public const (A&B)|false DNF_INCORRECT_START = false;
42+
final const false|(A&B) DNF_INCORRECT_END = false;
43+
44+
private (A&B)|false $dnfCorrectStart = false;
45+
readonly false|(A&B) $dnfCorrectEnd = false;
46+
static (A&B)|false $dnfIncorrectStart = false;
47+
final false|(A&B) $dnfIncorrectEnd = false;
48+
49+
public function DNFCorrect(
50+
(A&B)|false $paramA, (A&B)|false $paramB,
51+
null|(\FQN&\Partially\Qualified) $paramC,
52+
): (A&B)|(C&D) {
53+
}
54+
55+
public function DNFIncorrect(
56+
(A&B)|false $paramA, (A&B)|false $paramB,
57+
null|(\FQN&\Partially\Qualified) $paramC,
58+
): (A&B)|(C&D) {
59+
}
60+
61+
public function DNFIncorrectReturnTypeNoSpace(): (A&B)|(C&D) {}
62+
}
63+
3564
// Live coding.
3665
// This test should be the last test in the file.
3766
function foo(int|

Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ public function getErrorList()
3838
33 => 1,
3939
35 => 5,
4040
37 => 6,
41+
45 => 4,
42+
46 => 4,
43+
50 => 4,
44+
51 => 4,
45+
60 => 6,
46+
61 => 4,
47+
63 => 5,
48+
64 => 1,
49+
65 => 3,
50+
68 => 2,
4151
];
4252
}
4353

0 commit comments

Comments
 (0)