Skip to content

Commit 6f4ac49

Browse files
committed
Added PSR12.ControlStructures.BooleanOperatorPlacement sniff to enforce that boolean operators between conditions are consistently at the start or end of the line (ref #750)
1 parent e201da5 commit 6f4ac49

File tree

5 files changed

+353
-0
lines changed

5 files changed

+353
-0
lines changed

package.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
6767
-- Thanks to Mponos George for the contribution
6868
- Added Generic.PHP.RequireStrictTypes sniff
6969
-- Enforce the use of a strict types declaration in PHP files
70+
- Added PSR12.ControlStructures.BooleanOperatorPlacement sniff
71+
-- Enforces that boolean operators between conditions are consistently at the start or end of the line
7072
- Added PSR12.Files.DeclareStatement sniff
7173
-- Enforces the formatting of declare statements within a file
7274
- Added PSR12.Files.FileHeader sniff
@@ -1116,6 +1118,9 @@ http://pear.php.net/dtd/package-2.0.xsd">
11161118
<dir name="Classes">
11171119
<file baseinstalldir="PHP/CodeSniffer" name="ClassInstantiationSniff.php" role="php" />
11181120
</dir>
1121+
<dir name="ControlStructures">
1122+
<file baseinstalldir="PHP/CodeSniffer" name="BooleanOperatorPlacementSniff.php" role="php" />
1123+
</dir>
11191124
<dir name="Files">
11201125
<file baseinstalldir="PHP/CodeSniffer" name="DeclareStatementSniff.php" role="php" />
11211126
<file baseinstalldir="PHP/CodeSniffer" name="FileHeaderSniff.php" role="php" />
@@ -1148,6 +1153,11 @@ http://pear.php.net/dtd/package-2.0.xsd">
11481153
<file baseinstalldir="PHP/CodeSniffer" name="ClassInstantiationUnitTest.inc.fixed" role="test" />
11491154
<file baseinstalldir="PHP/CodeSniffer" name="ClassInstantiationUnitTest.php" role="test" />
11501155
</dir>
1156+
<dir name="ControlStructures">
1157+
<file baseinstalldir="PHP/CodeSniffer" name="BooleanOperatorPlacementUnitTest.inc" role="test" />
1158+
<file baseinstalldir="PHP/CodeSniffer" name="BooleanOperatorPlacementUnitTest.inc.fixed" role="test" />
1159+
<file baseinstalldir="PHP/CodeSniffer" name="BooleanOperatorPlacementUnitTest.php" role="test" />
1160+
</dir>
11511161
<dir name="Files">
11521162
<file baseinstalldir="PHP/CodeSniffer" name="DeclareStatementUnitTest.inc" role="test" />
11531163
<file baseinstalldir="PHP/CodeSniffer" name="DeclareStatementUnitTest.inc.fixed" role="test" />
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
/**
3+
* Checks that control structures have boolean operators in the correct place.
4+
*
5+
* @author Greg Sherwood <gsherwood@squiz.net>
6+
* @copyright 2006-2019 Squiz Pty Ltd (ABN 77 084 670 600)
7+
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8+
*/
9+
10+
namespace PHP_CodeSniffer\Standards\PSR12\Sniffs\ControlStructures;
11+
12+
use PHP_CodeSniffer\Sniffs\Sniff;
13+
use PHP_CodeSniffer\Files\File;
14+
use PHP_CodeSniffer\Util\Tokens;
15+
16+
class BooleanOperatorPlacementSniff implements Sniff
17+
{
18+
19+
20+
/**
21+
* Returns an array of tokens this test wants to listen for.
22+
*
23+
* @return array
24+
*/
25+
public function register()
26+
{
27+
return [
28+
T_IF,
29+
T_WHILE,
30+
T_SWITCH,
31+
T_ELSEIF,
32+
];
33+
34+
}//end register()
35+
36+
37+
/**
38+
* Processes this test, when one of its tokens is encountered.
39+
*
40+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
41+
* @param int $stackPtr The position of the current token
42+
* in the stack passed in $tokens.
43+
*
44+
* @return void
45+
*/
46+
public function process(File $phpcsFile, $stackPtr)
47+
{
48+
$tokens = $phpcsFile->getTokens();
49+
50+
if (isset($tokens[$stackPtr]['parenthesis_opener']) === false
51+
|| isset($tokens[$stackPtr]['parenthesis_closer']) === false
52+
) {
53+
return;
54+
}
55+
56+
$parenOpener = $tokens[$stackPtr]['parenthesis_opener'];
57+
$parenCloser = $tokens[$stackPtr]['parenthesis_closer'];
58+
59+
if ($tokens[$parenOpener]['line'] === $tokens[$parenCloser]['line']) {
60+
// Conditions are all on the same line.
61+
return;
62+
}
63+
64+
$find = [
65+
T_BOOLEAN_AND,
66+
T_BOOLEAN_OR,
67+
];
68+
69+
$operator = $parenOpener;
70+
$position = null;
71+
$error = false;
72+
$operators = [];
73+
74+
do {
75+
$operator = $phpcsFile->findNext($find, ($operator + 1), $parenCloser);
76+
if ($operator === false) {
77+
break;
78+
}
79+
80+
$operators[] = $operator;
81+
82+
$prev = $phpcsFile->findPrevious(T_WHITESPACE, ($operator - 1), $parenOpener, true);
83+
if ($prev === false) {
84+
// Parse error.
85+
return;
86+
}
87+
88+
if ($tokens[$prev]['line'] < $tokens[$operator]['line']) {
89+
// The boolean operator is the first content on the line.
90+
if ($position === null) {
91+
$position = 'first';
92+
}
93+
94+
if ($position !== 'first') {
95+
$error = true;
96+
}
97+
98+
continue;
99+
}
100+
101+
$next = $phpcsFile->findNext(T_WHITESPACE, ($operator + 1), $parenCloser, true);
102+
if ($next === false) {
103+
// Parse error.
104+
return;
105+
}
106+
107+
if ($tokens[$next]['line'] > $tokens[$operator]['line']) {
108+
// The boolean operator is the last content on the line.
109+
if ($position === null) {
110+
$position = 'last';
111+
}
112+
113+
if ($position !== 'last') {
114+
$error = true;
115+
}
116+
117+
continue;
118+
}
119+
120+
if ($position === null) {
121+
$position = 'middle';
122+
}
123+
124+
// Error here regardless as boolean operators need to be at start/end of line.
125+
$msg = 'Boolean operators between conditions must be at the beginning or end of the line';
126+
$phpcsFile->addError($msg, $next, 'FoundMiddle');
127+
128+
if ($position !== 'middle') {
129+
$error = true;
130+
}
131+
} while ($operator !== false);
132+
133+
if ($error === false) {
134+
return;
135+
}
136+
137+
$error = 'Boolean operators between conditions must be at the beginning or end of the line, but not both';
138+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'FoundMixed');
139+
if ($fix === false) {
140+
return;
141+
}
142+
143+
$phpcsFile->fixer->beginChangeset();
144+
foreach ($operators as $operator) {
145+
$prev = $phpcsFile->findPrevious(T_WHITESPACE, ($operator - 1), $parenOpener, true);
146+
$next = $phpcsFile->findNext(T_WHITESPACE, ($operator + 1), $parenCloser, true);
147+
148+
if ($position === 'last') {
149+
if ($tokens[$next]['line'] === $tokens[$operator]['line']) {
150+
if ($tokens[$prev]['line'] === $tokens[$operator]['line']) {
151+
// Move the content after the operator to the next line.
152+
if ($tokens[($operator + 1)]['code'] === T_WHITESPACE) {
153+
$phpcsFile->fixer->replaceToken(($operator + 1), '');
154+
}
155+
156+
$first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $operator, true);
157+
$padding = str_repeat(' ', ($tokens[$first]['column'] - 1));
158+
$phpcsFile->fixer->addContent($operator, $phpcsFile->eolChar.$padding);
159+
} else {
160+
// Move the operator to the end of the previous line.
161+
if ($tokens[($operator + 1)]['code'] === T_WHITESPACE) {
162+
$phpcsFile->fixer->replaceToken(($operator + 1), '');
163+
}
164+
165+
$phpcsFile->fixer->addContent($prev, ' '.$tokens[$operator]['content']);
166+
$phpcsFile->fixer->replaceToken($operator, '');
167+
}
168+
}//end if
169+
} else {
170+
if ($tokens[$prev]['line'] === $tokens[$operator]['line']) {
171+
if ($tokens[$next]['line'] === $tokens[$operator]['line']) {
172+
// Move the operator, and the rest of the expression, to the next line.
173+
if ($tokens[($operator - 1)]['code'] === T_WHITESPACE) {
174+
$phpcsFile->fixer->replaceToken(($operator - 1), '');
175+
}
176+
177+
$first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $operator, true);
178+
$padding = str_repeat(' ', ($tokens[$first]['column'] - 1));
179+
$phpcsFile->fixer->addContentBefore($operator, $phpcsFile->eolChar.$padding);
180+
} else {
181+
// Move the operator to the start of the next line.
182+
if ($tokens[($operator - 1)]['code'] === T_WHITESPACE) {
183+
$phpcsFile->fixer->replaceToken(($operator - 1), '');
184+
}
185+
186+
$phpcsFile->fixer->addContentBefore($next, $tokens[$operator]['content'].' ');
187+
$phpcsFile->fixer->replaceToken($operator, '');
188+
}
189+
}//end if
190+
}//end if
191+
}//end foreach
192+
193+
$phpcsFile->fixer->endChangeset();
194+
195+
}//end process()
196+
197+
198+
}//end class
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
if (
3+
$expr1
4+
&& $expr2
5+
&& ($expr3
6+
|| $expr4)
7+
&& $expr5
8+
) {
9+
// if body
10+
} elseif (
11+
$expr1 &&
12+
($expr3 || $expr4)
13+
&& $expr5
14+
) {
15+
// elseif body
16+
} elseif (
17+
$expr1
18+
&& ($expr3 || $expr4) &&
19+
$expr5
20+
) {
21+
// elseif body
22+
}
23+
24+
if ($expr1 || $expr2) {
25+
}
26+
27+
do {
28+
} while (
29+
$expr1 || $expr2
30+
|| $expr3 ||
31+
$expr4
32+
);
33+
34+
switch (
35+
$expr1
36+
&& $expr2 &&
37+
$expr3 || $expr4 || $expr5 && $expr6 &&
38+
$expr7
39+
) {
40+
// structure body
41+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
if (
3+
$expr1
4+
&& $expr2
5+
&& ($expr3
6+
|| $expr4)
7+
&& $expr5
8+
) {
9+
// if body
10+
} elseif (
11+
$expr1 &&
12+
($expr3 ||
13+
$expr4) &&
14+
$expr5
15+
) {
16+
// elseif body
17+
} elseif (
18+
$expr1
19+
&& ($expr3
20+
|| $expr4)
21+
&& $expr5
22+
) {
23+
// elseif body
24+
}
25+
26+
if ($expr1 || $expr2) {
27+
}
28+
29+
do {
30+
} while (
31+
$expr1
32+
|| $expr2
33+
|| $expr3
34+
|| $expr4
35+
);
36+
37+
switch (
38+
$expr1
39+
&& $expr2
40+
&& $expr3
41+
|| $expr4
42+
|| $expr5
43+
&& $expr6
44+
&& $expr7
45+
) {
46+
// structure body
47+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
/**
3+
* Unit test class for the BooleanOperatorPlacement sniff.
4+
*
5+
* @author Greg Sherwood <gsherwood@squiz.net>
6+
* @copyright 2006-2019 Squiz Pty Ltd (ABN 77 084 670 600)
7+
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8+
*/
9+
10+
namespace PHP_CodeSniffer\Standards\PSR12\Tests\ControlStructures;
11+
12+
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
13+
14+
class BooleanOperatorPlacementUnitTest extends AbstractSniffUnitTest
15+
{
16+
17+
18+
/**
19+
* Returns the lines where errors should occur.
20+
*
21+
* The key of the array should represent the line number and the value
22+
* should represent the number of errors that should occur on that line.
23+
*
24+
* @return array<int, int>
25+
*/
26+
public function getErrorList()
27+
{
28+
return [
29+
10 => 1,
30+
12 => 1,
31+
16 => 1,
32+
18 => 1,
33+
28 => 1,
34+
29 => 1,
35+
34 => 1,
36+
37 => 3,
37+
];
38+
39+
}//end getErrorList()
40+
41+
42+
/**
43+
* Returns the lines where warnings should occur.
44+
*
45+
* The key of the array should represent the line number and the value
46+
* should represent the number of warnings that should occur on that line.
47+
*
48+
* @return array<int, int>
49+
*/
50+
public function getWarningList()
51+
{
52+
return [];
53+
54+
}//end getWarningList()
55+
56+
57+
}//end class

0 commit comments

Comments
 (0)