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

/**
 * Check for proper spacing in array key references.
 *
 * @link    https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.3.0
 * @since   0.7.0  This sniff now has the ability to fix a number of the issues it flags.
 * @since   0.12.0 This class now extends the WordPressCS native `Sniff` class.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 */
class ArrayKeySpacingRestrictionsSniff extends Sniff {

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

	/**
	 * 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 ) {

		$token = $this->tokens[ $stackPtr ];
		if ( ! isset( $token['bracket_closer'] ) ) {
			$this->phpcsFile->addWarning( 'Missing bracket closer.', $stackPtr, 'MissingBracketCloser' );
			return;
		}

		$need_spaces = $this->phpcsFile->findNext(
			array( \T_CONSTANT_ENCAPSED_STRING, \T_LNUMBER, \T_WHITESPACE, \T_MINUS ),
			( $stackPtr + 1 ),
			$token['bracket_closer'],
			true
		);

		$spaced1 = ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] );
		$spaced2 = ( \T_WHITESPACE === $this->tokens[ ( $token['bracket_closer'] - 1 ) ]['code'] );

		// It should have spaces unless if it only has strings or numbers as the key.
		if ( false !== $need_spaces && ! ( $spaced1 && $spaced2 ) ) {
			$error = 'Array keys must be surrounded by spaces unless they contain a string or an integer.';
			$fix   = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpacesAroundArrayKeys' );
			if ( true === $fix ) {
				if ( ! $spaced1 ) {
					$this->phpcsFile->fixer->addContentBefore( ( $stackPtr + 1 ), ' ' );
				}
				if ( ! $spaced2 ) {
					$this->phpcsFile->fixer->addContentBefore( $token['bracket_closer'], ' ' );
				}
			}
		} elseif ( false === $need_spaces && ( $spaced1 || $spaced2 ) ) {
			$error = 'Array keys must NOT be surrounded by spaces if they only contain a string or an integer.';
			$fix   = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpacesAroundArrayKeys' );
			if ( true === $fix ) {
				if ( $spaced1 ) {
					$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), '' );
				}
				if ( $spaced2 ) {
					$this->phpcsFile->fixer->replaceToken( ( $token['bracket_closer'] - 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\Sniffs\NamingConventions;

use WordPressCS\WordPress\AbstractFunctionParameterSniff;

/**
 * Use lowercase letters in action and filter names. Separate words via underscores.
 *
 * This sniff is only testing the hook invoke functions as when using 'add_action'/'add_filter'
 * you can't influence the hook name.
 *
 * Hook names invoked with `do_action_deprecated()` and `apply_filters_deprecated()` are ignored.
 *
 * @link    https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.10.0
 * @since   0.11.0 Extends the WordPressCS native `AbstractFunctionParameterSniff` class.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 */
class ValidHookNameSniff extends AbstractFunctionParameterSniff {

	/**
	 * Additional word separators.
	 *
	 * This public variable allows providing additional word separators which
	 * will be allowed in hook names via a property in the phpcs.xml config file.
	 *
	 * Example usage:
	 * <rule ref="WordPress.NamingConventions.ValidHookName">
	 *   <properties>
	 *     <property name="additionalWordDelimiters" value="-"/>
	 *   </properties>
	 * </rule>
	 *
	 * Provide several extra delimiters as one string:
	 * <rule ref="WordPress.NamingConventions.ValidHookName">
	 *   <properties>
	 *     <property name="additionalWordDelimiters" value="-/."/>
	 *   </properties>
	 * </rule>
	 *
	 * @var string
	 */
	public $additionalWordDelimiters = '';

	/**
	 * Regular expression to test for correct punctuation of a hook name.
	 *
	 * The placeholder will be replaced by potentially provided additional
	 * word delimiters in the `prepare_regex()` method.
	 *
	 * @var string
	 */
	protected $punctuation_regex = '`[^\w%s]`';

	/**
	 * Groups of function to restrict.
	 *
	 * @since 0.11.0
	 *
	 * @return array
	 */
	public function getGroups() {
		$this->target_functions = $this->hookInvokeFunctions;
		return parent::getGroups();
	}

	/**
	 * Process the parameters of a matched function.
	 *
	 * @since 0.11.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;
		}

		if ( ! isset( $parameters[1] ) ) {
			return;
		}

		$regex = $this->prepare_regex();

		$case_errors = 0;
		$underscores = 0;
		$content     = array();
		$expected    = array();

		for ( $i = $parameters[1]['start']; $i <= $parameters[1]['end']; $i++ ) {
			$content[ $i ]  = $this->tokens[ $i ]['content'];
			$expected[ $i ] = $this->tokens[ $i ]['content'];

			if ( \in_array( $this->tokens[ $i ]['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) {
				$string = $this->strip_quotes( $this->tokens[ $i ]['content'] );

				/*
				 * Here be dragons - a double quoted string can contain extrapolated variables
				 * which don't have to comply with these rules.
				 */
				if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code'] ) {
					$transform       = $this->transform_complex_string( $string, $regex );
					$case_transform  = $this->transform_complex_string( $string, $regex, 'case' );
					$punct_transform = $this->transform_complex_string( $string, $regex, 'punctuation' );
				} else {
					$transform       = $this->transform( $string, $regex );
					$case_transform  = $this->transform( $string, $regex, 'case' );
					$punct_transform = $this->transform( $string, $regex, 'punctuation' );
				}

				if ( $string === $transform ) {
					continue;
				}

				if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code'] ) {
					$expected[ $i ] = '"' . $transform . '"';
				} else {
					$expected[ $i ] = '\'' . $transform . '\'';
				}

				if ( $string !== $case_transform ) {
					$case_errors++;
				}
				if ( $string !== $punct_transform ) {
					$underscores++;
				}
			}
		}

		$data = array(
			implode( '', $expected ),
			implode( '', $content ),
		);

		if ( $case_errors > 0 ) {
			$error = 'Hook names should be lowercase. Expected: %s, but found: %s.';
			$this->phpcsFile->addError( $error, $stackPtr, 'NotLowercase', $data );
		}
		if ( $underscores > 0 ) {
			$error = 'Words in hook names should be separated using underscores. Expected: %s, but found: %s.';
			$this->phpcsFile->addWarning( $error, $stackPtr, 'UseUnderscores', $data );
		}
	}

	/**
	 * Prepare the punctuation regular expression.
	 *
	 * Merges the existing regular expression with potentially provided extra word delimiters to allow.
	 * This is done 'late' and for each found token as otherwise inline `phpcs:set` directives
	 * would be ignored.
	 *
	 * @return string
	 */
	protected function prepare_regex() {
		$extra = '';
		if ( '' !== $this->additionalWordDelimiters && \is_string( $this->additionalWordDelimiters ) ) {
			$extra = preg_quote( $this->additionalWordDelimiters, '`' );
		}

		return sprintf( $this->punctuation_regex, $extra );
	}

	/**
	 * Transform an arbitrary string to lowercase and replace punctuation and spaces with underscores.
	 *
	 * @param string $string         The target string.
	 * @param string $regex          The punctuation regular expression to use.
	 * @param string $transform_type Whether to a partial or complete transform.
	 *                               Valid values are: 'full', 'case', 'punctuation'.
	 * @return string
	 */
	protected function transform( $string, $regex, $transform_type = 'full' ) {

		switch ( $transform_type ) {
			case 'case':
				return strtolower( $string );

			case 'punctuation':
				return preg_replace( $regex, '_', $string );

			case 'full':
			default:
				return preg_replace( $regex, '_', strtolower( $string ) );
		}
	}

	/**
	 * Transform a complex string which may contain variable extrapolation.
	 *
	 * @param string $string         The target string.
	 * @param string $regex          The punctuation regular expression to use.
	 * @param string $transform_type Whether to a partial or complete transform.
	 *                               Valid values are: 'full', 'case', 'punctuation'.
	 * @return string
	 */
	protected function transform_complex_string( $string, $regex, $transform_type = 'full' ) {
		$output = preg_split( '`([\{\}\$\[\] ])`', $string, -1, \PREG_SPLIT_DELIM_CAPTURE );

		$is_variable = false;
		$has_braces  = false;
		$braces      = 0;

		foreach ( $output as $i => $part ) {
			if ( \in_array( $part, array( '$', '{' ), true ) ) {
				$is_variable = true;
				if ( '{' === $part ) {
					$has_braces = true;
					$braces++;
				}
				continue;
			}

			if ( true === $is_variable ) {
				if ( '[' === $part ) {
					$has_braces = true;
					$braces++;
				}
				if ( \in_array( $part, array( '}', ']' ), true ) ) {
					$braces--;
				}
				if ( false === $has_braces && ' ' === $part ) {
					$is_variable  = false;
					$output[ $i ] = $this->transform( $part, $regex, $transform_type );
				}

				if ( ( true === $has_braces && 0 === $braces ) && false === \in_array( $output[ ( $i + 1 ) ], array( '{', '[' ), true ) ) {
					$has_braces  = false;
					$is_variable = false;
				}
				continue;
			}

			$output[ $i ] = $this->transform( $part, $regex, $transform_type );
		}

		return implode( '', $output );
	}

}


<?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\WP;

use WordPressCS\WordPress\AbstractClassRestrictionsSniff;

/**
 * Restricts the use of deprecated WordPress classes and suggests alternatives.
 *
 * This sniff will throw an error when usage of a deprecated class is detected
 * if the class was deprecated before the minimum supported WP version;
 * a warning otherwise.
 * By default, it is set to presume that a project will support the current
 * WP version and up to three releases before.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.12.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   0.14.0 Now has the ability to handle minimum supported WP version
 *                 being provided via the command-line or as as <config> value
 *                 in a custom ruleset.
 *
 * @uses    \WordPressCS\WordPress\Sniff::$minimum_supported_version
 */
class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff {

	/**
	 * List of deprecated classes with alternative when available.
	 *
	 * To be updated after every major release.
	 *
	 * Version numbers should be fully qualified.
	 *
	 * @var array
	 */
	private $deprecated_classes = array(

		// WP 3.1.0.
		'WP_User_Search' => array(
			'alt'     => 'WP_User_Query',
			'version' => '3.1.0',
		),
	);

	/**
	 * Groups of classes to restrict.
	 *
	 * @return array
	 */
	public function getGroups() {
		// Make sure all array keys are lowercase.
		$this->deprecated_classes = array_change_key_case( $this->deprecated_classes, CASE_LOWER );

		return array(
			'deprecated_classes' => array(
				'classes' => array_keys( $this->deprecated_classes ),
			),
		);
	}

	/**
	 * Process a matched token.
	 *
	 * @param int    $stackPtr        The position of the current token in the stack.
	 * @param array  $group_name      The name of the group which was matched. Will
	 *                                always be 'deprecated_functions'.
	 * @param string $matched_content The token content (class name) which was matched.
	 *
	 * @return void
	 */
	public function process_matched_token( $stackPtr, $group_name, $matched_content ) {

		$this->get_wp_version_from_cl();

		$class_name = ltrim( strtolower( $matched_content ), '\\' );

		$message = 'The %s class has been deprecated since WordPress version %s.';
		$data    = array(
			ltrim( $matched_content, '\\' ),
			$this->deprecated_classes[ $class_name ]['version'],
		);

		if ( ! empty( $this->deprecated_classes[ $class_name ]['alt'] ) ) {
			$message .= ' Use %s instead.';
			$data[]   = $this->deprecated_classes[ $class_name ]['alt'];
		}

		$this->addMessage(
			$message,
			$stackPtr,
			( version_compare( $this->deprecated_classes[ $class_name ]['version'], $this->minimum_supported_version, '<' ) ),
			$this->string_to_errorcode( $class_name . 'Found' ),
			$data
		);
	}

}


<?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\WP;

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

/**
 * Check for usage of deprecated parameter values in WP functions and provide alternative based on the parameter passed.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   1.0.0
 *
 * @uses    \WordPressCS\WordPress\Sniff::$minimum_supported_version
 */
class DeprecatedParameterValuesSniff extends AbstractFunctionParameterSniff {

	/**
	 * The group name for this group of functions.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $group_name = 'wp_deprecated_parameter_values';

	/**
	 * Array of function, argument, and replacement function for deprecated argument.
	 *
	 * The list of deprecated parameter values can be found by
	 * looking for `_deprecated_argument()`.
	 * The list is sorted alphabetically by function name.
	 * Last updated for WordPress 4.9.6.
	 *
	 * @since 1.0.0
	 *
	 * @var array Multidimensional array with parameter details.
	 *     $target_functions = array(
	 *         (string) Function name. => array(
	 *             (int) Target parameter position, 1-based. => array(
	 *                 (string) Parameter value. => array(
	 *                     'alt'     => (string) Suggested alternative.
	 *                     'version' => (int) The WordPress version when deprecated.
	 *                 )
	 *             )
	 *         )
	 *     );
	 */
	protected $target_functions = array(
		'add_settings_field' => array(
			4 => array(
				'misc' => array(
					'alt'     => 'another settings group',
					'version' => '3.0.0',
				),
				'privacy' => array(
					'alt'     => 'another settings group',
					'version' => '3.5.0',
				),
			),
		),
		'add_settings_section' => array(
			4 => array(
				'misc' => array(
					'alt'     => 'another settings group',
					'version' => '3.0.0',
				),
				'privacy' => array(
					'alt'     => 'another settings group',
					'version' => '3.5.0',
				),
			),
		),
		'bloginfo' => array(
			1 => array(
				'home' => array(
					'alt'     => 'the "url" argument',
					'version' => '2.2.0',
				),
				'siteurl' => array(
					'alt'     => 'the "url" argument',
					'version' => '2.2.0',
				),
				'text_direction' => array(
					'alt'     => 'is_rtl()',
					'version' => '2.2.0',
				),
			),
		),
		'get_bloginfo' => array(
			1 => array(
				'home' => array(
					'alt'     => 'the "url" argument',
					'version' => '2.2.0',
				),
				'siteurl' => array(
					'alt'     => 'the "url" argument',
					'version' => '2.2.0',
				),
				'text_direction' => array(
					'alt'     => 'is_rtl()',
					'version' => '2.2.0',
				),
			),
		),
		'register_setting' => array(
			1 => array(
				'misc' => array(
					'alt'     => 'another settings group',
					'version' => '3.0.0',
				),
				'privacy' => array(
					'alt'     => 'another settings group',
					'version' => '3.5.0',
				),
			),
		),
		'unregister_setting' => array(
			1 => array(
				'misc' => array(
					'alt'     => 'another settings group',
					'version' => '3.0.0',
				),
				'privacy' => array(
					'alt'     => 'another settings group',
					'version' => '3.5.0',
				),
			),
		),
	);

	/**
	 * Process the parameters of a matched function.
	 *
	 * @since 1.0.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 ) {
		$this->get_wp_version_from_cl();
		$param_count = \count( $parameters );
		foreach ( $this->target_functions[ $matched_content ] as $position => $parameter_args ) {

			// Stop if the position is higher then the total number of parameters.
			if ( $position > $param_count ) {
				break;
			}

			$this->process_parameter( $matched_content, $parameters[ $position ], $parameter_args );
		}
	}

	/**
	 * Process the parameter of a matched function.
	 *
	 * @since 1.0.0
	 *
	 * @param string $matched_content The token content (function name) which was matched.
	 * @param array  $parameter       Array with start and end token positon of the parameter.
	 * @param array  $parameter_args  Array with alternative and WordPress deprecation version of the parameter.
	 *
	 * @return void
	 */
	protected function process_parameter( $matched_content, $parameter, $parameter_args ) {

		$parameter_position = $this->phpcsFile->findNext(
			Tokens::$emptyTokens,
			$parameter['start'],
			$parameter['end'] + 1,
			true
		);

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

		$matched_parameter = $this->strip_quotes( $this->tokens[ $parameter_position ]['content'] );
		if ( ! isset( $parameter_args[ $matched_parameter ] ) ) {
			return;
		}

		$message = 'The parameter value "%s" has been deprecated since WordPress version %s.';
		$data    = array(
			$matched_parameter,
			$parameter_args[ $matched_parameter ]['version'],
		);

		if ( ! empty( $parameter_args[ $matched_parameter ]['alt'] ) ) {
			$message .= ' Use %s instead.';
			$data[]   = $parameter_args[ $matched_parameter ]['alt'];
		}

		$is_error = version_compare( $parameter_args[ $matched_parameter ]['version'], $this->minimum_supported_version, '<' );
		$this->addMessage(
			$message,
			$parameter_position,
			$is_error,
			$this->string_to_errorcode( 'Found' ),
			$data
		);
	}

}


<?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\PHP;

use WordPressCS\WordPress\AbstractFunctionParameterSniff;

/**
 * Flag calling preg_quote() without the second ($delimiter) parameter.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   1.0.0
 */
class PregQuoteDelimiterSniff extends AbstractFunctionParameterSniff {

	/**
	 * The group name for this group of functions.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected $group_name = 'preg_quote';

	/**
	 * List of functions this sniff should examine.
	 *
	 * @link http://php.net/preg_quote
	 *
	 * @since 1.0.0
	 *
	 * @var array <string function_name> => <bool>
	 */
	protected $target_functions = array(
		'preg_quote' => true,
	);

	/**
	 * Process the parameters of a matched function.
	 *
	 * @since 1.0.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 ) {
		if ( \count( $parameters ) > 1 ) {
			return;
		}

		$this->phpcsFile->addWarning(
			'Passing the $delimiter as the second parameter to preg_quote() is strongly recommended.',
			$stackPtr,
			'Missing'
		);
	}

}


<?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\WP;

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

/**
 * Warns against usage of discouraged WP CONSTANTS and recommends alternatives.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.14.0
 */
class DiscouragedConstantsSniff extends AbstractFunctionParameterSniff {

	/**
	 * List of discouraged WP constants and their replacements.
	 *
	 * @since 0.14.0
	 *
	 * @var array
	 */
	protected $discouraged_constants = array(
		'STYLESHEETPATH'      => 'get_stylesheet_directory()',
		'TEMPLATEPATH'        => 'get_template_directory()',
		'PLUGINDIR'           => 'WP_PLUGIN_DIR',
		'MUPLUGINDIR'         => 'WPMU_PLUGIN_DIR',
		'HEADER_IMAGE'        => 'add_theme_support( \'custom-header\' )',
		'NO_HEADER_TEXT'      => 'add_theme_support( \'custom-header\' )',
		'HEADER_TEXTCOLOR'    => 'add_theme_support( \'custom-header\' )',
		'HEADER_IMAGE_WIDTH'  => 'add_theme_support( \'custom-header\' )',
		'HEADER_IMAGE_HEIGHT' => 'add_theme_support( \'custom-header\' )',
		'BACKGROUND_COLOR'    => 'add_theme_support( \'custom-background\' )',
		'BACKGROUND_IMAGE'    => 'add_theme_support( \'custom-background\' )',
	);

	/**
	 * Array of functions to check.
	 *
	 * @since 0.14.0
	 *
	 * @var array <string function name> => <int parameter position>
	 */
	protected $target_functions = array(
		'define' => 1,
	);

	/**
	 * Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a constant.
	 *
	 * @var array
	 */
	private $preceding_tokens_to_ignore = array(
		\T_NAMESPACE       => true,
		\T_USE             => true,
		\T_CLASS           => true,
		\T_TRAIT           => true,
		\T_INTERFACE       => true,
		\T_EXTENDS         => true,
		\T_IMPLEMENTS      => true,
		\T_NEW             => true,
		\T_FUNCTION        => true,
		\T_DOUBLE_COLON    => true,
		\T_OBJECT_OPERATOR => true,
		\T_INSTANCEOF      => true,
		\T_GOTO            => true,
	);

	/**
	 * 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 ) {
		if ( isset( $this->target_functions[ strtolower( $this->tokens[ $stackPtr ]['content'] ) ] ) ) {
			// Disallow excluding function groups for this sniff.
			$this->exclude = array();

			return parent::process_token( $stackPtr );

		} else {
			return $this->process_arbitrary_tstring( $stackPtr );
		}
	}

	/**
	 * Process an arbitrary T_STRING token to determine whether it is one of the target constants.
	 *
	 * @since 0.14.0
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return void
	 */
	public function process_arbitrary_tstring( $stackPtr ) {
		$content = $this->tokens[ $stackPtr ]['content'];

		if ( ! isset( $this->discouraged_constants[ $content ] ) ) {
			return;
		}

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

		$prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
		if ( false !== $prev && isset( $this->preceding_tokens_to_ignore[ $this->tokens[ $prev ]['code'] ] ) ) {
			// Not the use of a constant.
			return;
		}

		if ( $this->is_token_namespaced( $stackPtr ) === true ) {
			// Namespaced constant of the same name.
			return;
		}

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

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

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

		// Ok, this is really one of the discouraged constants.
		$this->phpcsFile->addWarning(
			'Found usage of constant "%s". Use %s instead.',
			$stackPtr,
			$this->string_to_errorcode( $content . 'UsageFound' ),
			array(
				$content,
				$this->discouraged_constants[ $content ],
			)
		);
	}

	/**
	 * Process the parameters of a matched `define` function call.
	 *
	 * @since 0.14.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 ) {
		$function_name = strtolower( $matched_content );
		$target_param  = $this->target_functions[ $function_name ];

		// Was the target parameter passed ?
		if ( ! isset( $parameters[ $target_param ] ) ) {
			return;
		}

		$raw_content = $this->strip_quotes( $parameters[ $target_param ]['raw'] );

		if ( isset( $this->discouraged_constants[ $raw_content ] ) ) {
			$this->phpcsFile->addWarning(
				'Found declaration of constant "%s". Use %s instead.',
				$stackPtr,
				$this->string_to_errorcode( $raw_content . 'DeclarationFound' ),
				array(
					$raw_content,
					$this->discouraged_constants[ $raw_content ],
				)
			);
		}
	}

}


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

/**
 * Check for incorrect use of the $wpdb->prepare method.
 *
 * Check the following issues:
 * - The only placeholders supported are: %d, %f (%F) and %s and their variations.
 * - Literal % signs need to be properly escaped as `%%`.
 * - Simple placeholders (%d, %f, %F, %s) should be left unquoted in the query string.
 * - Complex placeholders - numbered and formatted variants - will not be quoted
 *   automagically by $wpdb->prepare(), so if used for values, should be quoted in
 *   the query string.
 * - Either an array of replacements should be passed matching the number of
 *   placeholders found or individual parameters for each placeholder should
 *   be passed.
 * - Wildcards for LIKE compare values should be passed in via a replacement parameter.
 *
 * The sniff allows for a specific pattern with a variable number of placeholders
 * created using code along the lines of:
 * `sprintf( 'query .... IN (%s) ...', implode( ',', array_fill( 0, count( $something ), '%s' ) ) )`.
 *
 * A "PreparedSQLPlaceholders replacement count" whitelist comment is supported
 * specifically to silence the `ReplacementsWrongNumber` and `UnfinishedPrepare`
 * error codes. The other error codes are not affected by it.
 *
 * @link https://developer.wordpress.org/reference/classes/wpdb/prepare/
 * @link https://core.trac.wordpress.org/changeset/41496
 * @link https://core.trac.wordpress.org/changeset/41471
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.14.0
 */
class PreparedSQLPlaceholdersSniff extends Sniff {

	/**
	 * These regexes copied from http://php.net/manual/en/function.sprintf.php#93552
	 * and adjusted for limitations in `$wpdb->prepare()`.
	 *
	 * Near duplicate of the one used in the WP.I18n sniff, but with fewer types allowed.
	 *
	 * Note: The regex delimiters and modifiers are not included to allow this regex to be
	 * concatenated together with other regex partials.
	 *
	 * @since 0.14.0
	 *
	 * @var string
	 */
	const PREPARE_PLACEHOLDER_REGEX = '(?:
		(?<![^%]%)                     # Don\'t match a literal % (%%), including when it could overlap with a placeholder.
		(?:
			%                          # Start of placeholder.
			(?:[0-9]+\\\\?\$)?         # Optional ordering of the placeholders.
			[+-]?                      # Optional sign specifier.
			(?:
				(?:0|\'.)?                 # Optional padding specifier - excluding the space.
				-?                         # Optional alignment specifier.
				[0-9]*                     # Optional width specifier.
				(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
				|                      # Only recognize the space as padding in combination with a width specifier.
				(?:[ ])?                   # Optional space padding specifier.
				-?                         # Optional alignment specifier.
				[0-9]+                     # Width specifier.
				(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
			)
			[dfFs]                     # Type specifier.
		)
	)';

	/**
	 * Similar to above, but for the placeholder types *not* supported.
	 *
	 * Note: all optional parts are forced to be greedy to allow for the negative look ahead
	 * at the end to work.
	 *
	 * @since 0.14.0
	 *
	 * @var string
	 */
	const UNSUPPORTED_PLACEHOLDER_REGEX = '`(?:
		(?<!%)                     # Don\'t match a literal % (%%).
		(
			%                          # Start of placeholder.
			(?!                        # Negative look ahead.
				%[^%]                       # Not a correct literal % (%%).
				|
				%%[dfFs]                    # Nor a correct literal % (%%), followed by a simple placeholder.
			)
			(?:[0-9]+\\\\??\$)?+       # Optional ordering of the placeholders.
			[+-]?+                     # Optional sign specifier.
			(?:
				(?:0|\'.)?+                 # Optional padding specifier - excluding the space.
				-?+                         # Optional alignment specifier.
				[0-9]*+                     # Optional width specifier.
				(?:\.(?:[ 0]|\'.)?[0-9]+)?+ # Optional precision specifier with optional padding character.
				|                      # Only recognize the space as padding in combination with a width specifier.
				(?:[ ])?+                   # Optional space padding specifier.
				-?+                         # Optional alignment specifier.
				[0-9]++                     # Width specifier.
				(?:\.(?:[ 0]|\'.)?[0-9]+)?+ # Optional precision specifier with optional padding character.
			)
			(?![dfFs])                 # Negative look ahead: not one of the supported placeholders.
			(?:[^ \'"]*|$)             # but something else instead.
		)
	)`x';

	/**
	 * List of $wpdb methods we are interested in.
	 *
	 * @since 0.14.0
	 *
	 * @var array
	 */
	protected $target_methods = array(
		'prepare' => true,
	);

	/**
	 * Storage for the stack pointer to the method call token.
	 *
	 * @since 0.14.0
	 *
	 * @var int
	 */
	protected $methodPtr;

	/**
	 * Simple regex snippet to recognize and remember quotes.
	 *
	 * @since 0.14.0
	 *
	 * @var string
	 */
	private $regex_quote = '["\']';

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

	/**
	 * 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 void
	 */
	public function process_token( $stackPtr ) {

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

		$parameters = $this->get_function_call_parameters( $this->methodPtr );
		if ( empty( $parameters ) ) {
			return;
		}

		$query                    = $parameters[1];
		$text_string_tokens_found = false;
		$variable_found           = false;
		$sql_wildcard_found       = false;
		$total_placeholders       = 0;
		$total_parameters         = \count( $parameters );
		$valid_in_clauses         = array(
			'uses_in'          => 0,
			'implode_fill'     => 0,
			'adjustment_count' => 0,
		);

		for ( $i = $query['start']; $i <= $query['end']; $i++ ) {
			// Skip over groups of tokens if they are part of an inline function call.
			if ( isset( $skip_from, $skip_to ) && $i >= $skip_from && $i < $skip_to ) {
				$i = $skip_to;
				continue;
			}

			if ( ! isset( Tokens::$textStringTokens[ $this->tokens[ $i ]['code'] ] ) ) {
				if ( \T_VARIABLE === $this->tokens[ $i ]['code'] ) {
					if ( '$wpdb' !== $this->tokens[ $i ]['content'] ) {
						$variable_found = true;
					}
					continue;
				}

				// Detect a specific pattern for variable replacements in combination with `IN`.
				if ( \T_STRING === $this->tokens[ $i ]['code'] ) {

					if ( 'sprintf' === strtolower( $this->tokens[ $i ]['content'] ) ) {
						$sprintf_parameters = $this->get_function_call_parameters( $i );

						if ( ! empty( $sprintf_parameters ) ) {
							$skip_from  = ( $sprintf_parameters[1]['end'] + 1 );
							$last_param = end( $sprintf_parameters );
							$skip_to    = ( $last_param['end'] + 1 );

							$valid_in_clauses['implode_fill']     += $this->analyse_sprintf( $sprintf_parameters );
							$valid_in_clauses['adjustment_count'] += ( \count( $sprintf_parameters ) - 1 );
						}
						unset( $sprintf_parameters, $last_param );

					} elseif ( 'implode' === strtolower( $this->tokens[ $i ]['content'] ) ) {
						$prev = $this->phpcsFile->findPrevious(
							Tokens::$textStringTokens,
							( $i - 1 ),
							$query['start']
						);

						$prev_content = $this->strip_quotes( $this->tokens[ $prev ]['content'] );
						$regex_quote  = $this->get_regex_quote_snippet( $prev_content, $this->tokens[ $prev ]['content'] );

						// Only examine the implode if preceded by an ` IN (`.
						if ( preg_match( '`\s+IN\s*\(\s*(' . $regex_quote . ')?$`i', $prev_content, $match ) > 0 ) {

							if ( isset( $match[1] ) && $regex_quote !== $this->regex_quote ) {
								$this->phpcsFile->addError(
									'Dynamic placeholder generation should not have surrounding quotes.',
									$i,
									'QuotedDynamicPlaceholderGeneration'
								);
							}

							if ( $this->analyse_implode( $i ) === true ) {
								++$valid_in_clauses['uses_in'];
								++$valid_in_clauses['implode_fill'];

								$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
								if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code']
									&& isset( $this->tokens[ $next ]['parenthesis_closer'] )
								) {
									$skip_from = ( $i + 1 );
									$skip_to   = ( $this->tokens[ $next ]['parenthesis_closer'] + 1 );
								}
							}
						}
						unset( $prev, $next, $prev_content, $regex_quote, $match );
					}
				}

				continue;
			}

			$text_string_tokens_found = true;
			$content                  = $this->tokens[ $i ]['content'];

			$regex_quote = $this->regex_quote;
			if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) ) {
				$content     = $this->strip_quotes( $content );
				$regex_quote = $this->get_regex_quote_snippet( $content, $this->tokens[ $i ]['content'] );
			}

			if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code']
				|| \T_HEREDOC === $this->tokens[ $i ]['code']
			) {
				// Only interested in actual query text, so strip out variables.
				$stripped_content = $this->strip_interpolated_variables( $content );
				if ( $stripped_content !== $content ) {
					$interpolated_vars = $this->get_interpolated_variables( $content );
					$vars_without_wpdb = array_diff( $interpolated_vars, array( 'wpdb' ) );
					$content           = $stripped_content;

					if ( ! empty( $vars_without_wpdb ) ) {
						$variable_found = true;
					}
				}
				unset( $stripped_content, $interpolated_vars, $vars_without_wpdb );
			}

			$placeholders = preg_match_all( '`' . self::PREPARE_PLACEHOLDER_REGEX . '`x', $content, $matches );
			if ( $placeholders > 0 ) {
				$total_placeholders += $placeholders;
			}

			/*
			 * Analyse the query for incorrect LIKE queries.
			 *
			 * - `LIKE %s` is the only correct one.
			 * - `LIKE '%s'` or `LIKE "%s"` will not be reported here, but in the quote check.
			 * - Any other `LIKE` statement should be reported, either for using `LIKE` without
			 *   SQL wildcards or for not passing the SQL wildcards via the replacement.
			 */
			$regex = '`\s+LIKE\s*(?:(' . $regex_quote . ')(?!%s(?:\1|$))(?P<content>.*?)(?:\1|$)|(?:concat\((?![^\)]*%s[^\)]*\))(?P<concat>[^\)]*))\))`i';
			if ( preg_match_all( $regex, $content, $matches ) > 0 ) {
				$walk = array();
				if ( ! empty( $matches['content'] ) ) {
					$matches['content'] = array_filter( $matches['content'] );
					if ( ! empty( $matches['content'] ) ) {
						$walk[] = 'content';
					}
				}
				if ( ! empty( $matches['concat'] ) ) {
					$matches['concat'] = array_filter( $matches['concat'] );
					if ( ! empty( $matches['concat'] ) ) {
						$walk[] = 'concat';
					}
				}

				if ( ! empty( $walk ) ) {
					foreach ( $walk as $match_key ) {
						foreach ( $matches[ $match_key ] as $index => $match ) {
							$data = array( $matches[0][ $index ] );

							// Both a `%` as well as a `_` are wildcards in SQL.
							if ( strpos( $match, '%' ) === false && strpos( $match, '_' ) === false ) {
								$this->phpcsFile->addWarning(
									'Unless you are using SQL wildcards, using LIKE is inefficient. Use a straight compare instead. Found: %s.',
									$i,
									'LikeWithoutWildcards',
									$data
								);
							} else {
								$sql_wildcard_found = true;

								if ( strpos( $match, '%s' ) === false ) {
									$this->phpcsFile->addError(
										'SQL wildcards for a LIKE query should be passed in through a replacement parameter. Found: %s.',
										$i,
										'LikeWildcardsInQuery',
										$data
									);
								} else {
									$this->phpcsFile->addError(
										'SQL wildcards for a LIKE query should be passed in through a replacement parameter and the variable part of the replacement should be escaped using "esc_like()". Found: %s.',
										$i,
										'LikeWildcardsInQueryWithPlaceholder',
										$data
									);
								}
							}

							/*
							 * Don't throw `UnescapedLiteral`, `UnsupportedPlaceholder` or `QuotedPlaceholder`
							 * for this part of the SQL query.
							 */
							$content = preg_replace( '`' . preg_quote( $match, '`' ) . '`', '', $content, 1 );
						}
					}
				}
				unset( $matches, $index, $match, $data );
			}

			if ( strpos( $content, '%' ) === false ) {
				continue;
			}

			/*
			 * Analyse the query for unsupported placeholders.
			 */
			if ( preg_match_all( self::UNSUPPORTED_PLACEHOLDER_REGEX, $content, $matches ) > 0 ) {
				if ( ! empty( $matches[0] ) ) {
					foreach ( $matches[0] as $match ) {
						if ( '%' === $match ) {
							$this->phpcsFile->addError(
								'Found unescaped literal "%%" character.',
								$i,
								'UnescapedLiteral',
								array( $match )
							);
						} else {
							$this->phpcsFile->addError(
								'Unsupported placeholder used in $wpdb->prepare(). Found: "%s".',
								$i,
								'UnsupportedPlaceholder',
								array( $match )
							);
						}
					}
				}
				unset( $match, $matches );
			}

			/*
			 * Analyse the query for quoted placeholders.
			 */
			$regex = '`(' . $regex_quote . ')%[dfFs]\1`';
			if ( preg_match_all( $regex, $content, $matches ) > 0 ) {
				if ( ! empty( $matches[0] ) ) {
					foreach ( $matches[0] as $match ) {
						$this->phpcsFile->addError(
							'Simple placeholders should not be quoted in the query string in $wpdb->prepare(). Found: %s.',
							$i,
							'QuotedSimplePlaceholder',
							array( $match )
						);
					}
				}
				unset( $match, $matches );
			}

			/*
			 * Analyse the query for unquoted complex placeholders.
			 */
			$regex = '`(?<!' . $regex_quote . ')' . self::PREPARE_PLACEHOLDER_REGEX . '(?!' . $regex_quote . ')`x';
			if ( preg_match_all( $regex, $content, $matches ) > 0 ) {
				if ( ! empty( $matches[0] ) ) {
					foreach ( $matches[0] as $match ) {
						if ( preg_match( '`%[dfFs]`', $match ) !== 1 ) {
							$this->phpcsFile->addWarning(
								'Complex placeholders used for values in the query string in $wpdb->prepare() will NOT be quoted automagically. Found: %s.',
								$i,
								'UnquotedComplexPlaceholder',
								array( $match )
							);
						}
					}
				}
				unset( $match, $matches );
			}

			/*
			 * Check for an ` IN (%s)` clause.
			 */
			$found_in = preg_match_all( '`\s+IN\s*\(\s*%s\s*\)`i', $content, $matches );
			if ( $found_in > 0 ) {
				$valid_in_clauses['uses_in'] += $found_in;
			}
			unset( $found_in );
		}

		if ( false === $text_string_tokens_found ) {
			// Query string passed in as a variable or function call, nothing to examine.
			return;
		}

		$count_diff_whitelisted = $this->has_whitelist_comment(
			'PreparedSQLPlaceholders replacement count',
			$stackPtr
		);

		if ( 0 === $total_placeholders ) {
			if ( 1 === $total_parameters ) {
				if ( false === $variable_found && false === $sql_wildcard_found ) {
					/*
					 * Only throw this warning if the PreparedSQL sniff won't throw one about
					 * variables being found.
					 * Also don't throw it if we just advised to use a replacement variable to pass a
					 * string containing an SQL wildcard.
					 */
					$this->phpcsFile->addWarning(
						'It is not necessary to prepare a query which doesn\'t use variable replacement.',
						$i,
						'UnnecessaryPrepare'
					);
				}
			} elseif ( false === $count_diff_whitelisted && 0 === $valid_in_clauses['uses_in'] ) {
				$this->phpcsFile->addWarning(
					'Replacement variables found, but no valid placeholders found in the query.',
					$i,
					'UnfinishedPrepare'
				);
			}

			return;
		}

		if ( 1 === $total_parameters ) {
			$this->phpcsFile->addError(
				'Placeholders found in the query passed to $wpdb->prepare(), but no replacements found. Expected %d replacement(s) parameters.',
				$stackPtr,
				'MissingReplacements',
				array( $total_placeholders )
			);
			return;
		}

		if ( true === $count_diff_whitelisted ) {
			return;
		}

		$replacements = $parameters;
		array_shift( $replacements ); // Remove the query.

		// The parameters may have been passed as an array in parameter 2.
		if ( isset( $parameters[2] ) && 2 === $total_parameters ) {
			$next = $this->phpcsFile->findNext(
				Tokens::$emptyTokens,
				$parameters[2]['start'],
				( $parameters[2]['end'] + 1 ),
				true
			);

			if ( false !== $next
				&& ( \T_ARRAY === $this->tokens[ $next ]['code']
					|| \T_OPEN_SHORT_ARRAY === $this->tokens[ $next ]['code'] )
			) {
				$replacements = $this->get_function_call_parameters( $next );
			}
		}

		$total_replacements  = \count( $replacements );
		$total_placeholders -= $valid_in_clauses['adjustment_count'];

		// Bow out when `IN` clauses have been used which appear to be correct.
		if ( $valid_in_clauses['uses_in'] > 0
			&& $valid_in_clauses['uses_in'] === $valid_in_clauses['implode_fill']
			&& 1 === $total_replacements
		) {
			return;
		}

		/*
		 * Verify that the correct amount of replacements have been passed.
		 */
		if ( $total_replacements !== $total_placeholders ) {
			$this->phpcsFile->addWarning(
				'Incorrect number of replacements passed to $wpdb->prepare(). Found %d replacement parameters, expected %d.',
				$stackPtr,
				'ReplacementsWrongNumber',
				array( $total_replacements, $total_placeholders )
			);
		}
	}

	/**
	 * Retrieve a regex snippet to recognize and remember quotes based on the quote style
	 * used in the original string (if any).
	 *
	 * This allows for recognizing `"` and `\'` in single quoted strings,
	 * recognizing `'` and `\"` in double quotes strings and `'` and `"`when the quote
	 * style is unknown or it is a non-quoted string (heredoc/nowdoc and such).
	 *
	 * @since 0.14.0
	 *
	 * @param string $stripped_content Text string content without surrounding quotes.
	 * @param string $original_content Original content for the same text string.
	 *
	 * @return string
	 */
	protected function get_regex_quote_snippet( $stripped_content, $original_content ) {
		$regex_quote = $this->regex_quote;

		if ( $original_content !== $stripped_content ) {
			$quote_style = $original_content[0];

			if ( '"' === $quote_style ) {
				$regex_quote = '\\\\"|\'';
			} elseif ( "'" === $quote_style ) {
				$regex_quote = '"|\\\\\'';
			}
		}

		return $regex_quote;
	}

	/**
	 * Analyse a sprintf() query wrapper to see if it contains a specific code pattern
	 * to deal correctly with `IN` queries.
	 *
	 * The pattern we are searching for is:
	 * `sprintf( 'query ....', implode( ',', array_fill( 0, count( $something ), '%s' ) ) )`
	 *
	 * @since 0.14.0
	 *
	 * @param array $sprintf_params Parameters details for the sprintf call.
	 *
	 * @return int The number of times the pattern was found in the replacements.
	 */
	protected function analyse_sprintf( $sprintf_params ) {
		$found = 0;

		unset( $sprintf_params[1] );

		foreach ( $sprintf_params as $sprintf_param ) {
			if ( strpos( strtolower( $sprintf_param['raw'] ), 'implode' ) === false ) {
				continue;
			}

			$implode = $this->phpcsFile->findNext(
				Tokens::$emptyTokens,
				$sprintf_param['start'],
				$sprintf_param['end'],
				true
			);
			if ( \T_STRING === $this->tokens[ $implode ]['code']
				&& 'implode' === strtolower( $this->tokens[ $implode ]['content'] )
			) {
				if ( $this->analyse_implode( $implode ) === true ) {
					++$found;
				}
			}
		}

		return $found;
	}

	/**
	 * Analyse an implode() function call to see if it contains a specific code pattern
	 * to dynamically create placeholders.
	 *
	 * The pattern we are searching for is:
	 * `implode( ',', array_fill( 0, count( $something ), '%s' ) )`
	 *
	 * This pattern presumes unquoted placeholders!
	 *
	 * @since 0.14.0
	 *
	 * @param int $implode_token The stackPtr to the implode function call.
	 *
	 * @return bool True if the pattern is found, false otherwise.
	 */
	protected function analyse_implode( $implode_token ) {
		$implode_params = $this->get_function_call_parameters( $implode_token );

		if ( empty( $implode_params ) || \count( $implode_params ) !== 2 ) {
			return false;
		}

		if ( preg_match( '`^(["\']), ?\1$`', $implode_params[1]['raw'] ) !== 1 ) {
			return false;
		}

		if ( strpos( strtolower( $implode_params[2]['raw'] ), 'array_fill' ) === false ) {
			return false;
		}

		$array_fill = $this->phpcsFile->findNext(
			Tokens::$emptyTokens,
			$implode_params[2]['start'],
			$implode_params[2]['end'],
			true
		);

		if ( \T_STRING !== $this->tokens[ $array_fill ]['code']
			|| 'array_fill' !== strtolower( $this->tokens[ $array_fill ]['content'] )
		) {
			return false;
		}

		$array_fill_params = $this->get_function_call_parameters( $array_fill );

		if ( empty( $array_fill_params ) || \count( $array_fill_params ) !== 3 ) {
			return false;
		}

		return (bool) preg_match( '`^(["\'])%[dfFs]\1$`', $array_fill_params[3]['raw'] );
	}

}


<?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\WP;

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

/**
 * Makes sure scripts and styles are enqueued and not explicitly echo'd.
 *
 * @link    https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#inline-resources
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.3.0
 * @since   0.12.0 This class now extends the WordPressCS native `Sniff` class.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 */
class EnqueuedResourcesSniff extends Sniff {

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return Tokens::$textStringTokens;
	}

	/**
	 * 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 ) {
		$token = $this->tokens[ $stackPtr ];

		if ( preg_match( '# rel=\\\\?[\'"]?stylesheet\\\\?[\'"]?#', $token['content'] ) > 0 ) {
			$this->phpcsFile->addError(
				'Stylesheets must be registered/enqueued via wp_enqueue_style',
				$stackPtr,
				'NonEnqueuedStylesheet'
			);
		}

		if ( preg_match( '#<script[^>]*(?<=src=)#', $token['content'] ) > 0 ) {
			$this->phpcsFile->addError(
				'Scripts must be registered/enqueued via wp_enqueue_script',
				$stackPtr,
				'NonEnqueuedScript'
			);
		}
	}

}


<?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\PHP;

use WordPressCS\WordPress\AbstractFunctionParameterSniff;

/**
 * Detect use of the `ini_set()` function.
 *
 * - Won't throw notices for "safe" ini directives as listed in the whitelist.
 * - Throws errors for ini directives listed in the blacklist.
 * - A warning will be thrown in all other cases.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since 2.1.0
 */
class IniSetSniff extends AbstractFunctionParameterSniff {

	/**
	 * Array of functions that must be checked.
	 *
	 * @since 2.1.0
	 *
	 * @var array Multidimensional array with parameter details.
	 *     $target_functions = array(
	 *         (string) Function name.
	 *     );
	 */
	protected $target_functions = array(
		'ini_set'   => true,
		'ini_alter' => true,
	);

	/**
	 * Array of PHP configuration options that are allowed to be manipulated.
	 *
	 * @since 2.1.0
	 *
	 * @var array Multidimensional array with parameter details.
	 *     $whitelisted_options = array(
	 *         (string) option name. = array(
	 *             (string[]) 'valid_values' = array()
	 *         )
	 *     );
	 */
	protected $whitelisted_options = array(
		'auto_detect_line_endings' => array(),
		'highlight.bg'             => array(),
		'highlight.comment'        => array(),
		'highlight.default'        => array(),
		'highlight.html'           => array(),
		'highlight.keyword'        => array(),
		'highlight.string'         => array(),
		'short_open_tag'           => array(
			'valid_values' => array( 'true', '1', 'on' ),
		),
	);

	/**
	 * Array of PHP configuration options that are not allowed to be manipulated.
	 *
	 * @since 2.1.0
	 *
	 * @var array Multidimensional array with parameter details.
	 *     $blacklisted_options = array(
	 *         (string) option name. = array(
	 *             (string[]) 'invalid_values' = array()
	 *             (string) 'message'
	 *         )
	 *     );
	 */
	protected $blacklisted_options = array(
		'bcmath.scale' => array(
			'message' => 'Use `bcscale()` instead.',
		),
		'display_errors' => array(
			'message' => 'Use `WP_DEBUG_DISPLAY` instead.',
		),
		'error_reporting' => array(
			'message' => 'Use `WP_DEBUG` instead.',
		),
		'filter.default' => array(
			'message' => 'Changing the option value can break other plugins. Use the filter flag constants when calling the Filter functions instead.',
		),
		'filter.default_flags' => array(
			'message' => 'Changing the option value can break other plugins. Use the filter flag constants when calling the Filter functions instead.',
		),
		'iconv.input_encoding' => array(
			'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.',
		),
		'iconv.internal_encoding' => array(
			'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.',
		),
		'iconv.output_encoding' => array(
			'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.',
		),
		'ignore_user_abort' => array(
			'message' => 'Use `ignore_user_abort()` instead.',
		),
		'log_errors' => array(
			'message' => 'Use `WP_DEBUG_LOG` instead.',
		),
		'max_execution_time' => array(
			'message' => 'Use `set_time_limit()` instead.',
		),
		'memory_limit' => array(
			'message' => 'Use `wp_raise_memory_limit()` or hook into the filters in that function.',
		),
		'short_open_tag' => array(
			'invalid_values' => array( 'false', '0', 'off' ),
			'message'        => 'Turning off short_open_tag is prohibited as it can break other plugins.',
		),
	);

	/**
	 * Process the parameter of a matched function.
	 *
	 * Errors if an option is found in the blacklist. Warns as
	 * 'risky' when the option is not found in the whitelist.
	 *
	 * @since 2.1.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 ) {
		$option_name  = $this->strip_quotes( $parameters[1]['raw'] );
		$option_value = $this->strip_quotes( $parameters[2]['raw'] );
		if ( isset( $this->whitelisted_options[ $option_name ] ) ) {
			$whitelisted_option = $this->whitelisted_options[ $option_name ];
			if ( ! isset( $whitelisted_option['valid_values'] ) || in_array( strtolower( $option_value ), $whitelisted_option['valid_values'], true ) ) {
				return;
			}
		}

		if ( isset( $this->blacklisted_options[ $option_name ] ) ) {
			$blacklisted_option = $this->blacklisted_options[ $option_name ];
			if ( ! isset( $blacklisted_option['invalid_values'] ) || in_array( strtolower( $option_value ), $blacklisted_option['invalid_values'], true ) ) {
				$this->phpcsFile->addError(
					'%s(%s, %s) found. %s',
					$stackPtr,
					$this->string_to_errorcode( $option_name . '_Blacklisted' ),
					array(
						$matched_content,
						$parameters[1]['raw'],
						$parameters[2]['raw'],
						$blacklisted_option['message'],
					)
				);
				return;
			}
		}

		$this->phpcsFile->addWarning(
			'%s(%s, %s) found. Changing configuration values at runtime is strongly discouraged.',
			$stackPtr,
			'Risky',
			array(
				$matched_content,
				$parameters[1]['raw'],
				$parameters[2]['raw'],
			)
		);
	}
}


<?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\PHP;

use WordPressCS\WordPress\AbstractFunctionParameterSniff;

/**
 * Flag calling in_array(), array_search() and array_keys() without true as the third parameter.
 *
 * @link    https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#using-in_array-without-strict-parameter
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.9.0
 * @since   0.10.0 - This sniff not only checks for `in_array()`, but also `array_search()`
 *                   and `array_keys()`.
 *                 - The sniff no longer needlessly extends the `ArrayAssignmentRestrictionsSniff`
 *                   class which it didn't use.
 * @since   0.11.0 Refactored to extend the new WordPressCS native `AbstractFunctionParameterSniff` class.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 */
class StrictInArraySniff extends AbstractFunctionParameterSniff {

	/**
	 * The group name for this group of functions.
	 *
	 * @since 0.11.0
	 *
	 * @var string
	 */
	protected $group_name = 'strict';

	/**
	 * List of array functions to which a $strict parameter can be passed.
	 *
	 * The $strict parameter is the third and last parameter for each of these functions.
	 *
	 * The array_keys() function only requires the $strict parameter when the optional
	 * second parameter $search has been set.
	 *
	 * @link http://php.net/in-array
	 * @link http://php.net/array-search
	 * @link http://php.net/array-keys
	 *
	 * @since 0.10.0
	 * @since 0.11.0 Renamed from $array_functions to $target_functions.
	 *
	 * @var array <string function_name> => <bool always needed ?>
	 */
	protected $target_functions = array(
		'in_array'     => true,
		'array_search' => true,
		'array_keys'   => false,
	);

	/**
	 * Process the parameters of a matched function.
	 *
	 * @since 0.11.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 ) {
		// Check if the strict check is actually needed.
		if ( false === $this->target_functions[ $matched_content ] ) {
			if ( \count( $parameters ) === 1 ) {
				return;
			}
		}

		// We're only interested in the third parameter.
		if ( false === isset( $parameters[3] ) || 'true' !== strtolower( $parameters[3]['raw'] ) ) {
			$errorcode = 'MissingTrueStrict';

			/*
			 * Use a different error code when `false` is found to allow for excluding
			 * the warning as this will be a conscious choice made by the dev.
			 */
			if ( isset( $parameters[3] ) && 'false' === strtolower( $parameters[3]['raw'] ) ) {
				$errorcode = 'FoundNonStrictFalse';
			}

			$this->phpcsFile->addWarning(
				'Not using strict comparison for %s; supply true for third argument.',
				( isset( $parameters[3]['start'] ) ? $parameters[3]['start'] : $parameters[1]['start'] ),
				$errorcode,
				array( $matched_content )
			);
			return;
		}
	}

}


<?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\WP;

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

/**
 * Capital P Dangit!
 *
 * Verify the correct spelling of `WordPress` in text strings, comments and class names.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.12.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 */
class CapitalPDangitSniff extends Sniff {

	/**
	 * Regex to match a large number or spelling variations of WordPress in text strings.
	 *
	 * Prevents matches on:
	 * - URLs for wordpress.org/com/net/tv.
	 * - `@...` usernames starting with `wordpress`
	 * - email addresses with a domain starting with `wordpress`
	 * - email addresses with a user name ending with `wordpress`
	 * - (most) variable names.
	 * - directory paths containing a folder starting or ending with `wordpress`.
	 * - file names containing `wordpress` for a limited set of extensions.
	 * - `wordpress` prefixed or suffixed with dashes as those are indicators that the
	 *   term is probably used as part of a CSS class, such as `fa-wordpress`
	 *   or filename/path like `class-wordpress-importer.php`.
	 * - back-tick quoted `wordpress`.
	 *
	 * @var string
	 */
	const WP_REGEX = '#(?<![\\\\/\$@`-])\b(Word[ _-]*Pres+)\b(?![@/`-]|\.(?:org|com|net|tv)|[^\s<>\'"()]*?\.(?:php|js|css|png|j[e]?pg|gif|pot))#i';

	/**
	 * Regex to match a large number or spelling variations of WordPress in class names.
	 *
	 * @var string
	 */
	const WP_CLASSNAME_REGEX = '`(?:^|_)(Word[_]*Pres+)(?:_|$)`i';

	/**
	 * String tokens we want to listen for.
	 *
	 * @var array
	 */
	private $text_string_tokens = array(
		\T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
		\T_DOUBLE_QUOTED_STRING     => \T_DOUBLE_QUOTED_STRING,
		\T_HEREDOC                  => \T_HEREDOC,
		\T_NOWDOC                   => \T_NOWDOC,
		\T_INLINE_HTML              => \T_INLINE_HTML,
	);

	/**
	 * Comment tokens we want to listen for as they contain text strings.
	 *
	 * @var array
	 */
	private $comment_text_tokens = array(
		\T_DOC_COMMENT        => \T_DOC_COMMENT,
		\T_DOC_COMMENT_STRING => \T_DOC_COMMENT_STRING,
		\T_COMMENT            => \T_COMMENT,
	);

	/**
	 * Combined text string and comment tokens array.
	 *
	 * This property is set in the register() method and used for lookups.
	 *
	 * @var array
	 */
	private $text_and_comment_tokens = array();

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @since 0.12.0
	 *
	 * @return array
	 */
	public function register() {
		// Union the arrays - keeps the array keys.
		$this->text_and_comment_tokens = ( $this->text_string_tokens + $this->comment_text_tokens );

		$targets = ( $this->text_and_comment_tokens + Tokens::$ooScopeTokens );

		// Also sniff for array tokens to make skipping anything within those more efficient.
		$targets[ \T_ARRAY ]            = \T_ARRAY;
		$targets[ \T_OPEN_SHORT_ARRAY ] = \T_OPEN_SHORT_ARRAY;

		return $targets;
	}

	/**
	 * 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 ) {

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

		/*
		 * Ignore tokens within an array definition as this is a false positive in 80% of all cases.
		 *
		 * The return values skip to the end of the array.
		 * This prevents the sniff "hanging" on very long configuration arrays.
		 */
		if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ $stackPtr ]['bracket_closer'] ) ) {
			return $this->tokens[ $stackPtr ]['bracket_closer'];
		} elseif ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) ) {
			return $this->tokens[ $stackPtr ]['parenthesis_closer'];
		}

		/*
		 * Deal with misspellings in class/interface/trait names.
		 * These are not auto-fixable, but need the attention of a developer.
		 */
		if ( isset( Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) {
			$classname = $this->phpcsFile->getDeclarationName( $stackPtr );
			if ( empty( $classname ) ) {
				return;
			}

			if ( preg_match_all( self::WP_CLASSNAME_REGEX, $classname, $matches, \PREG_PATTERN_ORDER ) > 0 ) {
				$mispelled = $this->retrieve_misspellings( $matches[1] );

				if ( ! empty( $mispelled ) ) {
					$this->phpcsFile->addWarning(
						'Please spell "WordPress" correctly. Found: "%s" as part of the class/interface/trait name.',
						$stackPtr,
						'MisspelledClassName',
						array( implode( ', ', $mispelled ) )
					);
				}
			}

			return;
		}

		/*
		 * Deal with misspellings in text strings and documentation.
		 */

		// Ignore content of docblock @link tags.
		if ( \T_DOC_COMMENT_STRING === $this->tokens[ $stackPtr ]['code']
			|| \T_DOC_COMMENT === $this->tokens[ $stackPtr ]['code']
		) {

			$comment_start = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_OPEN_TAG, ( $stackPtr - 1 ) );
			if ( false !== $comment_start ) {
				$comment_tag = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_TAG, ( $stackPtr - 1 ), $comment_start );
				if ( false !== $comment_tag && '@link' === $this->tokens[ $comment_tag ]['content'] ) {
					// @link tag, so ignore.
					return;
				}
			}
		}

		// Ignore any text strings which are array keys `$var['key']` as this is a false positive in 80% of all cases.
		if ( \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $stackPtr ]['code'] ) {
			$prevToken = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true, null, true );
			if ( false !== $prevToken && \T_OPEN_SQUARE_BRACKET === $this->tokens[ $prevToken ]['code'] ) {
				$nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true );
				if ( false !== $nextToken && \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $nextToken ]['code'] ) {
					return;
				}
			}
		}

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

		if ( preg_match_all( self::WP_REGEX, $content, $matches, ( \PREG_PATTERN_ORDER | \PREG_OFFSET_CAPTURE ) ) > 0 ) {
			/*
			 * Prevent some typical false positives.
			 */
			if ( isset( $this->text_and_comment_tokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) {
				$offset = 0;
				foreach ( $matches[1] as $key => $match_data ) {
					$next_offset = ( $match_data[1] + \strlen( $match_data[0] ) );

					// Prevent matches on part of a URL.
					if ( preg_match( '`http[s]?://[^\s<>\'"()]*' . preg_quote( $match_data[0], '`' ) . '`', $content, $discard, 0, $offset ) === 1 ) {
						unset( $matches[1][ $key ] );
					} elseif ( preg_match( '`[a-z]+=(["\'])' . preg_quote( $match_data[0], '`' ) . '\1`', $content, $discard, 0, $offset ) === 1 ) {
						// Prevent matches on html attributes like: `value="wordpress"`.
						unset( $matches[1][ $key ] );
					} elseif ( preg_match( '`\\\\\'' . preg_quote( $match_data[0], '`' ) . '\\\\\'`', $content, $discard, 0, $offset ) === 1 ) {
						// Prevent matches on xpath queries and such: `\'wordpress\'`.
						unset( $matches[1][ $key ] );
					} elseif ( preg_match( '`(?:\?|&|&)[a-z0-9_]+=' . preg_quote( $match_data[0], '`' ) . '(?:&|$)`', $content, $discard, 0, $offset ) === 1 ) {
						// Prevent matches on url query strings: `?something=wordpress`.
						unset( $matches[1][ $key ] );
					}

					$offset = $next_offset;
				}

				if ( empty( $matches[1] ) ) {
					return;
				}
			}

			$mispelled = $this->retrieve_misspellings( $matches[1] );

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

			$fix = $this->phpcsFile->addFixableWarning(
				'Please spell "WordPress" correctly. Found %s misspelling(s): %s',
				$stackPtr,
				'Misspelled',
				array(
					\count( $mispelled ),
					implode( ', ', $mispelled ),
				)
			);

			if ( true === $fix ) {
				// Apply fixes based on offset to ensure we don't replace false positives.
				$replacement = $content;
				foreach ( $matches[1] as $match ) {
					$replacement = substr_replace( $replacement, 'WordPress', $match[1], \strlen( $match[0] ) );
				}

				$this->phpcsFile->fixer->replaceToken( $stackPtr, $replacement );
			}
		}
	}

	/**
	 * Retrieve a list of misspellings based on an array of matched variations on the target word.
	 *
	 * @param array $match_stack Array of matched variations of the target word.
	 * @return array Array containing only the misspelled variants.
	 */
	protected function retrieve_misspellings( $match_stack ) {
		$mispelled = array();
		foreach ( $match_stack as $match ) {
			// Deal with multi-dimensional arrays when capturing offset.
			if ( \is_array( $match ) ) {
				$match = $match[0];
			}

			if ( 'WordPress' !== $match ) {
				$mispelled[] = $match;
			}
		}

		return $mispelled;
	}

}


<?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\AbstractArrayAssignmentRestrictionsSniff;

/**
 * Flag potentially slow queries.
 *
 * @link    https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#uncached-pageload
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.3.0
 * @since   0.12.0 Introduced new and more intuitively named 'slow query' whitelist
 *                 comment, replacing the 'tax_query' whitelist comment which is now
 *                 deprecated.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   1.0.0  This sniff has been moved from the `VIP` category to the `DB` category.
 */
class SlowDBQuerySniff extends AbstractArrayAssignmentRestrictionsSniff {

	/**
	 * Groups of variables to restrict.
	 *
	 * @return array
	 */
	public function getGroups() {
		return array(
			'slow_db_query' => array(
				'type'    => 'warning',
				'message' => 'Detected usage of %s, possible slow query.',
				'keys'    => array(
					'tax_query',
					'meta_query',
					'meta_key',
					'meta_value',
				),
			),
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @since 0.10.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->has_whitelist_comment( 'slow query', $stackPtr ) ) {
			return;
		} elseif ( $this->has_whitelist_comment( 'tax_query', $stackPtr ) ) {
			return;
		}

		return parent::process_token( $stackPtr );
	}

	/**
	 * Callback to process each confirmed key, to check value.
	 * This 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().
	 */
	public function callback( $key, $val, $line, $group ) {
		return true;
	}

}


<?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\WP;

use WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff;

/**
 * Flag returning high or infinite posts_per_page.
 *
 * @link    https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#no-limit-queries
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.3.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   0.14.0 Added the posts_per_page property.
 * @since   1.0.0  This sniff has been split into two, with the check for high pagination
 *                 limit being part of the WP category, and the check for pagination
 *                 disabling being part of the VIP category.
 */
class PostsPerPageSniff extends AbstractArrayAssignmentRestrictionsSniff {

	/**
	 * Posts per page property
	 *
	 * Posts per page limit to check against.
	 *
	 * @since 0.14.0
	 *
	 * @var int
	 */
	public $posts_per_page = 100;

	/**
	 * Groups of variables to restrict.
	 *
	 * @return array
	 */
	public function getGroups() {
		return array(
			'posts_per_page' => array(
				'type' => 'warning',
				'keys' => array(
					'posts_per_page',
					'numberposts',
				),
			),
		);
	}

	/**
	 * Callback to process each confirmed key, to check 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().
	 */
	public function callback( $key, $val, $line, $group ) {
		$this->posts_per_page = (int) $this->posts_per_page;

		if ( $val > $this->posts_per_page ) {
			return 'Detected high pagination limit, `%s` is set to `%s`';
		}

		return false;
	}

}


<?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\Classes;

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

/**
 * Verifies object instantiation statements.
 *
 * - Demand the use of parenthesis.
 * - Demand no space between the class name and the parenthesis.
 * - Forbid assigning new by reference.
 *
 * {@internal Note: This sniff currently does not examine the parenthesis of new object
 * instantiations where the class name is held in a variable variable.}}
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.12.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 */
class ClassInstantiationSniff extends Sniff {

	/**
	 * A list of tokenizers this sniff supports.
	 *
	 * @var array
	 */
	public $supportedTokenizers = array(
		'PHP',
		'JS',
	);

	/**
	 * Tokens which can be part of a "classname".
	 *
	 * Set from within the register() method.
	 *
	 * @var array
	 */
	protected $classname_tokens = array();

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		/*
		 * Set the $classname_tokens property.
		 *
		 * Currently does not account for classnames passed as a variable variable.
		 */
		$this->classname_tokens                    = Tokens::$emptyTokens;
		$this->classname_tokens[ \T_NS_SEPARATOR ] = \T_NS_SEPARATOR;
		$this->classname_tokens[ \T_STRING ]       = \T_STRING;
		$this->classname_tokens[ \T_SELF ]         = \T_SELF;
		$this->classname_tokens[ \T_STATIC ]       = \T_STATIC;
		$this->classname_tokens[ \T_PARENT ]       = \T_PARENT;
		$this->classname_tokens[ \T_ANON_CLASS ]   = \T_ANON_CLASS;

		// Classname in a variable.
		$this->classname_tokens[ \T_VARIABLE ]                 = \T_VARIABLE;
		$this->classname_tokens[ \T_DOUBLE_COLON ]             = \T_DOUBLE_COLON;
		$this->classname_tokens[ \T_OBJECT_OPERATOR ]          = \T_OBJECT_OPERATOR;
		$this->classname_tokens[ \T_OPEN_SQUARE_BRACKET ]      = \T_OPEN_SQUARE_BRACKET;
		$this->classname_tokens[ \T_CLOSE_SQUARE_BRACKET ]     = \T_CLOSE_SQUARE_BRACKET;
		$this->classname_tokens[ \T_CONSTANT_ENCAPSED_STRING ] = \T_CONSTANT_ENCAPSED_STRING;
		$this->classname_tokens[ \T_LNUMBER ]                  = \T_LNUMBER;

		return array(
			\T_NEW,
			\T_STRING, // JS.
		);
	}

	/**
	 * 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 ) {
		// Make sure we have the right token, JS vs PHP.
		if ( ( 'PHP' === $this->phpcsFile->tokenizerType && \T_NEW !== $this->tokens[ $stackPtr ]['code'] )
			|| ( 'JS' === $this->phpcsFile->tokenizerType
				&& ( \T_STRING !== $this->tokens[ $stackPtr ]['code']
				|| 'new' !== strtolower( $this->tokens[ $stackPtr ]['content'] ) ) )
		) {
			return;
		}

		/*
		 * Check for new by reference used in PHP files.
		 */
		if ( 'PHP' === $this->phpcsFile->tokenizerType ) {
			$prev_non_empty = $this->phpcsFile->findPrevious(
				Tokens::$emptyTokens,
				( $stackPtr - 1 ),
				null,
				true
			);

			if ( false !== $prev_non_empty && 'T_BITWISE_AND' === $this->tokens[ $prev_non_empty ]['type'] ) {
				$this->phpcsFile->recordMetric( $stackPtr, 'Assigning new by reference', 'yes' );

				$this->phpcsFile->addError(
					'Assigning the return value of new by reference is no longer supported by PHP.',
					$stackPtr,
					'NewByReferenceFound'
				);
			} else {
				$this->phpcsFile->recordMetric( $stackPtr, 'Assigning new by reference', 'no' );
			}
		}

		/*
		 * Check for parenthesis & correct placement thereof.
		 */
		$next_non_empty_after_class_name = $this->phpcsFile->findNext(
			$this->classname_tokens,
			( $stackPtr + 1 ),
			null,
			true,
			null,
			true
		);

		if ( false === $next_non_empty_after_class_name ) {
			// Live coding.
			return;
		}

		// Walk back to the last part of the class name.
		$has_comment = false;
		for ( $classname_ptr = ( $next_non_empty_after_class_name - 1 ); $classname_ptr >= $stackPtr; $classname_ptr-- ) {
			if ( ! isset( Tokens::$emptyTokens[ $this->tokens[ $classname_ptr ]['code'] ] ) ) {
				// Prevent a false positive on variable variables, disregard them for now.
				if ( $stackPtr === $classname_ptr ) {
					return;
				}

				break;
			}

			if ( \T_WHITESPACE !== $this->tokens[ $classname_ptr ]['code'] ) {
				$has_comment = true;
			}
		}

		if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty_after_class_name ]['code'] ) {
			$this->phpcsFile->recordMetric( $stackPtr, 'Object instantiation with parenthesis', 'no' );

			$fix = $this->phpcsFile->addFixableError(
				'Parenthesis should always be used when instantiating a new object.',
				$classname_ptr,
				'MissingParenthesis'
			);

			if ( true === $fix ) {
				$this->phpcsFile->fixer->addContent( $classname_ptr, '()' );
			}
		} else {
			$this->phpcsFile->recordMetric( $stackPtr, 'Object instantiation with parenthesis', 'yes' );

			if ( ( $next_non_empty_after_class_name - 1 ) !== $classname_ptr ) {
				$this->phpcsFile->recordMetric(
					$stackPtr,
					'Space between classname and parenthesis',
					( $next_non_empty_after_class_name - $classname_ptr )
				);

				$error      = 'There must be no spaces between the class name and the open parenthesis when instantiating a new object.';
				$error_code = 'SpaceBeforeParenthesis';

				if ( false === $has_comment ) {
					$fix = $this->phpcsFile->addFixableError( $error, $next_non_empty_after_class_name, $error_code );

					if ( true === $fix ) {
						$this->phpcsFile->fixer->beginChangeset();
						for ( $i = ( $next_non_empty_after_class_name - 1 ); $i > $classname_ptr; $i-- ) {
							$this->phpcsFile->fixer->replaceToken( $i, '' );
						}
						$this->phpcsFile->fixer->endChangeset();
					}
				} else {
					$this->phpcsFile->addError( $error, $next_non_empty_after_class_name, $error_code );
				}
			} else {
				$this->phpcsFile->recordMetric( $stackPtr, 'Space between classname and parenthesis', 0 );
			}
		}
	}

}


<?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 a comma after each array item and the spacing around it.
 *
 * Rules:
 * - For multi-line arrays, a comma is needed after each array item.
 * - Same for single-line arrays, but no comma is allowed after the last array item.
 * - There should be no space between the comma and the end of the array item.
 * - There should be exactly one space between the comma and the start of the
 *   next array item for single-line items.
 *
 * @link    https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.12.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 */
class CommaAfterArrayItemSniff extends Sniff {

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

	/**
	 * 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 ) {
		/*
		 * 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 );

		// This array is empty, so the below checks aren't necessary.
		if ( ( $opener + 1 ) === $closer ) {
			return;
		}

		$single_line = true;
		if ( $this->tokens[ $opener ]['line'] !== $this->tokens[ $closer ]['line'] ) {
			$single_line = false;
		}

		$array_items = $this->get_function_call_parameters( $stackPtr );
		if ( empty( $array_items ) ) {
			// Strange, no array items found.
			return;
		}

		$array_item_count = \count( $array_items );

		// Note: $item_index is 1-based and the array items are split on the commas!
		foreach ( $array_items as $item_index => $item ) {
			$maybe_comma = ( $item['end'] + 1 );
			$is_comma    = false;
			if ( isset( $this->tokens[ $maybe_comma ] ) && \T_COMMA === $this->tokens[ $maybe_comma ]['code'] ) {
				$is_comma = true;
			}

			/*
			 * Check if this is a comma at the end of the last item in a single line array.
			 */
			if ( true === $single_line && $item_index === $array_item_count ) {

				if ( true === $is_comma ) {
					$fix = $this->phpcsFile->addFixableError(
						'Comma not allowed after last value in single-line array declaration',
						$maybe_comma,
						'CommaAfterLast'
					);

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

				/*
				 * No need to do the spacing checks for the last item in a single line array.
				 * This is handled by another sniff checking the spacing before the array closer.
				 */
				continue;
			}

			$last_content = $this->phpcsFile->findPrevious(
				Tokens::$emptyTokens,
				$item['end'],
				$item['start'],
				true
			);

			if ( false === $last_content ) {
				// Shouldn't be able to happen, but just in case, ignore this array item.
				continue;
			}

			/**
			 * Make sure every item in a multi-line array has a comma at the end.
			 *
			 * Should in reality only be triggered by the last item in a multi-line array
			 * as otherwise we'd have a parse error already.
			 */
			if ( false === $is_comma && false === $single_line ) {

				$fix = $this->phpcsFile->addFixableError(
					'Each array item in a multi-line array declaration must end in a comma',
					$last_content,
					'NoComma'
				);

				if ( true === $fix ) {
					$this->phpcsFile->fixer->addContent( $last_content, ',' );
				}
			}

			if ( false === $is_comma ) {
				// Can't check spacing around the comma if there is no comma.
				continue;
			}

			/*
			 * Check for whitespace at the end of the array item.
			 */
			if ( $last_content !== $item['end']
				// Ignore whitespace at the end of a multi-line item if it is the end of a heredoc/nowdoc.
				&& ( true === $single_line
					|| ! isset( Tokens::$heredocTokens[ $this->tokens[ $last_content ]['code'] ] ) )
			) {
				$newlines = 0;
				$spaces   = 0;
				for ( $i = $item['end']; $i > $last_content; $i-- ) {

					if ( \T_WHITESPACE === $this->tokens[ $i ]['code'] ) {
						if ( $this->tokens[ $i ]['content'] === $this->phpcsFile->eolChar ) {
							$newlines++;
						} else {
							$spaces += $this->tokens[ $i ]['length'];
						}
					} elseif ( \T_COMMENT === $this->tokens[ $i ]['code']
						|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $i ]['code'] ] )
					) {
						break;
					}
				}

				$space_phrases = array();
				if ( $spaces > 0 ) {
					$space_phrases[] = $spaces . ' spaces';
				}
				if ( $newlines > 0 ) {
					$space_phrases[] = $newlines . ' newlines';
				}
				unset( $newlines, $spaces );

				$fix = $this->phpcsFile->addFixableError(
					'Expected 0 spaces between "%s" and comma; %s found',
					$maybe_comma,
					'SpaceBeforeComma',
					array(
						$this->tokens[ $last_content ]['content'],
						implode( ' and ', $space_phrases ),
					)
				);

				if ( true === $fix ) {
					$this->phpcsFile->fixer->beginChangeset();
					for ( $i = $item['end']; $i > $last_content; $i-- ) {

						if ( \T_WHITESPACE === $this->tokens[ $i ]['code'] ) {
							$this->phpcsFile->fixer->replaceToken( $i, '' );

						} elseif ( \T_COMMENT === $this->tokens[ $i ]['code']
							|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $i ]['code'] ] )
						) {
							// We need to move the comma to before the comment.
							$this->phpcsFile->fixer->addContent( $last_content, ',' );
							$this->phpcsFile->fixer->replaceToken( $maybe_comma, '' );

							/*
							 * No need to worry about removing too much whitespace in
							 * combination with a `//` comment as in that case, the newline
							 * is part of the comment, so we're good.
							 */

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

			if ( ! isset( $this->tokens[ ( $maybe_comma + 1 ) ] ) ) {
				// Shouldn't be able to happen, but just in case.
				continue;
			}

			/*
			 * Check whitespace after the comma.
			 */
			$next_token = $this->tokens[ ( $maybe_comma + 1 ) ];

			if ( \T_WHITESPACE === $next_token['code'] ) {

				if ( false === $single_line && $this->phpcsFile->eolChar === $next_token['content'] ) {
					continue;
				}

				$next_non_whitespace = $this->phpcsFile->findNext(
					\T_WHITESPACE,
					( $maybe_comma + 1 ),
					$closer,
					true
				);

				if ( false === $next_non_whitespace
					|| ( false === $single_line
						&& $this->tokens[ $next_non_whitespace ]['line'] === $this->tokens[ $maybe_comma ]['line']
						&& ( \T_COMMENT === $this->tokens[ $next_non_whitespace ]['code']
							|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $next_non_whitespace ]['code'] ] ) ) )
				) {
					continue;
				}

				$space_length = $next_token['length'];
				if ( 1 === $space_length ) {
					continue;
				}

				$fix = $this->phpcsFile->addFixableError(
					'Expected 1 space between comma and "%s"; %s found',
					$maybe_comma,
					'SpaceAfterComma',
					array(
						$this->tokens[ $next_non_whitespace ]['content'],
						$space_length,
					)
				);

				if ( true === $fix ) {
					$this->phpcsFile->fixer->replaceToken( ( $maybe_comma + 1 ), ' ' );
				}
			} else {
				// This is either a comment or a mixed single/multi-line array.
				// Just add a space and let other sniffs sort out the array layout.
				$fix = $this->phpcsFile->addFixableError(
					'Expected 1 space between comma and "%s"; 0 found',
					$maybe_comma,
					'NoSpaceAfterComma',
					array( $next_token['content'] )
				);

				if ( true === $fix ) {
					$this->phpcsFile->fixer->addContent( $maybe_comma, ' ' );
				}
			}
		}
	}

}


<?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 PHP_CodeSniffer\Standards\PEAR\Sniffs\NamingConventions\ValidFunctionNameSniff as PHPCS_PEAR_ValidFunctionNameSniff;
use PHP_CodeSniffer\Files\File;
use WordPressCS\WordPress\Sniff;

/**
 * Enforces WordPress function name and method name format, based upon Squiz code.
 *
 * @link    https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.1.0
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   2.0.0  The `get_name_suggestion()` method has been moved to the
 *                 WordPress native `Sniff` base class as `get_snake_case_name_suggestion()`.
 *
 * Last synced with parent class December 2018 up to commit ee167761d7756273b8ad0ad68bf3db1f2c211bb8.
 * @link    https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php
 *
 * {@internal While this class extends the PEAR parent, it does not actually use the checks
 * contained in the parent. It only uses the properties and the token registration from the parent.}}
 */
class ValidFunctionNameSniff extends PHPCS_PEAR_ValidFunctionNameSniff {

	/**
	 * Processes the tokens outside the scope.
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed.
	 * @param int                         $stackPtr  The position where this token was
	 *                                               found.
	 *
	 * @return void
	 */
	protected function processTokenOutsideScope( File $phpcsFile, $stackPtr ) {
		$functionName = $phpcsFile->getDeclarationName( $stackPtr );

		if ( ! isset( $functionName ) ) {
			// Ignore closures.
			return;
		}

		if ( '' === ltrim( $functionName, '_' ) ) {
			// Ignore special functions.
			return;
		}

		$functionNameLc = strtolower( $functionName );

		// Is this a magic function ? I.e., it is prefixed with "__" ?
		// Outside class scope this basically just means __autoload().
		if ( 0 === strpos( $functionName, '__' ) ) {
			$magicPart = substr( $functionNameLc, 2 );
			if ( isset( $this->magicFunctions[ $magicPart ] ) ) {
				return;
			}

			$error     = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
			$errorData = array( $functionName );
			$phpcsFile->addError( $error, $stackPtr, 'FunctionDoubleUnderscore', $errorData );
		}

		if ( $functionNameLc !== $functionName ) {
			$error     = 'Function name "%s" is not in snake case format, try "%s"';
			$errorData = array(
				$functionName,
				Sniff::get_snake_case_name_suggestion( $functionName ),
			);
			$phpcsFile->addError( $error, $stackPtr, 'FunctionNameInvalid', $errorData );
		}
	}

	/**
	 * Processes the tokens within the scope.
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed.
	 * @param int                         $stackPtr  The position where this token was
	 *                                               found.
	 * @param int                         $currScope The position of the current scope.
	 *
	 * @return void
	 */
	protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currScope ) {

		$tokens = $phpcsFile->getTokens();

		// Determine if this is a function which needs to be examined.
		$conditions = $tokens[ $stackPtr ]['conditions'];
		end( $conditions );
		$deepestScope = key( $conditions );
		if ( $deepestScope !== $currScope ) {
			return;
		}

		$methodName = $phpcsFile->getDeclarationName( $stackPtr );

		if ( ! isset( $methodName ) ) {
			// Ignore closures.
			return;
		}

		$className = $phpcsFile->getDeclarationName( $currScope );
		if ( isset( $className ) === false ) {
			$className = '[Anonymous Class]';
		}

		$methodNameLc = strtolower( $methodName );
		$classNameLc  = strtolower( $className );

		// Ignore special functions.
		if ( '' === ltrim( $methodName, '_' ) ) {
			return;
		}

		// PHP4 constructors are allowed to break our rules.
		if ( $methodNameLc === $classNameLc ) {
			return;
		}

		// PHP4 destructors are allowed to break our rules.
		if ( '_' . $classNameLc === $methodNameLc ) {
			return;
		}

		$extended   = $phpcsFile->findExtendedClassName( $currScope );
		$interfaces = $phpcsFile->findImplementedInterfaceNames( $currScope );

		// If this is a child class or interface implementation, it may have to use camelCase or double underscores.
		if ( ! empty( $extended ) || ! empty( $interfaces ) ) {
			return;
		}

		// Is this a magic method ? I.e. is it prefixed with "__" ?
		if ( 0 === strpos( $methodName, '__' ) ) {
			$magicPart = substr( $methodNameLc, 2 );
			if ( isset( $this->magicMethods[ $magicPart ] ) ) {
				return;
			}

			$error     = 'Method name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
			$errorData = array( $className . '::' . $methodName );
			$phpcsFile->addError( $error, $stackPtr, 'MethodDoubleUnderscore', $errorData );
		}

		// Check for all lowercase.
		if ( $methodNameLc !== $methodName ) {
			$error     = 'Method name "%s" in class %s is not in snake case format, try "%s"';
			$errorData = array(
				$methodName,
				$className,
				Sniff::get_snake_case_name_suggestion( $methodName ),
			);
			$phpcsFile->addError( $error, $stackPtr, 'MethodNameInvalid', $errorData );
		}
	}

}


<?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\WhiteSpace;

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

/**
 * Warn on line indentation ending with spaces for precision alignment.
 *
 * WP demands tabs for indentation. In rare cases, spaces for precision alignment can be
 * intentional and acceptable, but more often than not, this is a typo.
 *
 * The `Generic.WhiteSpace.DisallowSpaceIndent` sniff already checks for space indentation
 * and auto-fixes to tabs.
 *
 * This sniff only checks for precision alignments which can not be corrected by the
 * `Generic.WhiteSpace.DisallowSpaceIndent` sniff.
 *
 * As this may be intentional, this sniff explicitly does *NOT* contain a fixer.
 *
 * @link    https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.14.0
 */
class PrecisionAlignmentSniff extends Sniff {

	/**
	 * A list of tokenizers this sniff supports.
	 *
	 * @var array
	 */
	public $supportedTokenizers = array(
		'PHP',
		'JS',
		'CSS',
	);

	/**
	 * Allow for providing a list of tokens for which (preceding) precision alignment should be ignored.
	 *
	 * <rule ref="WordPress.WhiteSpace.PrecisionAlignment">
	 *    <properties>
	 *        <property name="ignoreAlignmentTokens" type="array">
	 *            <element value="T_COMMENT"/>
	 *            <element value="T_INLINE_HTML"/>
	 *        </property>
	 *    </properties>
	 * </rule>
	 *
	 * @var array
	 */
	public $ignoreAlignmentTokens = array();

	/**
	 * The --tab-width CLI value that is being used.
	 *
	 * @var int
	 */
	private $tab_width;

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return array(
			\T_OPEN_TAG,
			\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 Integer stack pointer to skip the rest of the file.
	 */
	public function process_token( $stackPtr ) {
		if ( ! isset( $this->tab_width ) ) {
			$this->tab_width = PHPCSHelper::get_tab_width( $this->phpcsFile );
		}

		// Handle any custom ignore tokens received from a ruleset.
		$ignoreAlignmentTokens = $this->merge_custom_array( $this->ignoreAlignmentTokens );

		$check_tokens  = array(
			\T_WHITESPACE             => true,
			\T_INLINE_HTML            => true,
			\T_DOC_COMMENT_WHITESPACE => true,
			\T_COMMENT                => true,
		);
		$check_tokens += Tokens::$phpcsCommentTokens;

		for ( $i = 0; $i < $this->phpcsFile->numTokens; $i++ ) {

			if ( 1 !== $this->tokens[ $i ]['column'] ) {
				continue;
			} elseif ( isset( $check_tokens[ $this->tokens[ $i ]['code'] ] ) === false
				|| ( isset( $this->tokens[ ( $i + 1 ) ] )
					&& \T_WHITESPACE === $this->tokens[ ( $i + 1 ) ]['code'] )
				|| $this->tokens[ $i ]['content'] === $this->phpcsFile->eolChar
				|| isset( $ignoreAlignmentTokens[ $this->tokens[ $i ]['type'] ] )
				|| ( isset( $this->tokens[ ( $i + 1 ) ] )
					&& isset( $ignoreAlignmentTokens[ $this->tokens[ ( $i + 1 ) ]['type'] ] ) )
			) {
				continue;
			}

			$spaces = 0;
			switch ( $this->tokens[ $i ]['type'] ) {
				case 'T_WHITESPACE':
					$spaces = ( $this->tokens[ $i ]['length'] % $this->tab_width );
					break;

				case 'T_DOC_COMMENT_WHITESPACE':
					$length = $this->tokens[ $i ]['length'];
					$spaces = ( $length % $this->tab_width );

					if ( isset( $this->tokens[ ( $i + 1 ) ] )
						&& ( \T_DOC_COMMENT_STAR === $this->tokens[ ( $i + 1 ) ]['code']
							|| \T_DOC_COMMENT_CLOSE_TAG === $this->tokens[ ( $i + 1 ) ]['code'] )
						&& 0 !== $spaces
					) {
						// One alignment space expected before the *.
						--$spaces;
					}
					break;

				case 'T_COMMENT':
				case 'T_PHPCS_ENABLE':
				case 'T_PHPCS_DISABLE':
				case 'T_PHPCS_SET':
				case 'T_PHPCS_IGNORE':
				case 'T_PHPCS_IGNORE_FILE':
					/*
					 * Indentation whitespace for subsequent lines of multi-line comments
					 * are tokenized as part of the comment.
					 */
					$comment    = ltrim( $this->tokens[ $i ]['content'] );
					$whitespace = str_replace( $comment, '', $this->tokens[ $i ]['content'] );
					$length     = \strlen( $whitespace );
					$spaces     = ( $length % $this->tab_width );

					if ( isset( $comment[0] ) && '*' === $comment[0] && 0 !== $spaces ) {
						--$spaces;
					}
					break;

				case 'T_INLINE_HTML':
					if ( $this->tokens[ $i ]['content'] === $this->phpcsFile->eolChar ) {
						$spaces = 0;
					} else {
						/*
						 * Indentation whitespace for inline HTML is part of the T_INLINE_HTML token.
						 */
						$content    = ltrim( $this->tokens[ $i ]['content'] );
						$whitespace = str_replace( $content, '', $this->tokens[ $i ]['content'] );
						$spaces     = ( \strlen( $whitespace ) % $this->tab_width );
					}

					/*
					 * Prevent triggering on multi-line /*-style inline javascript comments.
					 * This may cause false negatives as there is no check for being in a
					 * <script> tag, but that will be rare.
					 */
					if ( isset( $content[0] ) && '*' === $content[0] && 0 !== $spaces ) {
						--$spaces;
					}
					break;
			}

			if ( $spaces > 0 && ! $this->has_whitelist_comment( 'precision alignment', $i ) ) {
				$this->phpcsFile->addWarning(
					'Found precision alignment of %s spaces.',
					$i,
					'Found',
					array( $spaces )
				);
			}
		}

		// Ignore the rest of the file.
		return ( $this->phpcsFile->numTokens + 1 );
	}

}