<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\NamingConventions;

use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use WordPressCS\WordPress\PHPCSHelper;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Verify that everything defined in the global namespace is prefixed with a theme/plugin specific prefix.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.12.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   1.2.0  Now also checks whether namespaces are prefixed.
 *
 * @uses    \WordPressCS\WordPress\Sniff::$custom_test_class_whitelist
 */
class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff {

	/**
	 * Error message template.
	 *
	 * @var string
	 */
	const ERROR_MSG = '%s by a theme/plugin should start with the theme/plugin prefix. Found: "%s".';

	/**
	 * Target prefixes.
	 *
	 * @since 0.12.0
	 *
	 * @var string[]|string
	 */
	public $prefixes = '';

	/**
	 * Prefix blacklist.
	 *
	 * @since 0.12.0
	 *
	 * @var string[]
	 */
	protected $prefix_blacklist = array(
		'wordpress' => true,
		'wp'        => true,
		'_'         => true,
	);

	/**
	 * Target prefixes after validation.
	 *
	 * All prefixes are lowercased for case-insensitive compare.
	 *
	 * @since 0.12.0
	 *
	 * @var string[]
	 */
	private $validated_prefixes = array();

	/**
	 * Target namespace prefixes after validation with regex indicator.
	 *
	 * All prefixes are lowercased for case-insensitive compare.
	 * If the prefix doesn't already contain a namespace separator, but does contain
	 * non-word characters, these will have been replaced with regex syntax to allow
	 * for namespace separators and the `is_regex` indicator will have been set to `true`.
	 *
	 * @since 1.2.0
	 *
	 * @var array
	 */
	private $validated_namespace_prefixes = array();

	/**
	 * Cache of previously set prefixes.
	 *
	 * Prevents having to do the same prefix validation over and over again.
	 *
	 * @since 0.12.0
	 *
	 * @var string[]
	 */
	private $previous_prefixes = array();

	/**
	 * A list of all PHP superglobals with the exception of $GLOBALS which is handled separately.
	 *
	 * @since 0.12.0
	 *
	 * @var array
	 */
	protected $superglobals = array(
		'_COOKIE'  => true,
		'_ENV'     => true,
		'_GET'     => true,
		'_FILES'   => true,
		'_POST'    => true,
		'_REQUEST' => true,
		'_SERVER'  => true,
		'_SESSION' => true,
	);

	/**
	 * A list of core hooks that are allowed to be called by plugins and themes.
	 *
	 * @since 0.14.0
	 *
	 * @var array
	 */
	protected $whitelisted_core_hooks = array(
		'widget_title'   => true,
		'add_meta_boxes' => true,
	);

	/**
	 * A list of core constants that are allowed to be defined by plugins and themes.
	 *
	 * @since 1.0.0
	 *
	 * Source: {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/default-constants.php#L0}
	 * The constants are listed in the order they are found in the source file
	 * to make life easier for future updates.
	 * Only overrulable constants are listed, i.e. those defined within core within
	 * a `if ( ! defined() ) {}` wrapper.
	 *
	 * @var array
	 */
	protected $whitelisted_core_constants = array(
		'WP_MEMORY_LIMIT'      => true,
		'WP_MAX_MEMORY_LIMIT'  => true,
		'WP_CONTENT_DIR'       => true,
		'WP_DEBUG'             => true,
		'WP_DEBUG_DISPLAY'     => true,
		'WP_DEBUG_LOG'         => true,
		'WP_CACHE'             => true,
		'SCRIPT_DEBUG'         => true,
		'MEDIA_TRASH'          => true,
		'SHORTINIT'            => true,
		'WP_CONTENT_URL'       => true,
		'WP_PLUGIN_DIR'        => true,
		'WP_PLUGIN_URL'        => true,
		'PLUGINDIR'            => true,
		'WPMU_PLUGIN_DIR'      => true,
		'WPMU_PLUGIN_URL'      => true,
		'MUPLUGINDIR'          => true,
		'COOKIEHASH'           => true,
		'USER_COOKIE'          => true,
		'PASS_COOKIE'          => true,
		'AUTH_COOKIE'          => true,
		'SECURE_AUTH_COOKIE'   => true,
		'LOGGED_IN_COOKIE'     => true,
		'TEST_COOKIE'          => true,
		'COOKIEPATH'           => true,
		'SITECOOKIEPATH'       => true,
		'ADMIN_COOKIE_PATH'    => true,
		'PLUGINS_COOKIE_PATH'  => true,
		'COOKIE_DOMAIN'        => true,
		'FORCE_SSL_ADMIN'      => true,
		'FORCE_SSL_LOGIN'      => true,
		'AUTOSAVE_INTERVAL'    => true,
		'EMPTY_TRASH_DAYS'     => true,
		'WP_POST_REVISIONS'    => true,
		'WP_CRON_LOCK_TIMEOUT' => true,
		'WP_DEFAULT_THEME'     => true,
	);

	/**
	 * List of all PHP native functions.
	 *
	 * Using this list rather than a call to `function_exists()` prevents
	 * false negatives from user-defined functions when those would be
	 * autoloaded via a Composer autoload files directives.
	 *
	 * @var array
	 */
	private $built_in_functions;

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @since 0.12.0
	 *
	 * @return array
	 */
	public function register() {
		// Get a list of all PHP native functions.
		$all_functions            = get_defined_functions();
		$this->built_in_functions = array_flip( $all_functions['internal'] );

		// Set the sniff targets.
		$targets  = array(
			\T_NAMESPACE => \T_NAMESPACE,
			\T_FUNCTION  => \T_FUNCTION,
			\T_CONST     => \T_CONST,
			\T_VARIABLE  => \T_VARIABLE,
			\T_DOLLAR    => \T_DOLLAR, // Variable variables.
		);
		$targets += Tokens::$ooScopeTokens; // T_ANON_CLASS is only used for skipping over test classes.

		// Add function call target for hook names and constants defined using define().
		$parent = parent::register();
		if ( ! empty( $parent ) ) {
			$targets[] = \T_STRING;
		}

		return $targets;
	}

	/**
	 * Groups of functions to restrict.
	 *
	 * @since 0.12.0
	 *
	 * @return array
	 */
	public function getGroups() {
		$this->target_functions           = $this->hookInvokeFunctions;
		$this->target_functions['define'] = true;

		return parent::getGroups();
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @since 0.12.0
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	public function process_token( $stackPtr ) {
		/*
		 * Allow for whitelisting.
		 *
		 * Generally speaking a theme/plugin should *only* execute their own hooks, but there may be a
		 * good reason to execute a core hook.
		 *
		 * Similarly, newer PHP or WP functions or constants may need to be emulated for continued support
		 * of older PHP and WP versions.
		 */
		if ( $this->has_whitelist_comment( 'prefix', $stackPtr ) ) {
			return;
		}

		// Allow overruling the prefixes set in a ruleset via the command line.
		$cl_prefixes = trim( PHPCSHelper::get_config_data( 'prefixes' ) );
		if ( ! empty( $cl_prefixes ) ) {
			$this->prefixes = array_filter( array_map( 'trim', explode( ',', $cl_prefixes ) ) );
		}

		$this->prefixes = $this->merge_custom_array( $this->prefixes, array(), false );
		if ( empty( $this->prefixes ) ) {
			// No prefixes passed, nothing to do.
			return;
		}

		$this->validate_prefixes();
		if ( empty( $this->validated_prefixes ) ) {
			// No _valid_ prefixes passed, nothing to do.
			return;
		}

		// Ignore test classes.
		if ( isset( Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] )
			&& true === $this->is_test_class( $stackPtr )
		) {
			if ( $this->tokens[ $stackPtr ]['scope_condition'] === $stackPtr && isset( $this->tokens[ $stackPtr ]['scope_closer'] ) ) {
				// Skip forward to end of test class.
				return $this->tokens[ $stackPtr ]['scope_closer'];
			}
			return;
		}

		if ( \T_ANON_CLASS === $this->tokens[ $stackPtr ]['code'] ) {
			// Token was only registered to allow skipping over test classes.
			return;
		}

		if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] ) {
			// Disallow excluding function groups for this sniff.
			$this->exclude = array();

			return parent::process_token( $stackPtr );

		} elseif ( \T_DOLLAR === $this->tokens[ $stackPtr ]['code'] ) {

			return $this->process_variable_variable( $stackPtr );

		} elseif ( \T_VARIABLE === $this->tokens[ $stackPtr ]['code'] ) {

			return $this->process_variable_assignment( $stackPtr );

		} elseif ( \T_NAMESPACE === $this->tokens[ $stackPtr ]['code'] ) {
			$namespace_name = $this->get_declared_namespace_name( $stackPtr );

			if ( false === $namespace_name || '' === $namespace_name || '\\' === $namespace_name ) {
				return;
			}

			foreach ( $this->validated_namespace_prefixes as $key => $prefix_info ) {
				if ( false === $prefix_info['is_regex'] ) {
					if ( stripos( $namespace_name, $prefix_info['prefix'] ) === 0 ) {
						$this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $key );
						return;
					}
				} else {
					// Ok, so this prefix should be used as a regex.
					$regex = '`^' . $prefix_info['prefix'] . '`i';
					if ( preg_match( $regex, $namespace_name ) > 0 ) {
						$this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $key );
						return;
					}
				}
			}

			// Still here ? In that case, we have a non-prefixed namespace name.
			$recorded = $this->phpcsFile->addError(
				self::ERROR_MSG,
				$stackPtr,
				'NonPrefixedNamespaceFound',
				array(
					'Namespaces declared',
					$namespace_name,
				)
			);

			if ( true === $recorded ) {
				$this->record_potential_prefix_metric( $stackPtr, $namespace_name );
			}

			return;

		} else {

			// Namespaced methods, classes and constants do not need to be prefixed.
			$namespace = $this->determine_namespace( $stackPtr );
			if ( '' !== $namespace && '\\' !== $namespace ) {
				return;
			}

			$item_name  = '';
			$error_text = 'Unknown syntax used';
			$error_code = 'NonPrefixedSyntaxFound';

			switch ( $this->tokens[ $stackPtr ]['type'] ) {
				case 'T_FUNCTION':
					// Methods in a class do not need to be prefixed.
					if ( $this->phpcsFile->hasCondition( $stackPtr, Tokens::$ooScopeTokens ) === true ) {
						return;
					}

					$item_name = $this->phpcsFile->getDeclarationName( $stackPtr );
					if ( isset( $this->built_in_functions[ $item_name ] ) ) {
						// Backfill for PHP native function.
						return;
					}

					$error_text = 'Functions declared in the global namespace';
					$error_code = 'NonPrefixedFunctionFound';
					break;

				case 'T_CLASS':
				case 'T_INTERFACE':
				case 'T_TRAIT':
					$item_name  = $this->phpcsFile->getDeclarationName( $stackPtr );
					$error_text = 'Classes declared';
					$error_code = 'NonPrefixedClassFound';

					switch ( $this->tokens[ $stackPtr ]['type'] ) {
						case 'T_CLASS':
							if ( class_exists( '\\' . $item_name, false ) ) {
								// Backfill for PHP native class.
								return;
							}
							break;

						case 'T_INTERFACE':
							if ( interface_exists( '\\' . $item_name, false ) ) {
								// Backfill for PHP native interface.
								return;
							}

							$error_text = 'Interfaces declared';
							$error_code = 'NonPrefixedInterfaceFound';
							break;

						case 'T_TRAIT':
							// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.trait_existsFound
							if ( function_exists( '\trait_exists' ) && trait_exists( '\\' . $item_name, false ) ) {
								// Backfill for PHP native trait.
								return;
							}

							$error_text = 'Traits declared';
							$error_code = 'NonPrefixedTraitFound';
							break;

						default:
							// Left empty on purpose.
							break;
					}

					break;

				case 'T_CONST':
					// Constants in a class do not need to be prefixed.
					if ( true === $this->is_class_constant( $stackPtr ) ) {
						return;
					}

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

					$item_name = $this->tokens[ $constant_name_ptr ]['content'];
					if ( \defined( '\\' . $item_name ) ) {
						// Backfill for PHP native constant.
						return;
					}

					if ( isset( $this->whitelisted_core_constants[ $item_name ] ) ) {
						// Defining a WP Core constant intended for overruling.
						return;
					}

					$error_text = 'Global constants defined';
					$error_code = 'NonPrefixedConstantFound';
					break;

				default:
					// Left empty on purpose.
					break;

			}

			if ( empty( $item_name ) || $this->is_prefixed( $stackPtr, $item_name ) === true ) {
				return;
			}

			$recorded = $this->phpcsFile->addError(
				self::ERROR_MSG,
				$stackPtr,
				$error_code,
				array(
					$error_text,
					$item_name,
				)
			);

			if ( true === $recorded ) {
				$this->record_potential_prefix_metric( $stackPtr, $item_name );
			}
		}
	}

	/**
	 * Handle variable variables defined in the global namespace.
	 *
	 * @since 0.12.0
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	protected function process_variable_variable( $stackPtr ) {
		static $indicators = array(
			\T_OPEN_CURLY_BRACKET => true,
			\T_VARIABLE           => true,
		);

		// Is this a variable variable ?
		// Not concerned with nested ones as those will be recognized on their own token.
		$next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
		if ( false === $next_non_empty || ! isset( $indicators[ $this->tokens[ $next_non_empty ]['code'] ] ) ) {
			return;
		}

		if ( \T_OPEN_CURLY_BRACKET === $this->tokens[ $next_non_empty ]['code']
			&& isset( $this->tokens[ $next_non_empty ]['bracket_closer'] )
		) {
			// Skip over the variable part.
			$next_non_empty = $this->tokens[ $next_non_empty ]['bracket_closer'];
		}

		$maybe_assignment = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true );

		while ( false !== $maybe_assignment
			&& \T_OPEN_SQUARE_BRACKET === $this->tokens[ $maybe_assignment ]['code']
			&& isset( $this->tokens[ $maybe_assignment ]['bracket_closer'] )
		) {
			$maybe_assignment = $this->phpcsFile->findNext(
				Tokens::$emptyTokens,
				( $this->tokens[ $maybe_assignment ]['bracket_closer'] + 1 ),
				null,
				true,
				null,
				true
			);
		}

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

		if ( ! isset( Tokens::$assignmentTokens[ $this->tokens[ $maybe_assignment ]['code'] ] ) ) {
			// Not an assignment.
			return;
		}

		$error = self::ERROR_MSG;

		/*
		 * Local variable variables in a function do not need to be prefixed.
		 * But a variable variable could evaluate to the name of an imported global
		 * variable.
		 * Not concerned with imported variable variables (global.. ) as that has been
		 * forbidden since PHP 7.0. Presuming cross-version code and if not, that
		 * is for the PHPCompatibility standard to detect.
		 */
		if ( $this->phpcsFile->hasCondition( $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ) === true ) {
			$condition = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
			if ( false === $condition ) {
				$condition = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE );
			}

			$has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $condition ]['scope_opener'] );
			if ( false === $has_global ) {
				// No variable import happening.
				return;
			}

			$error = 'Variable variable which could potentially override an imported global variable detected. ' . $error;
		}

		$variable_name = $this->phpcsFile->getTokensAsString( $stackPtr, ( ( $next_non_empty - $stackPtr ) + 1 ) );

		// Still here ? In that case, the variable name should be prefixed.
		$recorded = $this->phpcsFile->addWarning(
			$error,
			$stackPtr,
			'NonPrefixedVariableFound',
			array(
				'Global variables defined',
				$variable_name,
			)
		);

		if ( true === $recorded ) {
			$this->record_potential_prefix_metric( $stackPtr, $variable_name );
		}

		// Skip over the variable part of the variable.
		return ( $next_non_empty + 1 );
	}

	/**
	 * Check that defined global variables are prefixed.
	 *
	 * @since 0.12.0
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	protected function process_variable_assignment( $stackPtr ) {
		/*
		 * We're only concerned with variables which are being defined.
		 * `is_assigment()` will not recognize property assignments, which is good in this case.
		 * However it will also not recognize $b in `foreach( $a as $b )` as an assignment, so
		 * we need a separate check for that.
		 */
		if ( false === $this->is_assignment( $stackPtr )
			&& false === $this->is_foreach_as( $stackPtr )
		) {
			return;
		}

		$is_error      = true;
		$variable_name = substr( $this->tokens[ $stackPtr ]['content'], 1 ); // Strip the dollar sign.

		// Bow out early if we know for certain no prefix is needed.
		if ( $this->variable_prefixed_or_whitelisted( $stackPtr, $variable_name ) === true ) {
			return;
		}

		if ( 'GLOBALS' === $variable_name ) {
			$array_open = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
			if ( false === $array_open || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $array_open ]['code'] ) {
				// Live coding or something very silly.
				return;
			}

			$array_key = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $array_open + 1 ), null, true, null, true );
			if ( false === $array_key ) {
				// No key found, nothing to do.
				return;
			}

			$stackPtr      = $array_key;
			$variable_name = $this->strip_quotes( $this->tokens[ $array_key ]['content'] );

			// Check whether a prefix is needed.
			if ( isset( Tokens::$stringTokens[ $this->tokens[ $array_key ]['code'] ] )
				&& $this->variable_prefixed_or_whitelisted( $stackPtr, $variable_name ) === true
			) {
				return;
			}

			if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $array_key ]['code'] ) {
				// If the array key is a double quoted string, try again with only
				// the part before the first variable (if any).
				$exploded = explode( '$', $variable_name );
				$first    = rtrim( $exploded[0], '{' );
				if ( '' !== $first ) {
					if ( $this->variable_prefixed_or_whitelisted( $array_key, $first ) === true ) {
						return;
					}
				} else {
					// If the first part was dynamic, throw a warning.
					$is_error = false;
				}
			} elseif ( ! isset( Tokens::$stringTokens[ $this->tokens[ $array_key ]['code'] ] ) ) {
				// Dynamic array key, throw a warning.
				$is_error = false;
			}
		} else {
			// Function parameters do not need to be prefixed.
			if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
				foreach ( $this->tokens[ $stackPtr ]['nested_parenthesis'] as $opener => $closer ) {
					if ( isset( $this->tokens[ $opener ]['parenthesis_owner'] )
						&& ( \T_FUNCTION === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code']
							|| \T_CLOSURE === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code'] )
					) {
						return;
					}
				}
				unset( $opener, $closer );
			}

			// Properties in a class do not need to be prefixed.
			if ( true === $this->is_class_property( $stackPtr ) ) {
				return;
			}

			// Local variables in a function do not need to be prefixed unless they are being imported.
			if ( $this->phpcsFile->hasCondition( $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ) === true ) {
				$condition = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
				if ( false === $condition ) {
					$condition = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE );
				}

				$has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $condition ]['scope_opener'] );
				if ( false === $has_global ) {
					// No variable import happening.
					return;
				}

				// Ok, this may be an imported global variable.
				$end_of_statement = $this->phpcsFile->findNext( \T_SEMICOLON, ( $has_global + 1 ) );
				if ( false === $end_of_statement ) {
					// No semi-colon - live coding.
					return;
				}

				for ( $ptr = ( $has_global + 1 ); $ptr <= $end_of_statement; $ptr++ ) {
					// Move the stack pointer to the next variable.
					$ptr = $this->phpcsFile->findNext( \T_VARIABLE, $ptr, $end_of_statement, false, null, true );

					if ( false === $ptr ) {
						// Reached the end of the global statement without finding the variable,
						// so this must be a local variable.
						return;
					}

					if ( substr( $this->tokens[ $ptr ]['content'], 1 ) === $variable_name ) {
						break;
					}
				}

				unset( $condition, $has_global, $end_of_statement, $ptr, $imported );

			}
		}

		// Still here ? In that case, the variable name should be prefixed.
		$recorded = $this->addMessage(
			self::ERROR_MSG,
			$stackPtr,
			$is_error,
			'NonPrefixedVariableFound',
			array(
				'Global variables defined',
				'$' . $variable_name,
			)
		);

		if ( true === $recorded ) {
			$this->record_potential_prefix_metric( $stackPtr, $variable_name );
		}
	}

	/**
	 * Process the parameters of a matched function.
	 *
	 * @since 0.12.0
	 *
	 * @param int    $stackPtr        The position of the current token in the stack.
	 * @param array  $group_name      The name of the group which was matched.
	 * @param string $matched_content The token content (function name) which was matched.
	 * @param array  $parameters      Array with information about the parameters.
	 *
	 * @return void
	 */
	public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {

		// Ignore deprecated hook names.
		if ( strpos( $matched_content, '_deprecated' ) > 0 ) {
			return;
		}

		// No matter whether it is a constant definition or a hook call, both use the first parameter.
		if ( ! isset( $parameters[1] ) ) {
			return;
		}

		$is_error    = true;
		$raw_content = $this->strip_quotes( $parameters[1]['raw'] );

		if ( ( 'define' !== $matched_content
			&& isset( $this->whitelisted_core_hooks[ $raw_content ] ) )
			|| ( 'define' === $matched_content
			&& isset( $this->whitelisted_core_constants[ $raw_content ] ) )
		) {
			return;
		}

		if ( $this->is_prefixed( $parameters[1]['start'], $raw_content ) === true ) {
			return;
		} else {
			// This may be a dynamic hook/constant name.
			$first_non_empty = $this->phpcsFile->findNext(
				Tokens::$emptyTokens,
				$parameters[1]['start'],
				( $parameters[1]['end'] + 1 ),
				true
			);

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

			$first_non_empty_content = $this->strip_quotes( $this->tokens[ $first_non_empty ]['content'] );

			// Try again with just the first token if it's a text string.
			if ( isset( Tokens::$stringTokens[ $this->tokens[ $first_non_empty ]['code'] ] )
				&& $this->is_prefixed( $parameters[1]['start'], $first_non_empty_content ) === true
			) {
				return;
			}

			if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $first_non_empty ]['code'] ) {
				// If the first part of the parameter is a double quoted string, try again with only
				// the part before the first variable (if any).
				$exploded = explode( '$', $first_non_empty_content );
				$first    = rtrim( $exploded[0], '{' );
				if ( '' !== $first ) {
					if ( $this->is_prefixed( $parameters[1]['start'], $first ) === true ) {
						return;
					}
				} else {
					// Start of hook/constant name is dynamic, throw a warning.
					$is_error = false;
				}
			} elseif ( ! isset( Tokens::$stringTokens[ $this->tokens[ $first_non_empty ]['code'] ] ) ) {
				// Dynamic hook/constant name, throw a warning.
				$is_error = false;
			}
		}

		if ( 'define' === $matched_content ) {
			if ( \defined( '\\' . $raw_content ) ) {
				// Backfill for PHP native constant.
				return;
			}

			if ( strpos( $raw_content, '\\' ) !== false ) {
				// Namespaced or unreachable constant.
				return;
			}

			$data       = array( 'Global constants defined' );
			$error_code = 'NonPrefixedConstantFound';
			if ( false === $is_error ) {
				$error_code = 'VariableConstantNameFound';
			}
		} else {
			$data       = array( 'Hook names invoked' );
			$error_code = 'NonPrefixedHooknameFound';
			if ( false === $is_error ) {
				$error_code = 'DynamicHooknameFound';
			}
		}

		$data[] = $raw_content;

		$recorded = $this->addMessage( self::ERROR_MSG, $first_non_empty, $is_error, $error_code, $data );

		if ( true === $recorded ) {
			$this->record_potential_prefix_metric( $stackPtr, $raw_content );
		}
	}

	/**
	 * Check if a function/class/constant/variable name is prefixed with one of the expected prefixes.
	 *
	 * @since 0.12.0
	 * @since 0.14.0 Allows for other non-word characters as well as underscores to better support hook names.
	 * @since 1.0.0  Does not require a word seperator anymore after a prefix.
	 *               This allows for improved code style independent checking,
	 *               i.e. allows for camelCase naming and the likes.
	 * @since 1.0.1  - Added $stackPtr parameter.
	 *               - The function now also records metrics about the prefixes encountered.
	 *
	 * @param int    $stackPtr The position of the token to record the metric against.
	 * @param string $name     Name to check for a prefix.
	 *
	 * @return bool True when the name is one of the prefixes or starts with an allowed prefix.
	 *              False otherwise.
	 */
	private function is_prefixed( $stackPtr, $name ) {
		foreach ( $this->validated_prefixes as $prefix ) {
			if ( stripos( $name, $prefix ) === 0 ) {
				$this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: allowed prefixes', $prefix );
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if a variable name might need a prefix.
	 *
	 * Prefix is not needed for:
	 * - superglobals,
	 * - WP native globals,
	 * - variables which are already prefixed.
	 *
	 * @since 0.12.0
	 * @since 1.0.1  Added $stackPtr parameter.
	 *
	 * @param int    $stackPtr The position of the token to record the metric against.
	 * @param string $name     Variable name without the dollar sign.
	 *
	 * @return bool True if the variable name is whitelisted or already prefixed.
	 *              False otherwise.
	 */
	private function variable_prefixed_or_whitelisted( $stackPtr, $name ) {
		// Ignore superglobals and WP global variables.
		if ( isset( $this->superglobals[ $name ] ) || isset( $this->wp_globals[ $name ] ) ) {
			return true;
		}

		return $this->is_prefixed( $stackPtr, $name );
	}

	/**
	 * Validate an array of prefixes as passed through a custom property or via the command line.
	 *
	 * Checks that the prefix:
	 * - is not one of the blacklisted ones.
	 * - complies with the PHP rules for valid function, class, variable, constant names.
	 *
	 * @since 0.12.0
	 */
	private function validate_prefixes() {
		if ( $this->previous_prefixes === $this->prefixes ) {
			return;
		}

		// Set the cache *before* validation so as to not break the above compare.
		$this->previous_prefixes = $this->prefixes;

		// Validate the passed prefix(es).
		$prefixes    = array();
		$ns_prefixes = array();
		foreach ( $this->prefixes as $key => $prefix ) {
			$prefixLC = strtolower( $prefix );

			if ( isset( $this->prefix_blacklist[ $prefixLC ] ) ) {
				$this->phpcsFile->addError(
					'The "%s" prefix is not allowed.',
					0,
					'ForbiddenPrefixPassed',
					array( $prefix )
				);
				continue;
			}

			// Validate the prefix against characters allowed for function, class, constant names etc.
			if ( preg_match( '`^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*$`', $prefix ) !== 1 ) {
				$this->phpcsFile->addWarning(
					'The "%s" prefix is not a valid namespace/function/class/variable/constant prefix in PHP.',
					0,
					'InvalidPrefixPassed',
					array( $prefix )
				);
			}

			// Lowercase the prefix to allow for direct compare.
			$prefixes[ $key ] = $prefixLC;

			/*
			 * Replace non-word characters in the prefix with a regex snippet, but only if the
			 * string doesn't already contain namespace separators.
			 */
			$is_regex = false;
			if ( strpos( $prefix, '\\' ) === false && preg_match( '`[_\W]`', $prefix ) > 0 ) {
				$prefix   = preg_replace( '`([_\W])`', '[\\\\\\\\$1]', $prefixLC );
				$is_regex = true;
			}

			$ns_prefixes[ $prefixLC ] = array(
				'prefix'   => $prefix,
				'is_regex' => $is_regex,
			);
		}

		// Set the validated prefixes caches.
		$this->validated_prefixes           = $prefixes;
		$this->validated_namespace_prefixes = $ns_prefixes;
	}

	/**
	 * Record the "potential prefix" metric.
	 *
	 * @since 1.0.1
	 *
	 * @param int    $stackPtr       The position of the token to record the metric against.
	 * @param string $construct_name Name of the global construct to try and distill a potential prefix from.
	 *
	 * @return void
	 */
	private function record_potential_prefix_metric( $stackPtr, $construct_name ) {
		if ( preg_match( '`^([A-Z]*[a-z0-9]*+)`', ltrim( $construct_name, '\$_' ), $matches ) > 0
			&& isset( $matches[1] ) && '' !== $matches[1]
		) {
			$this->phpcsFile->recordMetric( $stackPtr, 'Prefix all globals: potential prefixes - start of non-prefixed construct', strtolower( $matches[1] ) );
		}
	}
}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress;

use WordPressCS\WordPress\Sniff;

/**
 * Restricts array assignment of certain keys.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.3.0
 * @since   0.10.0 Class became a proper abstract class. This was already the behaviour.
 *                 Moved the file and renamed the class from
 *                 `\WordPressCS\WordPress\Sniffs\Arrays\ArrayAssignmentRestrictionsSniff` to
 *                 `\WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff`.
 */
abstract class AbstractArrayAssignmentRestrictionsSniff extends Sniff {

	/**
	 * Exclude groups.
	 *
	 * Example: 'foo,bar'
	 *
	 * @since 0.3.0
	 * @since 1.0.0 This property now expects to be passed an array.
	 *              Previously a comma-delimited string was expected.
	 *
	 * @var array
	 */
	public $exclude = array();

	/**
	 * Groups of variable data to check against.
	 * Don't use this in extended classes, override getGroups() instead.
	 * This is only used for Unit tests.
	 *
	 * @var array
	 */
	public static $groups = array();

	/**
	 * Cache for the excluded groups information.
	 *
	 * @since 0.11.0
	 *
	 * @var array
	 */
	protected $excluded_groups = array();

	/**
	 * Cache for the group information.
	 *
	 * @since 0.13.0
	 *
	 * @var array
	 */
	protected $groups_cache = array();

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		// Retrieve the groups only once and don't set up a listener if there are no groups.
		if ( false === $this->setup_groups() ) {
			return array();
		}

		return array(
			\T_DOUBLE_ARROW,
			\T_CLOSE_SQUARE_BRACKET,
			\T_CONSTANT_ENCAPSED_STRING,
			\T_DOUBLE_QUOTED_STRING,
		);
	}

	/**
	 * Groups of variables to restrict.
	 *
	 * This method should be overridden in extending classes.
	 *
	 * Example: groups => array(
	 *  'groupname' => array(
	 *      'type'     => 'error' | 'warning',
	 *      'message'  => 'Dont use this one please!',
	 *      'keys'     => array( 'key1', 'another_key' ),
	 *      'callback' => array( 'class', 'method' ), // Optional.
	 *  )
	 * )
	 *
	 * @return array
	 */
	abstract public function getGroups();

	/**
	 * Cache the groups.
	 *
	 * @since 0.13.0
	 *
	 * @return bool True if the groups were setup. False if not.
	 */
	protected function setup_groups() {
		$this->groups_cache = $this->getGroups();

		if ( empty( $this->groups_cache ) && empty( self::$groups ) ) {
			return false;
		}

		// Allow for adding extra unit tests.
		if ( ! empty( self::$groups ) ) {
			$this->groups_cache = array_merge( $this->groups_cache, self::$groups );
		}

		return true;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return void
	 */
	public function process_token( $stackPtr ) {

		$this->excluded_groups = $this->merge_custom_array( $this->exclude );
		if ( array_diff_key( $this->groups_cache, $this->excluded_groups ) === array() ) {
			// All groups have been excluded.
			// Don't remove the listener as the exclude property can be changed inline.
			return;
		}

		$token = $this->tokens[ $stackPtr ];

		if ( \T_CLOSE_SQUARE_BRACKET === $token['code'] ) {
			$equal = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), null, true );
			if ( \T_EQUAL !== $this->tokens[ $equal ]['code'] ) {
				return; // This is not an assignment!
			}
		}

		// Instances: Multi-dimensional array, keyed by line.
		$inst = array();

		/*
		 * Covers:
		 * $foo = array( 'bar' => 'taz' );
		 * $foo['bar'] = $taz;
		 */
		if ( \in_array( $token['code'], array( \T_CLOSE_SQUARE_BRACKET, \T_DOUBLE_ARROW ), true ) ) {
			$operator = $stackPtr; // T_DOUBLE_ARROW.
			if ( \T_CLOSE_SQUARE_BRACKET === $token['code'] ) {
				$operator = $this->phpcsFile->findNext( \T_EQUAL, ( $stackPtr + 1 ) );
			}

			$keyIdx = $this->phpcsFile->findPrevious( array( \T_WHITESPACE, \T_CLOSE_SQUARE_BRACKET ), ( $operator - 1 ), null, true );
			if ( ! is_numeric( $this->tokens[ $keyIdx ]['content'] ) ) {
				$key            = $this->strip_quotes( $this->tokens[ $keyIdx ]['content'] );
				$valStart       = $this->phpcsFile->findNext( array( \T_WHITESPACE ), ( $operator + 1 ), null, true );
				$valEnd         = $this->phpcsFile->findNext( array( \T_COMMA, \T_SEMICOLON ), ( $valStart + 1 ), null, false, null, true );
				$val            = $this->phpcsFile->getTokensAsString( $valStart, ( $valEnd - $valStart ) );
				$val            = $this->strip_quotes( $val );
				$inst[ $key ][] = array( $val, $token['line'] );
			}
		} elseif ( \in_array( $token['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) {
			// $foo = 'bar=taz&other=thing';
			if ( preg_match_all( '#(?:^|&)([a-z_]+)=([^&]*)#i', $this->strip_quotes( $token['content'] ), $matches ) <= 0 ) {
				return; // No assignments here, nothing to check.
			}
			foreach ( $matches[1] as $i => $_k ) {
				$inst[ $_k ][] = array( $matches[2][ $i ], $token['line'] );
			}
		}

		if ( empty( $inst ) ) {
			return;
		}

		foreach ( $this->groups_cache as $groupName => $group ) {

			if ( isset( $this->excluded_groups[ $groupName ] ) ) {
				continue;
			}

			$callback = ( isset( $group['callback'] ) && is_callable( $group['callback'] ) ) ? $group['callback'] : array( $this, 'callback' );

			foreach ( $inst as $key => $assignments ) {
				foreach ( $assignments as $occurance ) {
					list( $val, $line ) = $occurance;

					if ( ! \in_array( $key, $group['keys'], true ) ) {
						continue;
					}

					$output = \call_user_func( $callback, $key, $val, $line, $group );

					if ( ! isset( $output ) || false === $output ) {
						continue;
					} elseif ( true === $output ) {
						$message = $group['message'];
					} else {
						$message = $output;
					}

					$this->addMessage(
						$message,
						$stackPtr,
						( 'error' === $group['type'] ),
						$this->string_to_errorcode( $groupName . '_' . $key ),
						array( $key, $val )
					);
				}
			}
		}
	}

	/**
	 * Callback to process each confirmed key, to check value.
	 *
	 * This method must be extended to add the logic to check assignment value.
	 *
	 * @param  string $key   Array index / key.
	 * @param  mixed  $val   Assigned value.
	 * @param  int    $line  Token line.
	 * @param  array  $group Group definition.
	 * @return mixed         FALSE if no match, TRUE if matches, STRING if matches
	 *                       with custom error message passed to ->process().
	 */
	abstract public function callback( $key, $val, $line, $group );

}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\Arrays;

use WordPressCS\WordPress\Sniff;

/**
 * Enforces alignment of the double arrow assignment operator for multi-item, multi-line arrays.
 *
 * - Align the double arrow operator to the same column for each item in a multi-item array.
 * - Allows for setting a maxColumn property to aid in managing line-length.
 * - Allows for new line(s) before a double arrow (configurable).
 * - Allows for handling multi-line array items differently if so desired (configurable).
 *
 * @link    https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.14.0
 *
 * {@internal This sniff should eventually be pulled upstream as part of a solution
 * for https://github.com/squizlabs/PHP_CodeSniffer/issues/582 }}
 */
class MultipleStatementAlignmentSniff extends Sniff {

	/**
	 * Whether or not to ignore an array item for the purpose of alignment
	 * when a new line is found between the array key and the double arrow.
	 *
	 * @since 0.14.0
	 *
	 * @var bool
	 */
	public $ignoreNewlines = true;

	/**
	 * Whether the alignment should be exact.
	 *
	 * Exact in this context means "largest index key + 1 space".
	 * When `false`, that is seen as the minimum alignment.
	 *
	 * @since 0.14.0
	 *
	 * @var bool
	 */
	public $exact = true;

	/**
	 * The maximum column on which the double arrow alignment should be set.
	 *
	 * This property allows for limiting the whitespace padding to prevent
	 * overly long lines.
	 *
	 * If this value is set to, for instance, 60, it will:
	 * - if the expected column < 60, align at the expected column.
	 * - if the expected column >= 60, align at column 60.
	 *   - for the outliers, i.e. the array indexes where the end position
	 *     goes past column 60, it will not align the arrow, the sniff will
	 *     just make sure there is only one space between the end of the
	 *     array index and the double arrow.
	 *
	 * The column value is regarded as a hard value, i.e. includes indentation,
	 * so setting it very low is not a good idea.
	 *
	 * @since 0.14.0
	 *
	 * @var int
	 */
	public $maxColumn = 1000;

	/**
	 * Whether or not to align the arrow operator for multi-line array items.
	 *
	 * Whether or not an item is regarded as multi-line is based on the **value**
	 * of the item, not the key.
	 *
	 * Valid values are:
	 * - 'always':   Default. Align all arrays items regardless of single/multi-line.
	 * - 'never':    Never align array items which span multiple lines.
	 *               This will enforce one space between the array index and the
	 *               double arrow operator for multi-line array items, independently
	 *               of the alignment of the rest of the array items.
	 *               Multi-line items where the arrow is already aligned with the
	 *               "expected" alignment, however, will be left alone.
	 * - operator :  Only align the operator for multi-line arrays items if the
	 *   + number    percentage of multi-line items passes the comparison.
	 *               - As it is a percentage, the number has to be between 0 and 100.
	 *               - Supported operators: <, <=, >, >=, ==, =, !=, <>
	 *               - The percentage is calculated against all array items
	 *                 (with and without assignment operator).
	 *               - The (new) expected alignment will be calculated based only
	 *                 on the items being aligned.
	 *               - Multi-line items where the arrow is already aligned with the
	 *                 (new) "expected" alignment, however, will be left alone.
	 *               Examples:
	 *               * Setting this to `!=100` or `<100` means that alignment will
	 *                 be enforced, unless *all* array items are multi-line.
	 *                 This is probably the most commonly desired situation.
	 *               * Setting this to `=100` means that alignment will only
	 *                 be enforced, if *all* array items are multi-line.
	 *               * Setting this to `<50` means that the majority of array items
	 *                 need to be single line before alignment is enforced for
	 *                 multi-line items in the array.
	 *               * Setting this to `=0` is useless as in that case there are
	 *                 no multi-line items in the array anyway.
	 *
	 * This setting will respect the `ignoreNewlines` and `maxColumnn` settings.
	 *
	 * @since 0.14.0
	 *
	 * @var string|int
	 */
	public $alignMultilineItems = 'always';

	/**
	 * Storage for parsed $alignMultilineItems operator part.
	 *
	 * @since 0.14.0
	 *
	 * @var string
	 */
	private $operator;

	/**
	 * Storage for parsed $alignMultilineItems numeric part.
	 *
	 * Stored as a string as the comparison will be done string based.
	 *
	 * @since 0.14.0
	 *
	 * @var string
	 */
	private $number;

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @since 0.14.0
	 *
	 * @return array
	 */
	public function register() {
		return array(
			\T_ARRAY,
			\T_OPEN_SHORT_ARRAY,
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @since 0.14.0
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	public function process_token( $stackPtr ) {
		/*
		 * Determine the array opener & closer.
		 */
		$array_open_close = $this->find_array_open_close( $stackPtr );
		if ( false === $array_open_close ) {
			// Array open/close could not be determined.
			return;
		}

		$opener = $array_open_close['opener'];
		$closer = $array_open_close['closer'];

		$array_items = $this->get_function_call_parameters( $stackPtr );
		if ( empty( $array_items ) ) {
			return;
		}

		// Pass off to either the single line or multi-line array analysis.
		if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
			return $this->process_single_line_array( $stackPtr, $array_items, $opener, $closer );
		} else {
			return $this->process_multi_line_array( $stackPtr, $array_items, $opener, $closer );
		}
	}

	/**
	 * Process a single-line array.
	 *
	 * While the WP standard does not allow single line multi-item associative arrays,
	 * this sniff should function independently of that.
	 *
	 * The `WordPress.WhiteSpace.OperatorSpacing` sniff already covers checking that
	 * there is a space between the array key and the double arrow, but doesn't
	 * enforce it to be exactly one space for single line arrays.
	 * That is what this method covers.
	 *
	 * @since 0.14.0
	 *
	 * @param int   $stackPtr The position of the current token in the stack.
	 * @param array $items    Info array containing information on each array item.
	 * @param int   $opener   The position of the array opener.
	 * @param int   $closer   The position of the array closer.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	protected function process_single_line_array( $stackPtr, $items, $opener, $closer ) {
		/*
		 * For single line arrays, we don't care about what level the arrow is from.
		 * Just find and fix them all.
		 */
		$next_arrow = $this->phpcsFile->findNext(
			\T_DOUBLE_ARROW,
			( $opener + 1 ),
			$closer
		);

		while ( false !== $next_arrow ) {
			if ( \T_WHITESPACE === $this->tokens[ ( $next_arrow - 1 ) ]['code'] ) {
				$space_length = $this->tokens[ ( $next_arrow - 1 ) ]['length'];
				if ( 1 !== $space_length ) {
					$error = 'Expected 1 space between "%s" and double arrow; %s found';
					$data  = array(
						$this->tokens[ ( $next_arrow - 2 ) ]['content'],
						$space_length,
					);

					$fix = $this->phpcsFile->addFixableWarning( $error, $next_arrow, 'SpaceBeforeDoubleArrow', $data );
					if ( true === $fix ) {
						$this->phpcsFile->fixer->replaceToken( ( $next_arrow - 1 ), ' ' );
					}
				}
			}

			// Find the position of the next double arrow.
			$next_arrow = $this->phpcsFile->findNext(
				\T_DOUBLE_ARROW,
				( $next_arrow + 1 ),
				$closer
			);
		}

		// Ignore any child-arrays as the double arrows in these will already have been handled.
		return ( $closer + 1 );
	}

	/**
	 * Process a multi-line array.
	 *
	 * @since 0.14.0
	 *
	 * @param int   $stackPtr The position of the current token in the stack.
	 * @param array $items    Info array containing information on each array item.
	 * @param int   $opener   The position of the array opener.
	 * @param int   $closer   The position of the array closer.
	 *
	 * @return void
	 */
	protected function process_multi_line_array( $stackPtr, $items, $opener, $closer ) {

		$this->maxColumn = (int) $this->maxColumn;
		$this->validate_align_multiline_items();

		/*
		 * Determine what the spacing before the arrow should be.
		 *
		 * Will unset any array items without double arrow and with new line whitespace
		 * if newlines are to be ignored, so the second foreach loop only has to deal
		 * with items which need attention.
		 *
		 * This sniff does not take incorrect indentation of array keys into account.
		 * That's for the `WordPress.Arrays.ArrayIndentation` sniff to fix.
		 * If that would affect the alignment, a second (or third) loop of the fixer
		 * will correct it (again) after the indentation has been fixed.
		 */
		$index_end_cols    = array(); // Keep track of the end column position of index keys.
		$double_arrow_cols = array(); // Keep track of arrow column position and count.
		$multi_line_count  = 0;
		$total_items       = \count( $items );

		foreach ( $items as $key => $item ) {
			if ( strpos( $item['raw'], '=>' ) === false ) {
				// Ignore items without assignment operators.
				unset( $items[ $key ] );
				continue;
			}

			// Find the position of the first double arrow.
			$double_arrow = $this->phpcsFile->findNext(
				\T_DOUBLE_ARROW,
				$item['start'],
				( $item['end'] + 1 )
			);

			if ( false === $double_arrow ) {
				// Shouldn't happen, just in case.
				unset( $items[ $key ] );
				continue;
			}

			// Make sure the arrow is for this item and not for a nested array value assignment.
			$has_array_opener = $this->phpcsFile->findNext(
				$this->register(),
				$item['start'],
				$double_arrow
			);

			if ( false !== $has_array_opener ) {
				// Double arrow is for a nested array.
				unset( $items[ $key ] );
				continue;
			}

			// Find the end of the array key.
			$last_index_token = $this->phpcsFile->findPrevious(
				\T_WHITESPACE,
				( $double_arrow - 1 ),
				$item['start'],
				true
			);

			if ( false === $last_index_token ) {
				// Shouldn't happen, but just in case.
				unset( $items[ $key ] );
				continue;
			}

			if ( true === $this->ignoreNewlines
				&& $this->tokens[ $last_index_token ]['line'] !== $this->tokens[ $double_arrow ]['line']
			) {
				// Ignore this item as it has a new line between the item key and the double arrow.
				unset( $items[ $key ] );
				continue;
			}

			$index_end_position                = ( $this->tokens[ $last_index_token ]['column'] + ( $this->tokens[ $last_index_token ]['length'] - 1 ) );
			$items[ $key ]['operatorPtr']      = $double_arrow;
			$items[ $key ]['last_index_token'] = $last_index_token;
			$items[ $key ]['last_index_col']   = $index_end_position;

			if ( $this->tokens[ $last_index_token ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
				$items[ $key ]['single_line'] = true;
			} else {
				$items[ $key ]['single_line'] = false;
				$multi_line_count++;
			}

			if ( ( $index_end_position + 2 ) <= $this->maxColumn ) {
				$index_end_cols[] = $index_end_position;
			}

			if ( ! isset( $double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] ) ) {
				$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] = 1;
			} else {
				$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ]++;
			}
		}
		unset( $key, $item, $double_arrow, $has_array_opener, $last_index_token );

		if ( empty( $items ) || empty( $index_end_cols ) ) {
			// No actionable array items found.
			return;
		}

		/*
		 * Determine whether the operators for multi-line items should be aligned.
		 */
		if ( 'always' === $this->alignMultilineItems ) {
			$alignMultilineItems = true;
		} elseif ( 'never' === $this->alignMultilineItems ) {
			$alignMultilineItems = false;
		} else {
			$percentage = (string) round( ( $multi_line_count / $total_items ) * 100, 0 );

			// Bit hacky, but this is the only comparison function in PHP which allows to
			// pass the comparison operator. And hey, it works ;-).
			$alignMultilineItems = version_compare( $percentage, $this->number, $this->operator );
		}

		/*
		 * If necessary, rebuild the $index_end_cols and $double_arrow_cols arrays
		 * excluding multi-line items.
		 */
		if ( false === $alignMultilineItems ) {
			$select_index_end_cols = array();
			$double_arrow_cols     = array();

			foreach ( $items as $item ) {
				if ( false === $item['single_line'] ) {
					continue;
				}

				if ( ( $item['last_index_col'] + 2 ) <= $this->maxColumn ) {
					$select_index_end_cols[] = $item['last_index_col'];
				}

				if ( ! isset( $double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] ) ) {
					$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] = 1;
				} else {
					$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ]++;
				}
			}
		}

		/*
		 * Determine the expected position of the double arrows.
		 */
		if ( ! empty( $select_index_end_cols ) ) {
			$max_index_width = max( $select_index_end_cols );
		} else {
			$max_index_width = max( $index_end_cols );
		}

		$expected_col = ( $max_index_width + 2 );

		if ( false === $this->exact && ! empty( $double_arrow_cols ) ) {
			/*
			 * If the alignment does not have to be exact, see if a majority
			 * group of the arrows is already at an acceptable position.
			 */
			arsort( $double_arrow_cols, \SORT_NUMERIC );
			reset( $double_arrow_cols );
			$count = current( $double_arrow_cols );

			if ( $count > 1 || ( 1 === $count && \count( $items ) === 1 ) ) {
				// Allow for several groups of arrows having the same $count.
				$filtered_double_arrow_cols = array_keys( $double_arrow_cols, $count, true );

				foreach ( $filtered_double_arrow_cols as $col ) {
					if ( $col > $expected_col && $col <= $this->maxColumn ) {
						$expected_col = $col;
						break;
					}
				}
			}
		}
		unset( $max_index_width, $count, $filtered_double_arrow_cols, $col );

		/*
		 * Verify and correct the spacing around the double arrows.
		 */
		foreach ( $items as $item ) {
			if ( $this->tokens[ $item['operatorPtr'] ]['column'] === $expected_col
				&& $this->tokens[ $item['operatorPtr'] ]['line'] === $this->tokens[ $item['last_index_token'] ]['line']
			) {
				// Already correctly aligned.
				continue;
			}

			if ( \T_WHITESPACE !== $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['code'] ) {
				$before = 0;
			} else {
				if ( $this->tokens[ $item['last_index_token'] ]['line'] !== $this->tokens[ $item['operatorPtr'] ]['line'] ) {
					$before = 'newline';
				} else {
					$before = $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['length'];
				}
			}

			/*
			 * Deal with index sizes larger than maxColumn and with multi-line
			 * array items which should not be aligned.
			 */
			if ( ( $item['last_index_col'] + 2 ) > $this->maxColumn
				|| ( false === $alignMultilineItems && false === $item['single_line'] )
			) {

				if ( ( $item['last_index_col'] + 2 ) === $this->tokens[ $item['operatorPtr'] ]['column']
					&& $this->tokens[ $item['operatorPtr'] ]['line'] === $this->tokens[ $item['last_index_token'] ]['line']
				) {
					// MaxColumn/Multi-line item exception, already correctly aligned.
					continue;
				}

				$prefix = 'LongIndex';
				if ( false === $alignMultilineItems && false === $item['single_line'] ) {
					$prefix = 'MultilineItem';
				}

				$error_code = $prefix . 'SpaceBeforeDoubleArrow';
				if ( 0 === $before ) {
					$error_code = $prefix . 'NoSpaceBeforeDoubleArrow';
				}

				$fix = $this->phpcsFile->addFixableWarning(
					'Expected 1 space between "%s" and double arrow; %s found.',
					$item['operatorPtr'],
					$error_code,
					array(
						$this->tokens[ $item['last_index_token'] ]['content'],
						$before,
					)
				);

				if ( true === $fix ) {
					$this->phpcsFile->fixer->beginChangeset();

					// Remove whitespace tokens between the end of the index and the arrow, if any.
					for ( $i = ( $item['last_index_token'] + 1 ); $i < $item['operatorPtr']; $i++ ) {
						$this->phpcsFile->fixer->replaceToken( $i, '' );
					}

					// Add the correct whitespace.
					$this->phpcsFile->fixer->addContent( $item['last_index_token'], ' ' );

					$this->phpcsFile->fixer->endChangeset();
				}
				continue;
			}

			/*
			 * Deal with the space before double arrows in all other cases.
			 */
			$expected_whitespace = $expected_col - ( $this->tokens[ $item['last_index_token'] ]['column'] + $this->tokens[ $item['last_index_token'] ]['length'] );

			$fix = $this->phpcsFile->addFixableWarning(
				'Array double arrow not aligned correctly; expected %s space(s) between "%s" and double arrow, but found %s.',
				$item['operatorPtr'],
				'DoubleArrowNotAligned',
				array(
					$expected_whitespace,
					$this->tokens[ $item['last_index_token'] ]['content'],
					$before,
				)
			);

			if ( true === $fix ) {
				if ( 0 === $before || 'newline' === $before ) {
					$this->phpcsFile->fixer->beginChangeset();

					// Remove whitespace tokens between the end of the index and the arrow, if any.
					for ( $i = ( $item['last_index_token'] + 1 ); $i < $item['operatorPtr']; $i++ ) {
						$this->phpcsFile->fixer->replaceToken( $i, '' );
					}

					// Add the correct whitespace.
					$this->phpcsFile->fixer->addContent(
						$item['last_index_token'],
						str_repeat( ' ', $expected_whitespace )
					);

					$this->phpcsFile->fixer->endChangeset();
				} elseif ( $expected_whitespace > $before ) {
					// Add to the existing whitespace to prevent replacing tabs with spaces.
					// That's the concern of another sniff.
					$this->phpcsFile->fixer->addContent(
						( $item['operatorPtr'] - 1 ),
						str_repeat( ' ', ( $expected_whitespace - $before ) )
					);
				} else {
					// Too much whitespace found.
					$this->phpcsFile->fixer->replaceToken(
						( $item['operatorPtr'] - 1 ),
						str_repeat( ' ', $expected_whitespace )
					);
				}
			}
		}
	}

	/**
	 * Validate that a valid value has been received for the alignMultilineItems property.
	 *
	 * This message may be thrown more than once if the property is being changed inline in a file.
	 *
	 * @since 0.14.0
	 */
	protected function validate_align_multiline_items() {
		$alignMultilineItems = $this->alignMultilineItems;

		if ( 'always' === $alignMultilineItems || 'never' === $alignMultilineItems ) {
			return;
		} else {
			// Correct for a potentially added % sign.
			$alignMultilineItems = rtrim( $alignMultilineItems, '%' );

			if ( preg_match( '`^([=<>!]{1,2})(100|[0-9]{1,2})$`', $alignMultilineItems, $matches ) > 0 ) {
				$operator = $matches[1];
				$number   = (int) $matches[2];

				if ( \in_array( $operator, array( '<', '<=', '>', '>=', '==', '=', '!=', '<>' ), true ) === true
					&& ( $number >= 0 && $number <= 100 )
				) {
					$this->alignMultilineItems = $alignMultilineItems;
					$this->number              = (string) $number;
					$this->operator            = $operator;
					return;
				}
			}
		}

		$this->phpcsFile->addError(
			'Invalid property value passed: "%s". The value for the "alignMultilineItems" property for the "WordPress.Arrays.MultipleStatementAlignment" sniff should be either "always", "never" or an comparison operator + a number between 0 and 100.',
			0,
			'InvalidPropertyPassed',
			array( $this->alignMultilineItems )
		);

		// Reset to the default if an invalid value was received.
		$this->alignMultilineItems = 'always';
	}

}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\DB;

use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Sniff for prepared SQL.
 *
 * Makes sure that variables aren't directly interpolated into SQL statements.
 *
 * @link    https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#formatting-sql-statements
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.8.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   1.0.0  This sniff has been moved from the `WP` category to the `DB` category.
 */
class PreparedSQLSniff extends Sniff {

	/**
	 * The lists of $wpdb methods.
	 *
	 * @since 0.8.0
	 * @since 0.11.0 Changed from static to non-static.
	 *
	 * @var array
	 */
	protected $methods = array(
		'get_var'     => true,
		'get_col'     => true,
		'get_row'     => true,
		'get_results' => true,
		'prepare'     => true,
		'query'       => true,
	);

	/**
	 * Tokens that we don't flag when they are found in a $wpdb method call.
	 *
	 * @since 0.9.0
	 *
	 * @var array
	 */
	protected $ignored_tokens = array(
		\T_OBJECT_OPERATOR          => true,
		\T_OPEN_PARENTHESIS         => true,
		\T_CLOSE_PARENTHESIS        => true,
		\T_STRING_CONCAT            => true,
		\T_CONSTANT_ENCAPSED_STRING => true,
		\T_OPEN_SQUARE_BRACKET      => true,
		\T_CLOSE_SQUARE_BRACKET     => true,
		\T_COMMA                    => true,
		\T_LNUMBER                  => true,
		\T_START_HEREDOC            => true,
		\T_END_HEREDOC              => true,
		\T_START_NOWDOC             => true,
		\T_NOWDOC                   => true,
		\T_END_NOWDOC               => true,
		\T_INT_CAST                 => true,
		\T_DOUBLE_CAST              => true,
		\T_BOOL_CAST                => true,
	);

	/**
	 * A loop pointer.
	 *
	 * It is a property so that we can access it in all of our methods.
	 *
	 * @since 0.9.0
	 *
	 * @var int
	 */
	protected $i;

	/**
	 * The loop end marker.
	 *
	 * It is a property so that we can access it in all of our methods.
	 *
	 * @since 0.9.0
	 *
	 * @var int
	 */
	protected $end;

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @since 0.8.0
	 *
	 * @return array
	 */
	public function register() {

		$this->ignored_tokens += Tokens::$emptyTokens;

		return array(
			\T_VARIABLE,
			\T_STRING,
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @since 0.8.0
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	public function process_token( $stackPtr ) {

		if ( ! $this->is_wpdb_method_call( $stackPtr, $this->methods ) ) {
			return;
		}

		if ( $this->has_whitelist_comment( 'unprepared SQL', $stackPtr ) ) {
			return;
		}

		for ( $this->i; $this->i < $this->end; $this->i++ ) {

			if ( isset( $this->ignored_tokens[ $this->tokens[ $this->i ]['code'] ] ) ) {
				continue;
			}

			if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $this->i ]['code']
				|| \T_HEREDOC === $this->tokens[ $this->i ]['code']
			) {

				$bad_variables = array_filter(
					$this->get_interpolated_variables( $this->tokens[ $this->i ]['content'] ),
					function ( $symbol ) {
						return ( 'wpdb' !== $symbol );
					}
				);

				foreach ( $bad_variables as $bad_variable ) {
					$this->phpcsFile->addError(
						'Use placeholders and $wpdb->prepare(); found interpolated variable $%s at %s',
						$this->i,
						'InterpolatedNotPrepared',
						array(
							$bad_variable,
							$this->tokens[ $this->i ]['content'],
						)
					);
				}
				continue;
			}

			if ( \T_VARIABLE === $this->tokens[ $this->i ]['code'] ) {
				if ( '$wpdb' === $this->tokens[ $this->i ]['content'] ) {
					$this->is_wpdb_method_call( $this->i, $this->methods );
					continue;
				}

				if ( $this->is_safe_casted( $this->i ) ) {
					continue;
				}
			}

			if ( \T_STRING === $this->tokens[ $this->i ]['code'] ) {

				if (
					isset( $this->SQLEscapingFunctions[ $this->tokens[ $this->i ]['content'] ] )
					|| isset( $this->SQLAutoEscapedFunctions[ $this->tokens[ $this->i ]['content'] ] )
				) {

					// Find the opening parenthesis.
					$opening_paren = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $this->i + 1 ), null, true, null, true );

					if ( false !== $opening_paren
						&& \T_OPEN_PARENTHESIS === $this->tokens[ $opening_paren ]['code']
						&& isset( $this->tokens[ $opening_paren ]['parenthesis_closer'] )
					) {
						// Skip past the end of the function.
						$this->i = $this->tokens[ $opening_paren ]['parenthesis_closer'];
						continue;
					}
				} elseif ( isset( $this->formattingFunctions[ $this->tokens[ $this->i ]['content'] ] ) ) {
					continue;
				}
			}

			$this->phpcsFile->addError(
				'Use placeholders and $wpdb->prepare(); found %s',
				$this->i,
				'NotPrepared',
				array( $this->tokens[ $this->i ]['content'] )
			);
		}

		return $this->end;
	}

}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\CodeAnalysis;

use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Checks against empty statements.
 *
 * - Check against two semi-colons with no executable code in between.
 * - Check against an empty PHP open - close tag combination.
 *
 * {@internal This check should at some point in the future be pulled upstream and probably
 *            merged into the upstream `Generic.CodeAnalysis.EmptyStatement` sniff.
 *            This will need to wait until the WPCS minimum requirements have gone up
 *            beyond PHPCS 3.x though as it is not likely that new features will be accepted
 *            still for the PHPCS 2.x branch.}}
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.12.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 */
class EmptyStatementSniff extends Sniff {

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return array(
			\T_SEMICOLON,
			\T_CLOSE_TAG,
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int Integer stack pointer to skip the rest of the file.
	 */
	public function process_token( $stackPtr ) {

		switch ( $this->tokens[ $stackPtr ]['type'] ) {
			/*
			 * Detect `something();;`.
			 */
			case 'T_SEMICOLON':
				$prevNonEmpty = $this->phpcsFile->findPrevious(
					Tokens::$emptyTokens,
					( $stackPtr - 1 ),
					null,
					true
				);

				if ( false === $prevNonEmpty
					|| ( \T_SEMICOLON !== $this->tokens[ $prevNonEmpty ]['code']
						&& \T_OPEN_TAG !== $this->tokens[ $prevNonEmpty ]['code']
						&& \T_OPEN_TAG_WITH_ECHO !== $this->tokens[ $prevNonEmpty ]['code'] )
				) {
					return;
				}

				if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
					$nested      = $this->tokens[ $stackPtr ]['nested_parenthesis'];
					$last_closer = array_pop( $nested );
					if ( isset( $this->tokens[ $last_closer ]['parenthesis_owner'] )
						&& \T_FOR === $this->tokens[ $this->tokens[ $last_closer ]['parenthesis_owner'] ]['code']
					) {
						// Empty for() condition.
						return;
					}
				}

				$fix = $this->phpcsFile->addFixableWarning(
					'Empty PHP statement detected: superfluous semi-colon.',
					$stackPtr,
					'SemicolonWithoutCodeDetected'
				);
				if ( true === $fix ) {
					$this->phpcsFile->fixer->beginChangeset();

					if ( \T_OPEN_TAG === $this->tokens[ $prevNonEmpty ]['code']
						|| \T_OPEN_TAG_WITH_ECHO === $this->tokens[ $prevNonEmpty ]['code']
					) {
						/*
						 * Check for superfluous whitespace after the semi-colon which will be
						 * removed as the `<?php ` open tag token already contains whitespace,
						 * either a space or a new line and in case of a new line, the indentation
						 * should be done via tabs, so spaces can be safely removed.
						 */
						if ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] ) {
							$replacement = str_replace( ' ', '', $this->tokens[ ( $stackPtr + 1 ) ]['content'] );
							$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), $replacement );
						}
					}

					for ( $i = $stackPtr; $i > $prevNonEmpty; $i-- ) {
						if ( \T_SEMICOLON !== $this->tokens[ $i ]['code']
							&& \T_WHITESPACE !== $this->tokens[ $i ]['code']
						) {
							break;
						}
						$this->phpcsFile->fixer->replaceToken( $i, '' );
					}

					$this->phpcsFile->fixer->endChangeset();
				}
				break;

			/*
			 * Detect `<?php ?>`.
			 */
			case 'T_CLOSE_TAG':
				$prevNonEmpty = $this->phpcsFile->findPrevious(
					\T_WHITESPACE,
					( $stackPtr - 1 ),
					null,
					true
				);

				if ( false === $prevNonEmpty
					|| ( \T_OPEN_TAG !== $this->tokens[ $prevNonEmpty ]['code']
						&& \T_OPEN_TAG_WITH_ECHO !== $this->tokens[ $prevNonEmpty ]['code'] )
				) {
					return;
				}

				$fix = $this->phpcsFile->addFixableWarning(
					'Empty PHP open/close tag combination detected.',
					$prevNonEmpty,
					'EmptyPHPOpenCloseTagsDetected'
				);
				if ( true === $fix ) {
					$this->phpcsFile->fixer->beginChangeset();
					for ( $i = $prevNonEmpty; $i <= $stackPtr; $i++ ) {
						$this->phpcsFile->fixer->replaceToken( $i, '' );
					}
					$this->phpcsFile->fixer->endChangeset();
				}
				break;

			default:
				/* Deliberately left empty. */
				break;
		}
	}

}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\Security;

use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Verifies that all outputted strings are escaped.
 *
 * @link    http://codex.wordpress.org/Data_Validation Data Validation on WordPress Codex
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   2013-06-11
 * @since   0.4.0  This class now extends the WordPressCS native `Sniff` class.
 * @since   0.5.0  The various function list properties which used to be contained in this class
 *                 have been moved to the WordPressCS native `Sniff` parent class.
 * @since   0.12.0 This sniff will now also check for output escaping when using shorthand
 *                 echo tags `<?=`.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   1.0.0  This sniff has been moved from the `XSS` category to the `Security` category.
 */
class EscapeOutputSniff extends Sniff {

	/**
	 * Custom list of functions which escape values for output.
	 *
	 * @since 0.5.0
	 *
	 * @var string|string[]
	 */
	public $customEscapingFunctions = array();

	/**
	 * Custom list of functions whose return values are pre-escaped for output.
	 *
	 * @since 0.3.0
	 *
	 * @var string|string[]
	 */
	public $customAutoEscapedFunctions = array();

	/**
	 * Custom list of functions which print output incorporating the passed values.
	 *
	 * @since 0.4.0
	 *
	 * @var string|string[]
	 */
	public $customPrintingFunctions = array();

	/**
	 * Printing functions that incorporate unsafe values.
	 *
	 * @since 0.4.0
	 * @since 0.11.0 Changed from public static to protected non-static.
	 *
	 * @var array
	 */
	protected $unsafePrintingFunctions = array(
		'_e'  => 'esc_html_e() or esc_attr_e()',
		'_ex' => 'echo esc_html_x() or echo esc_attr_x()',
	);

	/**
	 * Cache of previously added custom functions.
	 *
	 * Prevents having to do the same merges over and over again.
	 *
	 * @since 0.4.0
	 * @since 0.11.0 - Changed from public static to protected non-static.
	 *               - Changed the format from simple bool to array.
	 *
	 * @var array
	 */
	protected $addedCustomFunctions = array(
		'escape'     => array(),
		'autoescape' => array(),
		'sanitize'   => array(),
		'print'      => array(),
	);

	/**
	 * List of names of the tokens representing PHP magic constants.
	 *
	 * @since 0.10.0
	 *
	 * @var array
	 */
	private $magic_constant_tokens = array(
		'T_CLASS_C'  => true, // __CLASS__
		'T_DIR'      => true, // __DIR__
		'T_FILE'     => true, // __FILE__
		'T_FUNC_C'   => true, // __FUNCTION__
		'T_LINE'     => true, // __LINE__
		'T_METHOD_C' => true, // __METHOD__
		'T_NS_C'     => true, // __NAMESPACE__
		'T_TRAIT_C'  => true, // __TRAIT__
	);

	/**
	 * List of names of the native PHP constants which can be considered safe.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $safe_php_constants = array(
		'PHP_EOL'             => true, // String.
		'PHP_VERSION'         => true, // Integer.
		'PHP_MAJOR_VERSION'   => true, // Integer.
		'PHP_MINOR_VERSION'   => true, // Integer.
		'PHP_RELEASE_VERSION' => true, // Integer.
		'PHP_VERSION_ID'      => true, // Integer.
		'PHP_EXTRA_VERSION'   => true, // String.
		'PHP_DEBUG'           => true, // Integer.
	);

	/**
	 * List of names of the cast tokens which can be considered as a safe escaping method.
	 *
	 * @since 0.12.0
	 *
	 * @var array
	 */
	private $safe_cast_tokens = array(
		'T_INT_CAST'    => true, // (int)
		'T_DOUBLE_CAST' => true, // (float)
		'T_BOOL_CAST'   => true, // (bool)
		'T_UNSET_CAST'  => true, // (unset)
	);

	/**
	 * List of tokens which can be considered as a safe when directly part of the output.
	 *
	 * @since 0.12.0
	 *
	 * @var array
	 */
	private $safe_components = array(
		'T_CONSTANT_ENCAPSED_STRING' => true,
		'T_LNUMBER'                  => true,
		'T_MINUS'                    => true,
		'T_PLUS'                     => true,
		'T_MULTIPLY'                 => true,
		'T_DIVIDE'                   => true,
		'T_MODULUS'                  => true,
		'T_TRUE'                     => true,
		'T_FALSE'                    => true,
		'T_NULL'                     => true,
		'T_DNUMBER'                  => true,
		'T_START_NOWDOC'             => true,
		'T_NOWDOC'                   => true,
		'T_END_NOWDOC'               => true,
	);

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {

		return array(
			\T_ECHO,
			\T_PRINT,
			\T_EXIT,
			\T_STRING,
			\T_OPEN_TAG_WITH_ECHO,
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	public function process_token( $stackPtr ) {

		$this->mergeFunctionLists();

		$function = $this->tokens[ $stackPtr ]['content'];

		// Find the opening parenthesis (if present; T_ECHO might not have it).
		$open_paren = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );

		// If function, not T_ECHO nor T_PRINT.
		if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] ) {
			// Skip if it is a function but is not one of the printing functions.
			if ( ! isset( $this->printingFunctions[ $this->tokens[ $stackPtr ]['content'] ] ) ) {
				return;
			}

			if ( isset( $this->tokens[ $open_paren ]['parenthesis_closer'] ) ) {
				$end_of_statement = $this->tokens[ $open_paren ]['parenthesis_closer'];
			}

			// These functions only need to have the first argument escaped.
			if ( \in_array( $function, array( 'trigger_error', 'user_error' ), true ) ) {
				$first_param      = $this->get_function_call_parameter( $stackPtr, 1 );
				$end_of_statement = ( $first_param['end'] + 1 );
				unset( $first_param );
			}
		}

		// Checking for the ignore comment, ex: //xss ok.
		if ( $this->has_whitelist_comment( 'xss', $stackPtr ) ) {
			return;
		}

		if ( isset( $this->unsafePrintingFunctions[ $function ] ) ) {
			$error = $this->phpcsFile->addError(
				"All output should be run through an escaping function (like %s), found '%s'.",
				$stackPtr,
				'UnsafePrintingFunction',
				array( $this->unsafePrintingFunctions[ $function ], $function )
			);

			// If the error was reported, don't bother checking the function's arguments.
			if ( $error ) {
				return isset( $end_of_statement ) ? $end_of_statement : null;
			}
		}

		$ternary = false;

		// This is already determined if this is a function and not T_ECHO.
		if ( ! isset( $end_of_statement ) ) {

			$end_of_statement = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), $stackPtr );
			$last_token       = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $end_of_statement - 1 ), null, true );

			// Check for the ternary operator. We only need to do this here if this
			// echo is lacking parenthesis. Otherwise it will be handled below.
			if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $open_paren ]['code'] || \T_CLOSE_PARENTHESIS !== $this->tokens[ $last_token ]['code'] ) {

				$ternary = $this->phpcsFile->findNext( \T_INLINE_THEN, $stackPtr, $end_of_statement );

				// If there is a ternary skip over the part before the ?. However, if
				// the ternary is within parentheses, it will be handled in the loop.
				if ( false !== $ternary && empty( $this->tokens[ $ternary ]['nested_parenthesis'] ) ) {
					$stackPtr = $ternary;
				}
			}
		}

		// Ignore the function itself.
		$stackPtr++;

		$in_cast = false;

		// Looping through echo'd components.
		$watch = true;
		for ( $i = $stackPtr; $i < $end_of_statement; $i++ ) {

			// Ignore whitespaces and comments.
			if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
				continue;
			}

			// Ignore namespace separators.
			if ( \T_NS_SEPARATOR === $this->tokens[ $i ]['code'] ) {
				continue;
			}

			if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] ) {

				if ( ! isset( $this->tokens[ $i ]['parenthesis_closer'] ) ) {
					// Live coding or parse error.
					break;
				}

				if ( $in_cast ) {

					// Skip to the end of a function call if it has been casted to a safe value.
					$i       = $this->tokens[ $i ]['parenthesis_closer'];
					$in_cast = false;

				} else {

					// Skip over the condition part of a ternary (i.e., to after the ?).
					$ternary = $this->phpcsFile->findNext( \T_INLINE_THEN, $i, $this->tokens[ $i ]['parenthesis_closer'] );

					if ( false !== $ternary ) {

						$next_paren = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, ( $i + 1 ), $this->tokens[ $i ]['parenthesis_closer'] );

						// We only do it if the ternary isn't within a subset of parentheses.
						if ( false === $next_paren || ( isset( $this->tokens[ $next_paren ]['parenthesis_closer'] ) && $ternary > $this->tokens[ $next_paren ]['parenthesis_closer'] ) ) {
							$i = $ternary;
						}
					}
				}

				continue;
			}

			// Handle arrays for those functions that accept them.
			if ( \T_ARRAY === $this->tokens[ $i ]['code'] ) {
				$i++; // Skip the opening parenthesis.
				continue;
			}

			if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $i ]['code']
				|| \T_CLOSE_SHORT_ARRAY === $this->tokens[ $i ]['code']
			) {
				continue;
			}

			if ( \in_array( $this->tokens[ $i ]['code'], array( \T_DOUBLE_ARROW, \T_CLOSE_PARENTHESIS ), true ) ) {
				continue;
			}

			// Handle magic constants for debug functions.
			if ( isset( $this->magic_constant_tokens[ $this->tokens[ $i ]['type'] ] ) ) {
				continue;
			}

			// Handle safe PHP native constants.
			if ( \T_STRING === $this->tokens[ $i ]['code']
				&& isset( $this->safe_php_constants[ $this->tokens[ $i ]['content'] ] )
				&& $this->is_use_of_global_constant( $i )
			) {
				continue;
			}

			// Wake up on concatenation characters, another part to check.
			if ( \T_STRING_CONCAT === $this->tokens[ $i ]['code'] ) {
				$watch = true;
				continue;
			}

			// Wake up after a ternary else (:).
			if ( false !== $ternary && \T_INLINE_ELSE === $this->tokens[ $i ]['code'] ) {
				$watch = true;
				continue;
			}

			// Wake up for commas.
			if ( \T_COMMA === $this->tokens[ $i ]['code'] ) {
				$in_cast = false;
				$watch   = true;
				continue;
			}

			if ( false === $watch ) {
				continue;
			}

			// Allow T_CONSTANT_ENCAPSED_STRING eg: echo 'Some String';
			// Also T_LNUMBER, e.g.: echo 45; exit -1; and booleans.
			if ( isset( $this->safe_components[ $this->tokens[ $i ]['type'] ] ) ) {
				continue;
			}

			$watch = false;

			// Allow int/double/bool casted variables.
			if ( isset( $this->safe_cast_tokens[ $this->tokens[ $i ]['type'] ] ) ) {
				$in_cast = true;
				continue;
			}

			// Now check that next token is a function call.
			if ( \T_STRING === $this->tokens[ $i ]['code'] ) {

				$ptr                    = $i;
				$functionName           = $this->tokens[ $i ]['content'];
				$function_opener        = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, ( $i + 1 ), null, false, null, true );
				$is_formatting_function = isset( $this->formattingFunctions[ $functionName ] );

				if ( false !== $function_opener ) {

					if ( isset( $this->arrayWalkingFunctions[ $functionName ] ) ) {

						// Get the callback parameter.
						$callback = $this->get_function_call_parameter(
							$ptr,
							$this->arrayWalkingFunctions[ $functionName ]
						);

						if ( ! empty( $callback ) ) {
							/*
							 * If this is a function callback (not a method callback array) and we're able
							 * to resolve the function name, do so.
							 */
							$mapped_function = $this->phpcsFile->findNext(
								Tokens::$emptyTokens,
								$callback['start'],
								( $callback['end'] + 1 ),
								true
							);

							if ( false !== $mapped_function
								&& \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $mapped_function ]['code']
							) {
								$functionName = $this->strip_quotes( $this->tokens[ $mapped_function ]['content'] );
								$ptr          = $mapped_function;
							}
						}
					}

					// Skip pointer to after the function.
					// If this is a formatting function we just skip over the opening
					// parenthesis. Otherwise we skip all the way to the closing.
					if ( $is_formatting_function ) {
						$i     = ( $function_opener + 1 );
						$watch = true;
					} else {
						if ( isset( $this->tokens[ $function_opener ]['parenthesis_closer'] ) ) {
							$i = $this->tokens[ $function_opener ]['parenthesis_closer'];
						} else {
							// Live coding or parse error.
							break;
						}
					}
				}

				// If this is a safe function, we don't flag it.
				if (
					$is_formatting_function
					|| isset( $this->autoEscapedFunctions[ $functionName ] )
					|| isset( $this->escapingFunctions[ $functionName ] )
				) {
					continue;
				}

				$content = $functionName;

			} else {
				$content = $this->tokens[ $i ]['content'];
				$ptr     = $i;
			}

			$this->phpcsFile->addError(
				"All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '%s'.",
				$ptr,
				'OutputNotEscaped',
				$content
			);
		}

		return $end_of_statement;
	}

	/**
	 * Merge custom functions provided via a custom ruleset with the defaults, if we haven't already.
	 *
	 * @since 0.11.0 Split out from the `process()` method.
	 *
	 * @return void
	 */
	protected function mergeFunctionLists() {
		if ( $this->customEscapingFunctions !== $this->addedCustomFunctions['escape'] ) {
			$customEscapeFunctions = $this->merge_custom_array( $this->customEscapingFunctions, array(), false );

			$this->escapingFunctions = $this->merge_custom_array(
				$customEscapeFunctions,
				$this->escapingFunctions
			);

			$this->addedCustomFunctions['escape'] = $this->customEscapingFunctions;
		}

		if ( $this->customAutoEscapedFunctions !== $this->addedCustomFunctions['autoescape'] ) {
			$this->autoEscapedFunctions = $this->merge_custom_array(
				$this->customAutoEscapedFunctions,
				$this->autoEscapedFunctions
			);

			$this->addedCustomFunctions['autoescape'] = $this->customAutoEscapedFunctions;
		}

		if ( $this->customPrintingFunctions !== $this->addedCustomFunctions['print'] ) {

			$this->printingFunctions = $this->merge_custom_array(
				$this->customPrintingFunctions,
				$this->printingFunctions
			);

			$this->addedCustomFunctions['print'] = $this->customPrintingFunctions;
		}
	}

}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\Arrays;

use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Enforces WordPress array spacing format.
 *
 * - Check for no space between array keyword and array opener.
 * - Check for no space between the parentheses of an empty array.
 * - Checks for one space after the array opener / before the array closer in single-line arrays.
 * - Checks that associative arrays are multi-line.
 * - Checks that each array item in a multi-line array starts on a new line.
 * - Checks that the array closer in a multi-line array is on a new line.
 *
 * @link    https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.11.0 - The WordPress specific additional checks have now been split off
 *                   from the `WordPress.Arrays.ArrayDeclaration` sniff into this sniff.
 *                 - Added sniffing & fixing for associative arrays.
 * @since   0.12.0 Decoupled this sniff from the upstream sniff completely.
 *                 This sniff now extends the WordPressCS native `Sniff` class instead.
 * @since   0.13.0 Added the last remaining checks from the `WordPress.Arrays.ArrayDeclaration`
 *                 sniff which were not covered elsewhere.
 *                 The `WordPress.Arrays.ArrayDeclaration` sniff has now been deprecated.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   0.14.0 Single item associative arrays are now by default exempt from the
 *                 "must be multi-line" rule. This behaviour can be changed using the
 *                 `allow_single_item_single_line_associative_arrays` property.
 */
class ArrayDeclarationSpacingSniff extends Sniff {

	/**
	 * Whether or not to allow single item associative arrays to be single line.
	 *
	 * @since 0.14.0
	 *
	 * @var bool Defaults to true.
	 */
	public $allow_single_item_single_line_associative_arrays = true;

	/**
	 * Token this sniff targets.
	 *
	 * Also used for distinguishing between the array and an array value
	 * which is also an array.
	 *
	 * @since 0.12.0
	 *
	 * @var array
	 */
	private $targets = array(
		\T_ARRAY            => \T_ARRAY,
		\T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY,
	);

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @since 0.12.0
	 *
	 * @return array
	 */
	public function register() {
		return $this->targets;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @since 0.12.0 The actual checks contained in this method used to
	 *               be in the `processSingleLineArray()` method.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return void
	 */
	public function process_token( $stackPtr ) {
		/*
		 * Determine the array opener & closer.
		 */
		$array_open_close = $this->find_array_open_close( $stackPtr );
		if ( false === $array_open_close ) {
			// Array open/close could not be determined.
			return;
		}

		$opener = $array_open_close['opener'];
		$closer = $array_open_close['closer'];
		unset( $array_open_close );

		/*
		 * Long arrays only: Check for space between the array keyword and the open parenthesis.
		 */
		if ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] ) {

			if ( ( $stackPtr + 1 ) !== $opener ) {
				$error      = 'There must be no space between the "array" keyword and the opening parenthesis';
				$error_code = 'SpaceAfterKeyword';

				$nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), ( $opener + 1 ), true );
				if ( $nextNonWhitespace !== $opener ) {
					// Don't auto-fix: Something other than whitespace found between keyword and open parenthesis.
					$this->phpcsFile->addError( $error, $stackPtr, $error_code );
				} else {

					$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code );

					if ( true === $fix ) {
						$this->phpcsFile->fixer->beginChangeset();
						for ( $i = ( $stackPtr + 1 ); $i < $opener; $i++ ) {
							$this->phpcsFile->fixer->replaceToken( $i, '' );
						}
						$this->phpcsFile->fixer->endChangeset();
						unset( $i );
					}
				}
				unset( $error, $error_code, $nextNonWhitespace, $fix );
			}
		}

		/*
		 * Check for empty arrays.
		 */
		$nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $opener + 1 ), ( $closer + 1 ), true );
		if ( $nextNonWhitespace === $closer ) {

			if ( ( $opener + 1 ) !== $closer ) {
				$fix = $this->phpcsFile->addFixableError(
					'Empty array declaration must have no space between the parentheses',
					$stackPtr,
					'SpaceInEmptyArray'
				);

				if ( true === $fix ) {
					$this->phpcsFile->fixer->beginChangeset();
					for ( $i = ( $opener + 1 ); $i < $closer; $i++ ) {
						$this->phpcsFile->fixer->replaceToken( $i, '' );
					}
					$this->phpcsFile->fixer->endChangeset();
					unset( $i );
				}
			}

			// This array is empty, so the below checks aren't necessary.
			return;
		}
		unset( $nextNonWhitespace );

		// Pass off to either the single line or multi-line array analysis.
		if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
			$this->process_single_line_array( $stackPtr, $opener, $closer );
		} else {
			$this->process_multi_line_array( $stackPtr, $opener, $closer );
		}
	}

	/**
	 * Process a single-line array.
	 *
	 * @since 0.13.0 The actual checks contained in this method used to
	 *               be in the `process()` method.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 * @param int $opener   The position of the array opener.
	 * @param int $closer   The position of the array closer.
	 *
	 * @return void
	 */
	protected function process_single_line_array( $stackPtr, $opener, $closer ) {
		/*
		 * Check that associative arrays are always multi-line.
		 */
		$array_has_keys = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $opener, $closer );
		if ( false !== $array_has_keys ) {

			$array_items = $this->get_function_call_parameters( $stackPtr );

			if ( ( false === $this->allow_single_item_single_line_associative_arrays
					&& ! empty( $array_items ) )
				|| ( true === $this->allow_single_item_single_line_associative_arrays
					&& \count( $array_items ) > 1 )
			) {
				/*
				 * Make sure the double arrow is for *this* array, not for a nested one.
				 */
				$array_has_keys = false; // Reset before doing more detailed check.
				foreach ( $array_items as $item ) {
					for ( $ptr = $item['start']; $ptr <= $item['end']; $ptr++ ) {
						if ( \T_DOUBLE_ARROW === $this->tokens[ $ptr ]['code'] ) {
							$array_has_keys = true;
							break 2;
						}

						// Skip passed any nested arrays.
						if ( isset( $this->targets[ $this->tokens[ $ptr ]['code'] ] ) ) {
							$nested_array_open_close = $this->find_array_open_close( $ptr );
							if ( false === $nested_array_open_close ) {
								// Nested array open/close could not be determined.
								continue;
							}

							$ptr = $nested_array_open_close['closer'];
						}
					}
				}

				if ( true === $array_has_keys ) {

					$phrase = 'an';
					if ( true === $this->allow_single_item_single_line_associative_arrays ) {
						$phrase = 'a multi-item';
					}
					$fix = $this->phpcsFile->addFixableError(
						'When %s array uses associative keys, each value should start on a new line.',
						$closer,
						'AssociativeArrayFound',
						array( $phrase )
					);

					if ( true === $fix ) {

						$this->phpcsFile->fixer->beginChangeset();

						foreach ( $array_items as $item ) {
							/*
							 * Add a line break before the first non-empty token in the array item.
							 * Prevents extraneous whitespace at the start of the line which could be
							 * interpreted as alignment whitespace.
							 */
							$first_non_empty = $this->phpcsFile->findNext(
								Tokens::$emptyTokens,
								$item['start'],
								( $item['end'] + 1 ),
								true
							);
							if ( false === $first_non_empty ) {
								continue;
							}

							if ( $item['start'] <= ( $first_non_empty - 1 )
								&& \T_WHITESPACE === $this->tokens[ ( $first_non_empty - 1 ) ]['code']
							) {
								// Remove whitespace which would otherwise becoming trailing
								// (as it gives problems with the fixed file).
								$this->phpcsFile->fixer->replaceToken( ( $first_non_empty - 1 ), '' );
							}

							$this->phpcsFile->fixer->addNewlineBefore( $first_non_empty );
						}

						$this->phpcsFile->fixer->endChangeset();
					}

					// No need to check for spacing around opener/closer as this array should be multi-line.
					return;
				}
			}
		}

		/*
		 * Check that there is a single space after the array opener and before the array closer.
		 */
		if ( \T_WHITESPACE !== $this->tokens[ ( $opener + 1 ) ]['code'] ) {

			$fix = $this->phpcsFile->addFixableError(
				'Missing space after array opener.',
				$opener,
				'NoSpaceAfterArrayOpener'
			);

			if ( true === $fix ) {
				$this->phpcsFile->fixer->addContent( $opener, ' ' );
			}
		} elseif ( ' ' !== $this->tokens[ ( $opener + 1 ) ]['content'] ) {

			$fix = $this->phpcsFile->addFixableError(
				'Expected 1 space after array opener, found %s.',
				$opener,
				'SpaceAfterArrayOpener',
				array( \strlen( $this->tokens[ ( $opener + 1 ) ]['content'] ) )
			);

			if ( true === $fix ) {
				$this->phpcsFile->fixer->replaceToken( ( $opener + 1 ), ' ' );
			}
		}

		if ( \T_WHITESPACE !== $this->tokens[ ( $closer - 1 ) ]['code'] ) {

			$fix = $this->phpcsFile->addFixableError(
				'Missing space before array closer.',
				$closer,
				'NoSpaceBeforeArrayCloser'
			);

			if ( true === $fix ) {
				$this->phpcsFile->fixer->addContentBefore( $closer, ' ' );
			}
		} elseif ( ' ' !== $this->tokens[ ( $closer - 1 ) ]['content'] ) {

			$fix = $this->phpcsFile->addFixableError(
				'Expected 1 space before array closer, found %s.',
				$closer,
				'SpaceBeforeArrayCloser',
				array( \strlen( $this->tokens[ ( $closer - 1 ) ]['content'] ) )
			);

			if ( true === $fix ) {
				$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), ' ' );
			}
		}
	}

	/**
	 * Process a multi-line array.
	 *
	 * @since 0.13.0 The actual checks contained in this method used to
	 *               be in the `ArrayDeclaration` sniff.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 * @param int $opener   The position of the array opener.
	 * @param int $closer   The position of the array closer.
	 *
	 * @return void
	 */
	protected function process_multi_line_array( $stackPtr, $opener, $closer ) {
		/*
		 * Check that the closing bracket is on a new line.
		 */
		$last_content = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $closer - 1 ), $opener, true );
		if ( false !== $last_content
			&& $this->tokens[ $last_content ]['line'] === $this->tokens[ $closer ]['line']
		) {
			$fix = $this->phpcsFile->addFixableError(
				'Closing parenthesis of array declaration must be on a new line',
				$closer,
				'CloseBraceNewLine'
			);
			if ( true === $fix ) {
				$this->phpcsFile->fixer->beginChangeset();

				if ( $last_content < ( $closer - 1 )
					&& \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
				) {
					// Remove whitespace which would otherwise becoming trailing
					// (as it gives problems with the fixed file).
					$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), '' );
				}

				$this->phpcsFile->fixer->addNewlineBefore( $closer );
				$this->phpcsFile->fixer->endChangeset();
			}
		}

		/*
		 * Check that each array item starts on a new line.
		 */
		$array_items      = $this->get_function_call_parameters( $stackPtr );
		$end_of_last_item = $opener;

		foreach ( $array_items as $item ) {
			$end_of_this_item = ( $item['end'] + 1 );

			// Find the line on which the item starts.
			$first_content = $this->phpcsFile->findNext(
				array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
				$item['start'],
				$end_of_this_item,
				true
			);

			// Ignore comments after array items if the next real content starts on a new line.
			if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_last_item ]['line']
				&& ( \T_COMMENT === $this->tokens[ $first_content ]['code']
				|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $first_content ]['code'] ] ) )
			) {
				$end_of_comment = $first_content;

				// Find the end of (multi-line) /* */- style trailing comments.
				if ( substr( ltrim( $this->tokens[ $end_of_comment ]['content'] ), 0, 2 ) === '/*' ) {
					while ( ( \T_COMMENT === $this->tokens[ $end_of_comment ]['code']
						|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $end_of_comment ]['code'] ] ) )
						&& substr( rtrim( $this->tokens[ $end_of_comment ]['content'] ), -2 ) !== '*/'
						&& ( $end_of_comment + 1 ) < $end_of_this_item
					) {
						$end_of_comment++;
					}

					if ( $this->tokens[ $end_of_comment ]['line'] !== $this->tokens[ $end_of_last_item ]['line'] ) {
						// Multi-line trailing comment.
						$end_of_last_item = $end_of_comment;
					}
				}

				$next = $this->phpcsFile->findNext(
					array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
					( $end_of_comment + 1 ),
					$end_of_this_item,
					true
				);

				if ( false === $next ) {
					// Shouldn't happen, but just in case.
					$end_of_last_item = $end_of_this_item;
					continue;
				}

				if ( $this->tokens[ $next ]['line'] !== $this->tokens[ $first_content ]['line'] ) {
					$first_content = $next;
				}
			}

			if ( false === $first_content ) {
				// Shouldn't happen, but just in case.
				$end_of_last_item = $end_of_this_item;
				continue;
			}

			if ( $this->tokens[ $end_of_last_item ]['line'] === $this->tokens[ $first_content ]['line'] ) {

				$fix = $this->phpcsFile->addFixableError(
					'Each item in a multi-line array must be on a new line',
					$first_content,
					'ArrayItemNoNewLine'
				);

				if ( true === $fix ) {

					$this->phpcsFile->fixer->beginChangeset();

					if ( ( $end_of_last_item + 1 ) <= ( $first_content - 1 )
						&& \T_WHITESPACE === $this->tokens[ ( $first_content - 1 ) ]['code']
					) {
						// Remove whitespace which would otherwise becoming trailing
						// (as it gives problems with the fixed file).
						$this->phpcsFile->fixer->replaceToken( ( $first_content - 1 ), '' );
					}

					$this->phpcsFile->fixer->addNewlineBefore( $first_content );
					$this->phpcsFile->fixer->endChangeset();
				}
			}

			$end_of_last_item = $end_of_this_item;
		}
	}

}