<?php
/**
 * PHPCompatibility, an external standard for PHP_CodeSniffer.
 *
 * @package   PHPCompatibility
 * @copyright 2012-2019 PHPCompatibility Contributors
 * @license   https://opensource.org/licenses/LGPL-3.0 LGPL3
 * @link      https://github.com/PHPCompatibility/PHPCompatibility
 */

namespace PHPCompatibility;

use PHPCompatibility\PHPCSHelper;
use PHP_CodeSniffer_Exception as PHPCS_Exception;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Sniff as PHPCS_Sniff;
use PHP_CodeSniffer_Tokens as Tokens;

/**
 * Base class from which all PHPCompatibility sniffs extend.
 *
 * @since 5.6
 */
abstract class Sniff implements PHPCS_Sniff {


	/**
	 * Regex to match variables in a double quoted string.
	 *
	 * This matches plain variables, but also more complex variables, such
	 * as $obj->prop, self::prop and $var[].
	 *
	 * @since 7.1.2
	 *
	 * @var string
	 */
	const REGEX_COMPLEX_VARS = '`(?:(\{)?(?<!\\\\)\$)?(\{)?(?<!\\\\)\$(\{)?(?P<varname>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:->\$?(?P>varname)|\[[^\]]+\]|::\$?(?P>varname)|\([^\)]*\))*(?(3)\}|)(?(2)\}|)(?(1)\}|)`';

	/**
	 * List of superglobals as an array of strings.
	 *
	 * Used by the ForbiddenParameterShadowSuperGlobals and ForbiddenClosureUseVariableNames sniffs.
	 *
	 * @since 7.0.0
	 * @since 7.1.4 Moved from the `ForbiddenParameterShadowSuperGlobals` sniff to the base `Sniff` class.
	 *
	 * @var array
	 */
	protected $superglobals = array(
		'$GLOBALS'  => true,
		'$_SERVER'  => true,
		'$_GET'     => true,
		'$_POST'    => true,
		'$_FILES'   => true,
		'$_COOKIE'  => true,
		'$_SESSION' => true,
		'$_REQUEST' => true,
		'$_ENV'     => true,
	);

	/**
	 * List of functions using hash algorithm as parameter (always the first parameter).
	 *
	 * Used by the new/removed hash algorithm sniffs.
	 * Key is the function name, value is the 1-based parameter position in the function call.
	 *
	 * @since 5.5
	 * @since 7.0.7 Moved from the `RemovedHashAlgorithms` sniff to the base `Sniff` class.
	 *
	 * @var array
	 */
	protected $hashAlgoFunctions = array(
		'hash_file'      => 1,
		'hash_hmac_file' => 1,
		'hash_hmac'      => 1,
		'hash_init'      => 1,
		'hash_pbkdf2'    => 1,
		'hash'           => 1,
	);


	/**
	 * List of functions which take an ini directive as parameter (always the first parameter).
	 *
	 * Used by the new/removed ini directives sniffs.
	 * Key is the function name, value is the 1-based parameter position in the function call.
	 *
	 * @since 7.1.0
	 *
	 * @var array
	 */
	protected $iniFunctions = array(
		'ini_get' => 1,
		'ini_set' => 1,
	);


	/**
	 * Get the testVersion configuration variable.
	 *
	 * The testVersion configuration variable may be in any of the following formats:
	 * 1) Omitted/empty, in which case no version is specified. This effectively
	 *    disables all the checks for new PHP features provided by this standard.
	 * 2) A single PHP version number, e.g. "5.4" in which case the standard checks that
	 *    the code will run on that version of PHP (no deprecated features or newer
	 *    features being used).
	 * 3) A range, e.g. "5.0-5.5", in which case the standard checks the code will run
	 *    on all PHP versions in that range, and that it doesn't use any features that
	 *    were deprecated by the final version in the list, or which were not available
	 *    for the first version in the list.
	 *    We accept ranges where one of the components is missing, e.g. "-5.6" means
	 *    all versions up to PHP 5.6, and "7.0-" means all versions above PHP 7.0.
	 * PHP version numbers should always be in Major.Minor format.  Both "5", "5.3.2"
	 * would be treated as invalid, and ignored.
	 *
	 * @since 7.0.0
	 * @since 7.1.3 Now allows for partial ranges such as `5.2-`.
	 *
	 * @return array $arrTestVersions will hold an array containing min/max version
	 *               of PHP that we are checking against (see above).  If only a
	 *               single version number is specified, then this is used as
	 *               both the min and max.
	 *
	 * @throws \PHP_CodeSniffer_Exception If testVersion is invalid.
	 */
	private function getTestVersion() {
		static $arrTestVersions = array();

		$default     = array( null, null );
		$testVersion = trim( PHPCSHelper::getConfigData( 'testVersion' ) );

		if ( empty( $testVersion ) === false && isset( $arrTestVersions[ $testVersion ] ) === false ) {

			$arrTestVersions[ $testVersion ] = $default;

			if ( preg_match( '`^\d+\.\d+$`', $testVersion ) ) {
				$arrTestVersions[ $testVersion ] = array( $testVersion, $testVersion );
				return $arrTestVersions[ $testVersion ];
			}

			if ( preg_match( '`^(\d+\.\d+)?\s*-\s*(\d+\.\d+)?$`', $testVersion, $matches ) ) {
				if ( empty( $matches[1] ) === false || empty( $matches[2] ) === false ) {
					// If no lower-limit is set, we set the min version to 4.0.
					// Whilst development focuses on PHP 5 and above, we also accept
					// sniffs for PHP 4, so we include that as the minimum.
					// (It makes no sense to support PHP 3 as this was effectively a
					// different language).
					$min = empty( $matches[1] ) ? '4.0' : $matches[1];

					// If no upper-limit is set, we set the max version to 99.9.
					$max = empty( $matches[2] ) ? '99.9' : $matches[2];

					if ( version_compare( $min, $max, '>' ) ) {
						trigger_error(
							"Invalid range in testVersion setting: '" . $testVersion . "'",
							\E_USER_WARNING
						);
						return $default;
					} else {
						$arrTestVersions[ $testVersion ] = array( $min, $max );
						return $arrTestVersions[ $testVersion ];
					}
				}
			}

			trigger_error(
				"Invalid testVersion setting: '" . $testVersion . "'",
				\E_USER_WARNING
			);
			return $default;
		}

		if ( isset( $arrTestVersions[ $testVersion ] ) ) {
			return $arrTestVersions[ $testVersion ];
		}

		return $default;
	}


	/**
	 * Check whether a specific PHP version is equal to or higher than the maximum
	 * supported PHP version as provided by the user in `testVersion`.
	 *
	 * Should be used when sniffing for *old* PHP features (deprecated/removed).
	 *
	 * @since 5.6
	 *
	 * @param string $phpVersion A PHP version number in 'major.minor' format.
	 *
	 * @return bool True if testVersion has not been provided or if the PHP version
	 *              is equal to or higher than the highest supported PHP version
	 *              in testVersion. False otherwise.
	 */
	public function supportsAbove( $phpVersion ) {
		$testVersion = $this->getTestVersion();
		$testVersion = $testVersion[1];

		if ( \is_null( $testVersion )
			|| version_compare( $testVersion, $phpVersion ) >= 0
		) {
			return true;
		} else {
			return false;
		}
	}


	/**
	 * Check whether a specific PHP version is equal to or lower than the minimum
	 * supported PHP version as provided by the user in `testVersion`.
	 *
	 * Should be used when sniffing for *new* PHP features.
	 *
	 * @since 5.6
	 *
	 * @param string $phpVersion A PHP version number in 'major.minor' format.
	 *
	 * @return bool True if the PHP version is equal to or lower than the lowest
	 *              supported PHP version in testVersion.
	 *              False otherwise or if no testVersion is provided.
	 */
	public function supportsBelow( $phpVersion ) {
		$testVersion = $this->getTestVersion();
		$testVersion = $testVersion[0];

		if ( \is_null( $testVersion ) === false
			&& version_compare( $testVersion, $phpVersion ) <= 0
		) {
			return true;
		} else {
			return false;
		}
	}


	/**
	 * Add a PHPCS message to the output stack as either a warning or an error.
	 *
	 * @since 7.1.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file the message applies to.
	 * @param string                $message   The message.
	 * @param int                   $stackPtr  The position of the token
	 *                                         the message relates to.
	 * @param bool                  $isError   Whether to report the message as an
	 *                                         'error' or 'warning'.
	 *                                         Defaults to true (error).
	 * @param string                $code      The error code for the message.
	 *                                         Defaults to 'Found'.
	 * @param array                 $data      Optional input for the data replacements.
	 *
	 * @return void
	 */
	public function addMessage( File $phpcsFile, $message, $stackPtr, $isError, $code = 'Found', $data = array() ) {
		if ( $isError === true ) {
			$phpcsFile->addError( $message, $stackPtr, $code, $data );
		} else {
			$phpcsFile->addWarning( $message, $stackPtr, $code, $data );
		}
	}


	/**
	 * Convert an arbitrary string to an alphanumeric string with underscores.
	 *
	 * Pre-empt issues with arbitrary strings being used as error codes in XML and PHP.
	 *
	 * @since 7.1.0
	 *
	 * @param string $baseString Arbitrary string.
	 *
	 * @return string
	 */
	public function stringToErrorCode( $baseString ) {
		return preg_replace( '`[^a-z0-9_]`i', '_', strtolower( $baseString ) );
	}


	/**
	 * Strip quotes surrounding an arbitrary string.
	 *
	 * Intended for use with the contents of a T_CONSTANT_ENCAPSED_STRING / T_DOUBLE_QUOTED_STRING.
	 *
	 * @since 7.0.6
	 *
	 * @param string $string The raw string.
	 *
	 * @return string String without quotes around it.
	 */
	public function stripQuotes( $string ) {
		return preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $string );
	}


	/**
	 * Strip variables from an arbitrary double quoted string.
	 *
	 * Intended for use with the contents of a T_DOUBLE_QUOTED_STRING.
	 *
	 * @since 7.1.2
	 *
	 * @param string $string The raw string.
	 *
	 * @return string String without variables in it.
	 */
	public function stripVariables( $string ) {
		if ( strpos( $string, '$' ) === false ) {
			return $string;
		}

		return preg_replace( self::REGEX_COMPLEX_VARS, '', $string );
	}


	/**
	 * Make all top level array keys in an array lowercase.
	 *
	 * @since 7.1.0
	 *
	 * @param array $array Initial array.
	 *
	 * @return array Same array, but with all lowercase top level keys.
	 */
	public function arrayKeysToLowercase( $array ) {
		return array_change_key_case( $array, \CASE_LOWER );
	}


	/**
	 * Checks if a function call has parameters.
	 *
	 * Expects to be passed the T_STRING or T_VARIABLE stack pointer for the function call.
	 * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
	 *
	 * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer, it
	 * will detect whether the array has values or is empty.
	 *
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/120
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/152
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the function call token.
	 *
	 * @return bool
	 */
	public function doesFunctionCallHaveParameters( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( isset( $tokens[ $stackPtr ] ) === false ) {
			return false;
		}

		// Is this one of the tokens this function handles ?
		if ( \in_array( $tokens[ $stackPtr ]['code'], array( \T_STRING, \T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_VARIABLE ), true ) === false ) {
			return false;
		}

		$nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true );

		// Deal with short array syntax.
		if ( $tokens[ $stackPtr ]['code'] === \T_OPEN_SHORT_ARRAY ) {
			if ( isset( $tokens[ $stackPtr ]['bracket_closer'] ) === false ) {
				return false;
			}

			if ( $nextNonEmpty === $tokens[ $stackPtr ]['bracket_closer'] ) {
				// No parameters.
				return false;
			} else {
				return true;
			}
		}

		// Deal with function calls & long arrays.
		// Next non-empty token should be the open parenthesis.
		if ( $nextNonEmpty === false && $tokens[ $nextNonEmpty ]['code'] !== \T_OPEN_PARENTHESIS ) {
			return false;
		}

		if ( isset( $tokens[ $nextNonEmpty ]['parenthesis_closer'] ) === false ) {
			return false;
		}

		$closeParenthesis = $tokens[ $nextNonEmpty ]['parenthesis_closer'];
		$nextNextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, $nextNonEmpty + 1, $closeParenthesis + 1, true );

		if ( $nextNextNonEmpty === $closeParenthesis ) {
			// No parameters.
			return false;
		}

		return true;
	}


	/**
	 * Count the number of parameters a function call has been passed.
	 *
	 * Expects to be passed the T_STRING or T_VARIABLE stack pointer for the function call.
	 * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
	 *
	 * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer,
	 * it will return the number of values in the array.
	 *
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/111
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/114
	 * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/151
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the function call token.
	 *
	 * @return int
	 */
	public function getFunctionCallParameterCount( File $phpcsFile, $stackPtr ) {
		if ( $this->doesFunctionCallHaveParameters( $phpcsFile, $stackPtr ) === false ) {
			return 0;
		}

		return \count( $this->getFunctionCallParameters( $phpcsFile, $stackPtr ) );
	}


	/**
	 * Get information on all parameters passed to a function call.
	 *
	 * Expects to be passed the T_STRING or T_VARIABLE stack pointer for the function call.
	 * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
	 *
	 * Will return an multi-dimentional array with the start token pointer, end token
	 * pointer and raw parameter value for all parameters. Index will be 1-based.
	 * If no parameters are found, will return an empty array.
	 *
	 * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer,
	 * it will tokenize the values / key/value pairs contained in the array call.
	 *
	 * @since 7.0.5 Split off from the `getFunctionCallParameterCount()` method.
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the function call token.
	 *
	 * @return array
	 */
	public function getFunctionCallParameters( File $phpcsFile, $stackPtr ) {
		if ( $this->doesFunctionCallHaveParameters( $phpcsFile, $stackPtr ) === false ) {
			return array();
		}

		// Ok, we know we have a T_STRING, T_VARIABLE, T_ARRAY or T_OPEN_SHORT_ARRAY with parameters
		// and valid open & close brackets/parenthesis.
		$tokens = $phpcsFile->getTokens();

		// Mark the beginning and end tokens.
		if ( $tokens[ $stackPtr ]['code'] === \T_OPEN_SHORT_ARRAY ) {
			$opener = $stackPtr;
			$closer = $tokens[ $stackPtr ]['bracket_closer'];

			$nestedParenthesisCount = 0;

		} else {
			$opener = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true );
			$closer = $tokens[ $opener ]['parenthesis_closer'];

			$nestedParenthesisCount = 1;
		}

		// Which nesting level is the one we are interested in ?
		if ( isset( $tokens[ $opener ]['nested_parenthesis'] ) ) {
			$nestedParenthesisCount += \count( $tokens[ $opener ]['nested_parenthesis'] );
		}

		$parameters = array();
		$nextComma  = $opener;
		$paramStart = $opener + 1;
		$cnt        = 1;
		while ( ( $nextComma = $phpcsFile->findNext( array( \T_COMMA, $tokens[ $closer ]['code'], \T_OPEN_SHORT_ARRAY, \T_CLOSURE ), $nextComma + 1, $closer + 1 ) ) !== false ) {
			// Ignore anything within short array definition brackets.
			if ( $tokens[ $nextComma ]['type'] === 'T_OPEN_SHORT_ARRAY'
				&& ( isset( $tokens[ $nextComma ]['bracket_opener'] )
					&& $tokens[ $nextComma ]['bracket_opener'] === $nextComma )
				&& isset( $tokens[ $nextComma ]['bracket_closer'] )
			) {
				// Skip forward to the end of the short array definition.
				$nextComma = $tokens[ $nextComma ]['bracket_closer'];
				continue;
			}

			// Skip past closures passed as function parameters.
			if ( $tokens[ $nextComma ]['type'] === 'T_CLOSURE'
				&& ( isset( $tokens[ $nextComma ]['scope_condition'] )
					&& $tokens[ $nextComma ]['scope_condition'] === $nextComma )
				&& isset( $tokens[ $nextComma ]['scope_closer'] )
			) {
				// Skip forward to the end of the closure declaration.
				$nextComma = $tokens[ $nextComma ]['scope_closer'];
				continue;
			}

			// Ignore comma's at a lower nesting level.
			if ( $tokens[ $nextComma ]['type'] === 'T_COMMA'
				&& isset( $tokens[ $nextComma ]['nested_parenthesis'] )
				&& \count( $tokens[ $nextComma ]['nested_parenthesis'] ) !== $nestedParenthesisCount
			) {
				continue;
			}

			// Ignore closing parenthesis/bracket if not 'ours'.
			if ( $tokens[ $nextComma ]['type'] === $tokens[ $closer ]['type'] && $nextComma !== $closer ) {
				continue;
			}

			// Ok, we've reached the end of the parameter.
			$parameters[ $cnt ]['start'] = $paramStart;
			$parameters[ $cnt ]['end']   = $nextComma - 1;
			$parameters[ $cnt ]['raw']   = trim( $phpcsFile->getTokensAsString( $paramStart, ( $nextComma - $paramStart ) ) );

			/*
			 * Check if there are more tokens before the closing parenthesis.
			 * Prevents code like the following from setting a third parameter:
			 * `functionCall( $param1, $param2, );`.
			 */
			$hasNextParam = $phpcsFile->findNext( Tokens::$emptyTokens, $nextComma + 1, $closer, true, null, true );
			if ( $hasNextParam === false ) {
				break;
			}

			// Prepare for the next parameter.
			$paramStart = $nextComma + 1;
			$cnt++;
		}

		return $parameters;
	}


	/**
	 * Get information on a specific parameter passed to a function call.
	 *
	 * Expects to be passed the T_STRING or T_VARIABLE stack pointer for the function call.
	 * If passed a T_STRING which is *not* a function call, the behaviour is unreliable.
	 *
	 * Will return a array with the start token pointer, end token pointer and the raw value
	 * of the parameter at a specific offset.
	 * If the specified parameter is not found, will return false.
	 *
	 * @since 7.0.5
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile   The file being scanned.
	 * @param int                   $stackPtr    The position of the function call token.
	 * @param int                   $paramOffset The 1-based index position of the parameter to retrieve.
	 *
	 * @return array|false
	 */
	public function getFunctionCallParameter( File $phpcsFile, $stackPtr, $paramOffset ) {
		$parameters = $this->getFunctionCallParameters( $phpcsFile, $stackPtr );

		if ( isset( $parameters[ $paramOffset ] ) === false ) {
			return false;
		} else {
			return $parameters[ $paramOffset ];
		}
	}


	/**
	 * Verify whether a token is within a scoped condition.
	 *
	 * If the optional $validScopes parameter has been passed, the function
	 * will check that the token has at least one condition which is of a
	 * type defined in $validScopes.
	 *
	 * @since 7.0.5 Largely split off from the `inClassScope()` method.
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile   The file being scanned.
	 * @param int                   $stackPtr    The position of the token.
	 * @param array|int             $validScopes Optional. Array of valid scopes
	 *                                           or int value of a valid scope.
	 *                                           Pass the T_.. constant(s) for the
	 *                                           desired scope to this parameter.
	 *
	 * @return bool Without the optional $scopeTypes: True if within a scope, false otherwise.
	 *              If the $scopeTypes are set: True if *one* of the conditions is a
	 *              valid scope, false otherwise.
	 */
	public function tokenHasScope( File $phpcsFile, $stackPtr, $validScopes = null ) {
		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( isset( $tokens[ $stackPtr ] ) === false ) {
			return false;
		}

		// No conditions = no scope.
		if ( empty( $tokens[ $stackPtr ]['conditions'] ) ) {
			return false;
		}

		// Ok, there are conditions, do we have to check for specific ones ?
		if ( isset( $validScopes ) === false ) {
			return true;
		}

		return $phpcsFile->hasCondition( $stackPtr, $validScopes );
	}


	/**
	 * Verify whether a token is within a class scope.
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the token.
	 * @param bool                  $strict    Whether to strictly check for the T_CLASS
	 *                                         scope or also accept interfaces and traits
	 *                                         as scope.
	 *
	 * @return bool True if within class scope, false otherwise.
	 */
	public function inClassScope( File $phpcsFile, $stackPtr, $strict = true ) {
		$validScopes = array( \T_CLASS );
		if ( \defined( 'T_ANON_CLASS' ) === true ) {
			$validScopes[] = \T_ANON_CLASS;
		}

		if ( $strict === false ) {
			$validScopes[] = \T_INTERFACE;
			$validScopes[] = \T_TRAIT;
		}

		return $phpcsFile->hasCondition( $stackPtr, $validScopes );
	}


	/**
	 * Returns the fully qualified class name for a new class instantiation.
	 *
	 * Returns an empty string if the class name could not be reliably inferred.
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of a T_NEW token.
	 *
	 * @return string
	 */
	public function getFQClassNameFromNewToken( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( isset( $tokens[ $stackPtr ] ) === false ) {
			return '';
		}

		if ( $tokens[ $stackPtr ]['code'] !== \T_NEW ) {
			return '';
		}

		$start = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true );
		if ( $start === false ) {
			return '';
		}

		// Bow out if the next token is a variable as we don't know where it was defined.
		if ( $tokens[ $start ]['code'] === \T_VARIABLE ) {
			return '';
		}

		// Bow out if the next token is the class keyword.
		if ( $tokens[ $start ]['type'] === 'T_ANON_CLASS' || $tokens[ $start ]['code'] === \T_CLASS ) {
			return '';
		}

		$find = array(
			\T_NS_SEPARATOR,
			\T_STRING,
			\T_NAMESPACE,
			\T_WHITESPACE,
		);

		$end       = $phpcsFile->findNext( $find, ( $start + 1 ), null, true, null, true );
		$className = $phpcsFile->getTokensAsString( $start, ( $end - $start ) );
		$className = trim( $className );

		return $this->getFQName( $phpcsFile, $stackPtr, $className );
	}


	/**
	 * Returns the fully qualified name of the class that the specified class extends.
	 *
	 * Returns an empty string if the class does not extend another class or if
	 * the class name could not be reliably inferred.
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of a T_CLASS token.
	 *
	 * @return string
	 */
	public function getFQExtendedClassName( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( isset( $tokens[ $stackPtr ] ) === false ) {
			return '';
		}

		if ( $tokens[ $stackPtr ]['code'] !== \T_CLASS
			&& $tokens[ $stackPtr ]['type'] !== 'T_ANON_CLASS'
			&& $tokens[ $stackPtr ]['type'] !== 'T_INTERFACE'
		) {
			return '';
		}

		$extends = PHPCSHelper::findExtendedClassName( $phpcsFile, $stackPtr );
		if ( empty( $extends ) || \is_string( $extends ) === false ) {
			return '';
		}

		return $this->getFQName( $phpcsFile, $stackPtr, $extends );
	}


	/**
	 * Returns the class name for the static usage of a class.
	 * This can be a call to a method, the use of a property or constant.
	 *
	 * Returns an empty string if the class name could not be reliably inferred.
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of a T_NEW token.
	 *
	 * @return string
	 */
	public function getFQClassNameFromDoubleColonToken( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( isset( $tokens[ $stackPtr ] ) === false ) {
			return '';
		}

		if ( $tokens[ $stackPtr ]['code'] !== \T_DOUBLE_COLON ) {
			return '';
		}

		// Nothing to do if previous token is a variable as we don't know where it was defined.
		if ( $tokens[ $stackPtr - 1 ]['code'] === \T_VARIABLE ) {
			return '';
		}

		// Nothing to do if 'parent' or 'static' as we don't know how far the class tree extends.
		if ( \in_array( $tokens[ $stackPtr - 1 ]['code'], array( \T_PARENT, \T_STATIC ), true ) ) {
			return '';
		}

		// Get the classname from the class declaration if self is used.
		if ( $tokens[ $stackPtr - 1 ]['code'] === \T_SELF ) {
			$classDeclarationPtr = $phpcsFile->findPrevious( \T_CLASS, $stackPtr - 1 );
			if ( $classDeclarationPtr === false ) {
				return '';
			}
			$className = $phpcsFile->getDeclarationName( $classDeclarationPtr );
			return $this->getFQName( $phpcsFile, $classDeclarationPtr, $className );
		}

		$find = array(
			\T_NS_SEPARATOR,
			\T_STRING,
			\T_NAMESPACE,
			\T_WHITESPACE,
		);

		$start = $phpcsFile->findPrevious( $find, $stackPtr - 1, null, true, null, true );
		if ( $start === false || isset( $tokens[ ( $start + 1 ) ] ) === false ) {
			return '';
		}

		$start     = ( $start + 1 );
		$className = $phpcsFile->getTokensAsString( $start, ( $stackPtr - $start ) );
		$className = trim( $className );

		return $this->getFQName( $phpcsFile, $stackPtr, $className );
	}


	/**
	 * Get the Fully Qualified name for a class/function/constant etc.
	 *
	 * Checks if a class/function/constant name is already fully qualified and
	 * if not, enrich it with the relevant namespace information.
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the token.
	 * @param string                $name      The class / function / constant name.
	 *
	 * @return string
	 */
	public function getFQName( File $phpcsFile, $stackPtr, $name ) {
		if ( strpos( $name, '\\' ) === 0 ) {
			// Already fully qualified.
			return $name;
		}

		// Remove the namespace keyword if used.
		if ( strpos( $name, 'namespace\\' ) === 0 ) {
			$name = substr( $name, 10 );
		}

		$namespace = $this->determineNamespace( $phpcsFile, $stackPtr );

		if ( $namespace === '' ) {
			return '\\' . $name;
		} else {
			return '\\' . $namespace . '\\' . $name;
		}
	}


	/**
	 * Is the class/function/constant name namespaced or global ?
	 *
	 * @since 7.0.3
	 *
	 * @param string $FQName Fully Qualified name of a class, function etc.
	 *                       I.e. should always start with a `\`.
	 *
	 * @return bool True if namespaced, false if global.
	 *
	 * @throws \PHP_CodeSniffer_Exception If the name in the passed parameter
	 *                                    is not fully qualified.
	 */
	public function isNamespaced( $FQName ) {
		if ( strpos( $FQName, '\\' ) !== 0 ) {
			throw new PHPCS_Exception( '$FQName must be a fully qualified name' );
		}

		return ( strpos( substr( $FQName, 1 ), '\\' ) !== false );
	}


	/**
	 * Determine the namespace name an arbitrary token lives in.
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
	 * @param int                   $stackPtr  The token position for which to determine the namespace.
	 *
	 * @return string Namespace name or empty string if it couldn't be determined or no namespace applies.
	 */
	public function determineNamespace( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( isset( $tokens[ $stackPtr ] ) === false ) {
			return '';
		}

		// Check for scoped namespace {}.
		if ( empty( $tokens[ $stackPtr ]['conditions'] ) === false ) {
			$namespacePtr = $phpcsFile->getCondition( $stackPtr, \T_NAMESPACE );
			if ( $namespacePtr !== false ) {
				$namespace = $this->getDeclaredNamespaceName( $phpcsFile, $namespacePtr );
				if ( $namespace !== false ) {
					return $namespace;
				}

				// We are in a scoped namespace, but couldn't determine the name. Searching for a global namespace is futile.
				return '';
			}
		}

		/*
		 * Not in a scoped namespace, so let's see if we can find a non-scoped namespace instead.
		 * Keeping in mind that:
		 * - there can be multiple non-scoped namespaces in a file (bad practice, but it happens).
		 * - the namespace keyword can also be used as part of a function/method call and such.
		 * - that a non-named namespace resolves to the global namespace.
		 */
		$previousNSToken = $stackPtr;
		$namespace       = false;
		do {
			$previousNSToken = $phpcsFile->findPrevious( \T_NAMESPACE, ( $previousNSToken - 1 ) );

			// Stop if we encounter a scoped namespace declaration as we already know we're not in one.
			if ( empty( $tokens[ $previousNSToken ]['scope_condition'] ) === false && $tokens[ $previousNSToken ]['scope_condition'] === $previousNSToken ) {
				break;
			}

			$namespace = $this->getDeclaredNamespaceName( $phpcsFile, $previousNSToken );

		} while ( $namespace === false && $previousNSToken !== false );

		// If we still haven't got a namespace, return an empty string.
		if ( $namespace === false ) {
			return '';
		} else {
			return $namespace;
		}
	}

	/**
	 * Get the complete namespace name for a namespace declaration.
	 *
	 * For hierarchical namespaces, the name will be composed of several tokens,
	 * i.e. MyProject\Sub\Level which will be returned together as one string.
	 *
	 * @since 7.0.3
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
	 * @param int|bool              $stackPtr  The position of a T_NAMESPACE token.
	 *
	 * @return string|false Namespace name or false if not a namespace declaration.
	 *                      Namespace name can be an empty string for global namespace declaration.
	 */
	public function getDeclaredNamespaceName( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( $stackPtr === false || isset( $tokens[ $stackPtr ] ) === false ) {
			return false;
		}

		if ( $tokens[ $stackPtr ]['code'] !== \T_NAMESPACE ) {
			return false;
		}

		if ( $tokens[ ( $stackPtr + 1 ) ]['code'] === \T_NS_SEPARATOR ) {
			// Not a namespace declaration, but use of, i.e. `namespace\someFunction();`.
			return false;
		}

		$nextToken = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
		if ( $tokens[ $nextToken ]['code'] === \T_OPEN_CURLY_BRACKET ) {
			/*
			 * Declaration for global namespace when using multiple namespaces in a file.
			 * I.e.: `namespace {}`.
			 */
			return '';
		}

		// Ok, this should be a namespace declaration, so get all the parts together.
		$validTokens = array(
			\T_STRING       => true,
			\T_NS_SEPARATOR => true,
			\T_WHITESPACE   => true,
		);

		$namespaceName = '';
		while ( isset( $validTokens[ $tokens[ $nextToken ]['code'] ] ) === true ) {
			$namespaceName .= trim( $tokens[ $nextToken ]['content'] );
			$nextToken++;
		}

		return $namespaceName;
	}


	/**
	 * Get the stack pointer for a return type token for a given function.
	 *
	 * Compatible layer for older PHPCS versions which don't recognize
	 * return type hints correctly.
	 *
	 * Expects to be passed T_RETURN_TYPE, T_FUNCTION or T_CLOSURE token.
	 *
	 * @since 7.1.2
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the token.
	 *
	 * @return int|false Stack pointer to the return type token or false if
	 *                   no return type was found or the passed token was
	 *                   not of the correct type.
	 */
	public function getReturnTypeHintToken( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		if ( \defined( 'T_RETURN_TYPE' ) && $tokens[ $stackPtr ]['code'] === \T_RETURN_TYPE ) {
			return $stackPtr;
		}

		if ( $tokens[ $stackPtr ]['code'] !== \T_FUNCTION && $tokens[ $stackPtr ]['code'] !== \T_CLOSURE ) {
			return false;
		}

		if ( isset( $tokens[ $stackPtr ]['parenthesis_closer'] ) === false ) {
			return false;
		}

		// Allow for interface and abstract method declarations.
		$endOfFunctionDeclaration = null;
		if ( isset( $tokens[ $stackPtr ]['scope_opener'] ) ) {
			$endOfFunctionDeclaration = $tokens[ $stackPtr ]['scope_opener'];
		} else {
			$nextSemiColon = $phpcsFile->findNext( \T_SEMICOLON, ( $tokens[ $stackPtr ]['parenthesis_closer'] + 1 ), null, false, null, true );
			if ( $nextSemiColon !== false ) {
				$endOfFunctionDeclaration = $nextSemiColon;
			}
		}

		if ( isset( $endOfFunctionDeclaration ) === false ) {
			return false;
		}

		$hasColon = $phpcsFile->findNext(
			array( \T_COLON, \T_INLINE_ELSE ),
			( $tokens[ $stackPtr ]['parenthesis_closer'] + 1 ),
			$endOfFunctionDeclaration
		);
		if ( $hasColon === false ) {
			return false;
		}

		/*
		 * - `self`, `parent` and `callable` are not being recognized as return types in PHPCS < 2.6.0.
		 * - Return types are not recognized at all in PHPCS < 2.4.0.
		 * - The T_RETURN_TYPE token is defined, but no longer in use since PHPCS 3.3.0+.
		 *   The token will now be tokenized as T_STRING.
		 * - An `array` (return) type declaration was tokenized as `T_ARRAY_HINT` in PHPCS 2.3.3 - 3.2.3
		 *   to prevent confusing sniffs looking for array declarations.
		 *   As of PHPCS 3.3.0 `array` as a type declaration will be tokenized as `T_STRING`.
		 */
		$unrecognizedTypes = array(
			\T_CALLABLE,
			\T_SELF,
			\T_PARENT,
			\T_ARRAY, // PHPCS < 2.4.0.
			\T_STRING,
		);

		return $phpcsFile->findPrevious( $unrecognizedTypes, ( $endOfFunctionDeclaration - 1 ), $hasColon );
	}


	/**
	 * Get the complete return type declaration for a given function.
	 *
	 * Cross-version compatible way to retrieve the complete return type declaration.
	 *
	 * For a classname-based return type, PHPCS, as well as the Sniff::getReturnTypeHintToken()
	 * method will mark the classname as the return type token.
	 * This method will find preceeding namespaces and namespace separators and will return a
	 * string containing the qualified return type declaration.
	 *
	 * Expects to be passed a T_RETURN_TYPE token or the return value from a call to
	 * the Sniff::getReturnTypeHintToken() method.
	 *
	 * @since 8.2.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the return type token.
	 *
	 * @return string The name of the return type token.
	 */
	public function getReturnTypeHintName( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		// In older PHPCS versions, the nullable indicator will turn a return type colon into a T_INLINE_ELSE.
		$colon = $phpcsFile->findPrevious( array( \T_COLON, \T_INLINE_ELSE, \T_FUNCTION, \T_CLOSE_PARENTHESIS ), ( $stackPtr - 1 ) );
		if ( $colon === false
			|| ( $tokens[ $colon ]['code'] !== \T_COLON && $tokens[ $colon ]['code'] !== \T_INLINE_ELSE )
		) {
			// Shouldn't happen, just in case.
			return '';
		}

		$returnTypeHint = '';
		for ( $i = ( $colon + 1 ); $i <= $stackPtr; $i++ ) {
			// As of PHPCS 3.3.0+, all tokens are tokenized as "normal", so T_CALLABLE, T_SELF etc are
			// all possible, just exclude anything that's regarded as empty and the nullable indicator.
			if ( isset( Tokens::$emptyTokens[ $tokens[ $i ]['code'] ] ) ) {
				continue;
			}

			if ( $tokens[ $i ]['type'] === 'T_NULLABLE' ) {
				continue;
			}

			if ( \defined( 'T_NULLABLE' ) === false && $tokens[ $i ]['code'] === \T_INLINE_THEN ) {
				// Old PHPCS.
				continue;
			}

			$returnTypeHint .= $tokens[ $i ]['content'];
		}

		return $returnTypeHint;
	}


	/**
	 * Check whether a T_VARIABLE token is a class property declaration.
	 *
	 * Compatibility layer for PHPCS cross-version compatibility
	 * as PHPCS 2.4.0 - 2.7.1 does not have good enough support for
	 * anonymous classes. Along the same lines, the`getMemberProperties()`
	 * method does not support the `var` prefix.
	 *
	 * @since 7.1.4
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
	 * @param int                   $stackPtr  The position in the stack of the
	 *                                         T_VARIABLE token to verify.
	 *
	 * @return bool
	 */
	public function isClassProperty( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		if ( isset( $tokens[ $stackPtr ] ) === false || $tokens[ $stackPtr ]['code'] !== \T_VARIABLE ) {
			return false;
		}

		// Note: interfaces can not declare properties.
		$validScopes = array(
			'T_CLASS'      => true,
			'T_ANON_CLASS' => true,
			'T_TRAIT'      => true,
		);

		$scopePtr = $this->validDirectScope( $phpcsFile, $stackPtr, $validScopes );
		if ( $scopePtr !== false ) {
			// Make sure it's not a method parameter.
			if ( empty( $tokens[ $stackPtr ]['nested_parenthesis'] ) === true ) {
				return true;
			} else {
				$parenthesis = array_keys( $tokens[ $stackPtr ]['nested_parenthesis'] );
				$deepestOpen = array_pop( $parenthesis );
				if ( $deepestOpen < $scopePtr
					|| isset( $tokens[ $deepestOpen ]['parenthesis_owner'] ) === false
					|| $tokens[ $tokens[ $deepestOpen ]['parenthesis_owner'] ]['code'] !== \T_FUNCTION
				) {
					return true;
				}
			}
		}

		return false;
	}


	/**
	 * Check whether a T_CONST token is a class constant declaration.
	 *
	 * @since 7.1.4
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
	 * @param int                   $stackPtr  The position in the stack of the
	 *                                         T_CONST token to verify.
	 *
	 * @return bool
	 */
	public function isClassConstant( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		if ( isset( $tokens[ $stackPtr ] ) === false || $tokens[ $stackPtr ]['code'] !== \T_CONST ) {
			return false;
		}

		// Note: traits can not declare constants.
		$validScopes = array(
			'T_CLASS'      => true,
			'T_ANON_CLASS' => true,
			'T_INTERFACE'  => true,
		);
		if ( $this->validDirectScope( $phpcsFile, $stackPtr, $validScopes ) !== false ) {
			return true;
		}

		return false;
	}


	/**
	 * Check whether the direct wrapping scope of a token is within a limited set of
	 * acceptable tokens.
	 *
	 * Used to check, for instance, if a T_CONST is a class constant.
	 *
	 * @since 7.1.4
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile   Instance of phpcsFile.
	 * @param int                   $stackPtr    The position in the stack of the
	 *                                           token to verify.
	 * @param array                 $validScopes Array of token types.
	 *                                           Keys should be the token types in string
	 *                                           format to allow for newer token types.
	 *                                           Value is irrelevant.
	 *
	 * @return int|bool StackPtr to the scope if valid, false otherwise.
	 */
	protected function validDirectScope( File $phpcsFile, $stackPtr, $validScopes ) {
		$tokens = $phpcsFile->getTokens();

		if ( empty( $tokens[ $stackPtr ]['conditions'] ) === true ) {
			return false;
		}

		/*
		 * Check only the direct wrapping scope of the token.
		 */
		$conditions = array_keys( $tokens[ $stackPtr ]['conditions'] );
		$ptr        = array_pop( $conditions );

		if ( isset( $tokens[ $ptr ] ) === false ) {
			return false;
		}

		if ( isset( $validScopes[ $tokens[ $ptr ]['type'] ] ) === true ) {
			return $ptr;
		}

		return false;
	}


	/**
	 * Get an array of just the type hints from a function declaration.
	 *
	 * Expects to be passed T_FUNCTION or T_CLOSURE token.
	 *
	 * Strips potential nullable indicator and potential global namespace
	 * indicator from the type hints before returning them.
	 *
	 * @since 7.1.4
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the token.
	 *
	 * @return array Array with type hints or an empty array if
	 *               - the function does not have any parameters
	 *               - no type hints were found
	 *               - or the passed token was not of the correct type.
	 */
	public function getTypeHintsFromFunctionDeclaration( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		if ( $tokens[ $stackPtr ]['code'] !== \T_FUNCTION && $tokens[ $stackPtr ]['code'] !== \T_CLOSURE ) {
			return array();
		}

		$parameters = PHPCSHelper::getMethodParameters( $phpcsFile, $stackPtr );
		if ( empty( $parameters ) || \is_array( $parameters ) === false ) {
			return array();
		}

		$typeHints = array();

		foreach ( $parameters as $param ) {
			if ( $param['type_hint'] === '' ) {
				continue;
			}

			// Strip off potential nullable indication.
			$typeHint = ltrim( $param['type_hint'], '?' );

			// Strip off potential (global) namespace indication.
			$typeHint = ltrim( $typeHint, '\\' );

			if ( $typeHint !== '' ) {
				$typeHints[] = $typeHint;
			}
		}

		return $typeHints;
	}


	/**
	 * Get the hash algorithm name from the parameter in a hash function call.
	 *
	 * @since 7.0.7 Logic was originally contained in the `RemovedHashAlgorithms` sniff.
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile Instance of phpcsFile.
	 * @param int                   $stackPtr  The position of the T_STRING function token.
	 *
	 * @return string|false The algorithm name without quotes if this was a relevant hash
	 *                      function call or false if it was not.
	 */
	public function getHashAlgorithmParameter( 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_STRING ) {
			return false;
		}

		$functionName   = $tokens[ $stackPtr ]['content'];
		$functionNameLc = strtolower( $functionName );

		// Bow out if not one of the functions we're targetting.
		if ( isset( $this->hashAlgoFunctions[ $functionNameLc ] ) === false ) {
			return false;
		}

		// Get the parameter from the function call which should contain the algorithm name.
		$algoParam = $this->getFunctionCallParameter( $phpcsFile, $stackPtr, $this->hashAlgoFunctions[ $functionNameLc ] );
		if ( $algoParam === false ) {
			return false;
		}

		// Algorithm is a text string, so we need to remove the quotes.
		$algo = strtolower( trim( $algoParam['raw'] ) );
		$algo = $this->stripQuotes( $algo );

		return $algo;
	}


	/**
	 * Determine whether an arbitrary T_STRING token is the use of a global constant.
	 *
	 * @since 8.1.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the T_STRING token.
	 *
	 * @return bool
	 */
	public function isUseOfGlobalConstant( File $phpcsFile, $stackPtr ) {
		static $isLowPHPCS, $isLowPHP;

		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( isset( $tokens[ $stackPtr ] ) === false ) {
			return false;
		}

		// Is this one of the tokens this function handles ?
		if ( $tokens[ $stackPtr ]['code'] !== \T_STRING ) {
			return false;
		}

		// Check for older PHP, PHPCS version so we can compensate for misidentified tokens.
		if ( isset( $isLowPHPCS, $isLowPHP ) === false ) {
			$isLowPHP   = false;
			$isLowPHPCS = false;
			if ( version_compare( \PHP_VERSION_ID, '50400', '<' ) ) {
				$isLowPHP   = true;
				$isLowPHPCS = version_compare( PHPCSHelper::getVersion(), '2.4.0', '<' );
			}
		}

		$next = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
		if ( $next !== false
			&& ( $tokens[ $next ]['code'] === \T_OPEN_PARENTHESIS
				|| $tokens[ $next ]['code'] === \T_DOUBLE_COLON )
		) {
			// Function call or declaration.
			return false;
		}

		// Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a global constant.
		$tokensToIgnore = array(
			'T_NAMESPACE'       => true,
			'T_USE'             => true,
			'T_CLASS'           => true,
			'T_TRAIT'           => true,
			'T_INTERFACE'       => true,
			'T_EXTENDS'         => true,
			'T_IMPLEMENTS'      => true,
			'T_NEW'             => true,
			'T_FUNCTION'        => true,
			'T_DOUBLE_COLON'    => true,
			'T_OBJECT_OPERATOR' => true,
			'T_INSTANCEOF'      => true,
			'T_INSTEADOF'       => true,
			'T_GOTO'            => true,
			'T_AS'              => true,
			'T_PUBLIC'          => true,
			'T_PROTECTED'       => true,
			'T_PRIVATE'         => true,
		);

		$prev = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
		if ( $prev !== false
			&& ( isset( $tokensToIgnore[ $tokens[ $prev ]['type'] ] ) === true
				|| ( $tokens[ $prev ]['code'] === \T_STRING
					&& ( ( $isLowPHPCS === true
						&& $tokens[ $prev ]['content'] === 'trait' )
					|| ( $isLowPHP === true
						&& $tokens[ $prev ]['content'] === 'insteadof' ) ) ) )
		) {
			// Not the use of a constant.
			return false;
		}

		if ( $prev !== false
			&& $tokens[ $prev ]['code'] === \T_NS_SEPARATOR
			&& $tokens[ ( $prev - 1 ) ]['code'] === \T_STRING
		) {
			// Namespaced constant of the same name.
			return false;
		}

		if ( $prev !== false
			&& $tokens[ $prev ]['code'] === \T_CONST
			&& $this->isClassConstant( $phpcsFile, $prev ) === true
		) {
			// Class constant declaration of the same name.
			return false;
		}

		/*
		 * Deal with a number of variations of use statements.
		 */
		for ( $i = $stackPtr; $i > 0; $i-- ) {
			if ( $tokens[ $i ]['line'] !== $tokens[ $stackPtr ]['line'] ) {
				break;
			}
		}

		$firstOnLine = $phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
		if ( $firstOnLine !== false && $tokens[ $firstOnLine ]['code'] === \T_USE ) {
			$nextOnLine = $phpcsFile->findNext( Tokens::$emptyTokens, ( $firstOnLine + 1 ), null, true );
			if ( $nextOnLine !== false ) {
				if ( ( $tokens[ $nextOnLine ]['code'] === \T_STRING && $tokens[ $nextOnLine ]['content'] === 'const' )
					|| $tokens[ $nextOnLine ]['code'] === \T_CONST // Happens in some PHPCS versions.
				) {
					$hasNsSep = $phpcsFile->findNext( \T_NS_SEPARATOR, ( $nextOnLine + 1 ), $stackPtr );
					if ( $hasNsSep !== false ) {
						// Namespaced const (group) use statement.
						return false;
					}
				} else {
					// Not a const use statement.
					return false;
				}
			}
		}

		return true;
	}


	/**
	 * Determine whether the tokens between $start and $end together form a positive number
	 * as recognized by PHP.
	 *
	 * The outcome of this function is reliable for `true`, `false` should be regarded as
	 * "undetermined".
	 *
	 * Note: Zero is *not* regarded as a positive number.
	 *
	 * @since 8.2.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile   The file being scanned.
	 * @param int                   $start       Start of the snippet (inclusive), i.e. this
	 *                                           token will be examined as part of the snippet.
	 * @param int                   $end         End of the snippet (inclusive), i.e. this
	 *                                           token will be examined as part of the snippet.
	 * @param bool                  $allowFloats Whether to only consider integers, or also floats.
	 *
	 * @return bool True if PHP would evaluate the snippet as a positive number.
	 *              False if not or if it could not be reliably determined
	 *              (variable or calculations and such).
	 */
	public function isPositiveNumber( File $phpcsFile, $start, $end, $allowFloats = false ) {
		$number = $this->isNumber( $phpcsFile, $start, $end, $allowFloats );

		if ( $number === false ) {
			return false;
		}

		return ( $number > 0 );
	}


	/**
	 * Determine whether the tokens between $start and $end together form a negative number
	 * as recognized by PHP.
	 *
	 * The outcome of this function is reliable for `true`, `false` should be regarded as
	 * "undetermined".
	 *
	 * Note: Zero is *not* regarded as a negative number.
	 *
	 * @since 8.2.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile   The file being scanned.
	 * @param int                   $start       Start of the snippet (inclusive), i.e. this
	 *                                           token will be examined as part of the snippet.
	 * @param int                   $end         End of the snippet (inclusive), i.e. this
	 *                                           token will be examined as part of the snippet.
	 * @param bool                  $allowFloats Whether to only consider integers, or also floats.
	 *
	 * @return bool True if PHP would evaluate the snippet as a negative number.
	 *              False if not or if it could not be reliably determined
	 *              (variable or calculations and such).
	 */
	public function isNegativeNumber( File $phpcsFile, $start, $end, $allowFloats = false ) {
		$number = $this->isNumber( $phpcsFile, $start, $end, $allowFloats );

		if ( $number === false ) {
			return false;
		}

		return ( $number < 0 );
	}

	/**
	 * Determine whether the tokens between $start and $end together form a number
	 * as recognized by PHP.
	 *
	 * The outcome of this function is reliable for "true-ish" values, `false` should
	 * be regarded as "undetermined".
	 *
	 * @link https://3v4l.org/npTeM
	 *
	 * Mainly intended for examining variable assignments, function call parameters, array values
	 * where the start and end of the snippet to examine is very clear.
	 *
	 * @since 8.2.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile   The file being scanned.
	 * @param int                   $start       Start of the snippet (inclusive), i.e. this
	 *                                           token will be examined as part of the snippet.
	 * @param int                   $end         End of the snippet (inclusive), i.e. this
	 *                                           token will be examined as part of the snippet.
	 * @param bool                  $allowFloats Whether to only consider integers, or also floats.
	 *
	 * @return int|float|bool The number found if PHP would evaluate the snippet as a number.
	 *                        The return type will be int if $allowFloats is false, if
	 *                        $allowFloats is true, the return type will be float.
	 *                        False will be returned when the snippet does not evaluate to a
	 *                        number or if it could not be reliably determined
	 *                        (variable or calculations and such).
	 */
	protected function isNumber( File $phpcsFile, $start, $end, $allowFloats = false ) {
		$stringTokens = Tokens::$heredocTokens + Tokens::$stringTokens;

		$validTokens               = array();
		$validTokens[ \T_LNUMBER ] = true;
		$validTokens[ \T_TRUE ]    = true; // Evaluates to int 1.
		$validTokens[ \T_FALSE ]   = true; // Evaluates to int 0.
		$validTokens[ \T_NULL ]    = true; // Evaluates to int 0.

		if ( $allowFloats === true ) {
			$validTokens[ \T_DNUMBER ] = true;
		}

		$maybeValidTokens = $stringTokens + $validTokens;

		$tokens         = $phpcsFile->getTokens();
		$searchEnd      = ( $end + 1 );
		$negativeNumber = false;

		if ( isset( $tokens[ $start ], $tokens[ $searchEnd ] ) === false ) {
			return false;
		}

		$nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, $start, $searchEnd, true );
		while ( $nextNonEmpty !== false
			&& ( $tokens[ $nextNonEmpty ]['code'] === \T_PLUS
			|| $tokens[ $nextNonEmpty ]['code'] === \T_MINUS )
		) {

			if ( $tokens[ $nextNonEmpty ]['code'] === \T_MINUS ) {
				$negativeNumber = ( $negativeNumber === false ) ? true : false;
			}

			$nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $nextNonEmpty + 1 ), $searchEnd, true );
		}

		if ( $nextNonEmpty === false || isset( $maybeValidTokens[ $tokens[ $nextNonEmpty ]['code'] ] ) === false ) {
			return false;
		}

		$content = false;
		if ( $tokens[ $nextNonEmpty ]['code'] === \T_LNUMBER
			|| $tokens[ $nextNonEmpty ]['code'] === \T_DNUMBER
		) {
			$content = (float) $tokens[ $nextNonEmpty ]['content'];
		} elseif ( $tokens[ $nextNonEmpty ]['code'] === \T_TRUE ) {
			$content = 1.0;
		} elseif ( $tokens[ $nextNonEmpty ]['code'] === \T_FALSE
			|| $tokens[ $nextNonEmpty ]['code'] === \T_NULL
		) {
			$content = 0.0;
		} elseif ( isset( $stringTokens[ $tokens[ $nextNonEmpty ]['code'] ] ) === true ) {

			if ( $tokens[ $nextNonEmpty ]['code'] === \T_START_HEREDOC
				|| $tokens[ $nextNonEmpty ]['code'] === \T_START_NOWDOC
			) {
				// Skip past heredoc/nowdoc opener to the first content.
				$firstDocToken = $phpcsFile->findNext( array( \T_HEREDOC, \T_NOWDOC ), ( $nextNonEmpty + 1 ), $searchEnd );
				if ( $firstDocToken === false ) {
					// Live coding or parse error.
					return false;
				}

				$stringContent = $content = $tokens[ $firstDocToken ]['content'];

				// Skip forward to the end in preparation for the next part of the examination.
				$nextNonEmpty = $phpcsFile->findNext( array( \T_END_HEREDOC, \T_END_NOWDOC ), ( $nextNonEmpty + 1 ), $searchEnd );
				if ( $nextNonEmpty === false ) {
					// Live coding or parse error.
					return false;
				}
			} else {
				// Gather subsequent lines for a multi-line string.
				for ( $i = $nextNonEmpty; $i < $searchEnd; $i++ ) {
					if ( $tokens[ $i ]['code'] !== $tokens[ $nextNonEmpty ]['code'] ) {
						break;
					}
					$content .= $tokens[ $i ]['content'];
				}

				$nextNonEmpty  = --$i;
				$content       = $this->stripQuotes( $content );
				$stringContent = $content;
			}

			/*
			 * Regexes based on the formats outlined in the manual, created by JRF.
			 * @link https://www.php.net/manual/en/language.types.float.php
			 */
			$regexInt   = '`^\s*[0-9]+`';
			$regexFloat = '`^\s*(?:[+-]?(?:(?:(?P<LNUM>[0-9]+)|(?P<DNUM>([0-9]*\.(?P>LNUM)|(?P>LNUM)\.[0-9]*)))[eE][+-]?(?P>LNUM))|(?P>DNUM))`';

			$intString   = preg_match( $regexInt, $content, $intMatch );
			$floatString = preg_match( $regexFloat, $content, $floatMatch );

			// Does the text string start with a number ? If so, PHP would juggle it and use it as a number.
			if ( $allowFloats === false ) {
				if ( $intString !== 1 || $floatString === 1 ) {
					if ( $floatString === 1 ) {
						// Found float. Only integers targetted.
						return false;
					}

					$content = 0.0;
				} else {
					$content = (float) trim( $intMatch[0] );
				}
			} else {
				if ( $intString !== 1 && $floatString !== 1 ) {
					$content = 0.0;
				} else {
					$content = ( $floatString === 1 ) ? (float) trim( $floatMatch[0] ) : (float) trim( $intMatch[0] );
				}
			}

			// Allow for different behaviour for hex numeric strings between PHP 5 vs PHP 7.
			if ( $intString === 1 && trim( $intMatch[0] ) === '0'
				&& preg_match( '`^\s*(0x[A-Fa-f0-9]+)`', $stringContent, $hexNumberString ) === 1
				&& $this->supportsBelow( '5.6' ) === true
			) {
				// The filter extension still allows for hex numeric strings in PHP 7, so
				// use that to get the numeric value if possible.
				// If the filter extension is not available, the value will be zero, but so be it.
				if ( function_exists( 'filter_var' ) ) {
					$filtered = filter_var( $hexNumberString[1], \FILTER_VALIDATE_INT, \FILTER_FLAG_ALLOW_HEX );
					if ( $filtered !== false ) {
						$content = $filtered;
					}
				}
			}
		}

		// OK, so we have a number, now is there still more code after it ?
		$nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $nextNonEmpty + 1 ), $searchEnd, true );
		if ( $nextNonEmpty !== false ) {
			return false;
		}

		if ( $negativeNumber === true ) {
			$content = -$content;
		}

		if ( $allowFloats === false ) {
			return (int) $content;
		}

		return $content;
	}


	/**
	 * Determine whether the tokens between $start and $end together form a numberic calculation
	 * as recognized by PHP.
	 *
	 * The outcome of this function is reliable for `true`, `false` should be regarded as "undetermined".
	 *
	 * Mainly intended for examining variable assignments, function call parameters, array values
	 * where the start and end of the snippet to examine is very clear.
	 *
	 * @since 9.0.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $start     Start of the snippet (inclusive), i.e. this
	 *                                         token will be examined as part of the snippet.
	 * @param int                   $end       End of the snippet (inclusive), i.e. this
	 *                                         token will be examined as part of the snippet.
	 *
	 * @return bool
	 */
	protected function isNumericCalculation( File $phpcsFile, $start, $end ) {
		$arithmeticTokens = Tokens::$arithmeticTokens;

        // phpcs:disable PHPCompatibility.Constants.NewConstants.t_powFound
		if ( \defined( 'T_POW' ) && isset( $arithmeticTokens[ \T_POW ] ) === false ) {
			// T_POW was not added to the arithmetic array until PHPCS 2.9.0.
			$arithmeticTokens[ \T_POW ] = \T_POW;
		}
        // phpcs:enable

		$skipTokens   = Tokens::$emptyTokens;
		$skipTokens[] = \T_MINUS;
		$skipTokens[] = \T_PLUS;

		// Find the first arithmetic operator, but skip past +/- signs before numbers.
		$nextNonEmpty = ( $start - 1 );
		do {
			$nextNonEmpty       = $phpcsFile->findNext( $skipTokens, ( $nextNonEmpty + 1 ), ( $end + 1 ), true );
			$arithmeticOperator = $phpcsFile->findNext( $arithmeticTokens, ( $nextNonEmpty + 1 ), ( $end + 1 ) );
		} while ( $nextNonEmpty !== false && $arithmeticOperator !== false && $nextNonEmpty === $arithmeticOperator );

		if ( $arithmeticOperator === false ) {
			return false;
		}

		$tokens      = $phpcsFile->getTokens();
		$subsetStart = $start;
		$subsetEnd   = ( $arithmeticOperator - 1 );

		while ( $this->isNumber( $phpcsFile, $subsetStart, $subsetEnd, true ) !== false
			&& isset( $tokens[ ( $arithmeticOperator + 1 ) ] ) === true
		) {
			// Recognize T_POW for PHPCS < 2.4.0 on low PHP versions.
			if ( \defined( 'T_POW' ) === false
				&& $tokens[ $arithmeticOperator ]['code'] === \T_MULTIPLY
				&& $tokens[ ( $arithmeticOperator + 1 ) ]['code'] === \T_MULTIPLY
				&& isset( $tokens[ $arithmeticOperator + 2 ] ) === true
			) {
				// Move operator one forward to the second * in T_POW.
				++$arithmeticOperator;
			}

			$subsetStart  = ( $arithmeticOperator + 1 );
			$nextNonEmpty = $arithmeticOperator;
			do {
				$nextNonEmpty       = $phpcsFile->findNext( $skipTokens, ( $nextNonEmpty + 1 ), ( $end + 1 ), true );
				$arithmeticOperator = $phpcsFile->findNext( $arithmeticTokens, ( $nextNonEmpty + 1 ), ( $end + 1 ) );
			} while ( $nextNonEmpty !== false && $arithmeticOperator !== false && $nextNonEmpty === $arithmeticOperator );

			if ( $arithmeticOperator === false ) {
				// Last calculation operator already reached.
				if ( $this->isNumber( $phpcsFile, $subsetStart, $end, true ) !== false ) {
					return true;
				}

				return false;
			}

			$subsetEnd = ( $arithmeticOperator - 1 );
		}

		return false;
	}



	/**
	 * Determine whether a ternary is a short ternary, i.e. without "middle".
	 *
	 * N.B.: This is a back-fill for a new method which is expected to go into
	 * PHP_CodeSniffer 3.5.0.
	 * Once that method has been merged into PHPCS, this one should be moved
	 * to the PHPCSHelper.php file.
	 *
	 * @since 9.2.0
	 *
	 * @codeCoverageIgnore Method as pulled upstream is accompanied by unit tests.
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the ternary operator
	 *                                         in the stack.
	 *
	 * @return bool True if short ternary, or false otherwise.
	 */
	public function isShortTernary( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();
		if ( isset( $tokens[ $stackPtr ] ) === false
			|| $tokens[ $stackPtr ]['code'] !== \T_INLINE_THEN
		) {
			return false;
		}

		$nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
		if ( $nextNonEmpty === false ) {
			// Live coding or parse error.
			return false;
		}

		if ( $tokens[ $nextNonEmpty ]['code'] === \T_INLINE_ELSE ) {
			return true;
		}

		return false;
	}


	/**
	 * Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a list() construct.
	 *
	 * Note: A variety of PHPCS versions have bugs in the tokenizing of short arrays.
	 * In that case, the tokens are identified as T_OPEN/CLOSE_SQUARE_BRACKET.
	 *
	 * @since 8.2.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the function call token.
	 *
	 * @return bool
	 */
	public function isShortList( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		// Check for the existence of the token.
		if ( isset( $tokens[ $stackPtr ] ) === false ) {
			return false;
		}

		// Is this one of the tokens this function handles ?
		if ( $tokens[ $stackPtr ]['code'] !== \T_OPEN_SHORT_ARRAY
			&& $tokens[ $stackPtr ]['code'] !== \T_CLOSE_SHORT_ARRAY
		) {
			return false;
		}

		switch ( $tokens[ $stackPtr ]['code'] ) {
			case \T_OPEN_SHORT_ARRAY:
				if ( isset( $tokens[ $stackPtr ]['bracket_closer'] ) === true ) {
					$opener = $stackPtr;
					$closer = $tokens[ $stackPtr ]['bracket_closer'];
				}
				break;

			case \T_CLOSE_SHORT_ARRAY:
				if ( isset( $tokens[ $stackPtr ]['bracket_opener'] ) === true ) {
					$opener = $tokens[ $stackPtr ]['bracket_opener'];
					$closer = $stackPtr;
				}
				break;
		}

		if ( isset( $opener, $closer ) === false ) {
			// Parse error, live coding or real square bracket.
			return false;
		}

		/*
		 * PHPCS cross-version compatibility: work around for square brackets misidentified
		 * as short array when preceded by a variable variable in older PHPCS versions.
		 */
		$prevNonEmpty = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $opener - 1 ), null, true, null, true );

		if ( $prevNonEmpty !== false
			&& $tokens[ $prevNonEmpty ]['code'] === \T_CLOSE_CURLY_BRACKET
			&& isset( $tokens[ $prevNonEmpty ]['bracket_opener'] ) === true
		) {
			$maybeVariableVariable = $phpcsFile->findPrevious(
				Tokens::$emptyTokens,
				( $tokens[ $prevNonEmpty ]['bracket_opener'] - 1 ),
				null,
				true,
				null,
				true
			);

			if ( $tokens[ $maybeVariableVariable ]['code'] === \T_VARIABLE
				|| $tokens[ $maybeVariableVariable ]['code'] === \T_DOLLAR
			) {
				return false;
			}
		}

		$nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $closer + 1 ), null, true, null, true );

		if ( $nextNonEmpty !== false && $tokens[ $nextNonEmpty ]['code'] === \T_EQUAL ) {
			return true;
		}

		if ( $prevNonEmpty !== false
			&& $tokens[ $prevNonEmpty ]['code'] === \T_AS
			&& isset( $tokens[ $prevNonEmpty ]['nested_parenthesis'] ) === true
		) {
			$parentheses = array_reverse( $tokens[ $prevNonEmpty ]['nested_parenthesis'], true );
			foreach ( $parentheses as $open => $close ) {
				if ( isset( $tokens[ $open ]['parenthesis_owner'] )
					&& $tokens[ $tokens[ $open ]['parenthesis_owner'] ]['code'] === \T_FOREACH
				) {
					return true;
				}
			}
		}

		// Maybe this is a short list syntax nested inside another short list syntax ?
		$parentOpener = $opener;
		do {
			$parentOpener = $phpcsFile->findPrevious(
				array( \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET ),
				( $parentOpener - 1 ),
				null,
				false,
				null,
				true
			);

			if ( $parentOpener === false ) {
				return false;
			}
		} while ( isset( $tokens[ $parentOpener ]['bracket_closer'] ) === true
			&& $tokens[ $parentOpener ]['bracket_closer'] < $opener
		);

		if ( isset( $tokens[ $parentOpener ]['bracket_closer'] ) === true
			&& $tokens[ $parentOpener ]['bracket_closer'] > $closer
		) {
			// Work around tokenizer issue in PHPCS 2.0 - 2.7.
			$phpcsVersion = PHPCSHelper::getVersion();
			if ( ( version_compare( $phpcsVersion, '2.0', '>' ) === true
				&& version_compare( $phpcsVersion, '2.8', '<' ) === true )
				&& $tokens[ $parentOpener ]['code'] === \T_OPEN_SQUARE_BRACKET
			) {
				$nextNonEmpty = $phpcsFile->findNext(
					Tokens::$emptyTokens,
					( $tokens[ $parentOpener ]['bracket_closer'] + 1 ),
					null,
					true,
					null,
					true
				);

				if ( $nextNonEmpty !== false && $tokens[ $nextNonEmpty ]['code'] === \T_EQUAL ) {
					return true;
				}

				return false;
			}

			return $this->isShortList( $phpcsFile, $parentOpener );
		}

		return false;
	}


	/**
	 * Determine whether the tokens between $start and $end could together represent a variable.
	 *
	 * @since 9.0.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile          The file being scanned.
	 * @param int                   $start              Starting point stack pointer. Inclusive.
	 *                                                  I.e. this token should be taken into account.
	 * @param int                   $end                End point stack pointer. Exclusive.
	 *                                                  I.e. this token should not be taken into account.
	 * @param int                   $targetNestingLevel The nesting level the variable should be at.
	 *
	 * @return bool
	 */
	public function isVariable( File $phpcsFile, $start, $end, $targetNestingLevel ) {
		static $tokenBlackList, $bracketTokens;

		// Create the token arrays only once.
		if ( isset( $tokenBlackList, $bracketTokens ) === false ) {

			$tokenBlackList  = array(
				\T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
				\T_STRING_CONCAT    => \T_STRING_CONCAT,
			);
			$tokenBlackList += Tokens::$assignmentTokens;
			$tokenBlackList += Tokens::$equalityTokens;
			$tokenBlackList += Tokens::$comparisonTokens;
			$tokenBlackList += Tokens::$operators;
			$tokenBlackList += Tokens::$booleanOperators;
			$tokenBlackList += Tokens::$castTokens;

			/*
			 * List of brackets which can be part of a variable variable.
			 *
			 * Key is the open bracket token, value the close bracket token.
			 */
			$bracketTokens = array(
				\T_OPEN_CURLY_BRACKET  => \T_CLOSE_CURLY_BRACKET,
				\T_OPEN_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET,
			);
		}

		$tokens = $phpcsFile->getTokens();

		// If no variable at all was found, then it's definitely a no-no.
		$hasVariable = $phpcsFile->findNext( \T_VARIABLE, $start, $end );
		if ( $hasVariable === false ) {
			return false;
		}

		// Check if the variable found is at the right level. Deeper levels are always an error.
		if ( isset( $tokens[ $hasVariable ]['nested_parenthesis'] )
			&& \count( $tokens[ $hasVariable ]['nested_parenthesis'] ) !== $targetNestingLevel
		) {
				return false;
		}

		// Ok, so the first variable is at the right level, now are there any
		// blacklisted tokens within the empty() ?
		$hasBadToken = $phpcsFile->findNext( $tokenBlackList, $start, $end );
		if ( $hasBadToken === false ) {
			return true;
		}

		// If there are also bracket tokens, the blacklisted token might be part of a variable
		// variable, but if there are no bracket tokens, we know we have an error.
		$hasBrackets = $phpcsFile->findNext( $bracketTokens, $start, $end );
		if ( $hasBrackets === false ) {
			return false;
		}

		// Ok, we have both a blacklisted token as well as brackets, so we need to walk
		// the tokens of the variable variable.
		for ( $i = $start; $i < $end; $i++ ) {
			// If this is a bracket token, skip to the end of the bracketed expression.
			if ( isset( $bracketTokens[ $tokens[ $i ]['code'] ], $tokens[ $i ]['bracket_closer'] ) ) {
				$i = $tokens[ $i ]['bracket_closer'];
				continue;
			}

			// If it's a blacklisted token, not within brackets, we have an error.
			if ( isset( $tokenBlackList[ $tokens[ $i ]['code'] ] ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Determine whether a T_MINUS/T_PLUS token is a unary operator.
	 *
	 * N.B.: This is a back-fill for a new method which is expected to go into
	 * PHP_CodeSniffer 3.5.0.
	 * Once that method has been merged into PHPCS, this one should be moved
	 * to the PHPCSHelper.php file.
	 *
	 * @since 9.2.0
	 *
	 * @codeCoverageIgnore Method as pulled upstream is accompanied by unit tests.
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the plus/minus token.
	 *
	 * @return bool True if the token passed is a unary operator.
	 *              False otherwise or if the token is not a T_PLUS/T_MINUS token.
	 */
	public static function isUnaryPlusMinus( File $phpcsFile, $stackPtr ) {
		$tokens = $phpcsFile->getTokens();

		if ( isset( $tokens[ $stackPtr ] ) === false
			|| ( $tokens[ $stackPtr ]['code'] !== \T_PLUS
			&& $tokens[ $stackPtr ]['code'] !== \T_MINUS )
		) {
			return false;
		}

		$next = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
		if ( $next === false ) {
			// Live coding or parse error.
			return false;
		}

		if ( isset( Tokens::$operators[ $tokens[ $next ]['code'] ] ) === true ) {
			// Next token is an operator, so this is not a unary.
			return false;
		}

		$prev = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );

		if ( $tokens[ $prev ]['code'] === \T_RETURN ) {
			// Just returning a positive/negative value; eg. (return -1).
			return true;
		}

		if ( isset( Tokens::$operators[ $tokens[ $prev ]['code'] ] ) === true ) {
			// Just trying to operate on a positive/negative value; eg. ($var * -1).
			return true;
		}

		if ( isset( Tokens::$comparisonTokens[ $tokens[ $prev ]['code'] ] ) === true ) {
			// Just trying to compare a positive/negative value; eg. ($var === -1).
			return true;
		}

		if ( isset( Tokens::$booleanOperators[ $tokens[ $prev ]['code'] ] ) === true ) {
			// Just trying to compare a positive/negative value; eg. ($var || -1 === $b).
			return true;
		}

		if ( isset( Tokens::$assignmentTokens[ $tokens[ $prev ]['code'] ] ) === true ) {
			// Just trying to assign a positive/negative value; eg. ($var = -1).
			return true;
		}

		if ( isset( Tokens::$castTokens[ $tokens[ $prev ]['code'] ] ) === true ) {
			// Just casting a positive/negative value; eg. (string) -$var.
			return true;
		}

		// Other indicators that a plus/minus sign is a unary operator.
		$invalidTokens = array(
			\T_COMMA               => true,
			\T_OPEN_PARENTHESIS    => true,
			\T_OPEN_SQUARE_BRACKET => true,
			\T_OPEN_SHORT_ARRAY    => true,
			\T_COLON               => true,
			\T_INLINE_THEN         => true,
			\T_INLINE_ELSE         => true,
			\T_CASE                => true,
			\T_OPEN_CURLY_BRACKET  => true,
			\T_STRING_CONCAT       => true,
		);

		if ( isset( $invalidTokens[ $tokens[ $prev ]['code'] ] ) === true ) {
			// Just trying to use a positive/negative value; eg. myFunction($var, -2).
			return true;
		}

		return false;
	}

	/**
	 * Get the complete contents of a multi-line text string.
	 *
	 * N.B.: This is a back-fill for a new method which is expected to go into
	 * PHP_CodeSniffer 3.5.0.
	 * Once that method has been merged into PHPCS, this one should be moved
	 * to the PHPCSHelper.php file.
	 *
	 * @since 9.3.0
	 *
	 * @codeCoverageIgnore Method as pulled upstream is accompanied by unit tests.
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile   The file being scanned.
	 * @param int                   $stackPtr    Pointer to the first text string token
	 *                                           of a multi-line text string or to a
	 *                                           Nowdoc/Heredoc opener.
	 * @param bool                  $stripQuotes Optional. Whether to strip text delimiter
	 *                                           quotes off the resulting text string.
	 *                                           Defaults to true.
	 *
	 * @return string
	 *
	 * @throws \PHP_CodeSniffer_Exception If the specified position is not a
	 *                                    valid text string token or if the
	 *                                    token is not the first text string token.
	 */
	public function getCompleteTextString( File $phpcsFile, $stackPtr, $stripQuotes = true ) {
		$tokens = $phpcsFile->getTokens();

		// Must be the start of a text string token.
		if ( $tokens[ $stackPtr ]['code'] !== \T_START_HEREDOC
			&& $tokens[ $stackPtr ]['code'] !== \T_START_NOWDOC
			&& $tokens[ $stackPtr ]['code'] !== \T_CONSTANT_ENCAPSED_STRING
			&& $tokens[ $stackPtr ]['code'] !== \T_DOUBLE_QUOTED_STRING
		) {
			throw new PHPCS_Exception( '$stackPtr must be of type T_START_HEREDOC, T_START_NOWDOC, T_CONSTANT_ENCAPSED_STRING or T_DOUBLE_QUOTED_STRING' );
		}

		if ( $tokens[ $stackPtr ]['code'] === \T_CONSTANT_ENCAPSED_STRING
			|| $tokens[ $stackPtr ]['code'] === \T_DOUBLE_QUOTED_STRING
		) {
			$prev = $phpcsFile->findPrevious( \T_WHITESPACE, ( $stackPtr - 1 ), null, true );
			if ( $tokens[ $stackPtr ]['code'] === $tokens[ $prev ]['code'] ) {
				throw new PHPCS_Exception( '$stackPtr must be the start of the text string' );
			}
		}

		switch ( $tokens[ $stackPtr ]['code'] ) {
			case \T_START_HEREDOC:
				$stripQuotes = false;
				$targetType  = \T_HEREDOC;
				$current     = ( $stackPtr + 1 );
				break;

			case \T_START_NOWDOC:
				$stripQuotes = false;
				$targetType  = \T_NOWDOC;
				$current     = ( $stackPtr + 1 );
				break;

			default:
				$targetType = $tokens[ $stackPtr ]['code'];
				$current    = $stackPtr;
				break;
		}

		$string = '';
		do {
			$string .= $tokens[ $current ]['content'];
			++$current;
		} while ( $tokens[ $current ]['code'] === $targetType );

		if ( $stripQuotes === true ) {
			return $this->stripQuotes( $string );
		}

		return $string;
	}
}
