<?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 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; } }