356 lines
10 KiB
PHP
356 lines
10 KiB
PHP
|
<?php declare(strict_types = 1);
|
||
|
|
||
|
namespace SlevomatCodingStandard\Helpers;
|
||
|
|
||
|
use PHP_CodeSniffer\Files\File;
|
||
|
use PHP_CodeSniffer\Util\Tokens;
|
||
|
use function array_key_exists;
|
||
|
use function array_merge;
|
||
|
use function count;
|
||
|
use function in_array;
|
||
|
use function preg_replace;
|
||
|
use function sprintf;
|
||
|
use function strtolower;
|
||
|
use function trim;
|
||
|
use const T_BITWISE_AND;
|
||
|
use const T_BOOLEAN_AND;
|
||
|
use const T_BOOLEAN_NOT;
|
||
|
use const T_BOOLEAN_OR;
|
||
|
use const T_CLOSE_PARENTHESIS;
|
||
|
use const T_COALESCE;
|
||
|
use const T_GREATER_THAN;
|
||
|
use const T_INLINE_THEN;
|
||
|
use const T_INSTANCEOF;
|
||
|
use const T_IS_EQUAL;
|
||
|
use const T_IS_GREATER_OR_EQUAL;
|
||
|
use const T_IS_IDENTICAL;
|
||
|
use const T_IS_NOT_EQUAL;
|
||
|
use const T_IS_NOT_IDENTICAL;
|
||
|
use const T_IS_SMALLER_OR_EQUAL;
|
||
|
use const T_LESS_THAN;
|
||
|
use const T_LOGICAL_AND;
|
||
|
use const T_LOGICAL_OR;
|
||
|
use const T_LOGICAL_XOR;
|
||
|
use const T_OPEN_PARENTHESIS;
|
||
|
use const T_PARENT;
|
||
|
use const T_SELF;
|
||
|
use const T_STATIC;
|
||
|
use const T_STRING;
|
||
|
use const T_VARIABLE;
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
class ConditionHelper
|
||
|
{
|
||
|
|
||
|
public static function conditionReturnsBoolean(
|
||
|
File $phpcsFile,
|
||
|
int $conditionBoundaryStartPointer,
|
||
|
int $conditionBoundaryEndPointer
|
||
|
): bool
|
||
|
{
|
||
|
$tokens = $phpcsFile->getTokens();
|
||
|
|
||
|
$conditionContent = strtolower(
|
||
|
trim(TokenHelper::getContent($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer))
|
||
|
);
|
||
|
|
||
|
if ($conditionContent === 'false' || $conditionContent === 'true') {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
$actualPointer = $conditionBoundaryStartPointer;
|
||
|
|
||
|
do {
|
||
|
$actualPointer = TokenHelper::findNext(
|
||
|
$phpcsFile,
|
||
|
array_merge(
|
||
|
[T_OPEN_PARENTHESIS, T_LESS_THAN, T_GREATER_THAN],
|
||
|
Tokens::$booleanOperators,
|
||
|
Tokens::$equalityTokens
|
||
|
),
|
||
|
$actualPointer,
|
||
|
$conditionBoundaryEndPointer + 1
|
||
|
);
|
||
|
|
||
|
if ($actualPointer === null) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if ($tokens[$actualPointer]['code'] === T_OPEN_PARENTHESIS) {
|
||
|
$actualPointer = $tokens[$actualPointer]['parenthesis_closer'];
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
|
||
|
} while (true);
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
public static function getNegativeCondition(
|
||
|
File $phpcsFile,
|
||
|
int $conditionBoundaryStartPointer,
|
||
|
int $conditionBoundaryEndPointer,
|
||
|
bool $nested = false
|
||
|
): string
|
||
|
{
|
||
|
/** @var int $conditionStartPointer */
|
||
|
$conditionStartPointer = TokenHelper::findNextEffective($phpcsFile, $conditionBoundaryStartPointer);
|
||
|
/** @var int $conditionEndPointer */
|
||
|
$conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $conditionBoundaryEndPointer);
|
||
|
|
||
|
$tokens = $phpcsFile->getTokens();
|
||
|
if (
|
||
|
$tokens[$conditionStartPointer]['code'] === T_OPEN_PARENTHESIS
|
||
|
&& $tokens[$conditionStartPointer]['parenthesis_closer'] === $conditionEndPointer
|
||
|
) {
|
||
|
/** @var int $conditionStartPointer */
|
||
|
$conditionStartPointer = TokenHelper::findNextEffective($phpcsFile, $conditionStartPointer + 1);
|
||
|
/** @var int $conditionEndPointer */
|
||
|
$conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $conditionEndPointer - 1);
|
||
|
}
|
||
|
|
||
|
return sprintf(
|
||
|
'%s%s%s',
|
||
|
$conditionBoundaryStartPointer !== $conditionStartPointer
|
||
|
? TokenHelper::getContent(
|
||
|
$phpcsFile,
|
||
|
$conditionBoundaryStartPointer,
|
||
|
$conditionStartPointer - 1
|
||
|
)
|
||
|
: '',
|
||
|
self::getNegativeConditionPart($phpcsFile, $conditionStartPointer, $conditionEndPointer, $nested),
|
||
|
$conditionBoundaryEndPointer !== $conditionEndPointer
|
||
|
? TokenHelper::getContent(
|
||
|
$phpcsFile,
|
||
|
$conditionEndPointer + 1,
|
||
|
$conditionBoundaryEndPointer
|
||
|
)
|
||
|
: ''
|
||
|
);
|
||
|
}
|
||
|
|
||
|
private static function getNegativeConditionPart(
|
||
|
File $phpcsFile,
|
||
|
int $conditionBoundaryStartPointer,
|
||
|
int $conditionBoundaryEndPointer,
|
||
|
bool $nested
|
||
|
): string
|
||
|
{
|
||
|
$tokens = $phpcsFile->getTokens();
|
||
|
|
||
|
$condition = TokenHelper::getContent($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer);
|
||
|
|
||
|
if (strtolower($condition) === 'true') {
|
||
|
return 'false';
|
||
|
}
|
||
|
if (strtolower($condition) === 'false') {
|
||
|
return 'true';
|
||
|
}
|
||
|
|
||
|
$pointerAfterConditionStart = TokenHelper::findNextEffective($phpcsFile, $conditionBoundaryStartPointer);
|
||
|
$booleanPointers = TokenHelper::findNextAll(
|
||
|
$phpcsFile,
|
||
|
Tokens::$booleanOperators,
|
||
|
$conditionBoundaryStartPointer,
|
||
|
$conditionBoundaryEndPointer + 1
|
||
|
);
|
||
|
|
||
|
if ($tokens[$pointerAfterConditionStart]['code'] === T_BOOLEAN_NOT) {
|
||
|
$pointerAfterBooleanNot = TokenHelper::findNextEffective($phpcsFile, $pointerAfterConditionStart + 1);
|
||
|
if ($tokens[$pointerAfterBooleanNot]['code'] === T_OPEN_PARENTHESIS) {
|
||
|
if ($nested && $booleanPointers !== []) {
|
||
|
return self::removeBooleanNot($condition);
|
||
|
}
|
||
|
|
||
|
$pointerAfterParenthesisCloser = TokenHelper::findNextEffective(
|
||
|
$phpcsFile,
|
||
|
$tokens[$pointerAfterBooleanNot]['parenthesis_closer'] + 1,
|
||
|
$conditionBoundaryEndPointer + 1
|
||
|
);
|
||
|
if (
|
||
|
$pointerAfterParenthesisCloser === null
|
||
|
|| $pointerAfterParenthesisCloser === $conditionBoundaryEndPointer
|
||
|
) {
|
||
|
return TokenHelper::getContent(
|
||
|
$phpcsFile,
|
||
|
$pointerAfterBooleanNot + 1,
|
||
|
$tokens[$pointerAfterBooleanNot]['parenthesis_closer'] - 1
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (count($booleanPointers) > 0) {
|
||
|
return self::getNegativeLogicalCondition($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer);
|
||
|
}
|
||
|
|
||
|
if ($tokens[$pointerAfterConditionStart]['code'] === T_BOOLEAN_NOT) {
|
||
|
return self::removeBooleanNot($condition);
|
||
|
}
|
||
|
|
||
|
if (TokenHelper::findNext(
|
||
|
$phpcsFile,
|
||
|
[T_INSTANCEOF, T_BITWISE_AND, T_COALESCE, T_INLINE_THEN],
|
||
|
$conditionBoundaryStartPointer,
|
||
|
$conditionBoundaryEndPointer + 1
|
||
|
) !== null) {
|
||
|
return sprintf('!(%s)', $condition);
|
||
|
}
|
||
|
|
||
|
if ($tokens[$pointerAfterConditionStart]['code'] === T_STRING) {
|
||
|
$pointerAfterConditionStart = TokenHelper::findNextEffective($phpcsFile, $pointerAfterConditionStart + 1);
|
||
|
if (
|
||
|
$tokens[$pointerAfterConditionStart]['code'] === T_OPEN_PARENTHESIS
|
||
|
&& $tokens[$pointerAfterConditionStart]['parenthesis_closer'] === $conditionBoundaryEndPointer
|
||
|
) {
|
||
|
return sprintf('!%s', $condition);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (in_array($tokens[$pointerAfterConditionStart]['code'], [T_VARIABLE, T_SELF, T_STATIC, T_PARENT], true)) {
|
||
|
$identificatorEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $pointerAfterConditionStart);
|
||
|
$pointerAfterIdentificatorEnd = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointer + 1);
|
||
|
if (
|
||
|
$tokens[$pointerAfterIdentificatorEnd]['code'] === T_OPEN_PARENTHESIS
|
||
|
&& $tokens[$pointerAfterIdentificatorEnd]['parenthesis_closer'] === $conditionBoundaryEndPointer
|
||
|
) {
|
||
|
return sprintf('!%s', $condition);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$comparisonPointer = TokenHelper::findNext(
|
||
|
$phpcsFile,
|
||
|
[T_IS_EQUAL, T_IS_NOT_EQUAL, T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_IS_SMALLER_OR_EQUAL, T_IS_GREATER_OR_EQUAL, T_LESS_THAN, T_GREATER_THAN],
|
||
|
$conditionBoundaryStartPointer,
|
||
|
$conditionBoundaryEndPointer + 1
|
||
|
);
|
||
|
if ($comparisonPointer !== null) {
|
||
|
$comparisonReplacements = [
|
||
|
T_IS_EQUAL => '!=',
|
||
|
T_IS_NOT_EQUAL => '==',
|
||
|
T_IS_IDENTICAL => '!==',
|
||
|
T_IS_NOT_IDENTICAL => '===',
|
||
|
T_IS_GREATER_OR_EQUAL => '<',
|
||
|
T_IS_SMALLER_OR_EQUAL => '>',
|
||
|
T_GREATER_THAN => '<=',
|
||
|
T_LESS_THAN => '>=',
|
||
|
];
|
||
|
|
||
|
$negativeCondition = '';
|
||
|
for ($i = $conditionBoundaryStartPointer; $i <= $conditionBoundaryEndPointer; $i++) {
|
||
|
// Skip calls()
|
||
|
if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) {
|
||
|
$negativeCondition .= TokenHelper::getContent($phpcsFile, $i, $tokens[$i]['parenthesis_closer']);
|
||
|
$i = $tokens[$i]['parenthesis_closer'];
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$negativeCondition .= array_key_exists($tokens[$i]['code'], $comparisonReplacements)
|
||
|
? $comparisonReplacements[$tokens[$i]['code']]
|
||
|
: $tokens[$i]['content'];
|
||
|
}
|
||
|
|
||
|
return $negativeCondition;
|
||
|
}
|
||
|
|
||
|
return sprintf('!%s', $condition);
|
||
|
}
|
||
|
|
||
|
private static function removeBooleanNot(string $condition): string
|
||
|
{
|
||
|
return preg_replace('~^!\\s*~', '', $condition);
|
||
|
}
|
||
|
|
||
|
private static function getNegativeLogicalCondition(
|
||
|
File $phpcsFile,
|
||
|
int $conditionBoundaryStartPointer,
|
||
|
int $conditionBoundaryEndPointer
|
||
|
): string
|
||
|
{
|
||
|
if (TokenHelper::findNext($phpcsFile, T_LOGICAL_XOR, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer) !== null) {
|
||
|
return sprintf('!(%s)', TokenHelper::getContent($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer));
|
||
|
}
|
||
|
|
||
|
$tokens = $phpcsFile->getTokens();
|
||
|
|
||
|
$booleanOperatorReplacements = [
|
||
|
T_BOOLEAN_AND => '||',
|
||
|
T_BOOLEAN_OR => '&&',
|
||
|
T_LOGICAL_AND => 'or',
|
||
|
T_LOGICAL_OR => 'and',
|
||
|
];
|
||
|
|
||
|
$negativeCondition = '';
|
||
|
|
||
|
$nestedConditionStartPointer = $conditionBoundaryStartPointer;
|
||
|
$actualPointer = $conditionBoundaryStartPointer;
|
||
|
$parenthesesLevel = 0;
|
||
|
|
||
|
$operatorsOnLevel = [];
|
||
|
|
||
|
do {
|
||
|
$actualPointer = TokenHelper::findNext(
|
||
|
$phpcsFile,
|
||
|
array_merge([T_OPEN_PARENTHESIS, T_CLOSE_PARENTHESIS], Tokens::$booleanOperators),
|
||
|
$actualPointer,
|
||
|
$conditionBoundaryEndPointer + 1
|
||
|
);
|
||
|
|
||
|
if ($actualPointer === null) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if ($tokens[$actualPointer]['code'] === T_OPEN_PARENTHESIS) {
|
||
|
$pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $actualPointer - 1);
|
||
|
if ($tokens[$pointerBeforeParenthesisOpener]['code'] === T_STRING) {
|
||
|
$actualPointer = $tokens[$actualPointer]['parenthesis_closer'] + 1;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$parenthesesLevel++;
|
||
|
$actualPointer++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ($tokens[$actualPointer]['code'] === T_CLOSE_PARENTHESIS) {
|
||
|
$parenthesesLevel--;
|
||
|
$actualPointer++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ($parenthesesLevel !== 0) {
|
||
|
$actualPointer++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
array_key_exists($parenthesesLevel, $operatorsOnLevel)
|
||
|
&& $operatorsOnLevel[$parenthesesLevel] !== $tokens[$actualPointer]['code']
|
||
|
) {
|
||
|
return sprintf('!(%s)', TokenHelper::getContent($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer));
|
||
|
}
|
||
|
|
||
|
$operatorsOnLevel[$parenthesesLevel] = $tokens[$actualPointer]['code'];
|
||
|
|
||
|
$negativeCondition .= self::getNegativeCondition($phpcsFile, $nestedConditionStartPointer, $actualPointer - 1, true);
|
||
|
$negativeCondition .= $booleanOperatorReplacements[$tokens[$actualPointer]['code']];
|
||
|
|
||
|
$nestedConditionStartPointer = $actualPointer + 1;
|
||
|
$actualPointer++;
|
||
|
|
||
|
} while (true);
|
||
|
|
||
|
return $negativeCondition . self::getNegativeCondition(
|
||
|
$phpcsFile,
|
||
|
$nestedConditionStartPointer,
|
||
|
$conditionBoundaryEndPointer,
|
||
|
true
|
||
|
);
|
||
|
}
|
||
|
|
||
|
}
|