<?php
/**
 * The base tokenizer class.
 *
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
 */

namespace PHP_CodeSniffer\Tokenizers;

use PHP_CodeSniffer\Exceptions\TokenizerException;
use PHP_CodeSniffer\Util;

abstract class Tokenizer {

	/**
	 * The config data for the run.
	 *
	 * @var \PHP_CodeSniffer\Config
	 */
	protected $config = null;

	/**
	 * The EOL char used in the content.
	 *
	 * @var string
	 */
	protected $eolChar = [];

	/**
	 * A token-based representation of the content.
	 *
	 * @var array
	 */
	protected $tokens = [];

	/**
	 * The number of tokens in the tokens array.
	 *
	 * @var integer
	 */
	protected $numTokens = 0;

	/**
	 * A list of tokens that are allowed to open a scope.
	 *
	 * @var array
	 */
	public $scopeOpeners = [];

	/**
	 * A list of tokens that end the scope.
	 *
	 * @var array
	 */
	public $endScopeTokens = [];

	/**
	 * Known lengths of tokens.
	 *
	 * @var array<int, int>
	 */
	public $knownLengths = [];

	/**
	 * A list of lines being ignored due to error suppression comments.
	 *
	 * @var array
	 */
	public $ignoredLines = [];


	/**
	 * Initialise and run the tokenizer.
	 *
	 * @param string $content The content to tokenize,
	 * @param \PHP_CodeSniffer\Config | null $config The config data for the run.
	 * @param string $eolChar The EOL char used in the content.
	 *
	 * @return void
	 * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the file appears to be minified.
	 */
	public function __construct( $content, $config, $eolChar = '\n' ) {
		$this->eolChar = $eolChar;

		$this->config = $config;
		$this->tokens = $this->tokenize( $content );

		if ( $config === null ) {
			return;
		}

		$this->createPositionMap();
		$this->createTokenMap();
		$this->createParenthesisNestingMap();
		$this->createScopeMap();
		$this->createLevelMap();

		// Allow the tokenizer to do additional processing if required.
		$this->processAdditional();

	}//end __construct()


	/**
	 * Checks the content to see if it looks minified.
	 *
	 * @param string $content The content to tokenize.
	 * @param string $eolChar The EOL char used in the content.
	 *
	 * @return boolean
	 */
	protected function isMinifiedContent( $content, $eolChar = '\n' ) {
		// Minified files often have a very large number of characters per line
		// and cause issues when tokenizing.
		$numChars = strlen( $content );
		$numLines = ( substr_count( $content, $eolChar ) + 1 );
		$average  = ( $numChars / $numLines );
		if ( $average > 100 ) {
			return true;
		}

		return false;

	}//end isMinifiedContent()


	/**
	 * Gets the array of tokens.
	 *
	 * @return array
	 */
	public function getTokens() {
		return $this->tokens;

	}//end getTokens()


	/**
	 * Creates an array of tokens when given some content.
	 *
	 * @param string $string The string to tokenize.
	 *
	 * @return array
	 */
	abstract protected function tokenize( $string );


	/**
	 * Performs additional processing after main tokenizing.
	 *
	 * @return void
	 */
	abstract protected function processAdditional();


	/**
	 * Sets token position information.
	 *
	 * Can also convert tabs into spaces. Each tab can represent between
	 * 1 and $width spaces, so this cannot be a straight string replace.
	 *
	 * @return void
	 */
	private function createPositionMap() {
		$currColumn = 1;
		$lineNumber = 1;
		$eolLen     = strlen( $this->eolChar );
		$ignoring   = null;
		$inTests    = defined( 'PHP_CODESNIFFER_IN_TESTS' );

		$checkEncoding = false;
		if ( function_exists( 'iconv_strlen' ) === true ) {
			$checkEncoding = true;
		}

		$checkAnnotations = $this->config->annotations;
		$encoding         = $this->config->encoding;
		$tabWidth         = $this->config->tabWidth;

		$tokensWithTabs = [
			T_WHITESPACE               => true,
			T_COMMENT                  => true,
			T_DOC_COMMENT              => true,
			T_DOC_COMMENT_WHITESPACE   => true,
			T_DOC_COMMENT_STRING       => true,
			T_CONSTANT_ENCAPSED_STRING => true,
			T_DOUBLE_QUOTED_STRING     => true,
			T_HEREDOC                  => true,
			T_NOWDOC                   => true,
			T_INLINE_HTML              => true,
		];

		$this->numTokens = count( $this->tokens );
		for ( $i = 0; $i < $this->numTokens; $i ++ ) {
			$this->tokens[ $i ]['line']   = $lineNumber;
			$this->tokens[ $i ]['column'] = $currColumn;

			if ( isset( $this->knownLengths[ $this->tokens[ $i ]['code'] ] ) === true ) {
				// There are no tabs in the tokens we know the length of.
				$length     = $this->knownLengths[ $this->tokens[ $i ]['code'] ];
				$currColumn += $length;
			} else if ( $tabWidth === 0
			            || isset( $tokensWithTabs[ $this->tokens[ $i ]['code'] ] ) === false
			            || strpos( $this->tokens[ $i ]['content'], "\t" ) === false
			) {
				// There are no tabs in this content, or we aren't replacing them.
				if ( $checkEncoding === true ) {
					// Not using the default encoding, so take a bit more care.
					$oldLevel = error_reporting();
					error_reporting( 0 );
					$length = iconv_strlen( $this->tokens[ $i ]['content'], $encoding );
					error_reporting( $oldLevel );

					if ( $length === false ) {
						// String contained invalid characters, so revert to default.
						$length = strlen( $this->tokens[ $i ]['content'] );
					}
				} else {
					$length = strlen( $this->tokens[ $i ]['content'] );
				}

				$currColumn += $length;
			} else {
				$this->replaceTabsInToken( $this->tokens[ $i ] );
				$length     = $this->tokens[ $i ]['length'];
				$currColumn += $length;
			}//end if

			$this->tokens[ $i ]['length'] = $length;

			if ( isset( $this->knownLengths[ $this->tokens[ $i ]['code'] ] ) === false
			     && strpos( $this->tokens[ $i ]['content'], $this->eolChar ) !== false
			) {
				$lineNumber ++;
				$currColumn = 1;

				// Newline chars are not counted in the token length.
				$this->tokens[ $i ]['length'] -= $eolLen;
			}

			if ( $this->tokens[ $i ]['code'] === T_COMMENT
			     || $this->tokens[ $i ]['code'] === T_DOC_COMMENT_STRING
			     || $this->tokens[ $i ]['code'] === T_DOC_COMMENT_TAG
			     || ( $inTests === true && $this->tokens[ $i ]['code'] === T_INLINE_HTML )
			) {
				$commentText      = ltrim( $this->tokens[ $i ]['content'], " \t/*" );
				$commentText      = rtrim( $commentText, " */\t\r\n" );
				$commentTextLower = strtolower( $commentText );
				if ( strpos( $commentText, '@codingStandards' ) !== false ) {
					// If this comment is the only thing on the line, it tells us
					// to ignore the following line. If the line contains other content
					// then we are just ignoring this one single line.
					$ownLine = false;
					if ( $i > 0 ) {
						for ( $prev = ( $i - 1 ); $prev >= 0; $prev -- ) {
							if ( $this->tokens[ $prev ]['code'] === T_WHITESPACE ) {
								continue;
							}

							break;
						}

						if ( $this->tokens[ $prev ]['line'] !== $this->tokens[ $i ]['line'] ) {
							$ownLine = true;
						}
					}

					if ( $ignoring === null
					     && strpos( $commentText, '@codingStandardsIgnoreStart' ) !== false
					) {
						$ignoring = [ '.all' => true ];
						if ( $ownLine === true ) {
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
						}
					} else if ( $ignoring !== null
					            && strpos( $commentText, '@codingStandardsIgnoreEnd' ) !== false
					) {
						if ( $ownLine === true ) {
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
						} else {
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
						}

						$ignoring = null;
					} else if ( $ignoring === null
					            && strpos( $commentText, '@codingStandardsIgnoreLine' ) !== false
					) {
						$ignoring = [ '.all' => true ];
						if ( $ownLine === true ) {
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ]         = $ignoring;
							$this->ignoredLines[ ( $this->tokens[ $i ]['line'] + 1 ) ] = $ignoring;
						} else {
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
						}

						$ignoring = null;
					}//end if
				} else if ( substr( $commentTextLower, 0, 6 ) === 'phpcs:'
				            || substr( $commentTextLower, 0, 7 ) === '@phpcs:'
				) {
					// If the @phpcs: syntax is being used, strip the @ to make
					// comparisons easier.
					if ( $commentText[0] === '@' ) {
						$commentText      = substr( $commentText, 1 );
						$commentTextLower = strtolower( $commentText );
					}

					// If there is a comment on the end, strip it off.
					$commentStart = strpos( $commentTextLower, ' --' );
					if ( $commentStart !== false ) {
						$commentText      = substr( $commentText, 0, $commentStart );
						$commentTextLower = strtolower( $commentText );
					}

					// If this comment is the only thing on the line, it tells us
					// to ignore the following line. If the line contains other content
					// then we are just ignoring this one single line.
					$lineHasOtherContent = false;
					$lineHasOtherTokens  = false;
					if ( $i > 0 ) {
						for ( $prev = ( $i - 1 ); $prev > 0; $prev -- ) {
							if ( $this->tokens[ $prev ]['line'] !== $this->tokens[ $i ]['line'] ) {
								// Changed lines.
								break;
							}

							if ( $this->tokens[ $prev ]['code'] === T_WHITESPACE
							     || ( $this->tokens[ $prev ]['code'] === T_INLINE_HTML
							          && trim( $this->tokens[ $prev ]['content'] ) === '' )
							) {
								continue;
							}

							$lineHasOtherTokens = true;

							if ( $this->tokens[ $prev ]['code'] === T_OPEN_TAG ) {
								continue;
							}

							$lineHasOtherContent = true;
							break;
						}//end for

						$changedLines = false;
						for ( $next = $i; $next < $this->numTokens; $next ++ ) {
							if ( $changedLines === true ) {
								// Changed lines.
								break;
							}

							if ( isset( $this->knownLengths[ $this->tokens[ $next ]['code'] ] ) === false
							     && strpos( $this->tokens[ $next ]['content'], $this->eolChar ) !== false
							) {
								// Last token on the current line.
								$changedLines = true;
							}

							if ( $next === $i ) {
								continue;
							}

							if ( $this->tokens[ $next ]['code'] === T_WHITESPACE
							     || ( $this->tokens[ $next ]['code'] === T_INLINE_HTML
							          && trim( $this->tokens[ $next ]['content'] ) === '' )
							) {
								continue;
							}

							$lineHasOtherTokens = true;

							if ( $this->tokens[ $next ]['code'] === T_CLOSE_TAG ) {
								continue;
							}

							$lineHasOtherContent = true;
							break;
						}//end for
					}//end if

					if ( substr( $commentTextLower, 0, 9 ) === 'phpcs:set' ) {
						// Ignore standards for complete lines that change sniff settings.
						if ( $lineHasOtherTokens === false ) {
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
						}

						// Need to maintain case here, to get the correct sniff code.
						$parts = explode( ' ', substr( $commentText, 10 ) );
						if ( count( $parts ) >= 2 ) {
							$sniffParts = explode( '.', $parts[0] );
							if ( count( $sniffParts ) >= 3 ) {
								$this->tokens[ $i ]['sniffCode']          = array_shift( $parts );
								$this->tokens[ $i ]['sniffProperty']      = array_shift( $parts );
								$this->tokens[ $i ]['sniffPropertyValue'] = rtrim( implode( ' ', $parts ), " */\r\n" );
							}
						}

						$this->tokens[ $i ]['code'] = T_PHPCS_SET;
						$this->tokens[ $i ]['type'] = 'T_PHPCS_SET';
					} else if ( substr( $commentTextLower, 0, 16 ) === 'phpcs:ignorefile' ) {
						// The whole file will be ignored, but at least set the correct token.
						$this->tokens[ $i ]['code'] = T_PHPCS_IGNORE_FILE;
						$this->tokens[ $i ]['type'] = 'T_PHPCS_IGNORE_FILE';
					} else if ( substr( $commentTextLower, 0, 13 ) === 'phpcs:disable' ) {
						if ( $lineHasOtherContent === false ) {
							// Completely ignore the comment line.
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
						}

						if ( $ignoring === null ) {
							$ignoring = [];
						}

						$disabledSniffs = [];

						$additionalText = substr( $commentText, 14 );
						if ( $additionalText === false ) {
							$ignoring = [ '.all' => true ];
						} else {
							$parts = explode( ',', substr( $commentText, 13 ) );
							foreach ( $parts as $sniffCode ) {
								$sniffCode                    = trim( $sniffCode );
								$disabledSniffs[ $sniffCode ] = true;
								$ignoring[ $sniffCode ]       = true;

								// This newly disabled sniff might be disabling an existing
								// enabled exception that we are tracking.
								if ( isset( $ignoring['.except'] ) === true ) {
									foreach ( array_keys( $ignoring['.except'] ) as $ignoredSniffCode ) {
										if ( $ignoredSniffCode === $sniffCode
										     || strpos( $ignoredSniffCode, $sniffCode . '.' ) === 0
										) {
											unset( $ignoring['.except'][ $ignoredSniffCode ] );
										}
									}

									if ( empty( $ignoring['.except'] ) === true ) {
										unset( $ignoring['.except'] );
									}
								}
							}//end foreach
						}//end if

						$this->tokens[ $i ]['code']       = T_PHPCS_DISABLE;
						$this->tokens[ $i ]['type']       = 'T_PHPCS_DISABLE';
						$this->tokens[ $i ]['sniffCodes'] = $disabledSniffs;
					} else if ( substr( $commentTextLower, 0, 12 ) === 'phpcs:enable' ) {
						if ( $ignoring !== null ) {
							$enabledSniffs = [];

							$additionalText = substr( $commentText, 13 );
							if ( $additionalText === false ) {
								$ignoring = null;
							} else {
								$parts = explode( ',', substr( $commentText, 13 ) );
								foreach ( $parts as $sniffCode ) {
									$sniffCode                   = trim( $sniffCode );
									$enabledSniffs[ $sniffCode ] = true;

									// This new enabled sniff might remove previously disabled
									// sniffs if it is actually a standard or category of sniffs.
									foreach ( array_keys( $ignoring ) as $ignoredSniffCode ) {
										if ( $ignoredSniffCode === $sniffCode
										     || strpos( $ignoredSniffCode, $sniffCode . '.' ) === 0
										) {
											unset( $ignoring[ $ignoredSniffCode ] );
										}
									}

									// This new enabled sniff might be able to clear up
									// previously enabled sniffs if it is actually a standard or
									// category of sniffs.
									if ( isset( $ignoring['.except'] ) === true ) {
										foreach ( array_keys( $ignoring['.except'] ) as $ignoredSniffCode ) {
											if ( $ignoredSniffCode === $sniffCode
											     || strpos( $ignoredSniffCode, $sniffCode . '.' ) === 0
											) {
												unset( $ignoring['.except'][ $ignoredSniffCode ] );
											}
										}
									}
								}//end foreach

								if ( empty( $ignoring ) === true ) {
									$ignoring = null;
								} else {
									if ( isset( $ignoring['.except'] ) === true ) {
										$ignoring['.except'] += $enabledSniffs;
									} else {
										$ignoring['.except'] = $enabledSniffs;
									}
								}
							}//end if

							if ( $lineHasOtherContent === false ) {
								// Completely ignore the comment line.
								$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = [ '.all' => true ];
							} else {
								// The comment is on the same line as the code it is ignoring,
								// so respect the new ignore rules.
								$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
							}

							$this->tokens[ $i ]['sniffCodes'] = $enabledSniffs;
						}//end if

						$this->tokens[ $i ]['code'] = T_PHPCS_ENABLE;
						$this->tokens[ $i ]['type'] = 'T_PHPCS_ENABLE';
					} else if ( substr( $commentTextLower, 0, 12 ) === 'phpcs:ignore' ) {
						$ignoreRules = [];

						$additionalText = substr( $commentText, 13 );
						if ( $additionalText === false ) {
							$ignoreRules = [ '.all' => true ];
						} else {
							$parts = explode( ',', substr( $commentText, 13 ) );
							foreach ( $parts as $sniffCode ) {
								$ignoreRules[ trim( $sniffCode ) ] = true;
							}
						}

						$this->tokens[ $i ]['code']       = T_PHPCS_IGNORE;
						$this->tokens[ $i ]['type']       = 'T_PHPCS_IGNORE';
						$this->tokens[ $i ]['sniffCodes'] = $ignoreRules;

						if ( $ignoring !== null ) {
							$ignoreRules += $ignoring;
						}

						if ( $lineHasOtherContent === false ) {
							// Completely ignore the comment line, and set the following
							// line to include the ignore rules we've set.
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ]         = [ '.all' => true ];
							$this->ignoredLines[ ( $this->tokens[ $i ]['line'] + 1 ) ] = $ignoreRules;
						} else {
							// The comment is on the same line as the code it is ignoring,
							// so respect the ignore rules it set.
							$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoreRules;
						}
					}//end if
				}//end if
			}//end if

			if ( $ignoring !== null && isset( $this->ignoredLines[ $this->tokens[ $i ]['line'] ] ) === false ) {
				$this->ignoredLines[ $this->tokens[ $i ]['line'] ] = $ignoring;
			}
		}//end for

		// If annotations are being ignored, we clear out all the ignore rules
		// but leave the annotations tokenized as normal.
		if ( $checkAnnotations === false ) {
			$this->ignoredLines = [];
		}

	}//end createPositionMap()


	/**
	 * Replaces tabs in original token content with spaces.
	 *
	 * Each tab can represent between 1 and $config->tabWidth spaces,
	 * so this cannot be a straight string replace. The original content
	 * is placed into an orig_content index and the new token length is also
	 * set in the length index.
	 *
	 * @param array $token The token to replace tabs inside.
	 * @param string $prefix The character to use to represent the start of a tab.
	 * @param string $padding The character to use to represent the end of a tab.
	 * @param int $tabWidth The number of spaces each tab represents.
	 *
	 * @return void
	 */
	public function replaceTabsInToken( &$token, $prefix = ' ', $padding = ' ', $tabWidth = null ) {
		$checkEncoding = false;
		if ( function_exists( 'iconv_strlen' ) === true ) {
			$checkEncoding = true;
		}

		$currColumn = $token['column'];
		if ( $tabWidth === null ) {
			$tabWidth = $this->config->tabWidth;
			if ( $tabWidth === 0 ) {
				$tabWidth = 1;
			}
		}

		if ( rtrim( $token['content'], "\t" ) === '' ) {
			// String only contains tabs, so we can shortcut the process.
			$numTabs = strlen( $token['content'] );

			$firstTabSize = ( $tabWidth - ( ( $currColumn - 1 ) % $tabWidth ) );
			$length       = ( $firstTabSize + ( $tabWidth * ( $numTabs - 1 ) ) );
			$newContent   = $prefix . str_repeat( $padding, ( $length - 1 ) );
		} else {
			// We need to determine the length of each tab.
			$tabs = explode( "\t", $token['content'] );

			$numTabs    = ( count( $tabs ) - 1 );
			$tabNum     = 0;
			$newContent = '';
			$length     = 0;

			foreach ( $tabs as $content ) {
				if ( $content !== '' ) {
					$newContent .= $content;
					if ( $checkEncoding === true ) {
						// Not using the default encoding, so take a bit more care.
						$oldLevel = error_reporting();
						error_reporting( 0 );
						$contentLength = iconv_strlen( $content, $this->config->encoding );
						error_reporting( $oldLevel );
						if ( $contentLength === false ) {
							// String contained invalid characters, so revert to default.
							$contentLength = strlen( $content );
						}
					} else {
						$contentLength = strlen( $content );
					}

					$currColumn += $contentLength;
					$length     += $contentLength;
				}

				// The last piece of content does not have a tab after it.
				if ( $tabNum === $numTabs ) {
					break;
				}

				// Process the tab that comes after the content.
				$lastCurrColumn = $currColumn;
				$tabNum ++;

				// Move the pointer to the next tab stop.
				if ( ( $currColumn % $tabWidth ) === 0 ) {
					// This is the first tab, and we are already at a
					// tab stop, so this tab counts as a single space.
					$currColumn ++;
				} else {
					$currColumn ++;
					while ( ( $currColumn % $tabWidth ) !== 0 ) {
						$currColumn ++;
					}

					$currColumn ++;
				}

				$length     += ( $currColumn - $lastCurrColumn );
				$newContent .= $prefix . str_repeat( $padding, ( $currColumn - $lastCurrColumn - 1 ) );
			}//end foreach
		}//end if

		$token['orig_content'] = $token['content'];
		$token['content']      = $newContent;
		$token['length']       = $length;

	}//end replaceTabsInToken()


	/**
	 * Creates a map of brackets positions.
	 *
	 * @return void
	 */
	private function createTokenMap() {
		if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
			echo "\t*** START TOKEN MAP ***" . PHP_EOL;
		}

		$squareOpeners   = [];
		$curlyOpeners    = [];
		$this->numTokens = count( $this->tokens );

		$openers   = [];
		$openOwner = null;

		for ( $i = 0; $i < $this->numTokens; $i ++ ) {
			/*
                Parenthesis mapping.
            */

			if ( isset( Util\Tokens::$parenthesisOpeners[ $this->tokens[ $i ]['code'] ] ) === true ) {
				$this->tokens[ $i ]['parenthesis_opener'] = null;
				$this->tokens[ $i ]['parenthesis_closer'] = null;
				$this->tokens[ $i ]['parenthesis_owner']  = $i;
				$openOwner                                = $i;

				if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					echo str_repeat( "\t", ( count( $openers ) + 1 ) );
					echo "=> Found parenthesis owner at $i" . PHP_EOL;
				}
			} else if ( $this->tokens[ $i ]['code'] === T_OPEN_PARENTHESIS ) {
				$openers[]                                = $i;
				$this->tokens[ $i ]['parenthesis_opener'] = $i;
				if ( $openOwner !== null ) {
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo str_repeat( "\t", count( $openers ) );
						echo "=> Found parenthesis opener at $i for $openOwner" . PHP_EOL;
					}

					$this->tokens[ $openOwner ]['parenthesis_opener'] = $i;
					$this->tokens[ $i ]['parenthesis_owner']          = $openOwner;
					$openOwner                                        = null;
				} else if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					echo str_repeat( "\t", count( $openers ) );
					echo "=> Found unowned parenthesis opener at $i" . PHP_EOL;
				}
			} else if ( $this->tokens[ $i ]['code'] === T_CLOSE_PARENTHESIS ) {
				// Did we set an owner for this set of parenthesis?
				$numOpeners = count( $openers );
				if ( $numOpeners !== 0 ) {
					$opener = array_pop( $openers );
					if ( isset( $this->tokens[ $opener ]['parenthesis_owner'] ) === true ) {
						$owner = $this->tokens[ $opener ]['parenthesis_owner'];

						$this->tokens[ $owner ]['parenthesis_closer'] = $i;
						$this->tokens[ $i ]['parenthesis_owner']      = $owner;

						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", ( count( $openers ) + 1 ) );
							echo "=> Found parenthesis closer at $i for $owner" . PHP_EOL;
						}
					} else if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo str_repeat( "\t", ( count( $openers ) + 1 ) );
						echo "=> Found unowned parenthesis closer at $i for $opener" . PHP_EOL;
					}

					$this->tokens[ $i ]['parenthesis_opener']      = $opener;
					$this->tokens[ $i ]['parenthesis_closer']      = $i;
					$this->tokens[ $opener ]['parenthesis_closer'] = $i;
				}//end if
			}//end if

			/*
                Bracket mapping.
            */

			switch ( $this->tokens[ $i ]['code'] ) {
				case T_OPEN_SQUARE_BRACKET:
					$squareOpeners[] = $i;

					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo str_repeat( "\t", count( $squareOpeners ) );
						echo str_repeat( "\t", count( $curlyOpeners ) );
						echo "=> Found square bracket opener at $i" . PHP_EOL;
					}
					break;
				case T_OPEN_CURLY_BRACKET:
					if ( isset( $this->tokens[ $i ]['scope_closer'] ) === false ) {
						$curlyOpeners[] = $i;

						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", count( $squareOpeners ) );
							echo str_repeat( "\t", count( $curlyOpeners ) );
							echo "=> Found curly bracket opener at $i" . PHP_EOL;
						}
					}
					break;
				case T_CLOSE_SQUARE_BRACKET:
					if ( empty( $squareOpeners ) === false ) {
						$opener                                    = array_pop( $squareOpeners );
						$this->tokens[ $i ]['bracket_opener']      = $opener;
						$this->tokens[ $i ]['bracket_closer']      = $i;
						$this->tokens[ $opener ]['bracket_opener'] = $opener;
						$this->tokens[ $opener ]['bracket_closer'] = $i;

						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", count( $squareOpeners ) );
							echo str_repeat( "\t", count( $curlyOpeners ) );
							echo "\t=> Found square bracket closer at $i for $opener" . PHP_EOL;
						}
					}
					break;
				case T_CLOSE_CURLY_BRACKET:
					if ( empty( $curlyOpeners ) === false
					     && isset( $this->tokens[ $i ]['scope_opener'] ) === false
					) {
						$opener                                    = array_pop( $curlyOpeners );
						$this->tokens[ $i ]['bracket_opener']      = $opener;
						$this->tokens[ $i ]['bracket_closer']      = $i;
						$this->tokens[ $opener ]['bracket_opener'] = $opener;
						$this->tokens[ $opener ]['bracket_closer'] = $i;

						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", count( $squareOpeners ) );
							echo str_repeat( "\t", count( $curlyOpeners ) );
							echo "\t=> Found curly bracket closer at $i for $opener" . PHP_EOL;
						}
					}
					break;
				default:
					continue 2;
			}//end switch
		}//end for

		// Cleanup for any openers that we didn't find closers for.
		// This typically means there was a syntax error breaking things.
		foreach ( $openers as $opener ) {
			unset( $this->tokens[ $opener ]['parenthesis_opener'] );
			unset( $this->tokens[ $opener ]['parenthesis_owner'] );
		}

		if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
			echo "\t*** END TOKEN MAP ***" . PHP_EOL;
		}

	}//end createTokenMap()


	/**
	 * Creates a map for the parenthesis tokens that surround other tokens.
	 *
	 * @return void
	 */
	private function createParenthesisNestingMap() {
		$map = [];
		for ( $i = 0; $i < $this->numTokens; $i ++ ) {
			if ( isset( $this->tokens[ $i ]['parenthesis_opener'] ) === true
			     && $i === $this->tokens[ $i ]['parenthesis_opener']
			) {
				if ( empty( $map ) === false ) {
					$this->tokens[ $i ]['nested_parenthesis'] = $map;
				}

				if ( isset( $this->tokens[ $i ]['parenthesis_closer'] ) === true ) {
					$map[ $this->tokens[ $i ]['parenthesis_opener'] ]
						= $this->tokens[ $i ]['parenthesis_closer'];
				}
			} else if ( isset( $this->tokens[ $i ]['parenthesis_closer'] ) === true
			            && $i === $this->tokens[ $i ]['parenthesis_closer']
			) {
				array_pop( $map );
				if ( empty( $map ) === false ) {
					$this->tokens[ $i ]['nested_parenthesis'] = $map;
				}
			} else {
				if ( empty( $map ) === false ) {
					$this->tokens[ $i ]['nested_parenthesis'] = $map;
				}
			}//end if
		}//end for

	}//end createParenthesisNestingMap()


	/**
	 * Creates a scope map of tokens that open scopes.
	 *
	 * @return void
	 * @see    recurseScopeMap()
	 */
	private function createScopeMap() {
		if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
			echo "\t*** START SCOPE MAP ***" . PHP_EOL;
		}

		for ( $i = 0; $i < $this->numTokens; $i ++ ) {
			// Check to see if the current token starts a new scope.
			if ( isset( $this->scopeOpeners[ $this->tokens[ $i ]['code'] ] ) === true ) {
				if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					$type    = $this->tokens[ $i ]['type'];
					$content = Util\Common::prepareForOutput( $this->tokens[ $i ]['content'] );
					echo "\tStart scope map at $i:$type => $content" . PHP_EOL;
				}

				if ( isset( $this->tokens[ $i ]['scope_condition'] ) === true ) {
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo "\t* already processed, skipping *" . PHP_EOL;
					}

					continue;
				}

				$i = $this->recurseScopeMap( $i );
			}//end if
		}//end for

		if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
			echo "\t*** END SCOPE MAP ***" . PHP_EOL;
		}

	}//end createScopeMap()


	/**
	 * Recurses though the scope openers to build a scope map.
	 *
	 * @param int $stackPtr The position in the stack of the token that
	 *                      opened the scope (eg. an IF token or FOR token).
	 * @param int $depth How many scope levels down we are.
	 * @param int $ignore How many curly braces we are ignoring.
	 *
	 * @return int The position in the stack that closed the scope.
	 * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the nesting level gets too deep.
	 */
	private function recurseScopeMap( $stackPtr, $depth = 1, &$ignore = 0 ) {
		if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
			echo str_repeat( "\t", $depth );
			echo "=> Begin scope map recursion at token $stackPtr with depth $depth" . PHP_EOL;
		}

		$opener    = null;
		$currType  = $this->tokens[ $stackPtr ]['code'];
		$startLine = $this->tokens[ $stackPtr ]['line'];

		// We will need this to restore the value if we end up
		// returning a token ID that causes our calling function to go back
		// over already ignored braces.
		$originalIgnore = $ignore;

		// If the start token for this scope opener is the same as
		// the scope token, we have already found our opener.
		if ( isset( $this->scopeOpeners[ $currType ]['start'][ $currType ] ) === true ) {
			$opener = $stackPtr;
		}

		for ( $i = ( $stackPtr + 1 ); $i < $this->numTokens; $i ++ ) {
			$tokenType = $this->tokens[ $i ]['code'];

			if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
				$type    = $this->tokens[ $i ]['type'];
				$line    = $this->tokens[ $i ]['line'];
				$content = Util\Common::prepareForOutput( $this->tokens[ $i ]['content'] );

				echo str_repeat( "\t", $depth );
				echo "Process token $i on line $line [";
				if ( $opener !== null ) {
					echo "opener:$opener;";
				}

				if ( $ignore > 0 ) {
					echo "ignore=$ignore;";
				}

				echo "]: $type => $content" . PHP_EOL;
			}//end if

			// Very special case for IF statements in PHP that can be defined without
			// scope tokens. E.g., if (1) 1; 1 ? (1 ? 1 : 1) : 1;
			// If an IF statement below this one has an opener but no
			// keyword, the opener will be incorrectly assigned to this IF statement.
			// The same case also applies to USE statements, which don't have to have
			// openers, so a following USE statement can cause an incorrect brace match.
			if ( ( $currType === T_IF || $currType === T_ELSE || $currType === T_USE )
			     && $opener === null
			     && ( $this->tokens[ $i ]['code'] === T_SEMICOLON
			          || $this->tokens[ $i ]['code'] === T_CLOSE_TAG )
			) {
				if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					$type = $this->tokens[ $stackPtr ]['type'];
					echo str_repeat( "\t", $depth );
					if ( $this->tokens[ $i ]['code'] === T_SEMICOLON ) {
						$closerType = 'semicolon';
					} else {
						$closerType = 'close tag';
					}

					echo "=> Found $closerType before scope opener for $stackPtr:$type, bailing" . PHP_EOL;
				}

				return $i;
			}

			// Special case for PHP control structures that have no braces.
			// If we find a curly brace closer before we find the opener,
			// we're not going to find an opener. That closer probably belongs to
			// a control structure higher up.
			if ( $opener === null
			     && $ignore === 0
			     && $tokenType === T_CLOSE_CURLY_BRACKET
			     && isset( $this->scopeOpeners[ $currType ]['end'][ $tokenType ] ) === true
			) {
				if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					$type = $this->tokens[ $stackPtr ]['type'];
					echo str_repeat( "\t", $depth );
					echo "=> Found curly brace closer before scope opener for $stackPtr:$type, bailing" . PHP_EOL;
				}

				return ( $i - 1 );
			}

			if ( $opener !== null
			     && ( isset( $this->tokens[ $i ]['scope_opener'] ) === false
			          || $this->scopeOpeners[ $this->tokens[ $stackPtr ]['code'] ]['shared'] === true )
			     && isset( $this->scopeOpeners[ $currType ]['end'][ $tokenType ] ) === true
			) {
				if ( $ignore > 0 && $tokenType === T_CLOSE_CURLY_BRACKET ) {
					// The last opening bracket must have been for a string
					// offset or alike, so let's ignore it.
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo str_repeat( "\t", $depth );
						echo '* finished ignoring curly brace *' . PHP_EOL;
					}

					$ignore --;
					continue;
				} else if ( $this->tokens[ $opener ]['code'] === T_OPEN_CURLY_BRACKET
				            && $tokenType !== T_CLOSE_CURLY_BRACKET
				) {
					// The opener is a curly bracket so the closer must be a curly bracket as well.
					// We ignore this closer to handle cases such as T_ELSE or T_ELSEIF being considered
					// a closer of T_IF when it should not.
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						$type = $this->tokens[ $stackPtr ]['type'];
						echo str_repeat( "\t", $depth );
						echo "=> Ignoring non-curly scope closer for $stackPtr:$type" . PHP_EOL;
					}
				} else {
					$scopeCloser = $i;
					$todo        = [
						$stackPtr,
						$opener,
					];

					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						$type       = $this->tokens[ $stackPtr ]['type'];
						$closerType = $this->tokens[ $scopeCloser ]['type'];
						echo str_repeat( "\t", $depth );
						echo "=> Found scope closer ($scopeCloser:$closerType) for $stackPtr:$type" . PHP_EOL;
					}

					$validCloser = true;
					if ( ( $this->tokens[ $stackPtr ]['code'] === T_IF || $this->tokens[ $stackPtr ]['code'] === T_ELSEIF )
					     && ( $tokenType === T_ELSE || $tokenType === T_ELSEIF )
					) {
						// To be a closer, this token must have an opener.
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", $depth );
							echo "* closer needs to be tested *" . PHP_EOL;
						}

						$i = self::recurseScopeMap( $i, ( $depth + 1 ), $ignore );

						if ( isset( $this->tokens[ $scopeCloser ]['scope_opener'] ) === false ) {
							$validCloser = false;
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								echo str_repeat( "\t", $depth );
								echo "* closer is not valid (no opener found) *" . PHP_EOL;
							}
						} else if ( $this->tokens[ $this->tokens[ $scopeCloser ]['scope_opener'] ]['code'] !== $this->tokens[ $opener ]['code'] ) {
							$validCloser = false;
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								echo str_repeat( "\t", $depth );
								$type       = $this->tokens[ $this->tokens[ $scopeCloser ]['scope_opener'] ]['type'];
								$openerType = $this->tokens[ $opener ]['type'];
								echo "* closer is not valid (mismatched opener type; $type != $openerType) *" . PHP_EOL;
							}
						} else if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", $depth );
							echo "* closer was valid *" . PHP_EOL;
						}
					} else {
						// The closer was not processed, so we need to
						// complete that token as well.
						$todo[] = $scopeCloser;
					}//end if

					if ( $validCloser === true ) {
						foreach ( $todo as $token ) {
							$this->tokens[ $token ]['scope_condition'] = $stackPtr;
							$this->tokens[ $token ]['scope_opener']    = $opener;
							$this->tokens[ $token ]['scope_closer']    = $scopeCloser;
						}

						if ( $this->scopeOpeners[ $this->tokens[ $stackPtr ]['code'] ]['shared'] === true ) {
							// As we are going back to where we started originally, restore
							// the ignore value back to its original value.
							$ignore = $originalIgnore;

							return $opener;
						} else if ( $scopeCloser === $i
						            && isset( $this->scopeOpeners[ $tokenType ] ) === true
						) {
							// Unset scope_condition here or else the token will appear to have
							// already been processed, and it will be skipped. Normally we want that,
							// but in this case, the token is both a closer and an opener, so
							// it needs to act like an opener. This is also why we return the
							// token before this one; so the closer has a chance to be processed
							// a second time, but as an opener.
							unset( $this->tokens[ $scopeCloser ]['scope_condition'] );

							return ( $i - 1 );
						} else {
							return $i;
						}
					} else {
						continue;
					}//end if
				}//end if
			}//end if

			// Is this an opening condition ?
			if ( isset( $this->scopeOpeners[ $tokenType ] ) === true ) {
				if ( $opener === null ) {
					if ( $tokenType === T_USE ) {
						// PHP use keywords are special because they can be
						// used as blocks but also inline in function definitions.
						// So if we find them nested inside another opener, just skip them.
						continue;
					}

					if ( $tokenType === T_FUNCTION
					     && $this->tokens[ $stackPtr ]['code'] !== T_FUNCTION
					) {
						// Probably a closure, so process it manually.
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							$type = $this->tokens[ $stackPtr ]['type'];
							echo str_repeat( "\t", $depth );
							echo "=> Found function before scope opener for $stackPtr:$type, processing manually" . PHP_EOL;
						}

						if ( isset( $this->tokens[ $i ]['scope_closer'] ) === true ) {
							// We've already processed this closure.
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								echo str_repeat( "\t", $depth );
								echo '* already processed, skipping *' . PHP_EOL;
							}

							$i = $this->tokens[ $i ]['scope_closer'];
							continue;
						}

						$i = self::recurseScopeMap( $i, ( $depth + 1 ), $ignore );
						continue;
					}//end if

					if ( $tokenType === T_CLASS ) {
						// Probably an anonymous class inside another anonymous class,
						// so process it manually.
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							$type = $this->tokens[ $stackPtr ]['type'];
							echo str_repeat( "\t", $depth );
							echo "=> Found class before scope opener for $stackPtr:$type, processing manually" . PHP_EOL;
						}

						if ( isset( $this->tokens[ $i ]['scope_closer'] ) === true ) {
							// We've already processed this anon class.
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								echo str_repeat( "\t", $depth );
								echo '* already processed, skipping *' . PHP_EOL;
							}

							$i = $this->tokens[ $i ]['scope_closer'];
							continue;
						}

						$i = self::recurseScopeMap( $i, ( $depth + 1 ), $ignore );
						continue;
					}//end if

					// Found another opening condition but still haven't
					// found our opener, so we are never going to find one.
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						$type = $this->tokens[ $stackPtr ]['type'];
						echo str_repeat( "\t", $depth );
						echo "=> Found new opening condition before scope opener for $stackPtr:$type, ";
					}

					if ( ( $this->tokens[ $stackPtr ]['code'] === T_IF
					       || $this->tokens[ $stackPtr ]['code'] === T_ELSEIF
					       || $this->tokens[ $stackPtr ]['code'] === T_ELSE )
					     && ( $this->tokens[ $i ]['code'] === T_ELSE
					          || $this->tokens[ $i ]['code'] === T_ELSEIF )
					) {
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo "continuing" . PHP_EOL;
						}

						return ( $i - 1 );
					} else {
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo "backtracking" . PHP_EOL;
						}

						return $stackPtr;
					}
				}//end if

				if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					echo str_repeat( "\t", $depth );
					echo '* token is an opening condition *' . PHP_EOL;
				}

				$isShared = ( $this->scopeOpeners[ $tokenType ]['shared'] === true );

				if ( isset( $this->tokens[ $i ]['scope_condition'] ) === true ) {
					// We've been here before.
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo str_repeat( "\t", $depth );
						echo '* already processed, skipping *' . PHP_EOL;
					}

					if ( $isShared === false
					     && isset( $this->tokens[ $i ]['scope_closer'] ) === true
					) {
						$i = $this->tokens[ $i ]['scope_closer'];
					}

					continue;
				} else if ( $currType === $tokenType
				            && $isShared === false
				            && $opener === null
				) {
					// We haven't yet found our opener, but we have found another
					// scope opener which is the same type as us, and we don't
					// share openers, so we will never find one.
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo str_repeat( "\t", $depth );
						echo '* it was another token\'s opener, bailing *' . PHP_EOL;
					}

					return $stackPtr;
				} else {
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo str_repeat( "\t", $depth );
						echo '* searching for opener *' . PHP_EOL;
					}

					if ( isset( $this->scopeOpeners[ $tokenType ]['end'][ T_CLOSE_CURLY_BRACKET ] ) === true ) {
						$oldIgnore = $ignore;
						$ignore    = 0;
					}

					// PHP has a max nesting level for functions. Stop before we hit that limit
					// because too many loops means we've run into trouble anyway.
					if ( $depth > 50 ) {
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", $depth );
							echo '* reached maximum nesting level; aborting *' . PHP_EOL;
						}

						throw new TokenizerException( 'Maximum nesting level reached; file could not be processed' );
					}

					$oldDepth = $depth;
					if ( $isShared === true
					     && isset( $this->scopeOpeners[ $tokenType ]['with'][ $currType ] ) === true
					) {
						// Don't allow the depth to increment because this is
						// possibly not a true nesting if we are sharing our closer.
						// This can happen, for example, when a SWITCH has a large
						// number of CASE statements with the same shared BREAK.
						$depth --;
					}

					$i     = self::recurseScopeMap( $i, ( $depth + 1 ), $ignore );
					$depth = $oldDepth;

					if ( isset( $this->scopeOpeners[ $tokenType ]['end'][ T_CLOSE_CURLY_BRACKET ] ) === true ) {
						$ignore = $oldIgnore;
					}
				}//end if
			}//end if

			if ( isset( $this->scopeOpeners[ $currType ]['start'][ $tokenType ] ) === true
			     && $opener === null
			) {
				if ( $tokenType === T_OPEN_CURLY_BRACKET ) {
					if ( isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) === true
					     && $i < $this->tokens[ $stackPtr ]['parenthesis_closer']
					) {
						// We found a curly brace inside the condition of the
						// current scope opener, so it must be a string offset.
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", $depth );
							echo '* ignoring curly brace inside condition *' . PHP_EOL;
						}

						$ignore ++;
					} else {
						// Make sure this is actually an opener and not a
						// string offset (e.g., $var{0}).
						for ( $x = ( $i - 1 ); $x > 0; $x -- ) {
							if ( isset( Util\Tokens::$emptyTokens[ $this->tokens[ $x ]['code'] ] ) === true ) {
								continue;
							} else {
								// If the first non-whitespace/comment token looks like this
								// brace is a string offset, or this brace is mid-way through
								// a new statement, it isn't a scope opener.
								$disallowed = Util\Tokens::$assignmentTokens;
								$disallowed += [
									T_DOLLAR           => true,
									T_VARIABLE         => true,
									T_OBJECT_OPERATOR  => true,
									T_COMMA            => true,
									T_OPEN_PARENTHESIS => true,
								];

								if ( isset( $disallowed[ $this->tokens[ $x ]['code'] ] ) === true ) {
									if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
										echo str_repeat( "\t", $depth );
										echo '* ignoring curly brace *' . PHP_EOL;
									}

									$ignore ++;
								}

								break;
							}//end if
						}//end for
					}//end if
				}//end if

				if ( $ignore === 0 || $tokenType !== T_OPEN_CURLY_BRACKET ) {
					// We found the opening scope token for $currType.
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						$type = $this->tokens[ $stackPtr ]['type'];
						echo str_repeat( "\t", $depth );
						echo "=> Found scope opener for $stackPtr:$type" . PHP_EOL;
					}

					$opener = $i;
				}
			} else if ( $tokenType === T_SEMICOLON
			            && $opener === null
			            && ( isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) === false
			                 || $i > $this->tokens[ $stackPtr ]['parenthesis_closer'] )
			) {
				// Found the end of a statement but still haven't
				// found our opener, so we are never going to find one.
				if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					$type = $this->tokens[ $stackPtr ]['type'];
					echo str_repeat( "\t", $depth );
					echo "=> Found end of statement before scope opener for $stackPtr:$type, continuing" . PHP_EOL;
				}

				return ( $i - 1 );
			} else if ( $tokenType === T_OPEN_PARENTHESIS ) {
				if ( isset( $this->tokens[ $i ]['parenthesis_owner'] ) === true ) {
					$owner = $this->tokens[ $i ]['parenthesis_owner'];
					if ( isset( Util\Tokens::$scopeOpeners[ $this->tokens[ $owner ]['code'] ] ) === true
					     && isset( $this->tokens[ $i ]['parenthesis_closer'] ) === true
					) {
						// If we get into here, then we opened a parenthesis for
						// a scope (eg. an if or else if) so we need to update the
						// start of the line so that when we check to see
						// if the closing parenthesis is more than n lines away from
						// the statement, we check from the closing parenthesis.
						$startLine = $this->tokens[ $this->tokens[ $i ]['parenthesis_closer'] ]['line'];
					}
				}
			} else if ( $tokenType === T_OPEN_CURLY_BRACKET && $opener !== null ) {
				// We opened something that we don't have a scope opener for.
				// Examples of this are curly brackets for string offsets etc.
				// We want to ignore this so that we don't have an invalid scope
				// map.
				if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					echo str_repeat( "\t", $depth );
					echo '* ignoring curly brace *' . PHP_EOL;
				}

				$ignore ++;
			} else if ( $tokenType === T_CLOSE_CURLY_BRACKET && $ignore > 0 ) {
				// We found the end token for the opener we were ignoring.
				if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
					echo str_repeat( "\t", $depth );
					echo '* finished ignoring curly brace *' . PHP_EOL;
				}

				$ignore --;
			} else if ( $opener === null
			            && isset( $this->scopeOpeners[ $currType ] ) === true
			) {
				// If we still haven't found the opener after 30 lines,
				// we're not going to find it, unless we know it requires
				// an opener (in which case we better keep looking) or the last
				// token was empty (in which case we'll just confirm there is
				// more code in this file and not just a big comment).
				if ( $this->tokens[ $i ]['line'] >= ( $startLine + 30 )
				     && isset( Util\Tokens::$emptyTokens[ $this->tokens[ ( $i - 1 ) ]['code'] ] ) === false
				) {
					if ( $this->scopeOpeners[ $currType ]['strict'] === true ) {
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							$type  = $this->tokens[ $stackPtr ]['type'];
							$lines = ( $this->tokens[ $i ]['line'] - $startLine );
							echo str_repeat( "\t", $depth );
							echo "=> Still looking for $stackPtr:$type scope opener after $lines lines" . PHP_EOL;
						}
					} else {
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							$type = $this->tokens[ $stackPtr ]['type'];
							echo str_repeat( "\t", $depth );
							echo "=> Couldn't find scope opener for $stackPtr:$type, bailing" . PHP_EOL;
						}

						return $stackPtr;
					}
				}
			} else if ( $opener !== null
			            && $tokenType !== T_BREAK
			            && isset( $this->endScopeTokens[ $tokenType ] ) === true
			) {
				if ( isset( $this->tokens[ $i ]['scope_condition'] ) === false ) {
					if ( $ignore > 0 ) {
						// We found the end token for the opener we were ignoring.
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							echo str_repeat( "\t", $depth );
							echo '* finished ignoring curly brace *' . PHP_EOL;
						}

						$ignore --;
					} else {
						// We found a token that closes the scope but it doesn't
						// have a condition, so it belongs to another token and
						// our token doesn't have a closer, so pretend this is
						// the closer.
						if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
							$type = $this->tokens[ $stackPtr ]['type'];
							echo str_repeat( "\t", $depth );
							echo "=> Found (unexpected) scope closer for $stackPtr:$type" . PHP_EOL;
						}

						foreach ( [ $stackPtr, $opener ] as $token ) {
							$this->tokens[ $token ]['scope_condition'] = $stackPtr;
							$this->tokens[ $token ]['scope_opener']    = $opener;
							$this->tokens[ $token ]['scope_closer']    = $i;
						}

						return ( $i - 1 );
					}//end if
				}//end if
			}//end if
		}//end for

		return $stackPtr;

	}//end recurseScopeMap()


	/**
	 * Constructs the level map.
	 *
	 * The level map adds a 'level' index to each token which indicates the
	 * depth that a token within a set of scope blocks. It also adds a
	 * 'conditions' index which is an array of the scope conditions that opened
	 * each of the scopes - position 0 being the first scope opener.
	 *
	 * @return void
	 */
	private function createLevelMap() {
		if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
			echo "\t*** START LEVEL MAP ***" . PHP_EOL;
		}

		$this->numTokens = count( $this->tokens );
		$level           = 0;
		$conditions      = [];
		$lastOpener      = null;
		$openers         = [];

		for ( $i = 0; $i < $this->numTokens; $i ++ ) {
			if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
				$type = $this->tokens[ $i ]['type'];
				$line = $this->tokens[ $i ]['line'];
				$len  = $this->tokens[ $i ]['length'];
				$col  = $this->tokens[ $i ]['column'];

				$content = Util\Common::prepareForOutput( $this->tokens[ $i ]['content'] );

				echo str_repeat( "\t", ( $level + 1 ) );
				echo "Process token $i on line $line [col:$col;len:$len;lvl:$level;";
				if ( empty( $conditions ) !== true ) {
					$conditionString = 'conds;';
					foreach ( $conditions as $condition ) {
						$conditionString .= Util\Tokens::tokenName( $condition ) . ',';
					}

					echo rtrim( $conditionString, ',' ) . ';';
				}

				echo "]: $type => $content" . PHP_EOL;
			}//end if

			$this->tokens[ $i ]['level']      = $level;
			$this->tokens[ $i ]['conditions'] = $conditions;

			if ( isset( $this->tokens[ $i ]['scope_condition'] ) === true ) {
				// Check to see if this token opened the scope.
				if ( $this->tokens[ $i ]['scope_opener'] === $i ) {
					$stackPtr = $this->tokens[ $i ]['scope_condition'];
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						$type = $this->tokens[ $stackPtr ]['type'];
						echo str_repeat( "\t", ( $level + 1 ) );
						echo "=> Found scope opener for $stackPtr:$type" . PHP_EOL;
					}

					$stackPtr = $this->tokens[ $i ]['scope_condition'];

					// If we find a scope opener that has a shared closer,
					// then we need to go back over the condition map that we
					// just created and fix ourselves as we just added some
					// conditions where there was none. This happens for T_CASE
					// statements that are using the same break statement.
					if ( $lastOpener !== null && $this->tokens[ $lastOpener ]['scope_closer'] === $this->tokens[ $i ]['scope_closer'] ) {
						// This opener shares its closer with the previous opener,
						// but we still need to check if the two openers share their
						// closer with each other directly (like CASE and DEFAULT)
						// or if they are just sharing because one doesn't have a
						// closer (like CASE with no BREAK using a SWITCHes closer).
						$thisType = $this->tokens[ $this->tokens[ $i ]['scope_condition'] ]['code'];
						$opener   = $this->tokens[ $lastOpener ]['scope_condition'];

						$isShared = isset( $this->scopeOpeners[ $thisType ]['with'][ $this->tokens[ $opener ]['code'] ] );

						reset( $this->scopeOpeners[ $thisType ]['end'] );
						reset( $this->scopeOpeners[ $this->tokens[ $opener ]['code'] ]['end'] );
						$sameEnd = ( current( $this->scopeOpeners[ $thisType ]['end'] ) === current( $this->scopeOpeners[ $this->tokens[ $opener ]['code'] ]['end'] ) );

						if ( $isShared === true && $sameEnd === true ) {
							$badToken = $opener;
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								$type = $this->tokens[ $badToken ]['type'];
								echo str_repeat( "\t", ( $level + 1 ) );
								echo "* shared closer, cleaning up $badToken:$type *" . PHP_EOL;
							}

							for ( $x = $this->tokens[ $i ]['scope_condition']; $x <= $i; $x ++ ) {
								$oldConditions = $this->tokens[ $x ]['conditions'];
								$oldLevel      = $this->tokens[ $x ]['level'];
								$this->tokens[ $x ]['level'] --;
								unset( $this->tokens[ $x ]['conditions'][ $badToken ] );
								if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
									$type     = $this->tokens[ $x ]['type'];
									$oldConds = '';
									foreach ( $oldConditions as $condition ) {
										$oldConds .= Util\Tokens::tokenName( $condition ) . ',';
									}

									$oldConds = rtrim( $oldConds, ',' );

									$newConds = '';
									foreach ( $this->tokens[ $x ]['conditions'] as $condition ) {
										$newConds .= Util\Tokens::tokenName( $condition ) . ',';
									}

									$newConds = rtrim( $newConds, ',' );

									$newLevel = $this->tokens[ $x ]['level'];
									echo str_repeat( "\t", ( $level + 1 ) );
									echo "* cleaned $x:$type *" . PHP_EOL;
									echo str_repeat( "\t", ( $level + 2 ) );
									echo "=> level changed from $oldLevel to $newLevel" . PHP_EOL;
									echo str_repeat( "\t", ( $level + 2 ) );
									echo "=> conditions changed from $oldConds to $newConds" . PHP_EOL;
								}//end if
							}//end for

							unset( $conditions[ $badToken ] );
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								$type = $this->tokens[ $badToken ]['type'];
								echo str_repeat( "\t", ( $level + 1 ) );
								echo "* token $badToken:$type removed from conditions array *" . PHP_EOL;
							}

							unset( $openers[ $lastOpener ] );

							$level --;
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								echo str_repeat( "\t", ( $level + 2 ) );
								echo '* level decreased *' . PHP_EOL;
							}
						}//end if
					}//end if

					$level ++;
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						echo str_repeat( "\t", ( $level + 1 ) );
						echo '* level increased *' . PHP_EOL;
					}

					$conditions[ $stackPtr ] = $this->tokens[ $stackPtr ]['code'];
					if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
						$type = $this->tokens[ $stackPtr ]['type'];
						echo str_repeat( "\t", ( $level + 1 ) );
						echo "* token $stackPtr:$type added to conditions array *" . PHP_EOL;
					}

					$lastOpener = $this->tokens[ $i ]['scope_opener'];
					if ( $lastOpener !== null ) {
						$openers[ $lastOpener ] = $lastOpener;
					}
				} else if ( $lastOpener !== null && $this->tokens[ $lastOpener ]['scope_closer'] === $i ) {
					foreach ( array_reverse( $openers ) as $opener ) {
						if ( $this->tokens[ $opener ]['scope_closer'] === $i ) {
							$oldOpener = array_pop( $openers );
							if ( empty( $openers ) === false ) {
								$lastOpener             = array_pop( $openers );
								$openers[ $lastOpener ] = $lastOpener;
							} else {
								$lastOpener = null;
							}

							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								$type = $this->tokens[ $oldOpener ]['type'];
								echo str_repeat( "\t", ( $level + 1 ) );
								echo "=> Found scope closer for $oldOpener:$type" . PHP_EOL;
							}

							$oldCondition = array_pop( $conditions );
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								echo str_repeat( "\t", ( $level + 1 ) );
								echo '* token ' . Util\Tokens::tokenName( $oldCondition ) . ' removed from conditions array *' . PHP_EOL;
							}

							// Make sure this closer actually belongs to us.
							// Either the condition also has to think this is the
							// closer, or it has to allow sharing with us.
							$condition = $this->tokens[ $this->tokens[ $i ]['scope_condition'] ]['code'];
							if ( $condition !== $oldCondition ) {
								if ( isset( $this->scopeOpeners[ $oldCondition ]['with'][ $condition ] ) === false ) {
									$badToken = $this->tokens[ $oldOpener ]['scope_condition'];

									if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
										$type = Util\Tokens::tokenName( $oldCondition );
										echo str_repeat( "\t", ( $level + 1 ) );
										echo "* scope closer was bad, cleaning up $badToken:$type *" . PHP_EOL;
									}

									for ( $x = ( $oldOpener + 1 ); $x <= $i; $x ++ ) {
										$oldConditions = $this->tokens[ $x ]['conditions'];
										$oldLevel      = $this->tokens[ $x ]['level'];
										$this->tokens[ $x ]['level'] --;
										unset( $this->tokens[ $x ]['conditions'][ $badToken ] );
										if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
											$type     = $this->tokens[ $x ]['type'];
											$oldConds = '';
											foreach ( $oldConditions as $condition ) {
												$oldConds .= Util\Tokens::tokenName( $condition ) . ',';
											}

											$oldConds = rtrim( $oldConds, ',' );

											$newConds = '';
											foreach ( $this->tokens[ $x ]['conditions'] as $condition ) {
												$newConds .= Util\Tokens::tokenName( $condition ) . ',';
											}

											$newConds = rtrim( $newConds, ',' );

											$newLevel = $this->tokens[ $x ]['level'];
											echo str_repeat( "\t", ( $level + 1 ) );
											echo "* cleaned $x:$type *" . PHP_EOL;
											echo str_repeat( "\t", ( $level + 2 ) );
											echo "=> level changed from $oldLevel to $newLevel" . PHP_EOL;
											echo str_repeat( "\t", ( $level + 2 ) );
											echo "=> conditions changed from $oldConds to $newConds" . PHP_EOL;
										}//end if
									}//end for
								}//end if
							}//end if

							$level --;
							if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
								echo str_repeat( "\t", ( $level + 2 ) );
								echo '* level decreased *' . PHP_EOL;
							}

							$this->tokens[ $i ]['level']      = $level;
							$this->tokens[ $i ]['conditions'] = $conditions;
						}//end if
					}//end foreach
				}//end if
			}//end if
		}//end for

		if ( PHP_CODESNIFFER_VERBOSITY > 1 ) {
			echo "\t*** END LEVEL MAP ***" . PHP_EOL;
		}

	}//end createLevelMap()


}//end class
