Skip to content

Commit 8704dd1

Browse files
authored
Merge pull request #97 from inpsyde/template-sniffs
[Template] AlternativeControlStructure and ShortEchoTag sniffs
2 parents b883fe6 + d2fb3b3 commit 8704dd1

File tree

5 files changed

+402
-3
lines changed

5 files changed

+402
-3
lines changed
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.ControlStructures.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+
}
+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
11+
/**
12+
* @psalm-type Token = array{
13+
* type: string,
14+
* code: string|int,
15+
* line: int
16+
* }
17+
*/
18+
final class ShortEchoTagSniff implements Sniff
19+
{
20+
/**
21+
* @return list<int|string>
22+
*/
23+
public function register(): array
24+
{
25+
return [
26+
T_ECHO,
27+
];
28+
}
29+
30+
/**
31+
* @param File $phpcsFile
32+
* @param int $stackPtr
33+
*
34+
* phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration
35+
*/
36+
public function process(File $phpcsFile, $stackPtr): void
37+
{
38+
// phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration
39+
40+
/** @var array<int, Token> $tokens */
41+
$tokens = $phpcsFile->getTokens();
42+
43+
$prevPtr = $phpcsFile->findPrevious(
44+
Tokens::$emptyTokens,
45+
($stackPtr - 1),
46+
null,
47+
true
48+
);
49+
50+
if (!is_int($prevPtr) || !isset($tokens[$prevPtr])) {
51+
return;
52+
}
53+
54+
$prevToken = $tokens[$prevPtr];
55+
$currentLine = $tokens[$stackPtr]['line'];
56+
57+
if ($prevToken['line'] !== $currentLine) {
58+
return;
59+
}
60+
61+
if ($prevToken['code'] !== T_OPEN_TAG) {
62+
return;
63+
}
64+
65+
$closeTagPtr = $phpcsFile->findNext(
66+
T_CLOSE_TAG,
67+
($stackPtr + 1),
68+
);
69+
70+
if (
71+
!is_int($closeTagPtr)
72+
|| !isset($tokens[$closeTagPtr])
73+
|| $tokens[$closeTagPtr]['line'] !== $currentLine
74+
) {
75+
return;
76+
}
77+
78+
$message = sprintf(
79+
'Single line output on line %d'
80+
. ' should use short echo tag `<?= ` instead of `<?php echo`.',
81+
$currentLine
82+
);
83+
84+
if ($phpcsFile->addFixableWarning($message, $stackPtr, 'Encouraged')) {
85+
$this->fix($prevPtr, $stackPtr, $phpcsFile);
86+
}
87+
}
88+
89+
private function fix(int $openTagPtr, int $echoPtr, File $file): void
90+
{
91+
$fixer = $file->fixer;
92+
$fixer->beginChangeset();
93+
94+
$fixer->replaceToken($echoPtr, '');
95+
$fixer->replaceToken($openTagPtr, '<?=');
96+
97+
$fixer->endChangeset();
98+
}
99+
}

Diff for: README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,11 @@ The recommended way to use the `InpsydeTemplates` ruleset is as follows:
173173

174174
The following template-specific rules are available:
175175

176-
| Sniff Name | Description | Has Config | Auto-Fixable |
177-
|:--------------------|:--------------------------------------------------|:----------:|:------------:|
178-
| `TrailingSemicolon` | Remove trailing semicolon before closing PHP tag. | ||
176+
| Sniff Name | Description | Has Config | Auto-Fixable |
177+
|:------------------------------|:------------------------------------------------------------|:----------:|:------------:|
178+
| `AlternativeControlStructure` | Encourage usage of alternative syntax with inline HTML. | | |
179+
| `ShortEchoTag` | Replace echo with short echo tag in single-line statements. | ||
180+
| `TrailingSemicolon` | Remove trailing semicolon before closing PHP tag. | ||
179181

180182
# Removing or Disabling Rules
181183

Diff for: tests/unit/fixtures/alternative-structure.php

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
while ($flag !== 'YES') { // @phpcsWarningOnThisLine
70+
$flag = 'YES';
71+
?>
72+
<div>No. Yes.</div>
73+
<?php }
74+
} else if ($flag === 'YES') { // @phpcsWarningOnThisLine
75+
echo 'yes';
76+
} else { // @phpcsWarningOnThisLine
77+
echo 'Non Empty value';
78+
} ?>
79+
80+
<?php if ($flag === 'MAYBE') { // @phpcsWarningOnThisLine
81+
return;
82+
} else if ($flag === 'NO') { // @phpcsWarningOnThisLine
83+
echo 'no';
84+
} elseif ($flag === 'YES') { // @phpcsWarningOnThisLine ?>
85+
<div>Yes.</div>
86+
<?php } else { // @phpcsWarningOnThisLine
87+
echo 'Non Empty value';
88+
} ?>
89+
90+
<?php
91+
for ($i = 1; $i <= 10; $i++) { // @phpcsWarningOnThisLine ?>
92+
<div><?= $i ?></div>
93+
<?php }
94+
95+
foreach ($arrayOfFlags as $item) { // @phpcsWarningOnThisLine ?>
96+
<div><?= $item ?></div>
97+
<?php }
98+
99+
switch ($flag) { // @phpcsWarningOnThisLine
100+
case 'YES':
101+
?>
102+
<div>YES</div>
103+
<?php
104+
break;
105+
case 'NO':
106+
echo 'It is false';
107+
break;
108+
}

0 commit comments

Comments
 (0)