Skip to content

Commit 2031373

Browse files
committed
SlevomatCodingStandard.ControlStructures.RequireNullCoalesceEqualOperator: New option "checkIfConditions"
1 parent 374ad13 commit 2031373

7 files changed

+219
-2
lines changed

SlevomatCodingStandard/Sniffs/ControlStructures/RequireNullCoalesceEqualOperatorSniff.php

+116
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,22 @@
77
use PHP_CodeSniffer\Util\Tokens;
88
use SlevomatCodingStandard\Helpers\FixerHelper;
99
use SlevomatCodingStandard\Helpers\IdentificatorHelper;
10+
use SlevomatCodingStandard\Helpers\IndentationHelper;
1011
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
1112
use SlevomatCodingStandard\Helpers\TokenHelper;
13+
use function array_keys;
14+
use function count;
15+
use function in_array;
16+
use function range;
17+
use function sprintf;
18+
use function trim;
1219
use const T_COALESCE;
20+
use const T_ELSE;
21+
use const T_ELSEIF;
1322
use const T_EQUAL;
23+
use const T_IF;
24+
use const T_IS_IDENTICAL;
25+
use const T_NULL;
1426
use const T_SEMICOLON;
1527

1628
class RequireNullCoalesceEqualOperatorSniff implements Sniff
@@ -20,6 +32,8 @@ class RequireNullCoalesceEqualOperatorSniff implements Sniff
2032

2133
public ?bool $enable = null;
2234

35+
public bool $checkIfConditions = false;
36+
2337
/**
2438
* @return array<int, (int|string)>
2539
*/
@@ -42,6 +56,12 @@ public function process(File $phpcsFile, $equalPointer): void
4256
return;
4357
}
4458

59+
$this->checkCoalesce($phpcsFile, $equalPointer);
60+
$this->checkIf($phpcsFile, $equalPointer);
61+
}
62+
63+
private function checkCoalesce(File $phpcsFile, int $equalPointer): void
64+
{
4565
/** @var int $variableStartPointer */
4666
$variableStartPointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1);
4767
$variableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $variableStartPointer);
@@ -95,4 +115,100 @@ public function process(File $phpcsFile, $equalPointer): void
95115
$phpcsFile->fixer->endChangeset();
96116
}
97117

118+
private function checkIf(File $phpcsFile, int $equalPointer): void
119+
{
120+
if (!$this->checkIfConditions) {
121+
return;
122+
}
123+
124+
$tokens = $phpcsFile->getTokens();
125+
126+
$conditionsCount = count($tokens[$equalPointer]['conditions']);
127+
if ($conditionsCount === 0) {
128+
return;
129+
}
130+
131+
$ifPointer = array_keys($tokens[$equalPointer]['conditions'])[$conditionsCount - 1];
132+
if ($tokens[$ifPointer]['code'] !== T_IF) {
133+
return;
134+
}
135+
136+
$pointerAfterIfCondition = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1);
137+
if ($pointerAfterIfCondition !== null && in_array($tokens[$pointerAfterIfCondition]['code'], [T_ELSEIF, T_ELSE], true)) {
138+
return;
139+
}
140+
141+
$ifVariableStartPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['parenthesis_opener'] + 1);
142+
$ifVariableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $ifVariableStartPointer);
143+
if ($ifVariableEndPointer === null) {
144+
return;
145+
}
146+
147+
$nextIfPointer = TokenHelper::findNextEffective($phpcsFile, $ifVariableEndPointer + 1);
148+
if ($tokens[$nextIfPointer]['code'] !== T_IS_IDENTICAL) {
149+
return;
150+
}
151+
152+
$nextIfPointer = TokenHelper::findNextEffective($phpcsFile, $nextIfPointer + 1);
153+
if ($tokens[$nextIfPointer]['code'] !== T_NULL) {
154+
return;
155+
}
156+
157+
if (TokenHelper::findNextEffective($phpcsFile, $nextIfPointer + 1) !== $tokens[$ifPointer]['parenthesis_closer']) {
158+
return;
159+
}
160+
161+
$beforeEqualVariableStartPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_opener'] + 1);
162+
$beforeEqualVariableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $beforeEqualVariableStartPointer);
163+
if ($beforeEqualVariableEndPointer === null) {
164+
return;
165+
}
166+
167+
if (TokenHelper::findNextEffective($phpcsFile, $beforeEqualVariableEndPointer + 1) !== $equalPointer) {
168+
return;
169+
}
170+
171+
$variableName = IdentificatorHelper::getContent($phpcsFile, $ifVariableStartPointer, $ifVariableEndPointer);
172+
173+
if ($variableName !== IdentificatorHelper::getContent(
174+
$phpcsFile,
175+
$beforeEqualVariableStartPointer,
176+
$beforeEqualVariableEndPointer,
177+
)) {
178+
return;
179+
}
180+
181+
$semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $equalPointer + 1);
182+
if (TokenHelper::findNextEffective($phpcsFile, $semicolonPointer + 1) !== $tokens[$ifPointer]['scope_closer']) {
183+
return;
184+
}
185+
186+
$fix = $phpcsFile->addFixableError(
187+
'Use "??=" operator instead of if condition and "=".',
188+
$ifPointer,
189+
self::CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR,
190+
);
191+
192+
if (!$fix) {
193+
return;
194+
}
195+
196+
$codeStartPointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1);
197+
198+
$afterNullCoalesceEqualCode = IndentationHelper::fixIndentation(
199+
$phpcsFile,
200+
range($codeStartPointer, $semicolonPointer),
201+
IndentationHelper::getIndentation($phpcsFile, $ifPointer),
202+
);
203+
204+
$phpcsFile->fixer->beginChangeset();
205+
FixerHelper::change(
206+
$phpcsFile,
207+
$ifPointer,
208+
$tokens[$ifPointer]['scope_closer'],
209+
sprintf('%s ??= %s', $variableName, trim($afterNullCoalesceEqualCode)),
210+
);
211+
$phpcsFile->fixer->endChangeset();
212+
}
213+
98214
}

doc/control-structures.md

+1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ Requires use of null coalesce equal operator when possible.
194194
This sniff provides the following setting:
195195

196196
* `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 7.4 or higher.
197+
* `checkIfCondition` (default: `false`): will check `if` conditions too.
197198

198199
#### SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator 🔧
199200

tests/Sniffs/ControlStructures/RequireNullCoalesceEqualOperatorSniffTest.php

+18-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public function testNoErrors(): void
1111
{
1212
$report = self::checkFile(__DIR__ . '/data/requireNullCoalesceEqualOperatorNoErrors.php', [
1313
'enable' => true,
14+
'checkIfConditions' => true,
1415
]);
1516
self::assertNoSniffErrorInFile($report);
1617
}
@@ -19,16 +20,20 @@ public function testErrors(): void
1920
{
2021
$report = self::checkFile(
2122
__DIR__ . '/data/requireNullCoalesceEqualOperatorErrors.php',
22-
['enable' => true],
23+
['enable' => true, 'checkIfConditions' => true],
2324
[RequireNullCoalesceEqualOperatorSniff::CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR],
2425
);
2526

26-
self::assertSame(11, $report->getErrorCount());
27+
self::assertSame(14, $report->getErrorCount());
2728

2829
foreach ([3, 5, 7, 9, 10, 12, 14, 15, 17, 21, 23] as $line) {
2930
self::assertSniffError($report, $line, RequireNullCoalesceEqualOperatorSniff::CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR);
3031
}
3132

33+
foreach ([25, 30, 35] as $line) {
34+
self::assertSniffError($report, $line, RequireNullCoalesceEqualOperatorSniff::CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR);
35+
}
36+
3237
self::assertAllFixedInFile($report);
3338
}
3439

@@ -43,4 +48,15 @@ public function testShouldNotReportIfSniffIsDisabled(): void
4348
self::assertNoSniffErrorInFile($report);
4449
}
4550

51+
public function testShouldNotReportIfCheckIfConditionIsDisabled(): void
52+
{
53+
$report = self::checkFile(
54+
__DIR__ . '/data/requireNullCoalesceEqualOperatorCheckIfConditionDisabled.php',
55+
['enable' => true, 'checkIfConditions' => false],
56+
[RequireNullCoalesceEqualOperatorSniff::CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR],
57+
);
58+
59+
self::assertNoSniffErrorInFile($report);
60+
}
61+
4662
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
if ($f === null) {
4+
$f = 1;
5+
}
6+
7+
function () use ($g) {
8+
if ($g === null) {
9+
$g = true;
10+
}
11+
};

tests/Sniffs/ControlStructures/data/requireNullCoalesceEqualOperatorErrors.fixed.php

+10
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,13 @@
1919
\Whatever\Something::$anything ??= 1;
2020

2121
$object->anything ??= 0;
22+
23+
$f ??= 1;
24+
25+
function () use ($g) {
26+
$g ??= true;
27+
};
28+
29+
$h ??= doSomething(
30+
'something',
31+
);

tests/Sniffs/ControlStructures/data/requireNullCoalesceEqualOperatorErrors.php

+16
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,19 @@
2121
\Whatever\Something::$anything = \Whatever\Something::$anything ?? 1;
2222

2323
$object->anything = $object->anything ?? 0;
24+
25+
if ($f === null) {
26+
$f = 1;
27+
}
28+
29+
function () use ($g) {
30+
if ($g === null) {
31+
$g = true;
32+
}
33+
};
34+
35+
if ($h === null) {
36+
$h = doSomething(
37+
'something',
38+
);
39+
}

tests/Sniffs/ControlStructures/data/requireNullCoalesceEqualOperatorNoErrors.php

+47
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,50 @@ public function moreOperators($a, $b, $c)
4040

4141
}
4242

43+
if (false === $a) {
44+
$a = true;
45+
}
46+
47+
if ($b == null) {
48+
$b = true;
49+
}
50+
51+
if ($c === false) {
52+
$c = true;
53+
}
54+
55+
if ($d === null && $dd !== null) {
56+
$d = true;
57+
}
58+
59+
if ($e === null) {
60+
[$e] = [];
61+
}
62+
63+
if ($f === null) {
64+
$ff = true;
65+
}
66+
67+
if ($g === null) {
68+
$g = true;
69+
$gg = true;
70+
}
71+
72+
if ($h === null) {
73+
$h = true;
74+
} else {
75+
doSomething();
76+
}
77+
78+
if ($i === null) {
79+
$i = true;
80+
} elseif (false) {
81+
doSomething();
82+
}
83+
84+
if ($j === null) {
85+
doSomething();
86+
$j = true;
87+
}
88+
89+
if ($k === null) $k = true;

0 commit comments

Comments
 (0)