diff --git a/InpsydeTemplates/Sniffs/Formatting/AlternativeControlStructureSniff.php b/InpsydeTemplates/Sniffs/Formatting/AlternativeControlStructureSniff.php new file mode 100644 index 0000000..a097452 --- /dev/null +++ b/InpsydeTemplates/Sniffs/Formatting/AlternativeControlStructureSniff.php @@ -0,0 +1,162 @@ + + */ + public function register(): array + { + return [ + T_IF, + T_WHILE, + T_FOR, + T_FOREACH, + T_SWITCH, + ]; + } + + /** + * @param File $phpcsFile + * @param int $stackPtr + * + * phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration + */ + public function process(File $phpcsFile, $stackPtr): void + { + if (ControlStructures::hasBody($phpcsFile, $stackPtr) === false) { + // Single line control structure is out of scope. + return; + } + + /** @var array $tokens */ + $tokens = $phpcsFile->getTokens(); + /** @var int | null $scopeOpener */ + $openerPtr = $tokens[$stackPtr]['scope_opener'] ?? null; + /** @var int | null $scopeCloser */ + $closerPtr = $tokens[$stackPtr]['scope_closer'] ?? null; + + if (!isset($openerPtr, $closerPtr, $tokens[$openerPtr])) { + // Inline control structure or parse error. + return; + } + + if ($tokens[$openerPtr]['code'] === T_COLON) { + // Alternative control structure. + return; + } + + $chainedIssues = $this->findChainedIssues($phpcsFile, $stackPtr); + + $message = 'Control structure having inline HTML should use alternative syntax.' + . ' Found "%s".'; + foreach ($chainedIssues as $conditionPtr) { + $phpcsFile->addWarning( + $message, + $conditionPtr, + 'Encouraged', + [$tokens[$conditionPtr]['content']] + ); + } + } + + /** + * We consider if - else (else if) chain as the single structure + * as they should be replaced with alternative syntax altogether. + * + * @return list List of scope condition positions + */ + private function findChainedIssues(File $phpcsFile, int $stackPtr): array + { + /** @var array $tokens */ + $tokens = $phpcsFile->getTokens(); + $hasInlineHtml = false; + $currentPtr = $stackPtr; + $chainedIssues = []; + + do { + $openerPtr = $tokens[$currentPtr]['scope_opener'] ?? null; + $closerPtr = $tokens[$currentPtr]['scope_closer'] ?? null; + if (!isset($openerPtr, $closerPtr)) { + // Something went wrong. + break; + } + + $chainedIssues[] = $currentPtr; + if (!$hasInlineHtml) { + $hasInlineHtml = $phpcsFile->findNext(T_INLINE_HTML, ($currentPtr + 1), $closerPtr) !== false; + } + + $currentPtr = $this->findNextChainPointer($phpcsFile, $closerPtr); + } while ( + is_int($currentPtr) + ); + + return $hasInlineHtml ? $chainedIssues : []; + } + + /** + * Find 3 possible options: + * - else + * - elseif + * - else if + */ + private function findNextChainPointer(File $phpcsFile, int $closerPtr): ?int + { + /** @var array $tokens */ + $tokens = $phpcsFile->getTokens(); + $firstPtr = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($closerPtr + 1), + null, + true + ); + + if (!is_int($firstPtr) || !isset($tokens[$firstPtr])) { + return null; + } + + if ($tokens[$firstPtr]['code'] === T_ELSEIF) { + return $firstPtr; + } + + if ($tokens[$firstPtr]['code'] !== T_ELSE) { + return null; + } + + $secondPtr = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($firstPtr + 1), + null, + true + ); + + $isIfOpenerPtr = is_int($secondPtr) && isset($tokens[$secondPtr]) && $tokens[$secondPtr]['code'] === T_IF; + + return $isIfOpenerPtr ? $secondPtr : $firstPtr; + } +} diff --git a/InpsydeTemplates/Sniffs/Formatting/ShortEchoTagSniff.php b/InpsydeTemplates/Sniffs/Formatting/ShortEchoTagSniff.php new file mode 100644 index 0000000..f3c4f71 --- /dev/null +++ b/InpsydeTemplates/Sniffs/Formatting/ShortEchoTagSniff.php @@ -0,0 +1,99 @@ + + */ + public function register(): array + { + return [ + T_ECHO, + ]; + } + + /** + * @param File $phpcsFile + * @param int $stackPtr + * + * phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration + */ + public function process(File $phpcsFile, $stackPtr): void + { + // phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration + + /** @var array $tokens */ + $tokens = $phpcsFile->getTokens(); + + $prevPtr = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + ($stackPtr - 1), + null, + true + ); + + if (!is_int($prevPtr) || !isset($tokens[$prevPtr])) { + return; + } + + $prevToken = $tokens[$prevPtr]; + $currentLine = $tokens[$stackPtr]['line']; + + if ($prevToken['line'] !== $currentLine) { + return; + } + + if ($prevToken['code'] !== T_OPEN_TAG) { + return; + } + + $closeTagPtr = $phpcsFile->findNext( + T_CLOSE_TAG, + ($stackPtr + 1), + ); + + if ( + !is_int($closeTagPtr) + || !isset($tokens[$closeTagPtr]) + || $tokens[$closeTagPtr]['line'] !== $currentLine + ) { + return; + } + + $message = sprintf( + 'Single line output on line %d' + . ' should use short echo tag `addFixableWarning($message, $stackPtr, 'Encouraged')) { + $this->fix($prevPtr, $stackPtr, $phpcsFile); + } + } + + private function fix(int $openTagPtr, int $echoPtr, File $file): void + { + $fixer = $file->fixer; + $fixer->beginChangeset(); + + $fixer->replaceToken($echoPtr, ''); + $fixer->replaceToken($openTagPtr, 'endChangeset(); + } +} diff --git a/README.md b/README.md index 9b5ffcf..0349004 100644 --- a/README.md +++ b/README.md @@ -173,9 +173,11 @@ The recommended way to use the `InpsydeTemplates` ruleset is as follows: The following template-specific rules are available: -| Sniff Name | Description | Has Config | Auto-Fixable | -|:--------------------|:--------------------------------------------------|:----------:|:------------:| -| `TrailingSemicolon` | Remove trailing semicolon before closing PHP tag. | | ✓ | +| Sniff Name | Description | Has Config | Auto-Fixable | +|:------------------------------|:------------------------------------------------------------|:----------:|:------------:| +| `AlternativeControlStructure` | Encourage usage of alternative syntax with inline HTML. | | | +| `ShortEchoTag` | Replace echo with short echo tag in single-line statements. | | ✓ | +| `TrailingSemicolon` | Remove trailing semicolon before closing PHP tag. | | ✓ | # Removing or Disabling Rules diff --git a/tests/unit/fixtures/alternative-structure.php b/tests/unit/fixtures/alternative-structure.php new file mode 100644 index 0000000..33a6713 --- /dev/null +++ b/tests/unit/fixtures/alternative-structure.php @@ -0,0 +1,108 @@ + + + +
Maybe.
+ +
No. Yes.
+ + + +
Yes.
+ + + +
+ +
+ +
YES
+ + + + + + + + + + + + +