<?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\Sniffs\ControlStructures;

use PHPCompatibility\Sniff;
use PHP_CodeSniffer_File as File;
use PHP_CodeSniffer_Tokens as Tokens;

/**
 * Detect use of `continue` in `switch` control structures.
 *
 * As of PHP 7.3, PHP will throw a warning when `continue` is used to target a `switch`
 * control structure.
 * The sniff takes numeric arguments used with `continue` into account.
 *
 * PHP version 7.3
 *
 * @link https://www.php.net/manual/en/migration73.incompatible.php#migration73.incompatible.core.continue-targeting-switch
 * @link https://wiki.php.net/rfc/continue_on_switch_deprecation
 * @link https://github.com/php/php-src/commit/04e3523b7d095341f65ed5e71a3cac82fca690e4
 *       (actual implementation which is different from the RFC).
 * @link https://www.php.net/manual/en/control-structures.switch.php
 *
 * @since 8.2.0
 */
class DiscouragedSwitchContinueSniff extends Sniff {


	/**
	 * Token codes of control structures which can be targeted using continue.
	 *
	 * @since 8.2.0
	 *
	 * @var array
	 */
	protected $loopStructures = array(
		\T_FOR     => \T_FOR,
		\T_FOREACH => \T_FOREACH,
		\T_WHILE   => \T_WHILE,
		\T_DO      => \T_DO,
		\T_SWITCH  => \T_SWITCH,
	);

	/**
	 * Tokens which start a new case within a switch.
	 *
	 * @since 8.2.0
	 *
	 * @var array
	 */
	protected $caseTokens = array(
		\T_CASE    => \T_CASE,
		\T_DEFAULT => \T_DEFAULT,
	);

	/**
	 * Token codes which are accepted to determine the level for the continue.
	 *
	 * This array is enriched with the arithmetic operators in the register() method.
	 *
	 * @since 8.2.0
	 *
	 * @var array
	 */
	protected $acceptedLevelTokens = array(
		\T_LNUMBER           => \T_LNUMBER,
		\T_OPEN_PARENTHESIS  => \T_OPEN_PARENTHESIS,
		\T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS,
	);


	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @since 8.2.0
	 *
	 * @return array
	 */
	public function register() {
		$this->acceptedLevelTokens += Tokens::$arithmeticTokens;
		$this->acceptedLevelTokens += Tokens::$emptyTokens;

		return array( \T_SWITCH );
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @since 8.2.0
	 *
	 * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
	 * @param int                   $stackPtr  The position of the current token in the
	 *                                         stack passed in $tokens.
	 *
	 * @return void
	 */
	public function process( File $phpcsFile, $stackPtr ) {
		if ( $this->supportsAbove( '7.3' ) === false ) {
			return;
		}

		$tokens = $phpcsFile->getTokens();

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

		$switchOpener = $tokens[ $stackPtr ]['scope_opener'];
		$switchCloser = $tokens[ $stackPtr ]['scope_closer'];

		// Quick check whether we need to bother with the more complex logic.
		$hasContinue = $phpcsFile->findNext( \T_CONTINUE, ( $switchOpener + 1 ), $switchCloser );
		if ( $hasContinue === false ) {
			return;
		}

		$caseDefault = $switchOpener;

		do {
			$caseDefault = $phpcsFile->findNext( $this->caseTokens, ( $caseDefault + 1 ), $switchCloser );
			if ( $caseDefault === false ) {
				break;
			}

			if ( isset( $tokens[ $caseDefault ]['scope_opener'] ) === false ) {
				// Unknown start of the case, skip.
				continue;
			}

			$caseOpener      = $tokens[ $caseDefault ]['scope_opener'];
			$nextCaseDefault = $phpcsFile->findNext( $this->caseTokens, ( $caseDefault + 1 ), $switchCloser );
			if ( $nextCaseDefault === false ) {
				$caseCloser = $switchCloser;
			} else {
				$caseCloser = $nextCaseDefault;
			}

			// Check for unscoped control structures within the case.
			$controlStructure = $caseOpener;
			$doCount          = 0;
			while ( ( $controlStructure = $phpcsFile->findNext( $this->loopStructures, ( $controlStructure + 1 ), $caseCloser ) ) !== false ) {
				if ( $tokens[ $controlStructure ]['code'] === \T_DO ) {
					$doCount++;
				}

				if ( isset( $tokens[ $controlStructure ]['scope_opener'], $tokens[ $controlStructure ]['scope_closer'] ) === false ) {
					if ( $tokens[ $controlStructure ]['code'] === \T_WHILE && $doCount > 0 ) {
						// While in a do-while construct.
						$doCount--;
						continue;
					}

					// Control structure without braces found within the case, ignore this case.
					continue 2;
				}
			}

			// Examine the contents of the case.
			$continue = $caseOpener;

			do {
				$continue = $phpcsFile->findNext( \T_CONTINUE, ( $continue + 1 ), $caseCloser );
				if ( $continue === false ) {
					break;
				}

				$nextSemicolon = $phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), ( $continue + 1 ), $caseCloser );
				$codeString    = '';
				for ( $i = ( $continue + 1 ); $i < $nextSemicolon; $i++ ) {
					if ( isset( $this->acceptedLevelTokens[ $tokens[ $i ]['code'] ] ) === false ) {
						// Function call/variable or other token which make numeric level impossible to determine.
						continue 2;
					}

					if ( isset( Tokens::$emptyTokens[ $tokens[ $i ]['code'] ] ) === true ) {
						continue;
					}

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

				$level = null;
				if ( $codeString !== '' ) {
					if ( is_numeric( $codeString ) ) {
						$level = (int) $codeString;
					} else {
						// With the above logic, the string can only contain digits and operators, eval!
						$level = eval( "return ( $codeString );" );
					}
				}

				if ( isset( $level ) === false || $level === 0 ) {
					$level = 1;
				}

				// Examine which control structure is being targeted by the continue statement.
				if ( isset( $tokens[ $continue ]['conditions'] ) === false ) {
					continue;
				}

				$conditions = array_reverse( $tokens[ $continue ]['conditions'], true );
				// PHPCS adds more structures to the conditions array than we want to take into
				// consideration, so clean up the array.
				foreach ( $conditions as $tokenPtr => $tokenCode ) {
					if ( isset( $this->loopStructures[ $tokenCode ] ) === false ) {
						unset( $conditions[ $tokenPtr ] );
					}
				}

				$targetCondition = \array_slice( $conditions, ( $level - 1 ), 1, true );
				if ( empty( $targetCondition ) ) {
					continue;
				}

				$conditionToken = key( $targetCondition );
				if ( $conditionToken === $stackPtr ) {
					$phpcsFile->addWarning(
						"Targeting a 'switch' control structure with a 'continue' statement is strongly discouraged and will throw a warning as of PHP 7.3.",
						$continue,
						'Found'
					);
				}
			} while ( $continue < $caseCloser );

		} while ( $caseDefault < $switchCloser );
	}
}
