Skip to content

Commit dacb6d6

Browse files
committed
feat: add sniff to enforce alternative syntax for control structures having inline HTML
1 parent b883fe6 commit dacb6d6

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace InpsydeTemplates\Sniffs\Formatting;
6+
7+
use PHP_CodeSniffer\Files\File;
8+
use PHP_CodeSniffer\Sniffs\Sniff;
9+
use PHP_CodeSniffer\Util\Tokens;
10+
use PHPCSUtils\Utils\ControlStructures;
11+
12+
/**
13+
* The implementation is inspired by Universal.DisallowAlternativeSyntaxSniff.
14+
*
15+
* @link https://github.com/PHPCSStandards/PHPCSExtra/blob/ed86bb117c340f654eab603a06b95a437ac619c9/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php
16+
*
17+
* @psalm-type Token = array{
18+
* type: string,
19+
* code: string|int,
20+
* line: int,
21+
* scope_opener?: int,
22+
* scope_closer?: int,
23+
* scope_condition?: int,
24+
* content: string,
25+
* }
26+
*/
27+
final class AlternativeControlStructureSniff implements Sniff
28+
{
29+
/**
30+
* @return list<int|string>
31+
*/
32+
public function register(): array
33+
{
34+
return [
35+
T_IF,
36+
T_WHILE,
37+
T_FOR,
38+
T_FOREACH,
39+
T_SWITCH,
40+
];
41+
}
42+
43+
/**
44+
* @param File $phpcsFile
45+
* @param int $stackPtr
46+
*
47+
* phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration
48+
*/
49+
public function process(File $phpcsFile, $stackPtr): void
50+
{
51+
if (ControlStructures::hasBody($phpcsFile, $stackPtr) === false) {
52+
// Single line control structure is out of scope.
53+
return;
54+
}
55+
56+
/** @var array<int, Token> $tokens */
57+
$tokens = $phpcsFile->getTokens();
58+
/** @var int | null $scopeOpener */
59+
$openerPtr = $tokens[$stackPtr]['scope_opener'] ?? null;
60+
/** @var int | null $scopeCloser */
61+
$closerPtr = $tokens[$stackPtr]['scope_closer'] ?? null;
62+
63+
if (!isset($openerPtr, $closerPtr, $tokens[$openerPtr])) {
64+
// Inline control structure or parse error.
65+
return;
66+
}
67+
68+
if ($tokens[$openerPtr]['code'] === T_COLON) {
69+
// Alternative control structure.
70+
return;
71+
}
72+
73+
$chainedIssues = $this->findChainedIssues($phpcsFile, $stackPtr);
74+
75+
$message = 'Control structure having inline HTML should use alternative syntax.'
76+
. ' Found "%s".';
77+
foreach ($chainedIssues as $conditionPtr) {
78+
$phpcsFile->addWarning(
79+
$message,
80+
$conditionPtr,
81+
'Encouraged',
82+
[$tokens[$conditionPtr]['content']]
83+
);
84+
}
85+
}
86+
87+
/**
88+
* We consider if - else (else if) chain as the single structure
89+
* as they should be replaced with alternative syntax altogether.
90+
*
91+
* @return list<int> List of scope condition positions
92+
*/
93+
private function findChainedIssues(File $phpcsFile, int $stackPtr): array
94+
{
95+
/** @var array<int, Token> $tokens */
96+
$tokens = $phpcsFile->getTokens();
97+
$hasInlineHtml = false;
98+
$currentPtr = $stackPtr;
99+
$chainedIssues = [];
100+
101+
do {
102+
$openerPtr = $tokens[$currentPtr]['scope_opener'] ?? null;
103+
$closerPtr = $tokens[$currentPtr]['scope_closer'] ?? null;
104+
if (!isset($openerPtr, $closerPtr)) {
105+
// Something went wrong.
106+
break;
107+
}
108+
109+
$chainedIssues[] = $currentPtr;
110+
if (!$hasInlineHtml) {
111+
$hasInlineHtml = $phpcsFile->findNext(T_INLINE_HTML, ($currentPtr + 1), $closerPtr) !== false;
112+
}
113+
114+
$currentPtr = $this->findNextChainPointer($phpcsFile, $closerPtr);
115+
} while (
116+
is_int($currentPtr)
117+
);
118+
119+
return $hasInlineHtml ? $chainedIssues : [];
120+
}
121+
122+
/**
123+
* Find 3 possible options:
124+
* - else
125+
* - elseif
126+
* - else if
127+
*/
128+
private function findNextChainPointer(File $phpcsFile, int $closerPtr): ?int
129+
{
130+
/** @var array<int, Token> $tokens */
131+
$tokens = $phpcsFile->getTokens();
132+
$firstPtr = $phpcsFile->findNext(
133+
Tokens::$emptyTokens,
134+
($closerPtr + 1),
135+
null,
136+
true
137+
);
138+
139+
if (!is_int($firstPtr) || !isset($tokens[$firstPtr])) {
140+
return null;
141+
}
142+
143+
if ($tokens[$firstPtr]['code'] === T_ELSEIF) {
144+
return $firstPtr;
145+
}
146+
147+
if ($tokens[$firstPtr]['code'] !== T_ELSE) {
148+
return null;
149+
}
150+
151+
$secondPtr = $phpcsFile->findNext(
152+
Tokens::$emptyTokens,
153+
($firstPtr + 1),
154+
null,
155+
true
156+
);
157+
158+
$isIfOpenerPtr = is_int($secondPtr) && isset($tokens[$secondPtr]) && $tokens[$secondPtr]['code'] === T_IF;
159+
160+
return $isIfOpenerPtr ? $secondPtr : $firstPtr;
161+
}
162+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
// @phpcsSniff InpsydeTemplates.Formatting.AlternativeControlStructure
6+
7+
const FLAGS = [
8+
'YES',
9+
'NO',
10+
'MAYBE',
11+
];
12+
13+
$flag = FLAGS[rand(0, 2)];
14+
15+
if ($flag === 'MAYBE') {
16+
echo 'maybe';
17+
18+
while ($flag !== 'YES') {
19+
$flag = 'YES';
20+
}
21+
} elseif ($flag === 'NO') {
22+
echo 'no';
23+
} else if ($flag === 'YES') {
24+
echo 'yes';
25+
} else {
26+
echo 'Non Empty value';
27+
}
28+
29+
if ($flag === 'MAYBE') :
30+
echo 'maybe';
31+
32+
while ($flag !== 'YES') {
33+
$flag = 'YES';
34+
}
35+
elseif ($flag === 'NO') :
36+
echo 'no';
37+
else :
38+
echo 'Non Empty value';
39+
endif;
40+
41+
42+
$arrayOfFlags = [];
43+
for ($i = 1; $i <= 10; $i++) {
44+
$arrayOfFlags[] = FLAGS[rand(0, 2)];
45+
}
46+
47+
foreach ($arrayOfFlags as &$item) {
48+
$item = false;
49+
}
50+
unset($item);
51+
52+
switch ($flag) {
53+
case 'YES':
54+
echo 'It is true';
55+
break;
56+
case 'NO':
57+
echo 'It is false';
58+
break;
59+
}
60+
61+
?>
62+
63+
<?php if ($flag === 'MAYBE') { // @phpcsWarningOnThisLine ?>
64+
<div>Maybe.</div>
65+
<?php while ($flag !== 'YES') {
66+
$flag = 'YES';
67+
}
68+
} elseif ($flag === 'NO') { // @phpcsWarningOnThisLine
69+
echo 'no';
70+
} else if ($flag === 'YES') { // @phpcsWarningOnThisLine
71+
echo 'yes';
72+
} else { // @phpcsWarningOnThisLine
73+
echo 'Non Empty value';
74+
} ?>
75+
76+
<?php if ($flag === 'MAYBE') { // @phpcsWarningOnThisLine
77+
return;
78+
} else if ($flag === 'NO') { // @phpcsWarningOnThisLine
79+
echo 'no';
80+
} elseif ($flag === 'YES') { // @phpcsWarningOnThisLine ?>
81+
<div>Yes.</div>
82+
<?php } else { // @phpcsWarningOnThisLine
83+
echo 'Non Empty value';
84+
} ?>
85+
86+
<?php
87+
for ($i = 1; $i <= 10; $i++) { // @phpcsWarningOnThisLine ?>
88+
<div><?= $i ?></div>
89+
<?php }
90+
91+
foreach ($arrayOfFlags as $item) { // @phpcsWarningOnThisLine ?>
92+
<div><?= $item ?></div>
93+
<?php }
94+
95+
switch ($flag) { // @phpcsWarningOnThisLine
96+
case 'YES':
97+
?>
98+
<div>YES</div>
99+
<?php
100+
break;
101+
case 'NO':
102+
echo 'It is false';
103+
break;
104+
}

0 commit comments

Comments
 (0)