Skip to content

Commit 87a410c

Browse files
esserjkukulich
authored andcommitted
SlevomatCodingStandard.ControlStructures.DisallowTrailingMultiLineTernaryOperator: New sniff
1 parent 19d598b commit 87a410c

File tree

7 files changed

+329
-0
lines changed

7 files changed

+329
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/squizlabs/PHP_
8686
- [SlevomatCodingStandard.ControlStructures.DisallowEmpty](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowempty)
8787
- [SlevomatCodingStandard.ControlStructures.DisallowNullSafeObjectOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallownullsafeobjectoperator)
8888
- [SlevomatCodingStandard.ControlStructures.DisallowShortTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowshortternaryoperator-) 🔧
89+
- [SlevomatCodingStandard.ControlStructures.DisallowTrailingMultiLineTernaryOperatorSniff](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowtrailingmultilineternaryoperator-) 🔧
8990
- [SlevomatCodingStandard.ControlStructures.DisallowYodaComparison](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowyodacomparison-) 🔧
9091
- [SlevomatCodingStandard.ControlStructures.EarlyExit](doc/control-structures.md#slevomatcodingstandardcontrolstructuresearlyexit-) 🔧
9192
- [SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing](doc/control-structures.md#slevomatcodingstandardcontrolstructuresjumpstatementsspacing-) 🔧
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use SlevomatCodingStandard\Helpers\FixerHelper;
8+
use SlevomatCodingStandard\Helpers\IndentationHelper;
9+
use SlevomatCodingStandard\Helpers\TernaryOperatorHelper;
10+
use SlevomatCodingStandard\Helpers\TokenHelper;
11+
use function array_merge;
12+
use function in_array;
13+
use function strlen;
14+
use function substr;
15+
use function trim;
16+
use const T_INLINE_ELSE;
17+
use const T_INLINE_THEN;
18+
use const T_OPEN_TAG;
19+
use const T_OPEN_TAG_WITH_ECHO;
20+
use const T_WHITESPACE;
21+
22+
class DisallowTrailingMultiLineTernaryOperatorSniff implements Sniff
23+
{
24+
25+
public const CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED = 'TrailingMultiLineTernaryOperatorUsed';
26+
27+
/**
28+
* @return array<int, (int|string)>
29+
*/
30+
public function register(): array
31+
{
32+
return [
33+
T_INLINE_THEN,
34+
];
35+
}
36+
37+
/**
38+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
39+
* @param int $inlineThenPointer
40+
*/
41+
public function process(File $phpcsFile, $inlineThenPointer): void
42+
{
43+
$tokens = $phpcsFile->getTokens();
44+
45+
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1);
46+
if ($tokens[$nextPointer]['code'] === T_INLINE_ELSE) {
47+
return;
48+
}
49+
50+
$inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer);
51+
52+
if ($tokens[$inlineThenPointer]['line'] === $tokens[$inlineElsePointer]['line']) {
53+
return;
54+
}
55+
56+
$endOfLineBeforeInlineThenPointer = $this->getEndOfLineBefore($phpcsFile, $inlineThenPointer);
57+
$endOfLineBeforeInlineElsePointer = $this->getEndOfLineBefore($phpcsFile, $inlineElsePointer);
58+
59+
$contentBeforeThen = TokenHelper::getContent($phpcsFile, $endOfLineBeforeInlineThenPointer + 1, $inlineThenPointer - 1);
60+
$contentBeforeElse = TokenHelper::getContent($phpcsFile, $endOfLineBeforeInlineElsePointer + 1, $inlineElsePointer - 1);
61+
if (trim($contentBeforeElse) === '' && trim($contentBeforeThen) === '') {
62+
return;
63+
}
64+
65+
$fix = $phpcsFile->addFixableError(
66+
'Ternary operator should be reformatted as leading the line.',
67+
$inlineThenPointer,
68+
self::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED
69+
);
70+
71+
if (!$fix) {
72+
return;
73+
}
74+
75+
$indentation = $this->getIndentation($phpcsFile, $endOfLineBeforeInlineThenPointer);
76+
$pointerBeforeInlineThen = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1);
77+
$pointerAfterInlineThen = TokenHelper::findNextExcluding($phpcsFile, [T_WHITESPACE], $inlineThenPointer + 1);
78+
$pointerBeforeInlineElse = TokenHelper::findPreviousEffective($phpcsFile, $inlineElsePointer - 1);
79+
$pointerAfterInlineElse = TokenHelper::findNextExcluding($phpcsFile, [T_WHITESPACE], $inlineElsePointer + 1);
80+
81+
$phpcsFile->fixer->beginChangeset();
82+
83+
FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineThen, $inlineThenPointer);
84+
FixerHelper::removeBetween($phpcsFile, $inlineThenPointer, $pointerAfterInlineThen);
85+
86+
$phpcsFile->fixer->addContentBefore($inlineThenPointer, $phpcsFile->eolChar . $indentation);
87+
$phpcsFile->fixer->addContentBefore($pointerAfterInlineThen, ' ');
88+
89+
FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineElse, $inlineElsePointer);
90+
FixerHelper::removeBetween($phpcsFile, $inlineElsePointer, $pointerAfterInlineElse);
91+
92+
$phpcsFile->fixer->addContentBefore($inlineElsePointer, $phpcsFile->eolChar . $indentation);
93+
$phpcsFile->fixer->addContentBefore($pointerAfterInlineElse, ' ');
94+
95+
$phpcsFile->fixer->endChangeset();
96+
}
97+
98+
private function getEndOfLineBefore(File $phpcsFile, int $pointer): int
99+
{
100+
$tokens = $phpcsFile->getTokens();
101+
102+
$endOfLineBefore = null;
103+
104+
$startPointer = $pointer - 1;
105+
while (true) {
106+
$possibleEndOfLinePointer = TokenHelper::findPrevious(
107+
$phpcsFile,
108+
array_merge([T_WHITESPACE, T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO], TokenHelper::$inlineCommentTokenCodes),
109+
$startPointer
110+
);
111+
if (
112+
$tokens[$possibleEndOfLinePointer]['code'] === T_WHITESPACE
113+
&& $tokens[$possibleEndOfLinePointer]['content'] === $phpcsFile->eolChar
114+
) {
115+
$endOfLineBefore = $possibleEndOfLinePointer;
116+
break;
117+
}
118+
119+
if (
120+
in_array($tokens[$possibleEndOfLinePointer]['code'], TokenHelper::$inlineCommentTokenCodes, true)
121+
&& substr($tokens[$possibleEndOfLinePointer]['content'], -1) === $phpcsFile->eolChar
122+
) {
123+
$endOfLineBefore = $possibleEndOfLinePointer;
124+
break;
125+
}
126+
127+
$startPointer = $possibleEndOfLinePointer - 1;
128+
}
129+
130+
/** @var int $endOfLineBefore */
131+
$endOfLineBefore = $endOfLineBefore;
132+
return $endOfLineBefore;
133+
}
134+
135+
private function getIndentation(File $phpcsFile, int $endOfLinePointer): string
136+
{
137+
$pointerAfterWhitespace = TokenHelper::findNextNonWhitespace($phpcsFile, $endOfLinePointer + 1);
138+
$actualIndentation = TokenHelper::getContent($phpcsFile, $endOfLinePointer + 1, $pointerAfterWhitespace - 1);
139+
140+
if (strlen($actualIndentation) !== 0) {
141+
return $actualIndentation . (
142+
substr($actualIndentation, -1) === IndentationHelper::TAB_INDENT
143+
? IndentationHelper::TAB_INDENT
144+
: IndentationHelper::SPACES_INDENT
145+
);
146+
}
147+
148+
$tabPointer = TokenHelper::findPreviousContent($phpcsFile, T_WHITESPACE, IndentationHelper::TAB_INDENT, $endOfLinePointer - 1);
149+
return $tabPointer !== null ? IndentationHelper::TAB_INDENT : IndentationHelper::SPACES_INDENT;
150+
}
151+
152+
}

doc/control-structures.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ Sniff provides the following settings:
111111

112112
* `fixable`: the sniff is fixable by default, however in strict code it makes sense to forbid this weakly typed form of ternary altogether, you can disable fixability with this option.
113113

114+
#### SlevomatCodingStandard.ControlStructures.DisallowTrailingMultiLineTernaryOperator 🔧
115+
116+
Ternary operator has to be reformatted when the operator is not leading the line.
117+
118+
```php
119+
# wrong
120+
$t = $someCondition ?
121+
$thenThis :
122+
$otherwiseThis;
123+
124+
# correct
125+
$t = $someCondition
126+
? $thenThis
127+
: $otherwiseThis;
128+
```
129+
114130
#### SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing 🔧
115131

116132
Enforces configurable number of lines around jump statements (continue, return, ...).
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
4+
5+
use SlevomatCodingStandard\Sniffs\TestCase;
6+
7+
class DisallowTrailingMultiLineTernaryOperatorSniffTest extends TestCase
8+
{
9+
10+
public function testNoErrors(): void
11+
{
12+
$report = self::checkFile(__DIR__ . '/data/disallowTrailingMultiLineTernaryOperatorNoErrors.php');
13+
self::assertNoSniffErrorInFile($report);
14+
}
15+
16+
public function testErrors(): void
17+
{
18+
$report = self::checkFile(__DIR__ . '/data/disallowTrailingMultiLineTernaryOperatorErrors.php');
19+
20+
self::assertSame(8, $report->getErrorCount());
21+
22+
self::assertSniffError($report, 4, DisallowTrailingMultiLineTernaryOperatorSniff::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED);
23+
self::assertSniffError($report, 9, DisallowTrailingMultiLineTernaryOperatorSniff::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED);
24+
self::assertSniffError($report, 13, DisallowTrailingMultiLineTernaryOperatorSniff::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED);
25+
self::assertSniffError($report, 17, DisallowTrailingMultiLineTernaryOperatorSniff::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED);
26+
self::assertSniffError($report, 21, DisallowTrailingMultiLineTernaryOperatorSniff::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED);
27+
self::assertSniffError($report, 25, DisallowTrailingMultiLineTernaryOperatorSniff::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED);
28+
self::assertSniffError($report, 29, DisallowTrailingMultiLineTernaryOperatorSniff::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED);
29+
30+
self::assertAllFixedInFile($report);
31+
}
32+
33+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
if (true) {
4+
return $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
5+
? 'bbbb'
6+
: 'cccccccccccccc';
7+
}
8+
9+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
10+
? 'bbbb'
11+
: 'cccccccccccccc';
12+
13+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
14+
? 'bbbb'
15+
: func(1, 2);
16+
17+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
18+
? 'bbbb'
19+
: func([]);
20+
21+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
22+
? 'bbbb'
23+
: func([1 => 2]);
24+
25+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
26+
? 'bbbb'
27+
: function () { return; };
28+
29+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
30+
? 'bbbb'
31+
: (false ? 1 : 2);
32+
33+
$array[$b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
34+
? 'bbbb'
35+
: 'ccccccc'] = 'b';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
if (true) {
4+
return $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ?
5+
'bbbb' :
6+
'cccccccccccccc';
7+
}
8+
9+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ?
10+
'bbbb' :
11+
'cccccccccccccc';
12+
13+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ?
14+
'bbbb' :
15+
func(1, 2);
16+
17+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ?
18+
'bbbb' :
19+
func([]);
20+
21+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ?
22+
'bbbb' :
23+
func([1 => 2]);
24+
25+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ?
26+
'bbbb' :
27+
function () { return; };
28+
29+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ?
30+
'bbbb' :
31+
(false ? 1 : 2);
32+
33+
$array[$b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb'
34+
: 'ccccccc'] = 'b';
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php $a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb' : 'ccccccc';
2+
3+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb' : 'ccccccc';
4+
5+
$a = $b ?: true;
6+
7+
$a = $b
8+
? true
9+
: false;
10+
11+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb' : 'ccccccccccccc';
12+
13+
$array[$b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb' : 'ccccccc'] = 'b';
14+
15+
doSomething($b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb' : 'ccccccc');
16+
17+
$x = doSomething(
18+
sprintf(
19+
'String %s %s',
20+
$y,
21+
$z !== 1 ? 's' : ''
22+
),
23+
$typeHintPointer,
24+
Whatever::XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
25+
);
26+
27+
$x = doSomething(
28+
[
29+
$z !== 1 ? 's' : ''
30+
],
31+
$typeHintPointer,
32+
Whatever::XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
33+
);
34+
35+
$order = [
36+
UseStatement::TYPE_DEFAULT => 1,
37+
UseStatement::TYPE_FUNCTION => $this->psr12Compatible ? 2 : 3,
38+
UseStatement::TYPE_CONSTANT => $this->psr12Compatible ? 3 : 2,
39+
];
40+
41+
// Comment
42+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb' : 'ccccccccccccc';
43+
44+
// phpcs:disable ABC
45+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb' : 'ccccccccccccc';
46+
47+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
48+
? 'bbbb'
49+
: 'ccccccccccccc';
50+
51+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
52+
// with comments
53+
? 'bbbb'
54+
// with comments
55+
: 'ccccccccccccc';
56+
57+
$a = $b === 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ? 'bbbb' :
58+
'ccccccccccccc';

0 commit comments

Comments
 (0)