Skip to content

Commit 6958894

Browse files
authored
Merge pull request #11 from vossik/update
2 parents 109bcf1 + f75a11f commit 6958894

File tree

3 files changed

+273
-11
lines changed

3 files changed

+273
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace InfinityloopCodingStandard\Sniffs\TypeHints;
6+
7+
/**
8+
* https://github.com/slevomat/coding-standard/blob/master/SlevomatCodingStandard/Sniffs/TypeHints/UnionTypeHintFormatSniff.php
9+
*/
10+
class UnionTypeHintFormatSniff implements \PHP_CodeSniffer\Sniffs\Sniff
11+
{
12+
public const CODE_DISALLOWED_WHITESPACE = 'DisallowedWhitespace';
13+
public const CODE_REQUIRED_SHORT_NULLABLE = 'RequiredShortNullable';
14+
public const CODE_NULL_TYPE_HINT_NOT_ON_LAST_POSITION = 'NullTypeHintNotOnLastPosition';
15+
public const UNION_SEPARATOR_NOT_ON_LAST_POSITION = 'Union type separator (|) should be placed at end of the line';
16+
public const MULTILINE_UNION_WRONG_INDENTATION = 'Union types should be intended on same level as the one';
17+
18+
/**
19+
* @return array<int, (int|string)>
20+
*/
21+
public function register() : array
22+
{
23+
return \array_merge(
24+
[\T_VARIABLE],
25+
\SlevomatCodingStandard\Helpers\TokenHelper::$functionTokenCodes,
26+
);
27+
}
28+
29+
/**
30+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
31+
* @param \PHP_CodeSniffer\Files\File $phpcsFile
32+
* @param int $pointer
33+
*/
34+
//@phpcs:ignore Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
35+
public function process(\PHP_CodeSniffer\Files\File $phpcsFile, $pointer) : void
36+
{
37+
if (!\SlevomatCodingStandard\Helpers\SniffSettingsHelper::isEnabledByPhpVersion(null, 80000)) {
38+
return;
39+
}
40+
41+
$tokens = $phpcsFile->getTokens();
42+
43+
if ($tokens[$pointer]['code'] === \T_VARIABLE) {
44+
if (!\SlevomatCodingStandard\Helpers\PropertyHelper::isProperty($phpcsFile, $pointer)) {
45+
return;
46+
}
47+
48+
$propertyTypeHint = \SlevomatCodingStandard\Helpers\PropertyHelper::findTypeHint($phpcsFile, $pointer);
49+
50+
if ($propertyTypeHint !== null) {
51+
$this->checkTypeHint($phpcsFile, $propertyTypeHint);
52+
}
53+
54+
return;
55+
}
56+
57+
$returnTypeHint = \SlevomatCodingStandard\Helpers\FunctionHelper::findReturnTypeHint($phpcsFile, $pointer);
58+
59+
if ($returnTypeHint !== null) {
60+
$this->checkTypeHint($phpcsFile, $returnTypeHint);
61+
}
62+
63+
foreach (\SlevomatCodingStandard\Helpers\FunctionHelper::getParametersTypeHints($phpcsFile, $pointer) as $parameterTypeHint) {
64+
if ($parameterTypeHint !== null) {
65+
$this->checkTypeHint($phpcsFile, $parameterTypeHint);
66+
}
67+
}
68+
}
69+
70+
private function checkTypeHint(\PHP_CodeSniffer\Files\File $phpcsFile, \SlevomatCodingStandard\Helpers\TypeHint $typeHint) : void
71+
{
72+
$tokens = $phpcsFile->getTokens();
73+
74+
$typeHintsCount = \substr_count($typeHint->getTypeHint(), '|') + 1;
75+
76+
if ($typeHintsCount > 1) {
77+
$firstUnionType = $tokens[$typeHint->getStartPointer()];
78+
$isOneline = true;
79+
80+
foreach (
81+
\SlevomatCodingStandard\Helpers\TokenHelper::findNextAll(
82+
$phpcsFile,
83+
[\T_TYPE_UNION],
84+
$typeHint->getStartPointer(),
85+
$typeHint->getEndPointer(),
86+
) as $unionSeparator
87+
) {
88+
if ($tokens[$unionSeparator]['line'] !== $firstUnionType['line'] && $tokens[$unionSeparator + 1]['content'] !== $phpcsFile->eolChar) {
89+
$phpcsFile->addError(
90+
self::UNION_SEPARATOR_NOT_ON_LAST_POSITION,
91+
$unionSeparator,
92+
self::UNION_SEPARATOR_NOT_ON_LAST_POSITION,
93+
);
94+
}
95+
96+
$nextUnionType = \SlevomatCodingStandard\Helpers\TokenHelper::findNextEffective($phpcsFile, $unionSeparator + 1);
97+
98+
if ($tokens[$nextUnionType]['line'] === $firstUnionType['line']) {
99+
continue;
100+
}
101+
102+
$isOneline = false;
103+
104+
if ($tokens[$typeHint->getStartPointer()]['column'] === $tokens[$nextUnionType]['column']) {
105+
continue;
106+
}
107+
108+
$fix = $phpcsFile->addFixableError(
109+
self::MULTILINE_UNION_WRONG_INDENTATION,
110+
$nextUnionType,
111+
self::MULTILINE_UNION_WRONG_INDENTATION,
112+
);
113+
114+
if (!$fix) {
115+
continue;
116+
}
117+
118+
$difference = $tokens[$typeHint->getStartPointer()]['column'] - $tokens[$nextUnionType]['column'];
119+
120+
if ($difference === 0) {
121+
continue;
122+
}
123+
124+
$phpcsFile->fixer->beginChangeset();
125+
126+
if ($difference > 0) {
127+
$phpcsFile->fixer->addContentBefore($nextUnionType, \str_repeat(' ', \abs($difference)));
128+
129+
$phpcsFile->fixer->endChangeset();
130+
131+
continue;
132+
}
133+
134+
for ($i = 0; $i < \abs($difference); $i++) {
135+
$token = \SlevomatCodingStandard\Helpers\TokenHelper::findPrevious(
136+
$phpcsFile,
137+
[\T_WHITESPACE],
138+
$nextUnionType - $i,
139+
);
140+
141+
if (\strlen($tokens[$token]['content']) > 1) { //Handle multiple spaces
142+
$phpcsFile->fixer->replaceToken(
143+
$token,
144+
\str_repeat(' ', \abs(\strlen($tokens[$token]['content']) - \abs($difference))),
145+
);
146+
147+
break;
148+
}
149+
150+
$phpcsFile->fixer->replaceToken($token, '');
151+
}
152+
153+
$phpcsFile->fixer->endChangeset();
154+
}
155+
156+
if ($isOneline) {
157+
$whitespacePointer = \SlevomatCodingStandard\Helpers\TokenHelper::findNext(
158+
$phpcsFile,
159+
\T_WHITESPACE,
160+
$typeHint->getStartPointer() + 1,
161+
$typeHint->getEndPointer(),
162+
);
163+
164+
if ($whitespacePointer !== null) {
165+
$originalTypeHint = \SlevomatCodingStandard\Helpers\TokenHelper::getContent(
166+
$phpcsFile,
167+
$typeHint->getStartPointer(),
168+
$typeHint->getEndPointer(),
169+
);
170+
171+
$fix = $phpcsFile->addFixableError(
172+
\sprintf('Spaces in type hint "%s" are disallowed.', $originalTypeHint),
173+
$typeHint->getStartPointer(),
174+
self::CODE_DISALLOWED_WHITESPACE,
175+
);
176+
177+
if ($fix) {
178+
$this->fixTypeHint($phpcsFile, $typeHint, $typeHint->getTypeHint());
179+
}
180+
}
181+
}
182+
}
183+
184+
if (!$typeHint->isNullable()) {
185+
return;
186+
}
187+
188+
$hasShortNullable = \strpos($typeHint->getTypeHint(), '?') === 0;
189+
190+
if ($typeHintsCount === 2 && !$hasShortNullable) {
191+
$fix = $phpcsFile->addFixableError(
192+
\sprintf('Short nullable type hint in "%s" is required.', $typeHint->getTypeHint()),
193+
$typeHint->getStartPointer(),
194+
self::CODE_REQUIRED_SHORT_NULLABLE,
195+
);
196+
197+
if ($fix) {
198+
$typeHintWithoutNull = self::getTypeHintContentWithoutNull($phpcsFile, $typeHint);
199+
$this->fixTypeHint($phpcsFile, $typeHint, '?' . $typeHintWithoutNull);
200+
}
201+
}
202+
203+
if ($hasShortNullable || ($typeHintsCount === 2) || \strtolower($tokens[$typeHint->getEndPointer()]['content']) === 'null') {
204+
return;
205+
}
206+
207+
$fix = $phpcsFile->addFixableError(
208+
\sprintf('Null type hint should be on last position in "%s".', $typeHint->getTypeHint()),
209+
$typeHint->getStartPointer(),
210+
self::CODE_NULL_TYPE_HINT_NOT_ON_LAST_POSITION,
211+
);
212+
213+
if ($fix) {
214+
$this->fixTypeHint($phpcsFile, $typeHint, self::getTypeHintContentWithoutNull($phpcsFile, $typeHint) . '|null');
215+
}
216+
}
217+
218+
private function getTypeHintContentWithoutNull(
219+
\PHP_CodeSniffer\Files\File $phpcsFile,
220+
\SlevomatCodingStandard\Helpers\TypeHint $typeHint,
221+
) : string
222+
{
223+
$tokens = $phpcsFile->getTokens();
224+
225+
if (\strtolower($tokens[$typeHint->getEndPointer()]['content']) === 'null') {
226+
$previousTypeHintPointer = \SlevomatCodingStandard\Helpers\TokenHelper::findPrevious(
227+
$phpcsFile,
228+
\SlevomatCodingStandard\Helpers\TokenHelper::getOnlyTypeHintTokenCodes(),
229+
$typeHint->getEndPointer() - 1,
230+
);
231+
232+
return \SlevomatCodingStandard\Helpers\TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $previousTypeHintPointer);
233+
}
234+
235+
$content = '';
236+
237+
for ($i = $typeHint->getStartPointer(); $i <= $typeHint->getEndPointer(); $i++) {
238+
if (\strtolower($tokens[$i]['content']) === 'null') {
239+
$i = \SlevomatCodingStandard\Helpers\TokenHelper::findNext(
240+
$phpcsFile,
241+
\SlevomatCodingStandard\Helpers\TokenHelper::getOnlyTypeHintTokenCodes(),
242+
$i + 1,
243+
);
244+
}
245+
246+
$content .= $tokens[$i]['content'];
247+
}
248+
249+
return $content;
250+
}
251+
252+
private function fixTypeHint(
253+
\PHP_CodeSniffer\Files\File $phpcsFile,
254+
\SlevomatCodingStandard\Helpers\TypeHint $typeHint,
255+
string $fixedTypeHint,
256+
) : void
257+
{
258+
$phpcsFile->fixer->beginChangeset();
259+
260+
$phpcsFile->fixer->replaceToken($typeHint->getStartPointer(), $fixedTypeHint);
261+
262+
for ($i = $typeHint->getStartPointer() + 1; $i <= $typeHint->getEndPointer(); $i++) {
263+
$phpcsFile->fixer->replaceToken($i, '');
264+
}
265+
266+
$phpcsFile->fixer->endChangeset();
267+
}
268+
}

InfinityloopCodingStandard/ruleset.xml

+1-7
Original file line numberDiff line numberDiff line change
@@ -459,13 +459,6 @@
459459
<rule ref="SlevomatCodingStandard.TypeHints.NullTypeHintOnLastPosition"/>
460460
<rule ref="SlevomatCodingStandard.TypeHints.DisallowArrayTypeHintSyntax"/>
461461
<rule ref="SlevomatCodingStandard.TypeHints.PropertyTypeHintSpacing"/>
462-
<rule ref="SlevomatCodingStandard.TypeHints.UnionTypeHintFormat">
463-
<properties>
464-
<property name="withSpaces" value="no"/>
465-
<property name="shortNullable" value="yes"/>
466-
<property name="nullPosition" value="last"/>
467-
</properties>
468-
</rule>
469462
<rule ref="SlevomatCodingStandard.Arrays.DisallowImplicitArrayCreation"/>
470463
<rule ref="SlevomatCodingStandard.Arrays.TrailingArrayComma">
471464
<properties>
@@ -508,4 +501,5 @@
508501
<rule ref="InfinityloopCodingStandard.Namespaces.UseDoesNotStartWithBackslash"/>
509502
<rule ref="InfinityloopCodingStandard.ControlStructures.RequireMultiLineNullCoalesce"/>
510503
<rule ref="InfinityloopCodingStandard.ControlStructures.SwitchCommentSpacing"/>
504+
<rule ref="InfinityloopCodingStandard.TypeHints.UnionTypeHintFormat"/>
511505
</ruleset>

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ Enforces null coalesce operator to be reformatted to new line
7878

7979
Checks that there is a certain number of blank lines between code and comment
8080

81+
#### InfinityloopCodingStandard.TypeHints.UnionTypeHintFormat :wrench:
82+
83+
Improved version of Slevomat UnionTypeHintFormat with added formatting of multiline unions
84+
8185
### Slevomat sniffs
8286

8387
Detailed list of Slevomat sniffs with configured settings. Some sniffs are not included, either because we dont find them helpful, the are too strict, or collide with their counter-sniff (require/disallow pairs).
@@ -220,10 +224,6 @@ Excluded sniffs:
220224
- SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue
221225
- SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing
222226
- SlevomatCodingStandard.TypeHints.PropertyTypeHintSpacing
223-
- SlevomatCodingStandard.TypeHints.UnionTypeHintFormat
224-
- withSpaces: no
225-
- shortNullable: yes
226-
- nullPosition: last
227227
- SlevomatCodingStandard.Namespaces.DisallowGroupUse
228228
- SlevomatCodingStandard.Namespaces.FullyQualifiedExceptions
229229
- specialExceptionNames: false

0 commit comments

Comments
 (0)