wishthis/vendor/phpcsstandards/phpcsutils/PHPCSUtils/Utils/ControlStructures.php
2023-12-19 15:31:43 +01:00

278 lines
9.7 KiB
PHP

<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2020 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/
namespace PHPCSUtils\Utils;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
/**
* Utility functions for use when examining control structures.
*
* @since 1.0.0
*/
final class ControlStructures
{
/**
* Check whether a control structure has a body.
*
* Some control structures - `while`, `for` and `declare` - can be declared without a body, like:
* ```php
* while (++$i < 10);
* ```
*
* All other control structures will always have a body, though the body may be empty, where "empty" means:
* no _code_ is found in the body. If a control structure body only contains a comment, it will be
* regarded as empty.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the token we are checking.
* @param bool $allowEmpty Whether a control structure with an empty body should
* still be considered as having a body.
* Defaults to `true`.
*
* @return bool `TRUE` when the control structure has a body, or in case `$allowEmpty` is set to `FALSE`:
* when it has a non-empty body.
* `FALSE` in all other cases, including when a non-control structure token has been passed.
*/
public static function hasBody(File $phpcsFile, $stackPtr, $allowEmpty = true)
{
$tokens = $phpcsFile->getTokens();
// Check for the existence of the token.
if (isset($tokens[$stackPtr]) === false
|| isset(Collections::controlStructureTokens()[$tokens[$stackPtr]['code']]) === false
) {
return false;
}
// Handle `else if`.
if ($tokens[$stackPtr]['code'] === \T_ELSE && isset($tokens[$stackPtr]['scope_opener']) === false) {
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($next !== false && $tokens[$next]['code'] === \T_IF) {
$stackPtr = $next;
}
}
/*
* The scope markers are set. This is the simplest situation.
*/
if (isset($tokens[$stackPtr]['scope_opener']) === true) {
if ($allowEmpty === true) {
return true;
}
// Check whether the body is empty.
$start = ($tokens[$stackPtr]['scope_opener'] + 1);
$end = ($phpcsFile->numTokens + 1);
if (isset($tokens[$stackPtr]['scope_closer']) === true) {
$end = $tokens[$stackPtr]['scope_closer'];
}
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $start, $end, true);
if ($nextNonEmpty !== false) {
return true;
}
return false;
}
/*
* Control structure without scope markers.
* Either single line statement or inline control structure.
*
* - Single line statement doesn't have a body and is therefore always empty.
* - Inline control structure has to have a body and can never be empty.
*
* This code also needs to take live coding into account where a scope opener is found, but
* no scope closer.
*/
$searchStart = ($stackPtr + 1);
if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) {
$searchStart = ($tokens[$stackPtr]['parenthesis_closer'] + 1);
}
$nextNonEmpty = $phpcsFile->findNext(
Tokens::$emptyTokens,
$searchStart,
null,
true
);
if ($nextNonEmpty === false
|| $tokens[$nextNonEmpty]['code'] === \T_SEMICOLON
|| $tokens[$nextNonEmpty]['code'] === \T_CLOSE_TAG
) {
// Parse error or single line statement.
return false;
}
if ($tokens[$nextNonEmpty]['code'] === \T_OPEN_CURLY_BRACKET) {
if ($allowEmpty === true) {
return true;
}
// Unrecognized scope opener due to parse error.
$nextNext = $phpcsFile->findNext(
Tokens::$emptyTokens,
($nextNonEmpty + 1),
null,
true
);
if ($nextNext === false) {
return false;
}
return true;
}
return true;
}
/**
* Check whether an IF or ELSE token is part of an "else if".
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the token we are checking.
*
* @return bool
*/
public static function isElseIf(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// Check for the existence of the token.
if (isset($tokens[$stackPtr]) === false) {
return false;
}
if ($tokens[$stackPtr]['code'] === \T_ELSEIF) {
return true;
}
if ($tokens[$stackPtr]['code'] !== \T_ELSE && $tokens[$stackPtr]['code'] !== \T_IF) {
return false;
}
if ($tokens[$stackPtr]['code'] === \T_ELSE && isset($tokens[$stackPtr]['scope_opener']) === true) {
return false;
}
switch ($tokens[$stackPtr]['code']) {
case \T_ELSE:
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($next !== false && $tokens[$next]['code'] === \T_IF) {
return true;
}
break;
case \T_IF:
$previous = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($previous !== false && $tokens[$previous]['code'] === \T_ELSE) {
return true;
}
break;
}
return false;
}
/**
* Retrieve the exception(s) being caught in a CATCH condition.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the token we are checking.
*
* @return array<int, array<string, string|int>>
* Array with information about the caught Exception(s).
* The returned array will contain the following information for
* each caught exception:
* ```php
* 0 => array(
* 'type' => string, // The type declaration for the exception being caught.
* 'type_token' => integer, // The stack pointer to the start of the type declaration.
* 'type_end_token' => integer, // The stack pointer to the end of the type declaration.
* )
* ```
* In case of an invalid catch structure, the array may be empty.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified `$stackPtr` is not of
* type `T_CATCH` or doesn't exist.
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If no parenthesis opener or closer can be
* determined (parse error).
*/
public static function getCaughtExceptions(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]) === false
|| $tokens[$stackPtr]['code'] !== \T_CATCH
) {
throw new RuntimeException('$stackPtr must be of type T_CATCH');
}
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
throw new RuntimeException('Parentheses opener/closer of the T_CATCH could not be determined');
}
$opener = $tokens[$stackPtr]['parenthesis_opener'];
$closer = $tokens[$stackPtr]['parenthesis_closer'];
$exceptions = [];
$foundName = '';
$firstToken = null;
$lastToken = null;
for ($i = ($opener + 1); $i <= $closer; $i++) {
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) {
continue;
}
if (isset(Collections::namespacedNameTokens()[$tokens[$i]['code']]) === false) {
// Add the current exception to the result array if one was found.
if ($foundName !== '') {
$exceptions[] = [
'type' => $foundName,
'type_token' => $firstToken,
'type_end_token' => $lastToken,
];
}
if ($tokens[$i]['code'] === \T_BITWISE_OR) {
// Multi-catch. Reset and continue.
$foundName = '';
$firstToken = null;
$lastToken = null;
continue;
}
break;
}
if (isset($firstToken) === false) {
$firstToken = $i;
}
$foundName .= $tokens[$i]['content'];
$lastToken = $i;
}
return $exceptions;
}
}