clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,510 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use Exception;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\CommentHelper;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use Throwable;
use function array_key_exists;
use function array_map;
use function array_values;
use function count;
use function in_array;
use function sprintf;
use function strlen;
use function substr;
use function substr_count;
use const T_ANON_CLASS;
use const T_BREAK;
use const T_CASE;
use const T_CATCH;
use const T_CLOSE_CURLY_BRACKET;
use const T_CLOSURE;
use const T_COLON;
use const T_CONTINUE;
use const T_DEFAULT;
use const T_DO;
use const T_ELSE;
use const T_ELSEIF;
use const T_FINALLY;
use const T_FN;
use const T_FOR;
use const T_FOREACH;
use const T_GOTO;
use const T_IF;
use const T_OPEN_CURLY_BRACKET;
use const T_OPEN_SHORT_ARRAY;
use const T_OPEN_TAG;
use const T_PARENT;
use const T_RETURN;
use const T_SEMICOLON;
use const T_SWITCH;
use const T_THROW;
use const T_TRY;
use const T_WHILE;
use const T_WHITESPACE;
use const T_YIELD;
use const T_YIELD_FROM;
/**
* @internal
*/
abstract class AbstractControlStructureSpacing implements Sniff
{
public const CODE_INCORRECT_LINES_COUNT_BEFORE_CONTROL_STRUCTURE = 'IncorrectLinesCountBeforeControlStructure';
public const CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_CONTROL_STRUCTURE = 'IncorrectLinesCountBeforeFirstControlStructure';
public const CODE_INCORRECT_LINES_COUNT_AFTER_CONTROL_STRUCTURE = 'IncorrectLinesCountAfterControlStructure';
public const CODE_INCORRECT_LINES_COUNT_AFTER_LAST_CONTROL_STRUCTURE = 'IncorrectLinesCountAfterLastControlStructure';
protected const KEYWORD_IF = 'if';
protected const KEYWORD_DO = 'do';
protected const KEYWORD_WHILE = 'while';
protected const KEYWORD_FOR = 'for';
protected const KEYWORD_FOREACH = 'foreach';
protected const KEYWORD_SWITCH = 'switch';
protected const KEYWORD_CASE = 'case';
protected const KEYWORD_DEFAULT = 'default';
protected const KEYWORD_TRY = 'try';
protected const KEYWORD_PARENT = 'parent';
protected const KEYWORD_GOTO = 'goto';
protected const KEYWORD_BREAK = 'break';
protected const KEYWORD_CONTINUE = 'continue';
protected const KEYWORD_RETURN = 'return';
protected const KEYWORD_THROW = 'throw';
protected const KEYWORD_YIELD = 'yield';
protected const KEYWORD_YIELD_FROM = 'yield_from';
/** @var array<(string|int)>|null */
private $tokensToCheck;
/**
* @return list<string>
*/
abstract protected function getSupportedKeywords(): array;
/**
* @return list<string>
*/
abstract protected function getKeywordsToCheck(): array;
abstract protected function getLinesCountBefore(): int;
abstract protected function getLinesCountBeforeFirst(File $phpcsFile, int $controlStructurePointer): int;
abstract protected function getLinesCountAfter(): int;
abstract protected function getLinesCountAfterLast(File $phpcsFile, int $controlStructurePointer, int $controlStructureEndPointer): int;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return $this->getTokensToCheck();
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $controlStructurePointer
*/
public function process(File $phpcsFile, $controlStructurePointer): void
{
$this->checkLinesBefore($phpcsFile, $controlStructurePointer);
try {
$this->checkLinesAfter($phpcsFile, $controlStructurePointer);
} catch (Throwable $e) {
// Unsupported syntax without curly braces.
return;
}
}
protected function checkLinesBefore(File $phpcsFile, int $controlStructurePointer): void
{
$tokens = $phpcsFile->getTokens();
if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) {
$pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $controlStructurePointer - 1);
if ($tokens[$pointerBefore]['code'] === T_COLON) {
return;
}
}
$nonWhitespacePointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $controlStructurePointer - 1);
$controlStructureStartPointer = $controlStructurePointer;
$pointerBefore = $nonWhitespacePointerBefore;
$pointerToCheckFirst = $pointerBefore;
if (in_array($tokens[$nonWhitespacePointerBefore]['code'], Tokens::$commentTokens, true)) {
$effectivePointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $pointerBefore - 1);
if ($tokens[$effectivePointerBefore]['line'] === $tokens[$nonWhitespacePointerBefore]['line']) {
$pointerToCheckFirst = $effectivePointerBefore;
} elseif ($tokens[$nonWhitespacePointerBefore]['line'] + 1 === $tokens[$controlStructurePointer]['line']) {
if ($tokens[$effectivePointerBefore]['line'] !== $tokens[$nonWhitespacePointerBefore]['line']) {
$controlStructureStartPointer = array_key_exists('comment_opener', $tokens[$nonWhitespacePointerBefore])
? $tokens[$nonWhitespacePointerBefore]['comment_opener']
: CommentHelper::getMultilineCommentStartPointer($phpcsFile, $nonWhitespacePointerBefore);
$pointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $controlStructureStartPointer - 1);
}
$pointerToCheckFirst = $pointerBefore;
}
}
$isFirstControlStructure = in_array($tokens[$pointerToCheckFirst]['code'], [T_OPEN_CURLY_BRACKET, T_COLON], true);
$whitespaceBefore = '';
if ($tokens[$pointerBefore]['code'] === T_OPEN_TAG) {
$whitespaceBefore .= substr($tokens[$pointerBefore]['content'], strlen('<?php'));
}
$hasCommentWithLineEndBefore = in_array($tokens[$pointerBefore]['code'], TokenHelper::$inlineCommentTokenCodes, true)
&& substr($tokens[$pointerBefore]['content'], -strlen($phpcsFile->eolChar)) === $phpcsFile->eolChar;
if ($hasCommentWithLineEndBefore) {
$whitespaceBefore .= $phpcsFile->eolChar;
}
if ($pointerBefore + 1 !== $controlStructurePointer) {
$whitespaceBefore .= TokenHelper::getContent($phpcsFile, $pointerBefore + 1, $controlStructureStartPointer - 1);
}
$requiredLinesCountBefore = $isFirstControlStructure
? $this->getLinesCountBeforeFirst($phpcsFile, $controlStructurePointer)
: $this->getLinesCountBefore();
$actualLinesCountBefore = substr_count($whitespaceBefore, $phpcsFile->eolChar) - 1;
if ($requiredLinesCountBefore === $actualLinesCountBefore) {
return;
}
$fix = $phpcsFile->addFixableError(
sprintf(
'Expected %d line%s before "%s", found %d.',
$requiredLinesCountBefore,
$requiredLinesCountBefore === 1 ? '' : 's',
$tokens[$controlStructurePointer]['content'],
$actualLinesCountBefore
),
$controlStructurePointer,
$isFirstControlStructure
? self::CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_CONTROL_STRUCTURE
: self::CODE_INCORRECT_LINES_COUNT_BEFORE_CONTROL_STRUCTURE
);
if (!$fix) {
return;
}
$endOfLineBeforePointer = TokenHelper::findPreviousContent(
$phpcsFile,
T_WHITESPACE,
$phpcsFile->eolChar,
$controlStructureStartPointer - 1
);
$phpcsFile->fixer->beginChangeset();
if ($tokens[$pointerBefore]['code'] === T_OPEN_TAG) {
$phpcsFile->fixer->replaceToken($pointerBefore, '<?php');
}
if ($endOfLineBeforePointer !== null) {
FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBefore + 1, $endOfLineBeforePointer);
}
$linesToAdd = $hasCommentWithLineEndBefore ? $requiredLinesCountBefore - 1 : $requiredLinesCountBefore;
for ($i = 0; $i <= $linesToAdd; $i++) {
$phpcsFile->fixer->addNewline($pointerBefore);
}
$phpcsFile->fixer->endChangeset();
}
protected function checkLinesAfter(File $phpcsFile, int $controlStructurePointer): void
{
$tokens = $phpcsFile->getTokens();
if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) {
$colonPointer = TokenHelper::findNext($phpcsFile, T_COLON, $controlStructurePointer + 1);
$pointerAfterColon = TokenHelper::findNextEffective($phpcsFile, $colonPointer + 1);
if (in_array($tokens[$pointerAfterColon]['code'], [T_CASE, T_DEFAULT], true)) {
return;
}
}
$controlStructureEndPointer = $this->findControlStructureEnd($phpcsFile, $controlStructurePointer);
$pointerAfterControlStructureEnd = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1);
if (
$pointerAfterControlStructureEnd !== null
&& $tokens[$pointerAfterControlStructureEnd]['code'] === T_SEMICOLON
) {
$controlStructureEndPointer = $pointerAfterControlStructureEnd;
}
$notWhitespacePointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $controlStructureEndPointer + 1);
if ($notWhitespacePointerAfter === null) {
return;
}
$hasCommentAfter = in_array($tokens[$notWhitespacePointerAfter]['code'], Tokens::$commentTokens, true);
$isCommentAfterOnSameLine = false;
$pointerAfter = $notWhitespacePointerAfter;
$isControlStructureEndAfterPointer = static function (int $pointer) use ($tokens, $controlStructurePointer): bool {
return in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)
? $tokens[$pointer]['code'] === T_CLOSE_CURLY_BRACKET
: in_array($tokens[$pointer]['code'], [T_CLOSE_CURLY_BRACKET, T_CASE, T_DEFAULT], true);
};
if ($hasCommentAfter) {
if ($tokens[$notWhitespacePointerAfter]['line'] === $tokens[$controlStructureEndPointer]['line'] + 1) {
$commentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $notWhitespacePointerAfter);
$pointerAfterComment = TokenHelper::findNextNonWhitespace($phpcsFile, $commentEndPointer + 1);
if ($isControlStructureEndAfterPointer($pointerAfterComment)) {
$controlStructureEndPointer = $commentEndPointer;
$pointerAfter = $pointerAfterComment;
}
} elseif ($tokens[$notWhitespacePointerAfter]['line'] === $tokens[$controlStructureEndPointer]['line']) {
$isCommentAfterOnSameLine = true;
$pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $notWhitespacePointerAfter + 1);
}
}
$isLastControlStructure = $isControlStructureEndAfterPointer($pointerAfter);
$requiredLinesCountAfter = $isLastControlStructure
? $this->getLinesCountAfterLast($phpcsFile, $controlStructurePointer, $controlStructureEndPointer)
: $this->getLinesCountAfter();
$actualLinesCountAfter = $tokens[$pointerAfter]['line'] - $tokens[$controlStructureEndPointer]['line'] - 1;
if ($requiredLinesCountAfter === $actualLinesCountAfter) {
return;
}
$fix = $phpcsFile->addFixableError(
sprintf(
'Expected %d line%s after "%s", found %d.',
$requiredLinesCountAfter,
$requiredLinesCountAfter === 1 ? '' : 's',
$tokens[$controlStructurePointer]['content'],
$actualLinesCountAfter
),
$controlStructurePointer,
$isLastControlStructure
? self::CODE_INCORRECT_LINES_COUNT_AFTER_LAST_CONTROL_STRUCTURE
: self::CODE_INCORRECT_LINES_COUNT_AFTER_CONTROL_STRUCTURE
);
if (!$fix) {
return;
}
$replaceStartPointer = $isCommentAfterOnSameLine ? $notWhitespacePointerAfter : $controlStructureEndPointer;
$endOfLineBeforeAfterPointer = TokenHelper::findLastTokenOnPreviousLine($phpcsFile, $pointerAfter);
$phpcsFile->fixer->beginChangeset();
FixerHelper::removeBetweenIncluding($phpcsFile, $replaceStartPointer + 1, $endOfLineBeforeAfterPointer);
if ($isCommentAfterOnSameLine) {
for ($i = 0; $i < $requiredLinesCountAfter; $i++) {
$phpcsFile->fixer->addNewline($notWhitespacePointerAfter);
}
} else {
$linesToAdd = substr($tokens[$controlStructureEndPointer]['content'], -strlen($phpcsFile->eolChar)) === $phpcsFile->eolChar
? $requiredLinesCountAfter - 1
: $requiredLinesCountAfter;
for ($i = 0; $i <= $linesToAdd; $i++) {
$phpcsFile->fixer->addNewline($controlStructureEndPointer);
}
}
$phpcsFile->fixer->endChangeset();
}
/**
* @return array<int, (int|string)>
*/
private function getTokensToCheck(): array
{
if ($this->tokensToCheck === null) {
$supportedKeywords = $this->getSupportedKeywords();
$supportedTokens = [
self::KEYWORD_IF => T_IF,
self::KEYWORD_DO => T_DO,
self::KEYWORD_WHILE => T_WHILE,
self::KEYWORD_FOR => T_FOR,
self::KEYWORD_FOREACH => T_FOREACH,
self::KEYWORD_SWITCH => T_SWITCH,
self::KEYWORD_CASE => T_CASE,
self::KEYWORD_DEFAULT => T_DEFAULT,
self::KEYWORD_TRY => T_TRY,
self::KEYWORD_PARENT => T_PARENT,
self::KEYWORD_GOTO => T_GOTO,
self::KEYWORD_BREAK => T_BREAK,
self::KEYWORD_CONTINUE => T_CONTINUE,
self::KEYWORD_RETURN => T_RETURN,
self::KEYWORD_THROW => T_THROW,
self::KEYWORD_YIELD => T_YIELD,
self::KEYWORD_YIELD_FROM => T_YIELD_FROM,
];
$this->tokensToCheck = array_values(array_map(
static function (string $keyword) use ($supportedKeywords, $supportedTokens) {
if (!in_array($keyword, $supportedKeywords, true)) {
throw new UnsupportedKeywordException($keyword);
}
return $supportedTokens[$keyword];
},
SniffSettingsHelper::normalizeArray($this->getKeywordsToCheck())
));
if (count($this->tokensToCheck) === 0) {
$this->tokensToCheck = array_map(static function (string $keyword) use ($supportedTokens) {
return $supportedTokens[$keyword];
}, $supportedKeywords);
}
}
return $this->tokensToCheck;
}
private function findControlStructureEnd(File $phpcsFile, int $controlStructurePointer): int
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$controlStructurePointer]['code'] === T_IF) {
if (!array_key_exists('scope_closer', $tokens[$controlStructurePointer])) {
throw new Exception('"if" without curly braces is not supported.');
}
$pointerAfterParenthesisCloser = TokenHelper::findNextEffective(
$phpcsFile,
$tokens[$controlStructurePointer]['parenthesis_closer'] + 1
);
if ($pointerAfterParenthesisCloser !== null && $tokens[$pointerAfterParenthesisCloser]['code'] === T_COLON) {
throw new Exception('"if" without curly braces is not supported.');
}
$controlStructureEndPointer = $tokens[$controlStructurePointer]['scope_closer'];
do {
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1);
if ($nextPointer === null) {
return $controlStructureEndPointer;
}
if ($tokens[$nextPointer]['code'] === T_ELSE) {
if (!array_key_exists('scope_closer', $tokens[$nextPointer])) {
throw new Exception('"else" without curly braces is not supported.');
}
return $tokens[$nextPointer]['scope_closer'];
}
if ($tokens[$nextPointer]['code'] !== T_ELSEIF) {
return $controlStructureEndPointer;
}
$controlStructureEndPointer = $tokens[$nextPointer]['scope_closer'];
} while (true);
}
if ($tokens[$controlStructurePointer]['code'] === T_DO) {
$whilePointer = TokenHelper::findNext($phpcsFile, T_WHILE, $tokens[$controlStructurePointer]['scope_closer'] + 1);
return (int) TokenHelper::findNext($phpcsFile, T_SEMICOLON, $tokens[$whilePointer]['parenthesis_closer'] + 1);
}
if ($tokens[$controlStructurePointer]['code'] === T_TRY) {
$controlStructureEndPointer = $tokens[$controlStructurePointer]['scope_closer'];
do {
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1);
if ($nextPointer === null) {
return $controlStructureEndPointer;
}
if (!in_array($tokens[$nextPointer]['code'], [T_CATCH, T_FINALLY], true)) {
return $controlStructureEndPointer;
}
$controlStructureEndPointer = $tokens[$nextPointer]['scope_closer'];
} while (true);
}
if (in_array($tokens[$controlStructurePointer]['code'], [T_WHILE, T_FOR, T_FOREACH, T_SWITCH], true)) {
return $tokens[$controlStructurePointer]['scope_closer'];
}
if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) {
$switchPointer = TokenHelper::findPrevious($phpcsFile, T_SWITCH, $controlStructurePointer - 1);
$pointers = TokenHelper::findNextAll(
$phpcsFile,
[T_CASE, T_DEFAULT],
$controlStructurePointer + 1,
$tokens[$switchPointer]['scope_closer']
);
foreach ($pointers as $pointer) {
if (TokenHelper::findPrevious($phpcsFile, T_SWITCH, $pointer - 1) === $switchPointer) {
$pointerBeforeCaseOrDefault = TokenHelper::findPreviousNonWhitespace($phpcsFile, $pointer - 1);
if (
in_array($tokens[$pointerBeforeCaseOrDefault]['code'], Tokens::$commentTokens, true)
&& $tokens[$pointerBeforeCaseOrDefault]['line'] + 1 === $tokens[$pointer]['line']
) {
$pointerBeforeCaseOrDefault = TokenHelper::findPreviousExcluding(
$phpcsFile,
T_WHITESPACE,
$pointerBeforeCaseOrDefault - 1
);
}
return $pointerBeforeCaseOrDefault;
}
}
return TokenHelper::findPreviousNonWhitespace($phpcsFile, $tokens[$switchPointer]['scope_closer'] - 1);
}
$nextPointer = TokenHelper::findNext(
$phpcsFile,
[T_SEMICOLON, T_ANON_CLASS, T_CLOSURE, T_FN, T_OPEN_SHORT_ARRAY],
$controlStructurePointer + 1
);
if ($tokens[$nextPointer]['code'] === T_SEMICOLON) {
return $nextPointer;
}
$scopeCloserPointer = $tokens[$nextPointer]['code'] === T_OPEN_SHORT_ARRAY
? $tokens[$nextPointer]['bracket_closer']
: $tokens[$nextPointer]['scope_closer'];
if ($tokens[$scopeCloserPointer]['code'] === T_SEMICOLON) {
return $scopeCloserPointer;
}
$nextPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $scopeCloserPointer + 1);
$level = $tokens[$controlStructurePointer]['level'];
while ($level !== $tokens[$nextPointer]['level']) {
$nextPointer = (int) TokenHelper::findNext($phpcsFile, T_SEMICOLON, $nextPointer + 1);
}
return $nextPointer;
}
}

View File

@@ -0,0 +1,128 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\IndentationHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_key_exists;
use function in_array;
use function preg_replace;
use function rtrim;
use function sprintf;
use function trim;
use const T_ELSEIF;
use const T_IF;
use const T_OPEN_CURLY_BRACKET;
use const T_WHILE;
abstract class AbstractLineCondition implements Sniff
{
protected const IF_CONTROL_STRUCTURE = 'if';
protected const WHILE_CONTROL_STRUCTURE = 'while';
protected const DO_CONTROL_STRUCTURE = 'do';
/** @var list<string> */
public $checkedControlStructures = [
self::IF_CONTROL_STRUCTURE,
self::WHILE_CONTROL_STRUCTURE,
self::DO_CONTROL_STRUCTURE,
];
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
$this->checkedControlStructures = SniffSettingsHelper::normalizeArray($this->checkedControlStructures);
$register = [];
if (in_array(self::IF_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) {
$register[] = T_IF;
$register[] = T_ELSEIF;
}
if (in_array(self::WHILE_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) {
$register[] = T_WHILE;
}
if (in_array(self::DO_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) {
$register[] = T_WHILE;
}
return $register;
}
protected function shouldBeSkipped(File $phpcsFile, int $controlStructurePointer): bool
{
$tokens = $phpcsFile->getTokens();
if (
!array_key_exists('parenthesis_opener', $tokens[$controlStructurePointer])
|| $tokens[$controlStructurePointer]['parenthesis_opener'] === null
|| !array_key_exists('parenthesis_closer', $tokens[$controlStructurePointer])
|| $tokens[$controlStructurePointer]['parenthesis_closer'] === null
) {
return true;
}
if ($tokens[$controlStructurePointer]['code'] === T_WHILE) {
$isPartOfDo = $this->isPartOfDo($phpcsFile, $controlStructurePointer);
if ($isPartOfDo && !in_array(self::DO_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) {
return true;
}
if (!$isPartOfDo && !in_array(self::WHILE_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) {
return true;
}
}
return false;
}
protected function getControlStructureName(File $phpcsFile, int $controlStructurePointer): string
{
$tokens = $phpcsFile->getTokens();
return $tokens[$controlStructurePointer]['code'] === T_WHILE && $this->isPartOfDo($phpcsFile, $controlStructurePointer)
? 'do-while'
: $tokens[$controlStructurePointer]['content'];
}
protected function isPartOfDo(File $phpcsFile, int $whilePointer): bool
{
$tokens = $phpcsFile->getTokens();
$parenthesisCloserPointer = $tokens[$whilePointer]['parenthesis_closer'];
$pointerAfterParenthesisCloser = TokenHelper::findNextEffective($phpcsFile, $parenthesisCloserPointer + 1);
return $tokens[$pointerAfterParenthesisCloser]['code'] !== T_OPEN_CURLY_BRACKET;
}
protected function getLineStart(File $phpcsFile, int $pointer): string
{
$firstPointerOnLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $pointer);
return IndentationHelper::convertTabsToSpaces($phpcsFile, TokenHelper::getContent($phpcsFile, $firstPointerOnLine, $pointer));
}
protected function getCondition(File $phpcsFile, int $parenthesisOpenerPointer, int $parenthesisCloserPointer): string
{
$condition = TokenHelper::getContent($phpcsFile, $parenthesisOpenerPointer + 1, $parenthesisCloserPointer - 1);
return trim(preg_replace(sprintf('~%s[ \t]*~', $phpcsFile->eolChar), ' ', $condition));
}
protected function getLineEnd(File $phpcsFile, int $pointer): string
{
$lastPointerOnLine = TokenHelper::findLastTokenOnLine($phpcsFile, $pointer);
return rtrim(TokenHelper::getContent($phpcsFile, $pointer, $lastPointerOnLine));
}
}

View File

@@ -0,0 +1,115 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_keys;
use function max;
use function sprintf;
use const T_DO;
use const T_ELSEIF;
use const T_EQUAL;
use const T_IF;
use const T_WHILE;
class AssignmentInConditionSniff implements Sniff
{
public const CODE_ASSIGNMENT_IN_CONDITION = 'AssignmentInCondition';
/** @var bool */
public $ignoreAssignmentsInsideFunctionCalls = false;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_IF,
T_ELSEIF,
T_DO,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $conditionStartPointer
*/
public function process(File $phpcsFile, $conditionStartPointer): void
{
$tokens = $phpcsFile->getTokens();
$token = $tokens[$conditionStartPointer];
if ($token['code'] === T_DO) {
$whilePointer = TokenHelper::findNext($phpcsFile, T_WHILE, $token['scope_closer'] + 1);
$whileToken = $tokens[$whilePointer];
$parenthesisOpener = $whileToken['parenthesis_opener'];
$parenthesisCloser = $whileToken['parenthesis_closer'];
$type = 'do-while';
} else {
$parenthesisOpener = $token['parenthesis_opener'];
$parenthesisCloser = $token['parenthesis_closer'];
$type = $token['code'] === T_IF ? 'if' : 'elseif';
}
if (
$parenthesisOpener === null
|| $parenthesisCloser === null
) {
return;
}
$this->processCondition($phpcsFile, $parenthesisOpener, $parenthesisCloser, $type);
}
private function processCondition(File $phpcsFile, int $parenthesisOpener, int $parenthesisCloser, string $conditionType): void
{
$equalsTokenPointers = TokenHelper::findNextAll($phpcsFile, T_EQUAL, $parenthesisOpener + 1, $parenthesisCloser);
if ($equalsTokenPointers === []) {
return;
}
if (!$this->ignoreAssignmentsInsideFunctionCalls) {
$this->error($phpcsFile, $conditionType, $equalsTokenPointers[0]);
return;
}
$tokens = $phpcsFile->getTokens();
foreach ($equalsTokenPointers as $equalsTokenPointer) {
$parenthesisStarts = array_keys($tokens[$equalsTokenPointer]['nested_parenthesis']);
/** @var int $insideParenthesis */
$insideParenthesis = max($parenthesisStarts);
if ($insideParenthesis === $parenthesisOpener) {
$this->error($phpcsFile, $conditionType, $equalsTokenPointer);
continue;
}
$functionCall = TokenHelper::findPrevious(
$phpcsFile,
TokenHelper::getOnlyNameTokenCodes(),
$insideParenthesis,
$parenthesisOpener
);
if ($functionCall !== null) {
continue;
}
$this->error($phpcsFile, $conditionType, $equalsTokenPointer);
}
}
private function error(File $phpcsFile, string $conditionType, int $equalsTokenPointer): void
{
$phpcsFile->addError(
sprintf('Assignment in %s condition is not allowed.', $conditionType),
$equalsTokenPointer,
self::CODE_ASSIGNMENT_IN_CONDITION
);
}
}

View File

@@ -0,0 +1,113 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_key_exists;
use const T_CLOSE_CURLY_BRACKET;
use const T_DO;
use const T_WHILE;
class BlockControlStructureSpacingSniff extends AbstractControlStructureSpacing
{
/** @var int */
public $linesCountBefore = 1;
/** @var int */
public $linesCountBeforeFirst = 0;
/** @var int */
public $linesCountAfter = 1;
/** @var int */
public $linesCountAfterLast = 0;
/** @var list<string> */
public $controlStructures = [];
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $controlStructurePointer
*/
public function process(File $phpcsFile, $controlStructurePointer): void
{
$this->linesCountBefore = SniffSettingsHelper::normalizeInteger($this->linesCountBefore);
$this->linesCountBeforeFirst = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeFirst);
$this->linesCountAfter = SniffSettingsHelper::normalizeInteger($this->linesCountAfter);
$this->linesCountAfterLast = SniffSettingsHelper::normalizeInteger($this->linesCountAfterLast);
if ($this->isWhilePartOfDo($phpcsFile, $controlStructurePointer)) {
return;
}
parent::process($phpcsFile, $controlStructurePointer);
}
/**
* @return list<string>
*/
protected function getSupportedKeywords(): array
{
return [
self::KEYWORD_IF,
self::KEYWORD_DO,
self::KEYWORD_WHILE,
self::KEYWORD_FOR,
self::KEYWORD_FOREACH,
self::KEYWORD_SWITCH,
self::KEYWORD_TRY,
self::KEYWORD_CASE,
self::KEYWORD_DEFAULT,
];
}
/**
* @return list<string>
*/
protected function getKeywordsToCheck(): array
{
return $this->controlStructures;
}
protected function getLinesCountBefore(): int
{
return $this->linesCountBefore;
}
/**
* @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
*/
protected function getLinesCountBeforeFirst(File $phpcsFile, int $controlStructurePointer): int
{
return $this->linesCountBeforeFirst;
}
protected function getLinesCountAfter(): int
{
return $this->linesCountAfter;
}
/**
* @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
*/
protected function getLinesCountAfterLast(File $phpcsFile, int $controlStructurePointer, int $controlStructureEndPointer): int
{
return $this->linesCountAfterLast;
}
private function isWhilePartOfDo(File $phpcsFile, int $controlStructurePointer): bool
{
$tokens = $phpcsFile->getTokens();
$pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $controlStructurePointer - 1);
return
$tokens[$controlStructurePointer]['code'] === T_WHILE
&& $tokens[$pointerBefore]['code'] === T_CLOSE_CURLY_BRACKET
&& array_key_exists('scope_condition', $tokens[$pointerBefore])
&& $tokens[$tokens[$pointerBefore]['scope_condition']]['code'] === T_DO;
}
}

View File

@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_reverse;
use function current;
use const T_CONTINUE;
use const T_LNUMBER;
use const T_SWITCH;
class DisallowContinueWithoutIntegerOperandInSwitchSniff implements Sniff
{
public const CODE_DISALLOWED_CONTINUE_WITHOUT_INTEGER_OPERAND_IN_SWITCH = 'DisallowedContinueWithoutIntegerOperandInSwitch';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_CONTINUE,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $continuePointer
*/
public function process(File $phpcsFile, $continuePointer): void
{
$tokens = $phpcsFile->getTokens();
$operandPointer = TokenHelper::findNextEffective($phpcsFile, $continuePointer + 1);
if ($tokens[$operandPointer]['code'] === T_LNUMBER) {
return;
}
$conditionTokenCode = current(array_reverse($tokens[$continuePointer]['conditions']));
if ($conditionTokenCode !== T_SWITCH) {
return;
}
$fix = $phpcsFile->addFixableError(
'Usage of "continue" without integer operand in "switch" is disallowed, use "break" instead.',
$continuePointer,
self::CODE_DISALLOWED_CONTINUE_WITHOUT_INTEGER_OPERAND_IN_SWITCH
);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->replaceToken($continuePointer, 'break');
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use const T_EMPTY;
class DisallowEmptySniff implements Sniff
{
public const CODE_DISALLOWED_EMPTY = 'DisallowedEmpty';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_EMPTY,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $emptyPointer
*/
public function process(File $phpcsFile, $emptyPointer): void
{
$phpcsFile->addError('Use of empty() is disallowed.', $emptyPointer, self::CODE_DISALLOWED_EMPTY);
}
}

View File

@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use const T_NULLSAFE_OBJECT_OPERATOR;
class DisallowNullSafeObjectOperatorSniff implements Sniff
{
public const CODE_DISALLOWED_NULL_SAFE_OBJECT_OPERATOR = 'DisallowedNullSafeObjectOperator';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_NULLSAFE_OBJECT_OPERATOR,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $operatorPointer
*/
public function process(File $phpcsFile, $operatorPointer): void
{
$phpcsFile->addError('Operator ?-> is disallowed.', $operatorPointer, self::CODE_DISALLOWED_NULL_SAFE_OBJECT_OPERATOR);
}
}

View File

@@ -0,0 +1,72 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function sprintf;
use const T_INLINE_ELSE;
use const T_INLINE_THEN;
use const T_VARIABLE;
class DisallowShortTernaryOperatorSniff implements Sniff
{
public const CODE_DISALLOWED_SHORT_TERNARY_OPERATOR = 'DisallowedShortTernaryOperator';
/** @var bool */
public $fixable = true;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_INLINE_THEN,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $inlineThenPointer
*/
public function process(File $phpcsFile, $inlineThenPointer): void
{
$tokens = $phpcsFile->getTokens();
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1);
if ($tokens[$nextPointer]['code'] !== T_INLINE_ELSE) {
return;
}
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1);
$message = 'Use of short ternary operator is disallowed.';
if ($tokens[$previousPointer]['code'] !== T_VARIABLE) {
$phpcsFile->addError($message, $inlineThenPointer, self::CODE_DISALLOWED_SHORT_TERNARY_OPERATOR);
return;
}
if (!$this->fixable) {
$phpcsFile->addError($message, $inlineThenPointer, self::CODE_DISALLOWED_SHORT_TERNARY_OPERATOR);
return;
}
$fix = $phpcsFile->addFixableError($message, $inlineThenPointer, self::CODE_DISALLOWED_SHORT_TERNARY_OPERATOR);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addContent($inlineThenPointer, sprintf(' %s ', $tokens[$previousPointer]['content']));
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,88 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\IndentationHelper;
use SlevomatCodingStandard\Helpers\TernaryOperatorHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use const T_INLINE_ELSE;
use const T_INLINE_THEN;
use const T_WHITESPACE;
class DisallowTrailingMultiLineTernaryOperatorSniff implements Sniff
{
public const CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED = 'TrailingMultiLineTernaryOperatorUsed';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_INLINE_THEN,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $inlineThenPointer
*/
public function process(File $phpcsFile, $inlineThenPointer): void
{
$tokens = $phpcsFile->getTokens();
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1);
if ($tokens[$nextPointer]['code'] === T_INLINE_ELSE) {
return;
}
if ($tokens[$inlineThenPointer]['line'] === $tokens[$nextPointer]['line']) {
return;
}
$fix = $phpcsFile->addFixableError(
'Ternary operator should be reformatted as leading the line.',
$inlineThenPointer,
self::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED
);
if (!$fix) {
return;
}
$inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer);
$pointerBeforeInlineThen = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1);
$pointerAfterInlineThen = TokenHelper::findNextExcluding($phpcsFile, [T_WHITESPACE], $inlineThenPointer + 1);
$pointerBeforeInlineElse = TokenHelper::findPreviousEffective($phpcsFile, $inlineElsePointer - 1);
$pointerAfterInlineElse = TokenHelper::findNextExcluding($phpcsFile, [T_WHITESPACE], $inlineElsePointer + 1);
$indentation = IndentationHelper::addIndentation(
IndentationHelper::getIndentation(
$phpcsFile,
TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $inlineThenPointer)
)
);
$phpcsFile->fixer->beginChangeset();
FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineThen, $inlineThenPointer);
FixerHelper::removeBetween($phpcsFile, $inlineThenPointer, $pointerAfterInlineThen);
$phpcsFile->fixer->addContentBefore($inlineThenPointer, $phpcsFile->eolChar . $indentation);
$phpcsFile->fixer->addContentBefore($pointerAfterInlineThen, ' ');
FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineElse, $inlineElsePointer);
FixerHelper::removeBetween($phpcsFile, $inlineElsePointer, $pointerAfterInlineElse);
$phpcsFile->fixer->addContentBefore($inlineElsePointer, $phpcsFile->eolChar . $indentation);
$phpcsFile->fixer->addContentBefore($pointerAfterInlineElse, ' ');
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,88 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\TokenHelper;
use SlevomatCodingStandard\Helpers\YodaHelper;
use function array_keys;
use function count;
use const T_EQUAL;
use const T_IS_EQUAL;
use const T_IS_IDENTICAL;
use const T_IS_NOT_EQUAL;
use const T_IS_NOT_IDENTICAL;
/**
* Bigger value must be on the left side:
*
* ($variable, Foo::$class, Foo::bar(), foo())
* > (Foo::BAR, BAR)
* > (true, false, null, 1, 1.0, arrays, 'foo')
*/
class DisallowYodaComparisonSniff implements Sniff
{
public const CODE_DISALLOWED_YODA_COMPARISON = 'DisallowedYodaComparison';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_IS_IDENTICAL,
T_IS_NOT_IDENTICAL,
T_IS_EQUAL,
T_IS_NOT_EQUAL,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $comparisonTokenPointer
*/
public function process(File $phpcsFile, $comparisonTokenPointer): void
{
$tokens = $phpcsFile->getTokens();
$leftSideTokens = YodaHelper::getLeftSideTokens($tokens, $comparisonTokenPointer);
$rightSideTokens = YodaHelper::getRightSideTokens($tokens, $comparisonTokenPointer);
$leftDynamism = YodaHelper::getDynamismForTokens($tokens, $leftSideTokens);
$rightDynamism = YodaHelper::getDynamismForTokens($tokens, $rightSideTokens);
if ($leftDynamism === null || $rightDynamism === null) {
return;
}
if ($leftDynamism >= $rightDynamism) {
return;
}
if ($leftDynamism >= 900 && $rightDynamism >= 900) {
return;
}
$errorParameters = [
'Yoda comparisons are disallowed.',
$comparisonTokenPointer,
self::CODE_DISALLOWED_YODA_COMPARISON,
];
$lastRightSideTokenPointer = array_keys($rightSideTokens)[count($rightSideTokens) - 1];
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $lastRightSideTokenPointer + 1);
if ($tokens[$nextPointer]['code'] === T_EQUAL) {
$phpcsFile->addError(...$errorParameters);
return;
}
$fix = $phpcsFile->addFixableError(...$errorParameters);
if (!$fix) {
return;
}
YodaHelper::fix($phpcsFile, $leftSideTokens, $rightSideTokens);
}
}

View File

@@ -0,0 +1,480 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use Exception;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\ConditionHelper;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\IndentationHelper;
use SlevomatCodingStandard\Helpers\ScopeHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use Throwable;
use function array_key_exists;
use function count;
use function in_array;
use function range;
use function sort;
use function sprintf;
use const T_CLOSE_CURLY_BRACKET;
use const T_CLOSURE;
use const T_COLON;
use const T_DO;
use const T_ELSE;
use const T_ELSEIF;
use const T_FOR;
use const T_FOREACH;
use const T_FUNCTION;
use const T_IF;
use const T_OPEN_CURLY_BRACKET;
use const T_SEMICOLON;
use const T_WHILE;
class EarlyExitSniff implements Sniff
{
public const CODE_EARLY_EXIT_NOT_USED = 'EarlyExitNotUsed';
public const CODE_USELESS_ELSEIF = 'UselessElseIf';
public const CODE_USELESS_ELSE = 'UselessElse';
/** @var bool */
public $ignoreStandaloneIfInScope = false;
/** @var bool */
public $ignoreOneLineTrailingIf = false;
/** @var bool */
public $ignoreTrailingIfWithOneInstruction = false;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_IF,
T_ELSEIF,
T_ELSE,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $pointer
*/
public function process(File $phpcsFile, $pointer): void
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$pointer]['code'] === T_IF) {
$this->processIf($phpcsFile, $pointer);
} elseif ($tokens[$pointer]['code'] === T_ELSEIF) {
$this->processElseIf($phpcsFile, $pointer);
} else {
$this->processElse($phpcsFile, $pointer);
}
}
private function processElse(File $phpcsFile, int $elsePointer): void
{
$tokens = $phpcsFile->getTokens();
if (!array_key_exists('scope_opener', $tokens[$elsePointer])) {
// Else without curly braces is not supported.
return;
}
try {
$allConditionsPointers = $this->getAllConditionsPointers($phpcsFile, $elsePointer);
} catch (Throwable $e) {
// Else without curly braces is not supported.
return;
}
if (TokenHelper::findNext(
$phpcsFile,
T_FUNCTION,
$tokens[$elsePointer]['scope_opener'] + 1,
$tokens[$elsePointer]['scope_closer']
) !== null) {
return;
}
$ifPointer = $allConditionsPointers[0];
$ifEarlyExitPointer = null;
$elseEarlyExitPointer = null;
$previousConditionPointer = null;
$previousConditionEarlyExitPointer = null;
foreach ($allConditionsPointers as $conditionPointer) {
$conditionEarlyExitPointer = $this->findEarlyExitInScope(
$phpcsFile,
$tokens[$conditionPointer]['scope_opener'],
$tokens[$conditionPointer]['scope_closer']
);
if ($conditionPointer === $elsePointer) {
$elseEarlyExitPointer = $conditionEarlyExitPointer;
continue;
}
if (count($allConditionsPointers) > 2 && $conditionEarlyExitPointer === null) {
return;
}
$previousConditionPointer = $conditionPointer;
$previousConditionEarlyExitPointer = $conditionEarlyExitPointer;
if ($conditionPointer === $ifPointer) {
$ifEarlyExitPointer = $conditionEarlyExitPointer;
continue;
}
}
if ($ifEarlyExitPointer === null && $elseEarlyExitPointer === null) {
return;
}
if ($elseEarlyExitPointer !== null && $previousConditionEarlyExitPointer === null) {
$fix = $phpcsFile->addFixableError('Use early exit instead of "else".', $elsePointer, self::CODE_EARLY_EXIT_NOT_USED);
if (!$fix) {
return;
}
$ifCodePointers = $this->getScopeCodePointers($phpcsFile, $ifPointer);
$elseCode = $this->getScopeCode($phpcsFile, $elsePointer);
$negativeIfCondition = ConditionHelper::getNegativeCondition(
$phpcsFile,
$tokens[$ifPointer]['parenthesis_opener'],
$tokens[$ifPointer]['parenthesis_closer']
);
$afterIfCode = IndentationHelper::fixIndentation(
$phpcsFile,
$ifCodePointers,
IndentationHelper::getIndentation($phpcsFile, $ifPointer)
);
$ifContent = sprintf('if %s {%s}%s%s', $negativeIfCondition, $elseCode, $phpcsFile->eolChar, $afterIfCode);
$phpcsFile->fixer->beginChangeset();
FixerHelper::change($phpcsFile, $ifPointer, $tokens[$elsePointer]['scope_closer'], $ifContent);
$phpcsFile->fixer->endChangeset();
return;
}
$fix = $phpcsFile->addFixableError('Remove useless "else" to reduce code nesting.', $elsePointer, self::CODE_USELESS_ELSE);
if (!$fix) {
return;
}
$elseCodePointers = $this->getScopeCodePointers($phpcsFile, $elsePointer);
$afterIfCode = IndentationHelper::fixIndentation(
$phpcsFile,
$elseCodePointers,
IndentationHelper::getIndentation($phpcsFile, $ifPointer)
);
$phpcsFile->fixer->beginChangeset();
$previousConditionContent = sprintf('%s%s', $phpcsFile->eolChar, $afterIfCode);
FixerHelper::change(
$phpcsFile,
$tokens[$previousConditionPointer]['scope_closer'] + 1,
$tokens[$elsePointer]['scope_closer'],
$previousConditionContent
);
$phpcsFile->fixer->endChangeset();
}
private function processElseIf(File $phpcsFile, int $elseIfPointer): void
{
$tokens = $phpcsFile->getTokens();
try {
$allConditionsPointers = $this->getAllConditionsPointers($phpcsFile, $elseIfPointer);
} catch (Throwable $e) {
// Elseif without curly braces is not supported.
return;
}
if (TokenHelper::findNext(
$phpcsFile,
T_FUNCTION,
$tokens[$elseIfPointer]['scope_opener'] + 1,
$tokens[$elseIfPointer]['scope_closer']
) !== null) {
return;
}
foreach ($allConditionsPointers as $conditionPointer) {
$conditionEarlyExitPointer = $this->findEarlyExitInScope(
$phpcsFile,
$tokens[$conditionPointer]['scope_opener'],
$tokens[$conditionPointer]['scope_closer']
);
if ($conditionPointer === $elseIfPointer) {
break;
}
if ($conditionEarlyExitPointer === null) {
return;
}
}
$fix = $phpcsFile->addFixableError('Use "if" instead of "elseif".', $elseIfPointer, self::CODE_USELESS_ELSEIF);
if (!$fix) {
return;
}
/** @var int $pointerBeforeElseIfPointer */
$pointerBeforeElseIfPointer = TokenHelper::findPreviousNonWhitespace($phpcsFile, $elseIfPointer - 1);
$phpcsFile->fixer->beginChangeset();
FixerHelper::removeBetween($phpcsFile, $pointerBeforeElseIfPointer, $elseIfPointer);
$phpcsFile->fixer->addNewline($pointerBeforeElseIfPointer);
$phpcsFile->fixer->addNewline($pointerBeforeElseIfPointer);
$phpcsFile->fixer->replaceToken(
$elseIfPointer,
sprintf('%sif', IndentationHelper::getIndentation($phpcsFile, $allConditionsPointers[0]))
);
$phpcsFile->fixer->endChangeset();
}
private function processIf(File $phpcsFile, int $ifPointer): void
{
$tokens = $phpcsFile->getTokens();
if (!array_key_exists('scope_closer', $tokens[$ifPointer])) {
// If without curly braces is not supported.
return;
}
$nextPointer = TokenHelper::findNextNonWhitespace($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1);
if ($nextPointer === null || $tokens[$nextPointer]['code'] !== T_CLOSE_CURLY_BRACKET) {
return;
}
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $ifPointer - 1);
if (
$this->ignoreStandaloneIfInScope
&& in_array($tokens[$previousPointer]['code'], [T_OPEN_CURLY_BRACKET, T_COLON], true)
) {
return;
}
if (
$this->ignoreOneLineTrailingIf
&& $tokens[$tokens[$ifPointer]['scope_opener']]['line'] + 2 === $tokens[$tokens[$ifPointer]['scope_closer']]['line']
) {
return;
}
if ($this->ignoreTrailingIfWithOneInstruction) {
$pointerBeforeScopeCloser = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] - 1);
if ($tokens[$pointerBeforeScopeCloser]['code'] === T_SEMICOLON) {
$ignore = true;
$searchStartPointer = $tokens[$ifPointer]['scope_opener'] + 1;
while (true) {
$anotherSemicolonPointer = TokenHelper::findNext(
$phpcsFile,
T_SEMICOLON,
$searchStartPointer,
$pointerBeforeScopeCloser
);
if ($anotherSemicolonPointer === null) {
break;
}
if (ScopeHelper::isInSameScope($phpcsFile, $anotherSemicolonPointer, $pointerBeforeScopeCloser)) {
$ignore = false;
break;
}
$searchStartPointer = $anotherSemicolonPointer + 1;
}
if ($ignore) {
return;
}
}
}
$scopePointer = $tokens[$nextPointer]['scope_condition'];
if (!in_array($tokens[$scopePointer]['code'], [T_FUNCTION, T_CLOSURE, T_WHILE, T_DO, T_FOREACH, T_FOR], true)) {
return;
}
if ($this->isEarlyExitInScope($phpcsFile, $tokens[$ifPointer]['scope_opener'], $tokens[$ifPointer]['scope_closer'])) {
return;
}
$fix = $phpcsFile->addFixableError('Use early exit to reduce code nesting.', $ifPointer, self::CODE_EARLY_EXIT_NOT_USED);
if (!$fix) {
return;
}
$ifCodePointers = $this->getScopeCodePointers($phpcsFile, $ifPointer);
$ifIndentation = IndentationHelper::getIndentation($phpcsFile, $ifPointer);
$earlyExitCode = $this->getEarlyExitCode($tokens[$scopePointer]['code']);
$earlyExitCodeIndentation = IndentationHelper::addIndentation($ifIndentation);
$negativeIfCondition = ConditionHelper::getNegativeCondition(
$phpcsFile,
$tokens[$ifPointer]['parenthesis_opener'],
$tokens[$ifPointer]['parenthesis_closer']
);
$afterIfCode = IndentationHelper::fixIndentation($phpcsFile, $ifCodePointers, $ifIndentation);
$ifContent = sprintf(
'if %s {%s%s%s;%s%s}%s%s',
$negativeIfCondition,
$phpcsFile->eolChar,
$earlyExitCodeIndentation,
$earlyExitCode,
$phpcsFile->eolChar,
$ifIndentation,
$phpcsFile->eolChar,
$afterIfCode
);
$phpcsFile->fixer->beginChangeset();
FixerHelper::change($phpcsFile, $ifPointer, $tokens[$ifPointer]['scope_closer'], $ifContent);
$phpcsFile->fixer->endChangeset();
}
private function getScopeCode(File $phpcsFile, int $scopePointer): string
{
$tokens = $phpcsFile->getTokens();
return TokenHelper::getContent($phpcsFile, $tokens[$scopePointer]['scope_opener'] + 1, $tokens[$scopePointer]['scope_closer'] - 1);
}
/**
* @return list<int>
*/
private function getScopeCodePointers(File $phpcsFile, int $scopePointer): array
{
$tokens = $phpcsFile->getTokens();
return range($tokens[$scopePointer]['scope_opener'] + 1, $tokens[$scopePointer]['scope_closer'] - 1);
}
/**
* @param string|int $code
*/
private function getEarlyExitCode($code): string
{
if (in_array($code, [T_WHILE, T_DO, T_FOREACH, T_FOR], true)) {
return 'continue';
}
return 'return';
}
private function findEarlyExitInScope(File $phpcsFile, int $startPointer, int $endPointer): ?int
{
$tokens = $phpcsFile->getTokens();
$ifPointers = TokenHelper::findNextAll($phpcsFile, T_IF, $startPointer + 1, $endPointer);
foreach ($ifPointers as $ifPointer) {
if ($tokens[$ifPointer]['level'] - 1 !== $tokens[$startPointer]['level']) {
continue;
}
$conditionPointers = $this->getAllConditionsPointers($phpcsFile, $ifPointer);
foreach ($conditionPointers as $conditionPointer) {
if ($this->findEarlyExitInScope(
$phpcsFile,
$tokens[$conditionPointer]['scope_opener'],
$tokens[$conditionPointer]['scope_closer']
) === null) {
return null;
}
}
}
$lastSemicolonInScopePointer = TokenHelper::findPreviousEffective($phpcsFile, $endPointer - 1, $startPointer);
return $tokens[$lastSemicolonInScopePointer]['code'] === T_SEMICOLON
? TokenHelper::findPreviousLocal($phpcsFile, TokenHelper::$earlyExitTokenCodes, $lastSemicolonInScopePointer - 1, $startPointer)
: null;
}
private function isEarlyExitInScope(File $phpcsFile, int $startPointer, int $endPointer): bool
{
return $this->findEarlyExitInScope($phpcsFile, $startPointer, $endPointer) !== null;
}
/**
* @return list<int>
*/
private function getAllConditionsPointers(File $phpcsFile, int $conditionPointer): array
{
$tokens = $phpcsFile->getTokens();
$conditionsPointers = [$conditionPointer];
if (
isset($tokens[$conditionPointer]['scope_opener'])
&& $tokens[$tokens[$conditionPointer]['scope_opener']]['code'] === T_COLON
) {
// Alternative control structure syntax.
throw new Exception(sprintf('"%s" without curly braces is not supported.', $tokens[$conditionPointer]['content']));
}
if ($tokens[$conditionPointer]['code'] !== T_IF) {
$currentConditionPointer = $conditionPointer;
do {
$previousConditionCloseParenthesisPointer = TokenHelper::findPreviousEffective($phpcsFile, $currentConditionPointer - 1);
$currentConditionPointer = $tokens[$previousConditionCloseParenthesisPointer]['scope_condition'];
$conditionsPointers[] = $currentConditionPointer;
} while ($tokens[$currentConditionPointer]['code'] !== T_IF);
}
if ($tokens[$conditionPointer]['code'] !== T_ELSE) {
if (!array_key_exists('scope_closer', $tokens[$conditionPointer])) {
throw new Exception(sprintf('"%s" without curly braces is not supported.', $tokens[$conditionPointer]['content']));
}
$currentConditionPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$conditionPointer]['scope_closer'] + 1);
if ($currentConditionPointer !== null) {
while (in_array($tokens[$currentConditionPointer]['code'], [T_ELSEIF, T_ELSE], true)) {
$conditionsPointers[] = $currentConditionPointer;
if (!array_key_exists('scope_closer', $tokens[$currentConditionPointer])) {
throw new Exception(
sprintf('"%s" without curly braces is not supported.', $tokens[$currentConditionPointer]['content'])
);
}
$currentConditionPointer = TokenHelper::findNextEffective(
$phpcsFile,
$tokens[$currentConditionPointer]['scope_closer'] + 1
);
}
}
}
sort($conditionsPointers);
return $conditionsPointers;
}
}

View File

@@ -0,0 +1,282 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function abs;
use function array_key_exists;
use function in_array;
use const T_CASE;
use const T_CLOSE_CURLY_BRACKET;
use const T_COLON;
use const T_DEFAULT;
use const T_OPEN_CURLY_BRACKET;
use const T_OPEN_TAG;
use const T_RETURN;
use const T_SEMICOLON;
use const T_SWITCH;
use const T_THROW;
use const T_YIELD;
use const T_YIELD_FROM;
class JumpStatementsSpacingSniff extends AbstractControlStructureSpacing
{
/** @var int */
public $linesCountBefore = 1;
/** @var int */
public $linesCountBeforeFirst = 0;
/** @var int|null */
public $linesCountBeforeWhenFirstInCaseOrDefault = null;
/** @var int */
public $linesCountAfter = 1;
/** @var int */
public $linesCountAfterLast = 0;
/** @var int|null */
public $linesCountAfterWhenLastInCaseOrDefault = null;
/** @var int|null */
public $linesCountAfterWhenLastInLastCaseOrDefault = null;
/** @var bool */
public $allowSingleLineYieldStacking = true;
/** @var list<string> */
public $jumpStatements = [];
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $jumpStatementPointer
*/
public function process(File $phpcsFile, $jumpStatementPointer): void
{
$this->linesCountBefore = SniffSettingsHelper::normalizeInteger($this->linesCountBefore);
$this->linesCountBeforeFirst = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeFirst);
$this->linesCountBeforeWhenFirstInCaseOrDefault = SniffSettingsHelper::normalizeNullableInteger(
$this->linesCountBeforeWhenFirstInCaseOrDefault
);
$this->linesCountAfter = SniffSettingsHelper::normalizeInteger($this->linesCountAfter);
$this->linesCountAfterLast = SniffSettingsHelper::normalizeInteger($this->linesCountAfterLast);
$this->linesCountAfterWhenLastInCaseOrDefault = SniffSettingsHelper::normalizeNullableInteger(
$this->linesCountAfterWhenLastInCaseOrDefault
);
$this->linesCountAfterWhenLastInLastCaseOrDefault = SniffSettingsHelper::normalizeNullableInteger(
$this->linesCountAfterWhenLastInLastCaseOrDefault
);
if ($this->isOneOfYieldSpecialCases($phpcsFile, $jumpStatementPointer)) {
return;
}
parent::process($phpcsFile, $jumpStatementPointer);
}
/**
* @return list<string>
*/
protected function getSupportedKeywords(): array
{
return [
self::KEYWORD_GOTO,
self::KEYWORD_BREAK,
self::KEYWORD_CONTINUE,
self::KEYWORD_RETURN,
self::KEYWORD_THROW,
self::KEYWORD_YIELD,
self::KEYWORD_YIELD_FROM,
];
}
/**
* @return list<string>
*/
protected function getKeywordsToCheck(): array
{
return $this->jumpStatements;
}
protected function getLinesCountBefore(): int
{
return $this->linesCountBefore;
}
protected function getLinesCountBeforeFirst(File $phpcsFile, int $jumpStatementPointer): int
{
if (
$this->linesCountBeforeWhenFirstInCaseOrDefault !== null
&& $this->isFirstInCaseOrDefault($phpcsFile, $jumpStatementPointer)
) {
return $this->linesCountBeforeWhenFirstInCaseOrDefault;
}
return $this->linesCountBeforeFirst;
}
protected function getLinesCountAfter(): int
{
return $this->linesCountAfter;
}
/**
* @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
*/
protected function getLinesCountAfterLast(File $phpcsFile, int $jumpStatementPointer, int $jumpStatementEndPointer): int
{
if (
$this->linesCountAfterWhenLastInLastCaseOrDefault !== null
&& $this->isLastInLastCaseOrDefault($phpcsFile, $jumpStatementEndPointer)
) {
return $this->linesCountAfterWhenLastInLastCaseOrDefault;
}
if (
$this->linesCountAfterWhenLastInCaseOrDefault !== null
&& $this->isLastInCaseOrDefault($phpcsFile, $jumpStatementEndPointer)
) {
return $this->linesCountAfterWhenLastInCaseOrDefault;
}
return $this->linesCountAfterLast;
}
protected function checkLinesBefore(File $phpcsFile, int $jumpStatementPointer): void
{
if (
$this->allowSingleLineYieldStacking
&& $this->isStackedSingleLineYield($phpcsFile, $jumpStatementPointer, true)
) {
return;
}
if ($this->isThrowExpression($phpcsFile, $jumpStatementPointer)) {
return;
}
parent::checkLinesBefore($phpcsFile, $jumpStatementPointer);
}
protected function checkLinesAfter(File $phpcsFile, int $jumpStatementPointer): void
{
if (
$this->allowSingleLineYieldStacking
&& $this->isStackedSingleLineYield($phpcsFile, $jumpStatementPointer, false)
) {
return;
}
if ($this->isThrowExpression($phpcsFile, $jumpStatementPointer)) {
return;
}
parent::checkLinesAfter($phpcsFile, $jumpStatementPointer);
}
private function isOneOfYieldSpecialCases(File $phpcsFile, int $jumpStatementPointer): bool
{
$tokens = $phpcsFile->getTokens();
$jumpStatementToken = $tokens[$jumpStatementPointer];
if ($jumpStatementToken['code'] !== T_YIELD && $jumpStatementToken['code'] !== T_YIELD_FROM) {
return false;
}
// check if yield is used inside parentheses (function call, while, ...)
if (array_key_exists('nested_parenthesis', $jumpStatementToken)) {
return true;
}
$pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $jumpStatementPointer - 1);
// check if yield is used in assignment
if (in_array($tokens[$pointerBefore]['code'], Tokens::$assignmentTokens, true)) {
return true;
}
// check if yield is used in a return statement
return $tokens[$pointerBefore]['code'] === T_RETURN;
}
private function isStackedSingleLineYield(File $phpcsFile, int $jumpStatementPointer, bool $previous): bool
{
$tokens = $phpcsFile->getTokens();
$yields = [T_YIELD, T_YIELD_FROM];
if (!in_array($tokens[$jumpStatementPointer]['code'], $yields, true)) {
return false;
}
$adjoiningYieldPointer = $previous
? TokenHelper::findPrevious($phpcsFile, $yields, $jumpStatementPointer - 1)
: TokenHelper::findNext($phpcsFile, $yields, $jumpStatementPointer + 1);
return $adjoiningYieldPointer !== null
&& abs($tokens[$adjoiningYieldPointer]['line'] - $tokens[$jumpStatementPointer]['line']) === 1;
}
private function isThrowExpression(File $phpcsFile, int $jumpStatementPointer): bool
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$jumpStatementPointer]['code'] !== T_THROW) {
return false;
}
$pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $jumpStatementPointer - 1);
return !in_array(
$tokens[$pointerBefore]['code'],
[T_SEMICOLON, T_COLON, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_OPEN_TAG],
true
);
}
private function isFirstInCaseOrDefault(File $phpcsFile, int $jumpStatementPointer): bool
{
$tokens = $phpcsFile->getTokens();
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $jumpStatementPointer - 1);
if ($tokens[$previousPointer]['code'] !== T_COLON) {
return false;
}
$firstPointerOnLine = TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $previousPointer);
return in_array($tokens[$firstPointerOnLine]['code'], [T_CASE, T_DEFAULT], true);
}
private function isLastInCaseOrDefault(File $phpcsFile, int $jumpStatementEndPointer): bool
{
$tokens = $phpcsFile->getTokens();
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $jumpStatementEndPointer + 1);
if (in_array($tokens[$nextPointer]['code'], [T_CASE, T_DEFAULT], true)) {
return true;
}
return $tokens[$nextPointer]['code'] === T_CLOSE_CURLY_BRACKET
&& array_key_exists('scope_condition', $tokens[$nextPointer])
&& $tokens[$tokens[$nextPointer]['scope_condition']]['code'] === T_SWITCH;
}
private function isLastInLastCaseOrDefault(File $phpcsFile, int $jumpStatementEndPointer): bool
{
if (!$this->isLastInCaseOrDefault($phpcsFile, $jumpStatementEndPointer)) {
return false;
}
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $jumpStatementEndPointer + 1);
return !in_array($phpcsFile->getTokens()[$nextPointer]['code'], [T_CASE, T_DEFAULT], true);
}
}

View File

@@ -0,0 +1,102 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function in_array;
use function sprintf;
use const T_BREAK;
use const T_CLOSE_PARENTHESIS;
use const T_CLOSE_SHORT_ARRAY;
use const T_CONTINUE;
use const T_ECHO;
use const T_EXIT;
use const T_INCLUDE;
use const T_INCLUDE_ONCE;
use const T_OPEN_PARENTHESIS;
use const T_PRINT;
use const T_REQUIRE;
use const T_REQUIRE_ONCE;
use const T_RETURN;
use const T_SEMICOLON;
use const T_THROW;
use const T_WHITESPACE;
use const T_YIELD;
use const T_YIELD_FROM;
class LanguageConstructWithParenthesesSniff implements Sniff
{
public const CODE_USED_WITH_PARENTHESES = 'UsedWithParentheses';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_BREAK,
T_CONTINUE,
T_ECHO,
T_EXIT,
T_INCLUDE,
T_INCLUDE_ONCE,
T_PRINT,
T_REQUIRE,
T_REQUIRE_ONCE,
T_RETURN,
T_THROW,
T_YIELD,
T_YIELD_FROM,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $languageConstructPointer
*/
public function process(File $phpcsFile, $languageConstructPointer): void
{
$tokens = $phpcsFile->getTokens();
/** @var int $openParenthesisPointer */
$openParenthesisPointer = TokenHelper::findNextEffective($phpcsFile, $languageConstructPointer + 1);
if ($tokens[$openParenthesisPointer]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
$closeParenthesisPointer = $tokens[$openParenthesisPointer]['parenthesis_closer'];
$afterCloseParenthesisPointer = TokenHelper::findNextEffective($phpcsFile, $closeParenthesisPointer + 1);
if (!in_array($tokens[$afterCloseParenthesisPointer]['code'], [T_SEMICOLON, T_CLOSE_PARENTHESIS, T_CLOSE_SHORT_ARRAY], true)) {
return;
}
$containsContentBetweenParentheses = TokenHelper::findNextEffective(
$phpcsFile,
$openParenthesisPointer + 1,
$closeParenthesisPointer
) !== null;
if ($tokens[$languageConstructPointer]['code'] === T_EXIT && $containsContentBetweenParentheses) {
return;
}
$fix = $phpcsFile->addFixableError(
sprintf('Usage of language construct "%s" with parentheses is disallowed.', $tokens[$languageConstructPointer]['content']),
$languageConstructPointer,
self::CODE_USED_WITH_PARENTHESES
);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->replaceToken($openParenthesisPointer, '');
if ($tokens[$openParenthesisPointer - 1]['code'] !== T_WHITESPACE && $containsContentBetweenParentheses) {
$phpcsFile->fixer->addContent($openParenthesisPointer, ' ');
}
$phpcsFile->fixer->replaceToken($closeParenthesisPointer, '');
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,114 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\AttributeHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use const T_ANON_CLASS;
use const T_ATTRIBUTE;
use const T_CLOSE_PARENTHESIS;
use const T_CLOSE_SHORT_ARRAY;
use const T_CLOSE_SQUARE_BRACKET;
use const T_COALESCE;
use const T_COMMA;
use const T_DOUBLE_ARROW;
use const T_INLINE_ELSE;
use const T_INLINE_THEN;
use const T_NEW;
use const T_OPEN_PARENTHESIS;
use const T_SEMICOLON;
class NewWithParenthesesSniff implements Sniff
{
public const CODE_MISSING_PARENTHESES = 'MissingParentheses';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_NEW,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $newPointer
*/
public function process(File $phpcsFile, $newPointer): void
{
$tokens = $phpcsFile->getTokens();
/** @var int $nextPointer */
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $newPointer + 1);
if ($tokens[$nextPointer]['code'] === T_ATTRIBUTE) {
$nextPointer = AttributeHelper::getAttributeTarget($phpcsFile, $nextPointer);
}
if ($tokens[$nextPointer]['code'] === T_ANON_CLASS) {
return;
}
if ($tokens[$nextPointer]['code'] === T_OPEN_PARENTHESIS) {
$nextPointer = $tokens[$nextPointer]['parenthesis_closer'];
}
$shouldBeOpenParenthesisPointer = $nextPointer + 1;
do {
$shouldBeOpenParenthesisPointer = TokenHelper::findNext(
$phpcsFile,
[
T_OPEN_PARENTHESIS,
T_SEMICOLON,
T_COMMA,
T_INLINE_THEN,
T_INLINE_ELSE,
T_COALESCE,
T_CLOSE_SHORT_ARRAY,
T_CLOSE_SQUARE_BRACKET,
T_CLOSE_PARENTHESIS,
T_DOUBLE_ARROW,
],
$shouldBeOpenParenthesisPointer
);
if (
$shouldBeOpenParenthesisPointer === null
|| $tokens[$shouldBeOpenParenthesisPointer]['code'] !== T_CLOSE_SQUARE_BRACKET
|| $tokens[$shouldBeOpenParenthesisPointer]['bracket_opener'] <= $newPointer
) {
break;
}
$shouldBeOpenParenthesisPointer++;
} while (true);
if (
$shouldBeOpenParenthesisPointer !== null
&& $tokens[$shouldBeOpenParenthesisPointer]['code'] === T_OPEN_PARENTHESIS
) {
return;
}
$fix = $phpcsFile->addFixableError(
'Usage of "new" without parentheses is disallowed.',
$newPointer,
self::CODE_MISSING_PARENTHESES
);
if (!$fix) {
return;
}
/** @var int $classNameEndPointer */
$classNameEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $shouldBeOpenParenthesisPointer - 1);
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addContent($classNameEndPointer, '()');
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,106 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use const T_ANON_CLASS;
use const T_CLOSE_PARENTHESIS;
use const T_CLOSE_SHORT_ARRAY;
use const T_CLOSE_SQUARE_BRACKET;
use const T_COALESCE;
use const T_COMMA;
use const T_DOUBLE_ARROW;
use const T_INLINE_ELSE;
use const T_INLINE_THEN;
use const T_NEW;
use const T_OPEN_PARENTHESIS;
use const T_SEMICOLON;
class NewWithoutParenthesesSniff implements Sniff
{
public const CODE_USELESS_PARENTHESES = 'UselessParentheses';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_NEW,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $newPointer
*/
public function process(File $phpcsFile, $newPointer): void
{
$tokens = $phpcsFile->getTokens();
/** @var int $nextPointer */
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $newPointer + 1);
if ($tokens[$nextPointer]['code'] === T_ANON_CLASS) {
return;
}
$parenthesisOpenerPointer = $nextPointer + 1;
do {
/** @var int $parenthesisOpenerPointer */
$parenthesisOpenerPointer = TokenHelper::findNext(
$phpcsFile,
[
T_OPEN_PARENTHESIS,
T_SEMICOLON,
T_COMMA,
T_INLINE_THEN,
T_INLINE_ELSE,
T_COALESCE,
T_CLOSE_SHORT_ARRAY,
T_CLOSE_SQUARE_BRACKET,
T_CLOSE_PARENTHESIS,
T_DOUBLE_ARROW,
],
$parenthesisOpenerPointer
);
if (
$tokens[$parenthesisOpenerPointer]['code'] !== T_CLOSE_SQUARE_BRACKET
|| $tokens[$parenthesisOpenerPointer]['bracket_opener'] <= $newPointer
) {
break;
}
$parenthesisOpenerPointer++;
} while (true);
if ($tokens[$parenthesisOpenerPointer]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
$nextPointer = TokenHelper::findNextNonWhitespace($phpcsFile, $parenthesisOpenerPointer + 1);
if ($nextPointer !== $tokens[$parenthesisOpenerPointer]['parenthesis_closer']) {
return;
}
$fix = $phpcsFile->addFixableError('Useless parentheses in "new".', $newPointer, self::CODE_USELESS_PARENTHESES);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
FixerHelper::removeBetweenIncluding(
$phpcsFile,
$parenthesisOpenerPointer,
$tokens[$parenthesisOpenerPointer]['parenthesis_closer']
);
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,188 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\IndentationHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function count;
use function in_array;
use function sprintf;
use function strlen;
use const T_CLOSE_PARENTHESIS;
use const T_OPEN_PARENTHESIS;
class RequireMultiLineConditionSniff extends AbstractLineCondition
{
public const CODE_REQUIRED_MULTI_LINE_CONDITION = 'RequiredMultiLineCondition';
/** @var int */
public $minLineLength = 121;
/** @var bool */
public $booleanOperatorOnPreviousLine = false;
/** @var bool */
public $alwaysSplitAllConditionParts = false;
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $controlStructurePointer
*/
public function process(File $phpcsFile, $controlStructurePointer): void
{
$this->minLineLength = SniffSettingsHelper::normalizeInteger($this->minLineLength);
if ($this->shouldBeSkipped($phpcsFile, $controlStructurePointer)) {
return;
}
$tokens = $phpcsFile->getTokens();
$parenthesisOpenerPointer = $tokens[$controlStructurePointer]['parenthesis_opener'];
$parenthesisCloserPointer = $tokens[$controlStructurePointer]['parenthesis_closer'];
$booleanOperatorPointers = TokenHelper::findNextAll(
$phpcsFile,
Tokens::$booleanOperators,
$parenthesisOpenerPointer + 1,
$parenthesisCloserPointer
);
if ($booleanOperatorPointers === []) {
return;
}
$conditionStartPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1);
$conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisCloserPointer - 1);
$conditionStartsOnNewLine = $tokens[$parenthesisOpenerPointer]['line'] !== $tokens[$conditionStartPointer]['line'];
$conditionEndsOnNewLine = $tokens[$parenthesisCloserPointer]['line'] !== $tokens[$conditionEndPointer]['line'];
$lineStart = $this->getLineStart($phpcsFile, $conditionStartsOnNewLine ? $conditionStartPointer - 1 : $parenthesisOpenerPointer);
$lineEnd = $this->getLineEnd($phpcsFile, $conditionEndsOnNewLine ? $conditionEndPointer + 1 : $parenthesisCloserPointer);
$condition = $this->getCondition($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer);
$lineLength = strlen($lineStart . $condition . $lineEnd);
$conditionLinesCount = $tokens[$conditionEndPointer]['line'] - $tokens[$conditionStartPointer]['line'] + 1;
if (!$this->shouldReportError($lineLength, $conditionLinesCount, count($booleanOperatorPointers))) {
return;
}
$fix = $phpcsFile->addFixableError(
sprintf(
'Condition of "%s" should be split to more lines so each condition part is on its own line.',
$this->getControlStructureName($phpcsFile, $controlStructurePointer)
),
$controlStructurePointer,
self::CODE_REQUIRED_MULTI_LINE_CONDITION
);
if (!$fix) {
return;
}
$controlStructureIndentation = IndentationHelper::getIndentation(
$phpcsFile,
$conditionStartsOnNewLine
? $conditionStartPointer
: TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $parenthesisOpenerPointer)
);
$conditionIndentation = $conditionStartsOnNewLine
? $controlStructureIndentation
: IndentationHelper::addIndentation($controlStructureIndentation);
$innerConditionLevel = 0;
$phpcsFile->fixer->beginChangeset();
if (!$conditionStartsOnNewLine) {
FixerHelper::removeWhitespaceBefore($phpcsFile, $conditionStartPointer);
$phpcsFile->fixer->addContentBefore($conditionStartPointer, $phpcsFile->eolChar . $conditionIndentation);
}
for ($i = $conditionStartPointer; $i <= $conditionEndPointer; $i++) {
if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) {
$containsBooleanOperator = TokenHelper::findNext(
$phpcsFile,
Tokens::$booleanOperators,
$i + 1,
$tokens[$i]['parenthesis_closer']
) !== null;
$innerConditionLevel++;
if ($containsBooleanOperator) {
FixerHelper::removeWhitespaceAfter($phpcsFile, $i);
$phpcsFile->fixer->addContent(
$i,
$phpcsFile->eolChar . IndentationHelper::addIndentation($conditionIndentation, $innerConditionLevel)
);
FixerHelper::removeWhitespaceBefore($phpcsFile, $tokens[$i]['parenthesis_closer']);
$phpcsFile->fixer->addContentBefore(
$tokens[$i]['parenthesis_closer'],
$phpcsFile->eolChar . IndentationHelper::addIndentation($conditionIndentation, $innerConditionLevel - 1)
);
}
continue;
}
if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
$innerConditionLevel--;
continue;
}
if (!in_array($tokens[$i]['code'], Tokens::$booleanOperators, true)) {
continue;
}
$innerConditionIndentation = $conditionIndentation;
if ($innerConditionLevel > 0) {
$innerConditionIndentation = IndentationHelper::addIndentation($innerConditionIndentation, $innerConditionLevel);
}
if ($this->booleanOperatorOnPreviousLine) {
$phpcsFile->fixer->addContent($i, $phpcsFile->eolChar . $innerConditionIndentation);
FixerHelper::removeWhitespaceAfter($phpcsFile, $i);
continue;
}
FixerHelper::removeWhitespaceBefore($phpcsFile, $i);
$phpcsFile->fixer->addContentBefore($i, $phpcsFile->eolChar . $innerConditionIndentation);
}
if (!$conditionEndsOnNewLine) {
FixerHelper::removeWhitespaceAfter($phpcsFile, $conditionEndPointer);
$phpcsFile->fixer->addContent($conditionEndPointer, $phpcsFile->eolChar . $controlStructureIndentation);
}
$phpcsFile->fixer->endChangeset();
}
private function shouldReportError(int $lineLength, int $conditionLinesCount, int $booleanOperatorPointersCount): bool
{
if ($conditionLinesCount === 1) {
return $this->minLineLength === 0 || $lineLength >= $this->minLineLength;
}
return $this->alwaysSplitAllConditionParts
? $conditionLinesCount < $booleanOperatorPointersCount + 1
: false;
}
}

View File

@@ -0,0 +1,178 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\IndentationHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TernaryOperatorHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_merge;
use function in_array;
use function strlen;
use function substr;
use const T_INLINE_ELSE;
use const T_INLINE_THEN;
use const T_OPEN_TAG;
use const T_OPEN_TAG_WITH_ECHO;
use const T_SEMICOLON;
use const T_WHITESPACE;
class RequireMultiLineTernaryOperatorSniff implements Sniff
{
public const CODE_MULTI_LINE_TERNARY_OPERATOR_NOT_USED = 'MultiLineTernaryOperatorNotUsed';
/** @var int */
public $lineLengthLimit = 0;
/** @var int|null */
public $minExpressionsLength = null;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_INLINE_THEN,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $inlineThenPointer
*/
public function process(File $phpcsFile, $inlineThenPointer): void
{
$this->lineLengthLimit = SniffSettingsHelper::normalizeInteger($this->lineLengthLimit);
$this->minExpressionsLength = SniffSettingsHelper::normalizeNullableInteger($this->minExpressionsLength);
$tokens = $phpcsFile->getTokens();
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1);
if ($tokens[$nextPointer]['code'] === T_INLINE_ELSE) {
return;
}
$inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer);
if ($tokens[$inlineThenPointer]['line'] !== $tokens[$inlineElsePointer]['line']) {
return;
}
$inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer);
$pointerAfterInlineElseEnd = TokenHelper::findNextEffective($phpcsFile, $inlineElseEndPointer + 1);
if ($pointerAfterInlineElseEnd === null || $tokens[$pointerAfterInlineElseEnd]['code'] !== T_SEMICOLON) {
return;
}
$endOfLineBeforeInlineThenPointer = $this->getEndOfLineBefore($phpcsFile, $inlineThenPointer);
$actualLineLength = strlen(TokenHelper::getContent($phpcsFile, $endOfLineBeforeInlineThenPointer + 1, $pointerAfterInlineElseEnd));
if ($actualLineLength <= $this->lineLengthLimit) {
return;
}
$expressionsLength = strlen(TokenHelper::getContent($phpcsFile, $inlineThenPointer + 1, $pointerAfterInlineElseEnd - 1));
if (
$this->minExpressionsLength !== null
&& $this->minExpressionsLength >= $expressionsLength
) {
return;
}
$fix = $phpcsFile->addFixableError(
'Ternary operator should be reformatted to more lines.',
$inlineThenPointer,
self::CODE_MULTI_LINE_TERNARY_OPERATOR_NOT_USED
);
if (!$fix) {
return;
}
$indentation = $this->getIndentation($phpcsFile, $endOfLineBeforeInlineThenPointer);
$pointerBeforeInlineThen = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1);
$pointerBeforeInlineElse = TokenHelper::findPreviousEffective($phpcsFile, $inlineElsePointer - 1);
$phpcsFile->fixer->beginChangeset();
FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineThen, $inlineThenPointer);
$phpcsFile->fixer->addContentBefore($inlineThenPointer, $phpcsFile->eolChar . $indentation);
FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineElse, $inlineElsePointer);
$phpcsFile->fixer->addContentBefore($inlineElsePointer, $phpcsFile->eolChar . $indentation);
$phpcsFile->fixer->endChangeset();
}
private function getEndOfLineBefore(File $phpcsFile, int $pointer): int
{
$tokens = $phpcsFile->getTokens();
$endOfLineBefore = null;
$startPointer = $pointer - 1;
while (true) {
$possibleEndOfLinePointer = TokenHelper::findPrevious(
$phpcsFile,
array_merge([T_WHITESPACE, T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO], TokenHelper::$inlineCommentTokenCodes),
$startPointer
);
if (
$tokens[$possibleEndOfLinePointer]['code'] === T_WHITESPACE
&& $tokens[$possibleEndOfLinePointer]['content'] === $phpcsFile->eolChar
) {
$endOfLineBefore = $possibleEndOfLinePointer;
break;
}
if (
$tokens[$possibleEndOfLinePointer]['code'] === T_OPEN_TAG
|| $tokens[$possibleEndOfLinePointer]['code'] === T_OPEN_TAG_WITH_ECHO
) {
$endOfLineBefore = $possibleEndOfLinePointer;
break;
}
if (
in_array($tokens[$possibleEndOfLinePointer]['code'], TokenHelper::$inlineCommentTokenCodes, true)
&& substr($tokens[$possibleEndOfLinePointer]['content'], -1) === $phpcsFile->eolChar
) {
$endOfLineBefore = $possibleEndOfLinePointer;
break;
}
$startPointer = $possibleEndOfLinePointer - 1;
}
/** @var int $endOfLineBefore */
$endOfLineBefore = $endOfLineBefore;
return $endOfLineBefore;
}
private function getIndentation(File $phpcsFile, int $endOfLinePointer): string
{
$pointerAfterWhitespace = TokenHelper::findNextNonWhitespace($phpcsFile, $endOfLinePointer + 1);
$actualIndentation = TokenHelper::getContent($phpcsFile, $endOfLinePointer + 1, $pointerAfterWhitespace - 1);
if (strlen($actualIndentation) !== 0) {
return $actualIndentation . (substr(
$actualIndentation,
-1
) === IndentationHelper::TAB_INDENT ? IndentationHelper::TAB_INDENT : IndentationHelper::SPACES_INDENT);
}
$tabPointer = TokenHelper::findPreviousContent($phpcsFile, T_WHITESPACE, IndentationHelper::TAB_INDENT, $endOfLinePointer - 1);
return $tabPointer !== null ? IndentationHelper::TAB_INDENT : IndentationHelper::SPACES_INDENT;
}
}

View File

@@ -0,0 +1,99 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\IdentificatorHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use const T_COALESCE;
use const T_EQUAL;
use const T_SEMICOLON;
class RequireNullCoalesceEqualOperatorSniff implements Sniff
{
public const CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR = 'RequiredNullCoalesceEqualOperator';
/** @var bool|null */
public $enable = null;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_EQUAL,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $equalPointer
*/
public function process(File $phpcsFile, $equalPointer): void
{
$this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 70400);
if (!$this->enable) {
return;
}
/** @var int $variableStartPointer */
$variableStartPointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1);
$variableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $variableStartPointer);
if ($variableEndPointer === null) {
return;
}
$nullCoalescePointer = TokenHelper::findNextEffective($phpcsFile, $variableEndPointer + 1);
$tokens = $phpcsFile->getTokens();
if ($tokens[$nullCoalescePointer]['code'] !== T_COALESCE) {
return;
}
$variableContent = IdentificatorHelper::getContent($phpcsFile, $variableStartPointer, $variableEndPointer);
/** @var int $beforeEqualEndPointer */
$beforeEqualEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $equalPointer - 1);
$beforeEqualStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $beforeEqualEndPointer);
if ($beforeEqualStartPointer === null) {
return;
}
$beforeEqualVariableContent = IdentificatorHelper::getContent($phpcsFile, $beforeEqualStartPointer, $beforeEqualEndPointer);
if ($beforeEqualVariableContent !== $variableContent) {
return;
}
$semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $equalPointer + 1);
if (TokenHelper::findNext($phpcsFile, Tokens::$operators, $nullCoalescePointer + 1, $semicolonPointer) !== null) {
return;
}
$fix = $phpcsFile->addFixableError(
'Use "??=" operator instead of "=" and "??".',
$equalPointer,
self::CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR
);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
FixerHelper::change($phpcsFile, $equalPointer, $nullCoalescePointer, '??=');
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,217 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\IdentificatorHelper;
use SlevomatCodingStandard\Helpers\TernaryOperatorHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function in_array;
use function sprintf;
use function trim;
use const T_BOOLEAN_NOT;
use const T_CLOSE_PARENTHESIS;
use const T_COMMA;
use const T_INLINE_THEN;
use const T_IS_IDENTICAL;
use const T_IS_NOT_IDENTICAL;
use const T_ISSET;
use const T_NULL;
class RequireNullCoalesceOperatorSniff implements Sniff
{
public const CODE_NULL_COALESCE_OPERATOR_NOT_USED = 'NullCoalesceOperatorNotUsed';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_ISSET,
T_IS_IDENTICAL,
T_IS_NOT_IDENTICAL,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $pointer
*/
public function process(File $phpcsFile, $pointer): void
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$pointer]['code'] === T_ISSET) {
$this->checkIsset($phpcsFile, $pointer);
} else {
$this->checkIdenticalOperator($phpcsFile, $pointer);
}
}
public function checkIsset(File $phpcsFile, int $issetPointer): void
{
$tokens = $phpcsFile->getTokens();
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $issetPointer - 1);
if ($tokens[$previousPointer]['code'] === T_BOOLEAN_NOT) {
return;
}
if (in_array($tokens[$previousPointer]['code'], Tokens::$booleanOperators, true)) {
return;
}
$openParenthesisPointer = TokenHelper::findNextEffective($phpcsFile, $issetPointer + 1);
$closeParenthesisPointer = $tokens[$openParenthesisPointer]['parenthesis_closer'];
/** @var int $inlineThenPointer */
$inlineThenPointer = TokenHelper::findNextEffective($phpcsFile, $closeParenthesisPointer + 1);
if ($tokens[$inlineThenPointer]['code'] !== T_INLINE_THEN) {
return;
}
$commaPointer = TokenHelper::findNext($phpcsFile, T_COMMA, $openParenthesisPointer + 1, $closeParenthesisPointer);
if ($commaPointer !== null) {
return;
}
$inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer);
$variableContent = IdentificatorHelper::getContent($phpcsFile, $openParenthesisPointer + 1, $closeParenthesisPointer - 1);
$thenContent = IdentificatorHelper::getContent($phpcsFile, $inlineThenPointer + 1, $inlineElsePointer - 1);
if ($variableContent !== $thenContent) {
return;
}
$fix = $phpcsFile->addFixableError(
'Use null coalesce operator instead of ternary operator.',
$inlineThenPointer,
self::CODE_NULL_COALESCE_OPERATOR_NOT_USED
);
if (!$fix) {
return;
}
$startPointer = $issetPointer;
if (in_array($tokens[$previousPointer]['code'], Tokens::$castTokens, true)) {
$startPointer = $previousPointer;
}
$phpcsFile->fixer->beginChangeset();
FixerHelper::change($phpcsFile, $startPointer, $inlineElsePointer, sprintf('%s ??', $variableContent));
$phpcsFile->fixer->endChangeset();
}
public function checkIdenticalOperator(File $phpcsFile, int $identicalOperator): void
{
$tokens = $phpcsFile->getTokens();
/** @var int $pointerBeforeIdenticalOperator */
$pointerBeforeIdenticalOperator = TokenHelper::findPreviousEffective($phpcsFile, $identicalOperator - 1);
/** @var int $pointerAfterIdenticalOperator */
$pointerAfterIdenticalOperator = TokenHelper::findNextEffective($phpcsFile, $identicalOperator + 1);
if (
$tokens[$pointerBeforeIdenticalOperator]['code'] !== T_NULL
&& $tokens[$pointerAfterIdenticalOperator]['code'] !== T_NULL
) {
return;
}
$isYodaCondition = $tokens[$pointerBeforeIdenticalOperator]['code'] === T_NULL;
$variableEndPointer = $isYodaCondition ? $pointerAfterIdenticalOperator : $pointerBeforeIdenticalOperator;
$tmpPointer = $variableEndPointer;
while ($tokens[$tmpPointer]['code'] === T_CLOSE_PARENTHESIS) {
/** @var int $tmpPointer */
$tmpPointer = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$tmpPointer]['parenthesis_opener'] - 1);
}
$variableStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $tmpPointer);
if ($variableStartPointer === null) {
return;
}
$pointerBeforeCondition = TokenHelper::findPreviousEffective(
$phpcsFile,
($isYodaCondition ? $pointerBeforeIdenticalOperator : $variableStartPointer) - 1
);
if (in_array($tokens[$pointerBeforeCondition]['code'], Tokens::$booleanOperators, true)) {
return;
}
/** @var int $inlineThenPointer */
$inlineThenPointer = TokenHelper::findNextEffective(
$phpcsFile,
($isYodaCondition ? $variableEndPointer : $pointerAfterIdenticalOperator) + 1
);
if ($tokens[$inlineThenPointer]['code'] !== T_INLINE_THEN) {
return;
}
$inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer);
$inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer);
$pointerAfterInlineElseEnd = TokenHelper::findNextEffective($phpcsFile, $inlineElseEndPointer + 1);
$variableContent = IdentificatorHelper::getContent($phpcsFile, $variableStartPointer, $variableEndPointer);
/** @var int $compareToStartPointer */
$compareToStartPointer = TokenHelper::findNextEffective(
$phpcsFile,
($tokens[$identicalOperator]['code'] === T_IS_IDENTICAL ? $inlineElsePointer : $inlineThenPointer) + 1
);
/** @var int $compareToEndPointer */
$compareToEndPointer = TokenHelper::findPreviousEffective(
$phpcsFile,
($tokens[$identicalOperator]['code'] === T_IS_IDENTICAL ? $pointerAfterInlineElseEnd : $inlineElsePointer) - 1
);
$compareToContent = IdentificatorHelper::getContent($phpcsFile, $compareToStartPointer, $compareToEndPointer);
if ($compareToContent !== $variableContent) {
return;
}
$fix = $phpcsFile->addFixableError(
'Use null coalesce operator instead of ternary operator.',
$inlineThenPointer,
self::CODE_NULL_COALESCE_OPERATOR_NOT_USED
);
if (!$fix) {
return;
}
/** @var int $conditionStart */
$conditionStart = $isYodaCondition ? $pointerBeforeIdenticalOperator : $variableStartPointer;
$variableContent = trim(TokenHelper::getContent($phpcsFile, $variableStartPointer, $variableEndPointer));
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->replaceToken($conditionStart, sprintf('%s ??', $variableContent));
if ($tokens[$identicalOperator]['code'] === T_IS_IDENTICAL) {
FixerHelper::removeBetweenIncluding($phpcsFile, $conditionStart + 1, $inlineThenPointer);
$pointerBeforeInlineElse = TokenHelper::findPreviousEffective($phpcsFile, $inlineElsePointer - 1);
FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBeforeInlineElse + 1, $inlineElseEndPointer);
} else {
FixerHelper::removeBetweenIncluding($phpcsFile, $conditionStart + 1, $inlineElsePointer);
}
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,513 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\IdentificatorHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TernaryOperatorHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_key_exists;
use function count;
use function in_array;
use function min;
use function preg_split;
use function sprintf;
use function strtolower;
use function substr_count;
use function trim;
use const PREG_SPLIT_DELIM_CAPTURE;
use const T_BOOLEAN_AND;
use const T_BOOLEAN_OR;
use const T_CLOSE_PARENTHESIS;
use const T_DOUBLE_COLON;
use const T_INLINE_THEN;
use const T_IS_IDENTICAL;
use const T_IS_NOT_IDENTICAL;
use const T_NULL;
use const T_NULLSAFE_OBJECT_OPERATOR;
use const T_OBJECT_OPERATOR;
use const T_OPEN_PARENTHESIS;
use const T_SEMICOLON;
use const T_STRING;
class RequireNullSafeObjectOperatorSniff implements Sniff
{
public const CODE_REQUIRED_NULL_SAFE_OBJECT_OPERATOR = 'RequiredNullSafeObjectOperator';
private const OPERATOR_REGEXP = '~(::|->|\?->)~';
/** @var bool|null */
public $enable = null;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_IS_IDENTICAL,
T_IS_NOT_IDENTICAL,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $identicalPointer
*/
public function process(File $phpcsFile, $identicalPointer): int
{
$this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000);
if (!$this->enable) {
return $identicalPointer + 1;
}
$tokens = $phpcsFile->getTokens();
[$pointerBeforeIdentical, $pointerAfterIdentical] = $this->getIdenticalData($phpcsFile, $identicalPointer);
if ($tokens[$pointerBeforeIdentical]['code'] !== T_NULL && $tokens[$pointerAfterIdentical]['code'] !== T_NULL) {
return $identicalPointer + 1;
}
[$identificatorStartPointer, $identificatorEndPointer, $conditionStartPointer] = $this->getConditionData(
$phpcsFile,
$pointerBeforeIdentical,
$pointerAfterIdentical
);
if ($identificatorStartPointer === null || $identificatorEndPointer === null) {
return $identicalPointer + 1;
}
$isYoda = $tokens[$pointerBeforeIdentical]['code'] === T_NULL;
$identificator = IdentificatorHelper::getContent($phpcsFile, $identificatorStartPointer, $identificatorEndPointer);
$pointerAfterCondition = TokenHelper::findNextEffective(
$phpcsFile,
($isYoda ? $identificatorEndPointer : $pointerAfterIdentical) + 1
);
$allowedBooleanCondition = $tokens[$identicalPointer]['code'] === T_IS_NOT_IDENTICAL ? T_BOOLEAN_AND : T_BOOLEAN_OR;
if ($tokens[$pointerAfterCondition]['code'] === $allowedBooleanCondition) {
return $this->checkNextCondition($phpcsFile, $identicalPointer, $conditionStartPointer, $identificator, $pointerAfterCondition);
}
if ($tokens[$pointerAfterCondition]['code'] === T_INLINE_THEN) {
$this->checkTernaryOperator($phpcsFile, $identicalPointer, $conditionStartPointer, $identificator, $pointerAfterCondition);
return $pointerAfterCondition + 1;
}
return $identicalPointer + 1;
}
private function checkTernaryOperator(
File $phpcsFile,
int $identicalPointer,
int $conditionStartPointer,
string $identificator,
int $inlineThenPointer
): void
{
$tokens = $phpcsFile->getTokens();
$ternaryOperatorStartPointer = TernaryOperatorHelper::getStartPointer($phpcsFile, $inlineThenPointer);
$searchStartPointer = $ternaryOperatorStartPointer;
do {
$booleanOperatorPointer = TokenHelper::findNext($phpcsFile, Tokens::$booleanOperators, $searchStartPointer, $inlineThenPointer);
if ($booleanOperatorPointer === null) {
break;
}
$identicalPointer = TokenHelper::findNext(
$phpcsFile,
[T_IS_IDENTICAL, T_IS_NOT_IDENTICAL],
$searchStartPointer,
$booleanOperatorPointer
);
if ($identicalPointer === null) {
return;
}
$pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $identicalPointer + 1);
if ($tokens[$pointerAfterIdentical]['code'] !== T_NULL) {
return;
}
$searchStartPointer = $booleanOperatorPointer + 1;
} while (true);
$pointerBeforeCondition = TokenHelper::findPreviousEffective($phpcsFile, $conditionStartPointer - 1);
if (in_array($tokens[$pointerBeforeCondition]['code'], [T_BOOLEAN_AND, T_BOOLEAN_OR], true)) {
$previousIdenticalPointer = TokenHelper::findPreviousLocal(
$phpcsFile,
[T_IS_IDENTICAL, T_IS_NOT_IDENTICAL],
$pointerBeforeCondition
);
if ($previousIdenticalPointer !== null) {
[$pointerBeforePreviousIdentical, $pointerAfterPreviousIdentical] = $this->getIdenticalData(
$phpcsFile,
$previousIdenticalPointer
);
[$previousIdentificatorStartPointer, $previousIdentificatorEndPointer] = $this->getConditionData(
$phpcsFile,
$pointerBeforePreviousIdentical,
$pointerAfterPreviousIdentical
);
if ($previousIdentificatorStartPointer !== null && $previousIdentificatorEndPointer !== null) {
$previousIdentificator = IdentificatorHelper::getContent(
$phpcsFile,
$previousIdentificatorStartPointer,
$previousIdentificatorEndPointer
);
if (!self::areIdentificatorsCompatible($previousIdentificator, $identificator)) {
return;
}
}
}
}
$defaultInElse = $tokens[$identicalPointer]['code'] === T_IS_NOT_IDENTICAL;
$inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer);
$inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer);
if ($defaultInElse) {
$nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $inlineThenPointer);
if ($nextIdentificatorPointers === null) {
return;
}
[$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers;
$nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer);
if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) {
return;
}
if (TokenHelper::findNextEffective($phpcsFile, $nextIdentificatorEndPointer + 1) !== $inlineElsePointer) {
return;
}
$identificatorDifference = $this->getIdentificatorDifference(
$phpcsFile,
$identificator,
$nextIdentificatorStartPointer,
$nextIdentificatorEndPointer
);
$firstPointerInElse = TokenHelper::findNextEffective($phpcsFile, $inlineElsePointer + 1);
$defaultContent = TokenHelper::getContent($phpcsFile, $firstPointerInElse, $inlineElseEndPointer);
$conditionEndPointer = $inlineElseEndPointer;
} else {
$nullPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1);
if ($tokens[$nullPointer]['code'] !== T_NULL) {
return;
}
if (TokenHelper::findNextEffective($phpcsFile, $nullPointer + 1) !== $inlineElsePointer) {
return;
}
$nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $inlineElsePointer);
if ($nextIdentificatorPointers === null) {
return;
}
[$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers;
if ($nextIdentificatorEndPointer !== $inlineElseEndPointer) {
return;
}
$nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer);
if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) {
return;
}
$identificatorDifference = $this->getIdentificatorDifference(
$phpcsFile,
$identificator,
$nextIdentificatorStartPointer,
$nextIdentificatorEndPointer
);
$defaultContent = trim(TokenHelper::getContent($phpcsFile, $inlineThenPointer + 1, $inlineElsePointer - 1));
$conditionEndPointer = $nextIdentificatorEndPointer;
}
$fix = $phpcsFile->addFixableError('Operator ?-> is required.', $identicalPointer, self::CODE_REQUIRED_NULL_SAFE_OBJECT_OPERATOR);
if (!$fix) {
return;
}
$conditionContent = sprintf('%s?%s', $identificator, $identificatorDifference);
if (strtolower($defaultContent) !== 'null') {
$conditionContent .= sprintf(' ?? %s', $defaultContent);
}
$phpcsFile->fixer->beginChangeset();
FixerHelper::change($phpcsFile, $conditionStartPointer, $conditionEndPointer, $conditionContent);
$phpcsFile->fixer->endChangeset();
}
private function checkNextCondition(
File $phpcsFile,
int $identicalPointer,
int $conditionStartPointer,
string $identificator,
int $nextConditionBooleanPointer
): int
{
$nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $nextConditionBooleanPointer);
if ($nextIdentificatorPointers === null) {
return $nextConditionBooleanPointer;
}
[$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers;
$nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer);
if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) {
return $nextIdentificatorEndPointer;
}
$pointerAfterNexIdentificator = TokenHelper::findNextEffective($phpcsFile, $nextIdentificatorEndPointer + 1);
$tokens = $phpcsFile->getTokens();
if (
$tokens[$pointerAfterNexIdentificator]['code'] !== $tokens[$identicalPointer]['code']
&& !in_array($tokens[$pointerAfterNexIdentificator]['code'], [T_INLINE_THEN, T_SEMICOLON], true)
) {
return $pointerAfterNexIdentificator;
}
if (!in_array($tokens[$pointerAfterNexIdentificator]['code'], [T_IS_IDENTICAL, T_IS_NOT_IDENTICAL], true)) {
return $pointerAfterNexIdentificator;
}
$pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $pointerAfterNexIdentificator + 1);
if ($tokens[$pointerAfterIdentical]['code'] !== T_NULL) {
return $pointerAfterNexIdentificator;
}
$identificatorDifference = $this->getIdentificatorDifference(
$phpcsFile,
$identificator,
$nextIdentificatorStartPointer,
$nextIdentificatorEndPointer
);
$fix = $phpcsFile->addFixableError('Operator ?-> is required.', $identicalPointer, self::CODE_REQUIRED_NULL_SAFE_OBJECT_OPERATOR);
if (!$fix) {
return $pointerAfterNexIdentificator;
}
$isConditionOfTernaryOperator = TernaryOperatorHelper::isConditionOfTernaryOperator($phpcsFile, $identicalPointer);
$phpcsFile->fixer->beginChangeset();
FixerHelper::change(
$phpcsFile,
$conditionStartPointer,
$nextIdentificatorEndPointer,
sprintf('%s?%s', $identificator, $identificatorDifference)
);
$phpcsFile->fixer->endChangeset();
if ($isConditionOfTernaryOperator) {
return TokenHelper::findNext($phpcsFile, T_INLINE_THEN, $identicalPointer + 1);
}
return $pointerAfterNexIdentificator;
}
/**
* @return array<int, int>|null
*/
private function getNextIdentificator(File $phpcsFile, int $pointerBefore): ?array
{
/** @var int $nextIdentificatorStartPointer */
$nextIdentificatorStartPointer = TokenHelper::findNextEffective($phpcsFile, $pointerBefore + 1);
$nextIdentificatorEndPointer = $this->findIdentificatorEnd($phpcsFile, $nextIdentificatorStartPointer);
if ($nextIdentificatorEndPointer === null) {
return null;
}
return [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer];
}
private function findIdentificatorStart(File $phpcsFile, int $identificatorEndPointer): ?int
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$identificatorEndPointer]['code'] === T_CLOSE_PARENTHESIS) {
$pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective(
$phpcsFile,
$tokens[$identificatorEndPointer]['parenthesis_opener'] - 1
);
$identificatorStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $pointerBeforeParenthesisOpener);
} else {
$identificatorStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $identificatorEndPointer);
}
if ($identificatorStartPointer !== null) {
$pointerBeforeIdentificatorStart = TokenHelper::findPreviousEffective($phpcsFile, $identificatorStartPointer - 1);
if (in_array(
$tokens[$pointerBeforeIdentificatorStart]['code'],
[T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR],
true
)) {
$pointerBeforeOperator = TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeIdentificatorStart - 1);
return $this->findIdentificatorStart($phpcsFile, $pointerBeforeOperator);
}
}
return $identificatorStartPointer;
}
private function findIdentificatorEnd(File $phpcsFile, int $identificatorStartPointer): ?int
{
$tokens = $phpcsFile->getTokens();
$identificatorEndPointer = $tokens[$identificatorStartPointer]['code'] === T_STRING
? $identificatorStartPointer
: IdentificatorHelper::findEndPointer($phpcsFile, $identificatorStartPointer);
if ($identificatorEndPointer !== null) {
$pointerAfterIdentificatorEnd = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointer + 1);
if ($tokens[$pointerAfterIdentificatorEnd]['code'] === T_OPEN_PARENTHESIS) {
$identificatorEndPointer = $tokens[$pointerAfterIdentificatorEnd]['parenthesis_closer'];
$pointerAfterIdentificatorEnd = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointer + 1);
}
if (in_array(
$tokens[$pointerAfterIdentificatorEnd]['code'],
[T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR],
true
)) {
$pointerAfterOperator = TokenHelper::findNextEffective($phpcsFile, $pointerAfterIdentificatorEnd + 1);
return $this->findIdentificatorEnd($phpcsFile, $pointerAfterOperator);
}
}
return $identificatorEndPointer;
}
private function areIdentificatorsCompatible(string $first, string $second): bool
{
/** @var list<string> $firstParts */
$firstParts = preg_split(self::OPERATOR_REGEXP, $first, -1, PREG_SPLIT_DELIM_CAPTURE);
/** @var list<string> $secondParts */
$secondParts = preg_split(self::OPERATOR_REGEXP, $second, -1, PREG_SPLIT_DELIM_CAPTURE);
$minPartsCount = min(count($firstParts), count($secondParts));
for ($i = 0; $i < $minPartsCount; $i++) {
if ($firstParts[$i] === '?->' && $secondParts[$i] === '->') {
continue;
}
if ($firstParts[$i] !== $secondParts[$i]) {
return false;
}
}
return array_key_exists($minPartsCount, $secondParts) && $secondParts[$minPartsCount] === '->';
}
private function getIdentificatorDifference(
File $phpcsFile,
string $identificator,
int $nextIdentificatorStartPointer,
int $nextIdentificatorEndPointer
): string
{
$objectOperatorsCountInIdentificator = substr_count($identificator, '->');
$tokens = $phpcsFile->getTokens();
$objectOperatorsCountInNextIdentificator = 0;
$differencePointer = $nextIdentificatorStartPointer;
for ($i = $nextIdentificatorStartPointer; $i <= $nextIdentificatorEndPointer; $i++) {
if (in_array($tokens[$i]['code'], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true)) {
$objectOperatorsCountInNextIdentificator++;
}
if ($objectOperatorsCountInNextIdentificator > $objectOperatorsCountInIdentificator) {
$differencePointer = $i;
break;
}
}
return TokenHelper::getContent($phpcsFile, $differencePointer, $nextIdentificatorEndPointer);
}
/**
* @return array{0: int, 1: int}
*/
private function getIdenticalData(File $phpcsFile, int $identicalPointer): array
{
/** @var int $pointerBeforeIdentical */
$pointerBeforeIdentical = TokenHelper::findPreviousEffective($phpcsFile, $identicalPointer - 1);
/** @var int $pointerAfterIdentical */
$pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $identicalPointer + 1);
return [$pointerBeforeIdentical, $pointerAfterIdentical];
}
/**
* @return array{0: int|null, 1: int|null, 2: int|null}
*/
private function getConditionData(File $phpcsFile, int $pointerBeforeIdentical, int $pointerAfterIdentical): array
{
$tokens = $phpcsFile->getTokens();
$isYoda = $tokens[$pointerBeforeIdentical]['code'] === T_NULL;
if ($isYoda) {
$identificatorStartPointer = $pointerAfterIdentical;
$identificatorEndPointer = $this->findIdentificatorEnd($phpcsFile, $identificatorStartPointer);
$conditionStartPointer = $pointerBeforeIdentical;
} else {
$identificatorEndPointer = $pointerBeforeIdentical;
$identificatorStartPointer = $this->findIdentificatorStart($phpcsFile, $identificatorEndPointer);
$conditionStartPointer = $identificatorStartPointer;
}
return [$identificatorStartPointer, $identificatorEndPointer, $conditionStartPointer];
}
}

View File

@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\TernaryOperatorHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function ltrim;
use function sprintf;
use function trim;
use const T_BOOLEAN_NOT;
use const T_INLINE_ELSE;
use const T_INLINE_THEN;
class RequireShortTernaryOperatorSniff implements Sniff
{
public const CODE_REQUIRED_SHORT_TERNARY_OPERATOR = 'RequiredShortTernaryOperator';
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_INLINE_THEN,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $inlineThenPointer
*/
public function process(File $phpcsFile, $inlineThenPointer): void
{
$tokens = $phpcsFile->getTokens();
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1);
if ($tokens[$nextPointer]['code'] === T_INLINE_ELSE) {
return;
}
$conditionStartPointer = TernaryOperatorHelper::getStartPointer($phpcsFile, $inlineThenPointer);
$inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer);
$inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer);
$thenContent = trim(TokenHelper::getContent($phpcsFile, $inlineThenPointer + 1, $inlineElsePointer - 1));
$elseContent = trim(TokenHelper::getContent($phpcsFile, $inlineElsePointer + 1, $inlineElseEndPointer));
$conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1);
$condition = TokenHelper::getContent($phpcsFile, $conditionStartPointer, $conditionEndPointer);
if ($tokens[$conditionStartPointer]['code'] === T_BOOLEAN_NOT) {
if ($elseContent !== ltrim($condition, '!')) {
return;
}
} else {
if ($thenContent !== $condition) {
return;
}
}
$fix = $phpcsFile->addFixableError('Use short ternary operator.', $inlineThenPointer, self::CODE_REQUIRED_SHORT_TERNARY_OPERATOR);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
if ($tokens[$conditionStartPointer]['code'] === T_BOOLEAN_NOT) {
$phpcsFile->fixer->replaceToken($conditionStartPointer, '');
FixerHelper::change($phpcsFile, $inlineThenPointer, $inlineElseEndPointer, sprintf('?: %s', $thenContent));
} else {
FixerHelper::removeBetween($phpcsFile, $inlineThenPointer, $inlineElsePointer);
}
$phpcsFile->fixer->endChangeset();
}
}

View File

@@ -0,0 +1,105 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function sprintf;
use function strlen;
class RequireSingleLineConditionSniff extends AbstractLineCondition
{
public const CODE_REQUIRED_SINGLE_LINE_CONDITION = 'RequiredSingleLineCondition';
/** @var int */
public $maxLineLength = 120;
/** @var bool */
public $alwaysForSimpleConditions = true;
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $controlStructurePointer
*/
public function process(File $phpcsFile, $controlStructurePointer): void
{
$this->maxLineLength = SniffSettingsHelper::normalizeInteger($this->maxLineLength);
if ($this->shouldBeSkipped($phpcsFile, $controlStructurePointer)) {
return;
}
$tokens = $phpcsFile->getTokens();
$parenthesisOpenerPointer = $tokens[$controlStructurePointer]['parenthesis_opener'];
$parenthesisCloserPointer = $tokens[$controlStructurePointer]['parenthesis_closer'];
if ($tokens[$parenthesisOpenerPointer]['line'] === $tokens[$parenthesisCloserPointer]['line']) {
return;
}
if (TokenHelper::findNext(
$phpcsFile,
TokenHelper::$inlineCommentTokenCodes,
$parenthesisOpenerPointer + 1,
$parenthesisCloserPointer
) !== null) {
return;
}
$lineStart = $this->getLineStart($phpcsFile, $parenthesisOpenerPointer);
$condition = $this->getCondition($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer);
$lineEnd = $this->getLineEnd($phpcsFile, $parenthesisCloserPointer);
$lineLength = strlen($lineStart . $condition . $lineEnd);
$isSimpleCondition = TokenHelper::findNext(
$phpcsFile,
Tokens::$booleanOperators,
$parenthesisOpenerPointer + 1,
$parenthesisCloserPointer
) === null;
if (!$this->shouldReportError($lineLength, $isSimpleCondition)) {
return;
}
$fix = $phpcsFile->addFixableError(
sprintf(
'Condition of "%s" should be placed on a single line.',
$this->getControlStructureName($phpcsFile, $controlStructurePointer)
),
$controlStructurePointer,
self::CODE_REQUIRED_SINGLE_LINE_CONDITION
);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addContent($parenthesisOpenerPointer, $condition);
FixerHelper::removeBetween($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer);
$phpcsFile->fixer->endChangeset();
}
private function shouldReportError(int $lineLength, bool $isSimpleCondition): bool
{
if ($this->maxLineLength === 0) {
return true;
}
if ($lineLength <= $this->maxLineLength) {
return true;
}
return $isSimpleCondition && $this->alwaysForSimpleConditions;
}
}

View File

@@ -0,0 +1,266 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\IdentificatorHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_key_exists;
use function sprintf;
use const T_BITWISE_AND;
use const T_ELSE;
use const T_EQUAL;
use const T_IF;
use const T_INLINE_THEN;
use const T_LOGICAL_AND;
use const T_LOGICAL_OR;
use const T_LOGICAL_XOR;
use const T_RETURN;
use const T_SEMICOLON;
use const T_WHITESPACE;
class RequireTernaryOperatorSniff implements Sniff
{
public const CODE_TERNARY_OPERATOR_NOT_USED = 'TernaryOperatorNotUsed';
/** @var bool */
public $ignoreMultiLine = false;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_IF,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $ifPointer
*/
public function process(File $phpcsFile, $ifPointer): void
{
$tokens = $phpcsFile->getTokens();
if (!array_key_exists('scope_closer', $tokens[$ifPointer])) {
// If without curly braces is not supported.
return;
}
$elsePointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1);
if ($elsePointer === null || $tokens[$elsePointer]['code'] !== T_ELSE) {
return;
}
if (!array_key_exists('scope_closer', $tokens[$elsePointer])) {
// Else without curly braces is not supported.
return;
}
if (
!$this->isCompatibleScope($phpcsFile, $tokens[$ifPointer]['scope_opener'], $tokens[$ifPointer]['scope_closer'])
|| !$this->isCompatibleScope($phpcsFile, $tokens[$elsePointer]['scope_opener'], $tokens[$elsePointer]['scope_closer'])
) {
return;
}
/** @var int $firstPointerInIf */
$firstPointerInIf = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_opener'] + 1);
/** @var int $firstPointerInElse */
$firstPointerInElse = TokenHelper::findNextEffective($phpcsFile, $tokens[$elsePointer]['scope_opener'] + 1);
if ($tokens[$firstPointerInIf]['code'] === T_RETURN && $tokens[$firstPointerInElse]['code'] === T_RETURN) {
$this->checkIfWithReturns($phpcsFile, $ifPointer, $elsePointer, $firstPointerInIf, $firstPointerInElse);
return;
}
$this->checkIfWithAssignments($phpcsFile, $ifPointer, $elsePointer, $firstPointerInIf, $firstPointerInElse);
}
private function checkIfWithReturns(File $phpcsFile, int $ifPointer, int $elsePointer, int $returnInIf, int $returnInElse): void
{
$ifContainsComment = $this->containsComment($phpcsFile, $ifPointer);
$elseContainsComment = $this->containsComment($phpcsFile, $elsePointer);
$conditionContainsLogicalOperators = $this->containsLogicalOperators($phpcsFile, $ifPointer);
$errorParameters = [
'Use ternary operator.',
$ifPointer,
self::CODE_TERNARY_OPERATOR_NOT_USED,
];
if ($ifContainsComment || $elseContainsComment || $conditionContainsLogicalOperators) {
$phpcsFile->addError(...$errorParameters);
return;
}
$fix = $phpcsFile->addFixableError(...$errorParameters);
if (!$fix) {
return;
}
$tokens = $phpcsFile->getTokens();
$pointerAfterReturnInIf = TokenHelper::findNextEffective($phpcsFile, $returnInIf + 1);
/** @var int $semicolonAfterReturnInIf */
$semicolonAfterReturnInIf = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfterReturnInIf + 1);
$pointerAfterReturnInElse = TokenHelper::findNextEffective($phpcsFile, $returnInElse + 1);
$semicolonAfterReturnInElse = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfterReturnInElse + 1);
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->replaceToken($ifPointer, 'return');
if ($ifPointer + 1 === $tokens[$ifPointer]['parenthesis_opener']) {
$phpcsFile->fixer->addContent($ifPointer, ' ');
}
$phpcsFile->fixer->replaceToken($tokens[$ifPointer]['parenthesis_opener'], '');
$phpcsFile->fixer->replaceToken($tokens[$ifPointer]['parenthesis_closer'], ' ? ');
FixerHelper::removeBetween($phpcsFile, $tokens[$ifPointer]['parenthesis_closer'], $pointerAfterReturnInIf);
FixerHelper::change($phpcsFile, $semicolonAfterReturnInIf, $pointerAfterReturnInElse - 1, ' : ');
FixerHelper::removeBetweenIncluding($phpcsFile, $semicolonAfterReturnInElse + 1, $tokens[$elsePointer]['scope_closer']);
$phpcsFile->fixer->endChangeset();
}
private function checkIfWithAssignments(
File $phpcsFile,
int $ifPointer,
int $elsePointer,
int $firstPointerInIf,
int $firstPointerInElse
): void
{
$tokens = $phpcsFile->getTokens();
$identificatorEndPointerInIf = IdentificatorHelper::findEndPointer($phpcsFile, $firstPointerInIf);
$identificatorEndPointerInElse = IdentificatorHelper::findEndPointer($phpcsFile, $firstPointerInElse);
if ($identificatorEndPointerInIf === null || $identificatorEndPointerInElse === null) {
return;
}
$identificatorInIf = TokenHelper::getContent($phpcsFile, $firstPointerInIf, $identificatorEndPointerInIf);
$identificatorInElse = TokenHelper::getContent($phpcsFile, $firstPointerInElse, $identificatorEndPointerInElse);
if ($identificatorInIf !== $identificatorInElse) {
return;
}
$assignmentPointerInIf = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointerInIf + 1);
$assignmentPointerInElse = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointerInElse + 1);
if (
$tokens[$assignmentPointerInIf]['code'] !== T_EQUAL
|| $tokens[$assignmentPointerInElse]['code'] !== T_EQUAL
) {
return;
}
$pointerAfterAssignmentInIf = TokenHelper::findNextEffective($phpcsFile, $assignmentPointerInIf + 1);
$pointerAfterAssignmentInElse = TokenHelper::findNextEffective($phpcsFile, $assignmentPointerInElse + 1);
if (
$tokens[$pointerAfterAssignmentInIf]['code'] === T_BITWISE_AND ||
$tokens[$pointerAfterAssignmentInElse]['code'] === T_BITWISE_AND
) {
return;
}
$ifContainsComment = $this->containsComment($phpcsFile, $ifPointer);
$elseContainsComment = $this->containsComment($phpcsFile, $elsePointer);
$conditionContainsLogicalOperators = $this->containsLogicalOperators($phpcsFile, $ifPointer);
$errorParameters = [
'Use ternary operator.',
$ifPointer,
self::CODE_TERNARY_OPERATOR_NOT_USED,
];
if ($ifContainsComment || $elseContainsComment || $conditionContainsLogicalOperators) {
$phpcsFile->addError(...$errorParameters);
return;
}
$fix = $phpcsFile->addFixableError(...$errorParameters);
if (!$fix) {
return;
}
/** @var int $semicolonAfterAssignmentInIf */
$semicolonAfterAssignmentInIf = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfterAssignmentInIf + 1);
$semicolonAfterAssignmentInElse = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfterAssignmentInElse + 1);
$phpcsFile->fixer->beginChangeset();
FixerHelper::change($phpcsFile, $ifPointer, $tokens[$ifPointer]['parenthesis_opener'], sprintf('%s = ', $identificatorInIf));
FixerHelper::change($phpcsFile, $tokens[$ifPointer]['parenthesis_closer'], $pointerAfterAssignmentInIf - 1, ' ? ');
FixerHelper::change($phpcsFile, $semicolonAfterAssignmentInIf, $pointerAfterAssignmentInElse - 1, ' : ');
FixerHelper::removeBetweenIncluding($phpcsFile, $semicolonAfterAssignmentInElse + 1, $tokens[$elsePointer]['scope_closer']);
$phpcsFile->fixer->endChangeset();
}
private function isCompatibleScope(File $phpcsFile, int $scopeOpenerPointer, int $scopeCloserPointer): bool
{
$semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $scopeOpenerPointer + 1, $scopeCloserPointer);
if ($semicolonPointer === null) {
return false;
}
if (TokenHelper::findNext($phpcsFile, T_INLINE_THEN, $scopeOpenerPointer + 1, $semicolonPointer) !== null) {
return false;
}
if ($this->ignoreMultiLine) {
$firstContentPointer = TokenHelper::findNextEffective($phpcsFile, $scopeOpenerPointer + 1);
if (TokenHelper::findNextContent(
$phpcsFile,
T_WHITESPACE,
$phpcsFile->eolChar,
$firstContentPointer + 1,
$semicolonPointer
) !== null) {
return false;
}
}
$pointerAfterSemicolon = TokenHelper::findNextEffective($phpcsFile, $semicolonPointer + 1);
return $pointerAfterSemicolon === $scopeCloserPointer;
}
private function containsComment(File $phpcsFile, int $scopeOwnerPointer): bool
{
$tokens = $phpcsFile->getTokens();
return TokenHelper::findNext(
$phpcsFile,
Tokens::$commentTokens,
$tokens[$scopeOwnerPointer]['scope_opener'] + 1,
$tokens[$scopeOwnerPointer]['scope_closer']
) !== null;
}
private function containsLogicalOperators(File $phpcsFile, int $scopeOwnerPointer): bool
{
$tokens = $phpcsFile->getTokens();
return TokenHelper::findNext(
$phpcsFile,
[T_LOGICAL_AND, T_LOGICAL_OR, T_LOGICAL_XOR],
$tokens[$scopeOwnerPointer]['parenthesis_opener'] + 1,
$tokens[$scopeOwnerPointer]['parenthesis_closer']
) !== null;
}
}

View File

@@ -0,0 +1,74 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\YodaHelper;
use function count;
use const T_IS_EQUAL;
use const T_IS_IDENTICAL;
use const T_IS_NOT_EQUAL;
use const T_IS_NOT_IDENTICAL;
/**
* Bigger value must be on the right side:
*
* ($variable, Foo::$class, Foo::bar(), foo())
* > (Foo::BAR, BAR)
* > (true, false, null, 1, 1.0, arrays, 'foo')
*/
class RequireYodaComparisonSniff implements Sniff
{
public const CODE_REQUIRED_YODA_COMPARISON = 'RequiredYodaComparison';
/** @var bool */
public $alwaysVariableOnRight = false;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_IS_IDENTICAL,
T_IS_NOT_IDENTICAL,
T_IS_EQUAL,
T_IS_NOT_EQUAL,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $comparisonTokenPointer
*/
public function process(File $phpcsFile, $comparisonTokenPointer): void
{
$tokens = $phpcsFile->getTokens();
$leftSideTokens = YodaHelper::getLeftSideTokens($tokens, $comparisonTokenPointer);
$rightSideTokens = YodaHelper::getRightSideTokens($tokens, $comparisonTokenPointer);
$leftDynamism = YodaHelper::getDynamismForTokens($tokens, $leftSideTokens);
$rightDynamism = YodaHelper::getDynamismForTokens($tokens, $rightSideTokens);
if ($leftDynamism === null || $rightDynamism === null) {
return;
}
if ($leftDynamism <= $rightDynamism) {
return;
}
if (!$this->alwaysVariableOnRight && $leftDynamism >= 900 && $rightDynamism >= 900) {
return;
}
$fix = $phpcsFile->addFixableError('Yoda comparison is required.', $comparisonTokenPointer, self::CODE_REQUIRED_YODA_COMPARISON);
if (!$fix || count($leftSideTokens) === 0 || count($rightSideTokens) === 0) {
return;
}
YodaHelper::fix($phpcsFile, $leftSideTokens, $rightSideTokens);
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use Exception;
use Throwable;
use function sprintf;
class UnsupportedKeywordException extends Exception
{
public function __construct(string $keyword, ?Throwable $previous = null)
{
parent::__construct(sprintf('"%s" is not supported.', $keyword), 0, $previous);
}
}

View File

@@ -0,0 +1,216 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\ConditionHelper;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_key_exists;
use function in_array;
use function sprintf;
use function strtolower;
use const T_ELSE;
use const T_FALSE;
use const T_IF;
use const T_RETURN;
use const T_SEMICOLON;
use const T_TRUE;
class UselessIfConditionWithReturnSniff implements Sniff
{
public const CODE_USELESS_IF_CONDITION = 'UselessIfCondition';
/** @var bool */
public $assumeAllConditionExpressionsAreAlreadyBoolean = false;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_IF,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $ifPointer
*/
public function process(File $phpcsFile, $ifPointer): void
{
$tokens = $phpcsFile->getTokens();
if (!array_key_exists('scope_closer', $tokens[$ifPointer])) {
// If without curly braces is not supported.
return;
}
$ifBooleanPointer = $this->findBooleanAfterReturnInScope($phpcsFile, $tokens[$ifPointer]['scope_opener']);
if ($ifBooleanPointer === null) {
return;
}
$newCondition = static function () use ($phpcsFile, $tokens, $ifBooleanPointer, $ifPointer): string {
return strtolower($tokens[$ifBooleanPointer]['content']) === 'true'
? TokenHelper::getContent(
$phpcsFile,
$tokens[$ifPointer]['parenthesis_opener'] + 1,
$tokens[$ifPointer]['parenthesis_closer'] - 1
)
: ConditionHelper::getNegativeCondition(
$phpcsFile,
$tokens[$ifPointer]['parenthesis_opener'] + 1,
$tokens[$ifPointer]['parenthesis_closer'] - 1
);
};
$elsePointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1);
$errorParameters = [
'Useless condition.',
$ifPointer,
self::CODE_USELESS_IF_CONDITION,
];
if (
$elsePointer !== null
&& $tokens[$elsePointer]['code'] === T_ELSE
) {
if (!array_key_exists('scope_closer', $tokens[$elsePointer])) {
// Else without curly braces is not supported.
return;
}
$elseBooleanPointer = $this->findBooleanAfterReturnInScope($phpcsFile, $tokens[$elsePointer]['scope_opener']);
if ($elseBooleanPointer === null) {
return;
}
if (!$this->isFixable($phpcsFile, $ifPointer, $tokens[$elsePointer]['scope_closer'])) {
$phpcsFile->addError(...$errorParameters);
return;
}
$fix = $phpcsFile->addFixableError(...$errorParameters);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
FixerHelper::change($phpcsFile, $ifPointer, $tokens[$elsePointer]['scope_closer'], sprintf('return %s;', $newCondition()));
$phpcsFile->fixer->endChangeset();
} else {
$returnPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1);
if ($returnPointer === null) {
return;
}
if ($tokens[$returnPointer]['code'] !== T_RETURN) {
return;
}
$semicolonPointer = $this->findSemicolonAfterReturnWithBoolean($phpcsFile, $returnPointer);
if ($semicolonPointer === null) {
return;
}
if (!$this->isFixable($phpcsFile, $ifPointer, $semicolonPointer)) {
$phpcsFile->addError(...$errorParameters);
return;
}
$fix = $phpcsFile->addFixableError(...$errorParameters);
if (!$fix) {
return;
}
$phpcsFile->fixer->beginChangeset();
FixerHelper::change($phpcsFile, $ifPointer, $semicolonPointer, sprintf('return %s;', $newCondition()));
$phpcsFile->fixer->endChangeset();
}
}
private function isFixable(File $phpcsFile, int $ifPointer, int $endPointer): bool
{
$tokens = $phpcsFile->getTokens();
if (TokenHelper::findNext($phpcsFile, Tokens::$commentTokens, $ifPointer + 1, $endPointer) !== null) {
return false;
}
if ($this->assumeAllConditionExpressionsAreAlreadyBoolean) {
return true;
}
return ConditionHelper::conditionReturnsBoolean(
$phpcsFile,
$tokens[$ifPointer]['parenthesis_opener'] + 1,
$tokens[$ifPointer]['parenthesis_closer'] - 1
);
}
private function findBooleanAfterReturnInScope(File $phpcsFile, int $scopeOpenerPointer): ?int
{
$tokens = $phpcsFile->getTokens();
/** @var int $returnPointer */
$returnPointer = TokenHelper::findNextEffective($phpcsFile, $scopeOpenerPointer + 1);
if ($tokens[$returnPointer]['code'] !== T_RETURN) {
return null;
}
$booleanPointer = $this->findBooleanAfterReturn($phpcsFile, $returnPointer);
if ($booleanPointer === null) {
return null;
}
$semicolonPointer = TokenHelper::findNextEffective($phpcsFile, $booleanPointer + 1);
if ($tokens[$semicolonPointer]['code'] !== T_SEMICOLON) {
return null;
}
return $booleanPointer;
}
private function findBooleanAfterReturn(File $phpcsFile, int $returnPointer): ?int
{
$tokens = $phpcsFile->getTokens();
$booleanPointer = TokenHelper::findNextEffective($phpcsFile, $returnPointer + 1);
if (in_array($tokens[$booleanPointer]['code'], [T_TRUE, T_FALSE], true)) {
return $booleanPointer;
}
return null;
}
private function findSemicolonAfterReturnWithBoolean(File $phpcsFile, int $returnPointer): ?int
{
$tokens = $phpcsFile->getTokens();
$booleanPointer = $this->findBooleanAfterReturn($phpcsFile, $returnPointer);
if ($booleanPointer === null) {
return null;
}
$semicolonPointer = TokenHelper::findNextEffective($phpcsFile, $booleanPointer + 1);
if ($tokens[$semicolonPointer]['code'] !== T_SEMICOLON) {
return null;
}
return $semicolonPointer;
}
}

View File

@@ -0,0 +1,105 @@
<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\ConditionHelper;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\TernaryOperatorHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function in_array;
use const T_FALSE;
use const T_INLINE_ELSE;
use const T_INLINE_THEN;
use const T_TRUE;
class UselessTernaryOperatorSniff implements Sniff
{
public const CODE_USELESS_TERNARY_OPERATOR = 'UselessTernaryOperator';
/** @var bool */
public $assumeAllConditionExpressionsAreAlreadyBoolean = false;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_INLINE_THEN,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $inlineThenPointer
*/
public function process(File $phpcsFile, $inlineThenPointer): void
{
$tokens = $phpcsFile->getTokens();
$pointerAfterInlineThen = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1);
if ($tokens[$pointerAfterInlineThen]['code'] === T_INLINE_ELSE) {
$inlineElsePointer = $pointerAfterInlineThen;
} else {
if (!in_array($tokens[$pointerAfterInlineThen]['code'], [T_TRUE, T_FALSE], true)) {
return;
}
$inlineElsePointer = TokenHelper::findNextEffective($phpcsFile, $pointerAfterInlineThen + 1);
if ($tokens[$inlineElsePointer]['code'] !== T_INLINE_ELSE) {
return;
}
}
$pointerAfterInlineElse = TokenHelper::findNextEffective($phpcsFile, $inlineElsePointer + 1);
if (!in_array($tokens[$pointerAfterInlineElse]['code'], [T_TRUE, T_FALSE], true)) {
return;
}
$conditionStartPointer = TernaryOperatorHelper::getStartPointer($phpcsFile, $inlineThenPointer);
/** @var int $conditionEndPointer */
$conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1);
$errorParameters = [
'Useless ternary operator.',
$inlineThenPointer,
self::CODE_USELESS_TERNARY_OPERATOR,
];
if (
!$this->assumeAllConditionExpressionsAreAlreadyBoolean
&& !ConditionHelper::conditionReturnsBoolean($phpcsFile, $conditionStartPointer, $conditionEndPointer)
) {
if ($tokens[$pointerAfterInlineThen]['code'] !== T_INLINE_ELSE) {
$phpcsFile->addError(...$errorParameters);
}
return;
}
$fix = $phpcsFile->addFixableError(...$errorParameters);
if (!$fix) {
return;
}
$inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer);
$pointerAfterTernaryOperator = TokenHelper::findNextEffective($phpcsFile, $inlineElseEndPointer + 1);
$phpcsFile->fixer->beginChangeset();
if ($tokens[$pointerAfterInlineThen]['code'] === T_FALSE) {
$negativeCondition = ConditionHelper::getNegativeCondition($phpcsFile, $conditionStartPointer, $conditionEndPointer);
FixerHelper::change($phpcsFile, $conditionStartPointer, $conditionEndPointer, $negativeCondition);
}
FixerHelper::removeBetween($phpcsFile, $conditionEndPointer, $pointerAfterTernaryOperator);
$phpcsFile->fixer->endChangeset();
}
}