基础代码

This commit is contained in:
2021-02-26 22:23:13 +08:00
parent 7884df52f0
commit a719feebba
2585 changed files with 328263 additions and 0 deletions
@@ -0,0 +1,468 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Enforces WordPress array spacing format.
*
* - Check for no space between array keyword and array opener.
* - Check for no space between the parentheses of an empty array.
* - Checks for one space after the array opener / before the array closer in single-line arrays.
* - Checks that associative arrays are multi-line.
* - Checks that each array item in a multi-line array starts on a new line.
* - Checks that the array closer in a multi-line array is on a new line.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0 - The WordPress specific additional checks have now been split off
* from the `WordPress.Arrays.ArrayDeclaration` sniff into this sniff.
* - Added sniffing & fixing for associative arrays.
* @since 0.12.0 Decoupled this sniff from the upstream sniff completely.
* This sniff now extends the WordPressCS native `Sniff` class instead.
* @since 0.13.0 Added the last remaining checks from the `WordPress.Arrays.ArrayDeclaration`
* sniff which were not covered elsewhere.
* The `WordPress.Arrays.ArrayDeclaration` sniff has now been deprecated.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 Single item associative arrays are now by default exempt from the
* "must be multi-line" rule. This behaviour can be changed using the
* `allow_single_item_single_line_associative_arrays` property.
*/
class ArrayDeclarationSpacingSniff extends Sniff {
/**
* Whether or not to allow single item associative arrays to be single line.
*
* @since 0.14.0
*
* @var bool Defaults to true.
*/
public $allow_single_item_single_line_associative_arrays = true;
/**
* Token this sniff targets.
*
* Also used for distinguishing between the array and an array value
* which is also an array.
*
* @since 0.12.0
*
* @var array
*/
private $targets = array(
\T_ARRAY => \T_ARRAY,
\T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.12.0
*
* @return array
*/
public function register() {
return $this->targets;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.12.0 The actual checks contained in this method used to
* be in the `processSingleLineArray()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr )
) {
// Short list, not short array.
return;
}
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
unset( $array_open_close );
/*
* Long arrays only: Check for space between the array keyword and the open parenthesis.
*/
if ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] ) {
if ( ( $stackPtr + 1 ) !== $opener ) {
$error = 'There must be no space between the "array" keyword and the opening parenthesis';
$error_code = 'SpaceAfterKeyword';
$nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), ( $opener + 1 ), true );
if ( $nextNonWhitespace !== $opener ) {
// Don't auto-fix: Something other than whitespace found between keyword and open parenthesis.
$this->phpcsFile->addError( $error, $stackPtr, $error_code );
} else {
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $stackPtr + 1 ); $i < $opener; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
unset( $i );
}
}
unset( $error, $error_code, $nextNonWhitespace, $fix );
}
}
/*
* Check for empty arrays.
*/
$nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $opener + 1 ), ( $closer + 1 ), true );
if ( $nextNonWhitespace === $closer ) {
if ( ( $opener + 1 ) !== $closer ) {
$fix = $this->phpcsFile->addFixableError(
'Empty array declaration must have no space between the parentheses',
$stackPtr,
'SpaceInEmptyArray'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $opener + 1 ); $i < $closer; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
unset( $i );
}
}
// This array is empty, so the below checks aren't necessary.
return;
}
unset( $nextNonWhitespace );
// Pass off to either the single line or multi-line array analysis.
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
$this->process_single_line_array( $stackPtr, $opener, $closer );
} else {
$this->process_multi_line_array( $stackPtr, $opener, $closer );
}
}
/**
* Process a single-line array.
*
* @since 0.13.0 The actual checks contained in this method used to
* be in the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_single_line_array( $stackPtr, $opener, $closer ) {
/*
* Check that associative arrays are always multi-line.
*/
$array_has_keys = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $opener, $closer );
if ( false !== $array_has_keys ) {
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( ( false === $this->allow_single_item_single_line_associative_arrays
&& ! empty( $array_items ) )
|| ( true === $this->allow_single_item_single_line_associative_arrays
&& \count( $array_items ) > 1 )
) {
/*
* Make sure the double arrow is for *this* array, not for a nested one.
*/
$array_has_keys = false; // Reset before doing more detailed check.
foreach ( $array_items as $item ) {
for ( $ptr = $item['start']; $ptr <= $item['end']; $ptr++ ) {
if ( \T_DOUBLE_ARROW === $this->tokens[ $ptr ]['code'] ) {
$array_has_keys = true;
break 2;
}
// Skip passed any nested arrays.
if ( isset( $this->targets[ $this->tokens[ $ptr ]['code'] ] ) ) {
$nested_array_open_close = $this->find_array_open_close( $ptr );
if ( false === $nested_array_open_close ) {
// Nested array open/close could not be determined.
continue;
}
$ptr = $nested_array_open_close['closer'];
}
}
}
if ( true === $array_has_keys ) {
$phrase = 'an';
if ( true === $this->allow_single_item_single_line_associative_arrays ) {
$phrase = 'a multi-item';
}
$fix = $this->phpcsFile->addFixableError(
'When %s array uses associative keys, each value should start on a new line.',
$closer,
'AssociativeArrayFound',
array( $phrase )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
foreach ( $array_items as $item ) {
/*
* Add a line break before the first non-empty token in the array item.
* Prevents extraneous whitespace at the start of the line which could be
* interpreted as alignment whitespace.
*/
$first_non_empty = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$item['start'],
( $item['end'] + 1 ),
true
);
if ( false === $first_non_empty ) {
continue;
}
if ( $item['start'] <= ( $first_non_empty - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $first_non_empty - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $first_non_empty - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $first_non_empty );
}
$this->phpcsFile->fixer->endChangeset();
}
// No need to check for spacing around opener/closer as this array should be multi-line.
return;
}
}
}
/*
* Check that there is a single space after the array opener and before the array closer.
*/
if ( \T_WHITESPACE !== $this->tokens[ ( $opener + 1 ) ]['code'] ) {
$fix = $this->phpcsFile->addFixableError(
'Missing space after array opener.',
$opener,
'NoSpaceAfterArrayOpener'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $opener, ' ' );
}
} elseif ( ' ' !== $this->tokens[ ( $opener + 1 ) ]['content'] ) {
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space after array opener, found %s.',
$opener,
'SpaceAfterArrayOpener',
array( \strlen( $this->tokens[ ( $opener + 1 ) ]['content'] ) )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $opener + 1 ), ' ' );
}
}
if ( \T_WHITESPACE !== $this->tokens[ ( $closer - 1 ) ]['code'] ) {
$fix = $this->phpcsFile->addFixableError(
'Missing space before array closer.',
$closer,
'NoSpaceBeforeArrayCloser'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContentBefore( $closer, ' ' );
}
} elseif ( ' ' !== $this->tokens[ ( $closer - 1 ) ]['content'] ) {
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space before array closer, found %s.',
$closer,
'SpaceBeforeArrayCloser',
array( \strlen( $this->tokens[ ( $closer - 1 ) ]['content'] ) )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), ' ' );
}
}
}
/**
* Process a multi-line array.
*
* @since 0.13.0 The actual checks contained in this method used to
* be in the `ArrayDeclaration` sniff.
*
* @param int $stackPtr The position of the current token in the stack.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_multi_line_array( $stackPtr, $opener, $closer ) {
/*
* Check that the closing bracket is on a new line.
*/
$last_content = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $closer - 1 ), $opener, true );
if ( false !== $last_content
&& $this->tokens[ $last_content ]['line'] === $this->tokens[ $closer ]['line']
) {
$fix = $this->phpcsFile->addFixableError(
'Closing parenthesis of array declaration must be on a new line',
$closer,
'CloseBraceNewLine'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
if ( $last_content < ( $closer - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $closer );
$this->phpcsFile->fixer->endChangeset();
}
}
/*
* Check that each array item starts on a new line.
*/
$array_items = $this->get_function_call_parameters( $stackPtr );
$end_of_last_item = $opener;
foreach ( $array_items as $item ) {
$end_of_this_item = ( $item['end'] + 1 );
// Find the line on which the item starts.
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$item['start'],
$end_of_this_item,
true
);
// Ignore comments after array items if the next real content starts on a new line.
if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_last_item ]['line']
&& ( \T_COMMENT === $this->tokens[ $first_content ]['code']
|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $first_content ]['code'] ] ) )
) {
$end_of_comment = $first_content;
// Find the end of (multi-line) /* */- style trailing comments.
if ( substr( ltrim( $this->tokens[ $end_of_comment ]['content'] ), 0, 2 ) === '/*' ) {
while ( ( \T_COMMENT === $this->tokens[ $end_of_comment ]['code']
|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $end_of_comment ]['code'] ] ) )
&& substr( rtrim( $this->tokens[ $end_of_comment ]['content'] ), -2 ) !== '*/'
&& ( $end_of_comment + 1 ) < $end_of_this_item
) {
$end_of_comment++;
}
if ( $this->tokens[ $end_of_comment ]['line'] !== $this->tokens[ $end_of_last_item ]['line'] ) {
// Multi-line trailing comment.
$end_of_last_item = $end_of_comment;
}
}
$next = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
( $end_of_comment + 1 ),
$end_of_this_item,
true
);
if ( false === $next ) {
// Shouldn't happen, but just in case.
$end_of_last_item = $end_of_this_item;
continue;
}
if ( $this->tokens[ $next ]['line'] !== $this->tokens[ $first_content ]['line'] ) {
$first_content = $next;
}
}
if ( false === $first_content ) {
// Shouldn't happen, but just in case.
$end_of_last_item = $end_of_this_item;
continue;
}
if ( $this->tokens[ $end_of_last_item ]['line'] === $this->tokens[ $first_content ]['line'] ) {
$fix = $this->phpcsFile->addFixableError(
'Each item in a multi-line array must be on a new line',
$first_content,
'ArrayItemNoNewLine'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
if ( ( $end_of_last_item + 1 ) <= ( $first_content - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $first_content - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $first_content - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $first_content );
$this->phpcsFile->fixer->endChangeset();
}
}
$end_of_last_item = $end_of_this_item;
}
}
}
@@ -0,0 +1,542 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
use WordPressCS\WordPress\PHPCSHelper;
use PHP_CodeSniffer\Util\Tokens;
/**
* Enforces WordPress array indentation for multi-line arrays.
*
* @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.
*
* {@internal This sniff should eventually be pulled upstream as part of a solution
* for https://github.com/squizlabs/PHP_CodeSniffer/issues/582 }}
*/
class ArrayIndentationSniff extends Sniff {
/**
* Should tabs be used for indenting?
*
* If TRUE, fixes will be made using tabs instead of spaces.
* The size of each tab is important, so it should be specified
* using the --tab-width CLI argument.
*
* {@internal While for WPCS this should always be `true`, this property
* was added in anticipation of upstreaming the sniff.
* This property is the same as used in `Generic.WhiteSpace.ScopeIndent`.}}
*
* @var bool
*/
public $tabIndent = true;
/**
* The --tab-width CLI value that is being used.
*
* @var int
*/
private $tab_width;
/**
* Tokens to ignore for subsequent lines in a multi-line array item.
*
* Property is set in the register() method.
*
* @var array
*/
private $ignore_tokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
/*
* Set the $ignore_tokens property.
*
* Existing heredoc, nowdoc and inline HTML indentation should be respected at all times.
*/
$this->ignore_tokens = Tokens::$heredocTokens;
unset( $this->ignore_tokens[ \T_START_HEREDOC ], $this->ignore_tokens[ \T_START_NOWDOC ] );
$this->ignore_tokens[ \T_INLINE_HTML ] = \T_INLINE_HTML;
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 int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
if ( ! isset( $this->tab_width ) ) {
$this->tab_width = PHPCSHelper::get_tab_width( $this->phpcsFile );
}
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr )
) {
// Short list, not short array.
return;
}
/*
* 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'];
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
// Not interested in single line arrays.
return;
}
/*
* Check the closing bracket is lined up with the start of the content on the line
* containing the array opener.
*/
$opener_line_spaces = $this->get_indentation_size( $opener );
$closer_line_spaces = ( $this->tokens[ $closer ]['column'] - 1 );
if ( $closer_line_spaces !== $opener_line_spaces ) {
$error = 'Array closer not aligned correctly; expected %s space(s) but found %s';
$error_code = 'CloseBraceNotAligned';
/*
* Report & fix the issue if the close brace is on its own line with
* nothing or only indentation whitespace before it.
*/
if ( 0 === $closer_line_spaces
|| ( \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
&& 1 === $this->tokens[ ( $closer - 1 ) ]['column'] )
) {
$this->add_array_alignment_error(
$closer,
$error,
$error_code,
$opener_line_spaces,
$closer_line_spaces,
$this->get_indentation_string( $opener_line_spaces )
);
} else {
/*
* Otherwise, only report the error, don't try and fix it (yet).
*
* It will get corrected in a future loop of the fixer once the closer
* has been moved to its own line by the `ArrayDeclarationSpacing` sniff.
*/
$this->phpcsFile->addError(
$error,
$closer,
$error_code,
array( $opener_line_spaces, $closer_line_spaces )
);
}
unset( $error, $error_code );
}
/*
* Verify & correct the array item indentation.
*/
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( empty( $array_items ) ) {
// Strange, no array items found.
return;
}
$expected_spaces = ( $opener_line_spaces + $this->tab_width );
$expected_indent = $this->get_indentation_string( $expected_spaces );
$end_of_previous_item = $opener;
foreach ( $array_items as $item ) {
$end_of_this_item = ( $item['end'] + 1 );
// Find the line on which the item starts.
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$item['start'],
$end_of_this_item,
true
);
// Deal with trailing comments.
if ( false !== $first_content
&& \T_COMMENT === $this->tokens[ $first_content ]['code']
&& $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_previous_item ]['line']
) {
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE, \T_COMMENT ),
( $first_content + 1 ),
$end_of_this_item,
true
);
}
if ( false === $first_content ) {
$end_of_previous_item = $end_of_this_item;
continue;
}
// Bow out from reporting and fixing mixed multi-line/single-line arrays.
// That is handled by the ArrayDeclarationSpacingSniff.
if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_previous_item ]['line']
|| ( 1 !== $this->tokens[ $first_content ]['column']
&& \T_WHITESPACE !== $this->tokens[ ( $first_content - 1 ) ]['code'] )
) {
return $closer;
}
$found_spaces = ( $this->tokens[ $first_content ]['column'] - 1 );
if ( $found_spaces !== $expected_spaces ) {
$this->add_array_alignment_error(
$first_content,
'Array item not aligned correctly; expected %s spaces but found %s',
'ItemNotAligned',
$expected_spaces,
$found_spaces,
$expected_indent
);
}
// No need for further checking if this is a one-line array item.
if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
$end_of_previous_item = $end_of_this_item;
continue;
}
/*
* Multi-line array items.
*
* Verify & if needed, correct the indentation of subsequent lines.
* Subsequent lines may be indented more or less than the mimimum expected indent,
* but the "first line after" should be indented - at least - as much as the very first line
* of the array item.
* Indentation correction for subsequent lines will be based on that diff.
*/
// Find first token on second line of the array item.
// If the second line is a heredoc/nowdoc, continue on until we find a line with a different token.
// Same for the second line of a multi-line text string.
for ( $ptr = ( $first_content + 1 ); $ptr <= $item['end']; $ptr++ ) {
if ( $this->tokens[ $first_content ]['line'] !== $this->tokens[ $ptr ]['line']
&& 1 === $this->tokens[ $ptr ]['column']
&& false === $this->ignore_token( $ptr )
) {
break;
}
}
$first_content_on_line2 = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$ptr,
$end_of_this_item,
true
);
if ( false === $first_content_on_line2 ) {
/*
* Apparently there were only tokens in the ignore list on subsequent lines.
*
* In that case, the comma after the array item might be on a line by itself,
* so check its placement.
*/
if ( $this->tokens[ $item['end'] ]['line'] !== $this->tokens[ $end_of_this_item ]['line']
&& \T_COMMA === $this->tokens[ $end_of_this_item ]['code']
&& ( $this->tokens[ $end_of_this_item ]['column'] - 1 ) !== $expected_spaces
) {
$this->add_array_alignment_error(
$end_of_this_item,
'Comma after multi-line array item not aligned correctly; expected %s spaces, but found %s',
'MultiLineArrayItemCommaNotAligned',
$expected_spaces,
( $this->tokens[ $end_of_this_item ]['column'] - 1 ),
$expected_indent
);
}
$end_of_previous_item = $end_of_this_item;
continue;
}
$found_spaces_on_line2 = $this->get_indentation_size( $first_content_on_line2 );
$expected_spaces_on_line2 = $expected_spaces;
if ( $found_spaces < $found_spaces_on_line2 ) {
$expected_spaces_on_line2 += ( $found_spaces_on_line2 - $found_spaces );
}
if ( $found_spaces_on_line2 !== $expected_spaces_on_line2 ) {
$fix = $this->phpcsFile->addFixableError(
'Multi-line array item not aligned correctly; expected %s spaces, but found %s',
$first_content_on_line2,
'MultiLineArrayItemNotAligned',
array(
$expected_spaces_on_line2,
$found_spaces_on_line2,
)
);
if ( true === $fix ) {
$expected_indent_on_line2 = $this->get_indentation_string( $expected_spaces_on_line2 );
$this->phpcsFile->fixer->beginChangeset();
// Fix second line for the array item.
if ( 1 === $this->tokens[ $first_content_on_line2 ]['column']
&& \T_COMMENT === $this->tokens[ $first_content_on_line2 ]['code']
) {
$actual_comment = ltrim( $this->tokens[ $first_content_on_line2 ]['content'] );
$replacement = $expected_indent_on_line2 . $actual_comment;
$this->phpcsFile->fixer->replaceToken( $first_content_on_line2, $replacement );
} else {
$this->fix_alignment_error( $first_content_on_line2, $expected_indent_on_line2 );
}
// Fix subsequent lines.
for ( $i = ( $first_content_on_line2 + 1 ); $i <= $item['end']; $i++ ) {
// We're only interested in the first token on each line.
if ( 1 !== $this->tokens[ $i ]['column'] ) {
if ( $this->tokens[ $i ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
// We might as well quit if we're past the first token on the last line.
break;
}
continue;
}
$first_content_on_line = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$i,
$end_of_this_item,
true
);
if ( false === $first_content_on_line ) {
break;
}
// Ignore lines with heredoc and nowdoc tokens and subsequent lines in multi-line strings.
if ( true === $this->ignore_token( $first_content_on_line ) ) {
$i = $first_content_on_line;
continue;
}
$found_spaces_on_line = $this->get_indentation_size( $first_content_on_line );
$expected_spaces_on_line = ( $expected_spaces_on_line2 + ( $found_spaces_on_line - $found_spaces_on_line2 ) );
$expected_spaces_on_line = max( $expected_spaces_on_line, 0 ); // Can't be below 0.
$expected_indent_on_line = $this->get_indentation_string( $expected_spaces_on_line );
if ( $found_spaces_on_line !== $expected_spaces_on_line ) {
if ( 1 === $this->tokens[ $first_content_on_line ]['column']
&& \T_COMMENT === $this->tokens[ $first_content_on_line ]['code']
) {
$actual_comment = ltrim( $this->tokens[ $first_content_on_line ]['content'] );
$replacement = $expected_indent_on_line . $actual_comment;
$this->phpcsFile->fixer->replaceToken( $first_content_on_line, $replacement );
} else {
$this->fix_alignment_error( $first_content_on_line, $expected_indent_on_line );
}
}
// Move past any potential empty lines between the previous non-empty line and this one.
// No need to do the fixes twice.
$i = $first_content_on_line;
}
/*
* Check the placement of the comma after the array item as it might be on a line by itself.
*/
if ( $this->tokens[ $item['end'] ]['line'] !== $this->tokens[ $end_of_this_item ]['line']
&& \T_COMMA === $this->tokens[ $end_of_this_item ]['code']
&& ( $this->tokens[ $end_of_this_item ]['column'] - 1 ) !== $expected_spaces
) {
$this->add_array_alignment_error(
$end_of_this_item,
'Comma after array item not aligned correctly; expected %s spaces, but found %s',
'MultiLineArrayItemCommaNotAligned',
$expected_spaces,
( $this->tokens[ $end_of_this_item ]['column'] - 1 ),
$expected_indent
);
}
$this->phpcsFile->fixer->endChangeset();
}
}
$end_of_previous_item = $end_of_this_item;
}
}
/**
* Should the token be ignored ?
*
* This method is only intended to be used with the first token on a line
* for subsequent lines in an multi-line array item.
*
* @param int $ptr Stack pointer to the first token on a line.
*
* @return bool
*/
protected function ignore_token( $ptr ) {
$token_code = $this->tokens[ $ptr ]['code'];
if ( isset( $this->ignore_tokens[ $token_code ] ) ) {
return true;
}
/*
* If it's a subsequent line of a multi-line sting, it will not start with a quote
* character, nor just *be* a quote character.
*/
if ( \T_CONSTANT_ENCAPSED_STRING === $token_code
|| \T_DOUBLE_QUOTED_STRING === $token_code
) {
// Deal with closing quote of a multi-line string being on its own line.
if ( "'" === $this->tokens[ $ptr ]['content']
|| '"' === $this->tokens[ $ptr ]['content']
) {
return true;
}
// Deal with subsequent lines of a multi-line string where the token is broken up per line.
if ( "'" !== $this->tokens[ $ptr ]['content'][0]
&& '"' !== $this->tokens[ $ptr ]['content'][0]
) {
return true;
}
}
return false;
}
/**
* Determine the line indentation whitespace.
*
* @param int $ptr Stack pointer to an arbitrary token on a line.
*
* @return int Nr of spaces found. Where necessary, tabs are translated to spaces.
*/
protected function get_indentation_size( $ptr ) {
// Find the first token on the line.
for ( ; $ptr >= 0; $ptr-- ) {
if ( 1 === $this->tokens[ $ptr ]['column'] ) {
break;
}
}
$whitespace = '';
if ( \T_WHITESPACE === $this->tokens[ $ptr ]['code']
|| \T_DOC_COMMENT_WHITESPACE === $this->tokens[ $ptr ]['code']
) {
return $this->tokens[ $ptr ]['length'];
}
/*
* Special case for multi-line, non-docblock comments.
* Only applicable for subsequent lines in an array item.
*
* First/Single line is tokenized as T_WHITESPACE + T_COMMENT
* Subsequent lines are tokenized as T_COMMENT including the indentation whitespace.
*/
if ( \T_COMMENT === $this->tokens[ $ptr ]['code'] ) {
$content = $this->tokens[ $ptr ]['content'];
$actual_comment = ltrim( $content );
$whitespace = str_replace( $actual_comment, '', $content );
}
return \strlen( $whitespace );
}
/**
* Create an indentation string.
*
* @param int $nr Number of spaces the indentation should be.
*
* @return string
*/
protected function get_indentation_string( $nr ) {
if ( 0 >= $nr ) {
return '';
}
// Space-based indentation.
if ( false === $this->tabIndent ) {
return str_repeat( ' ', $nr );
}
// Tab-based indentation.
$num_tabs = (int) floor( $nr / $this->tab_width );
$remaining = ( $nr % $this->tab_width );
$tab_indent = str_repeat( "\t", $num_tabs );
$tab_indent .= str_repeat( ' ', $remaining );
return $tab_indent;
}
/**
* Throw an error and fix incorrect array alignment.
*
* @param int $ptr Stack pointer to the first content on the line.
* @param string $error Error message.
* @param string $error_code Error code.
* @param int $expected Expected nr of spaces (tabs translated to space value).
* @param int $found Found nr of spaces (tabs translated to space value).
* @param string $new_indent Whitespace indent replacement content.
*/
protected function add_array_alignment_error( $ptr, $error, $error_code, $expected, $found, $new_indent ) {
$fix = $this->phpcsFile->addFixableError( $error, $ptr, $error_code, array( $expected, $found ) );
if ( true === $fix ) {
$this->fix_alignment_error( $ptr, $new_indent );
}
}
/**
* Fix incorrect array alignment.
*
* @param int $ptr Stack pointer to the first content on the line.
* @param string $new_indent Whitespace indent replacement content.
*/
protected function fix_alignment_error( $ptr, $new_indent ) {
if ( 1 === $this->tokens[ $ptr ]['column'] ) {
$this->phpcsFile->fixer->addContentBefore( $ptr, $new_indent );
} else {
$this->phpcsFile->fixer->replaceToken( ( $ptr - 1 ), $new_indent );
}
}
}
@@ -0,0 +1,192 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* 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.
* @since 2.2.0 The sniff now also checks the size of the spacing, if applicable.
*/
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
&& ( false === $spaced1 || false === $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->beginChangeset();
$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), '' );
for ( $i = ( $stackPtr + 2 ); $i < $token['bracket_closer']; $i++ ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
if ( $spaced2 ) {
$this->phpcsFile->fixer->beginChangeset();
$this->phpcsFile->fixer->replaceToken( ( $token['bracket_closer'] - 1 ), '' );
for ( $i = ( $token['bracket_closer'] - 2 ); $i > $stackPtr; $i-- ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
}
}
// If spaces are needed, check that there is only one space.
if ( false !== $need_spaces && ( $spaced1 || $spaced2 ) ) {
if ( $spaced1 ) {
$ptr = ( $stackPtr + 1 );
$length = 0;
if ( $this->tokens[ $ptr ]['line'] !== $this->tokens[ ( $ptr + 1 ) ]['line'] ) {
$length = 'newline';
} else {
$length = $this->tokens[ $ptr ]['length'];
}
if ( 1 !== $length ) {
$error = 'There should be exactly one space before the array key. Found: %s';
$data = array( $length );
$fix = $this->phpcsFile->addFixableError(
$error,
$ptr,
'TooMuchSpaceBeforeKey',
$data
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
$this->phpcsFile->fixer->replaceToken( $ptr, ' ' );
for ( $i = ( $ptr + 1 ); $i < $token['bracket_closer']; $i++ ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
}
}
if ( $spaced2 ) {
$prev_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $token['bracket_closer'] - 1 ), null, true );
$ptr = ( $prev_non_empty + 1 );
$length = 0;
if ( $this->tokens[ $ptr ]['line'] !== $this->tokens[ $token['bracket_closer'] ]['line'] ) {
$length = 'newline';
} else {
$length = $this->tokens[ $ptr ]['length'];
}
if ( 1 !== $length ) {
$error = 'There should be exactly one space after the array key. Found: %s';
$data = array( $length );
$fix = $this->phpcsFile->addFixableError(
$error,
$ptr,
'TooMuchSpaceAfterKey',
$data
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
$this->phpcsFile->fixer->replaceToken( $ptr, ' ' );
for ( $i = ( $ptr + 1 ); $i < $token['bracket_closer']; $i++ ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
}
}
}
}
}
@@ -0,0 +1,313 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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 ) {
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr )
) {
// Short list, not short array.
return;
}
/*
* 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 ) {
$this->phpcsFile->recordMetric(
$stackPtr,
'Single line array - comma after last item',
( true === $is_comma ? 'yes' : 'no' )
);
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 === $single_line && $item_index === $array_item_count ) {
$this->phpcsFile->recordMetric(
$stackPtr,
'Multi-line array - comma after last item',
( true === $is_comma ? 'yes' : 'no' )
);
}
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, ' ' );
}
}
}
}
}
@@ -0,0 +1,617 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Arrays;
use WordPressCS\WordPress\Sniff;
/**
* Enforces alignment of the double arrow assignment operator for multi-item, multi-line arrays.
*
* - Align the double arrow operator to the same column for each item in a multi-item array.
* - Allows for setting a maxColumn property to aid in managing line-length.
* - Allows for new line(s) before a double arrow (configurable).
* - Allows for handling multi-line array items differently if so desired (configurable).
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*
* {@internal This sniff should eventually be pulled upstream as part of a solution
* for https://github.com/squizlabs/PHP_CodeSniffer/issues/582 }}
*/
class MultipleStatementAlignmentSniff extends Sniff {
/**
* Whether or not to ignore an array item for the purpose of alignment
* when a new line is found between the array key and the double arrow.
*
* @since 0.14.0
*
* @var bool
*/
public $ignoreNewlines = true;
/**
* Whether the alignment should be exact.
*
* Exact in this context means "largest index key + 1 space".
* When `false`, that is seen as the minimum alignment.
*
* @since 0.14.0
*
* @var bool
*/
public $exact = true;
/**
* The maximum column on which the double arrow alignment should be set.
*
* This property allows for limiting the whitespace padding to prevent
* overly long lines.
*
* If this value is set to, for instance, 60, it will:
* - if the expected column < 60, align at the expected column.
* - if the expected column >= 60, align at column 60.
* - for the outliers, i.e. the array indexes where the end position
* goes past column 60, it will not align the arrow, the sniff will
* just make sure there is only one space between the end of the
* array index and the double arrow.
*
* The column value is regarded as a hard value, i.e. includes indentation,
* so setting it very low is not a good idea.
*
* @since 0.14.0
*
* @var int
*/
public $maxColumn = 1000;
/**
* Whether or not to align the arrow operator for multi-line array items.
*
* Whether or not an item is regarded as multi-line is based on the **value**
* of the item, not the key.
*
* Valid values are:
* - 'always': Default. Align all arrays items regardless of single/multi-line.
* - 'never': Never align array items which span multiple lines.
* This will enforce one space between the array index and the
* double arrow operator for multi-line array items, independently
* of the alignment of the rest of the array items.
* Multi-line items where the arrow is already aligned with the
* "expected" alignment, however, will be left alone.
* - operator : Only align the operator for multi-line arrays items if the
* + number percentage of multi-line items passes the comparison.
* - As it is a percentage, the number has to be between 0 and 100.
* - Supported operators: <, <=, >, >=, ==, =, !=, <>
* - The percentage is calculated against all array items
* (with and without assignment operator).
* - The (new) expected alignment will be calculated based only
* on the items being aligned.
* - Multi-line items where the arrow is already aligned with the
* (new) "expected" alignment, however, will be left alone.
* Examples:
* * Setting this to `!=100` or `<100` means that alignment will
* be enforced, unless *all* array items are multi-line.
* This is probably the most commonly desired situation.
* * Setting this to `=100` means that alignment will only
* be enforced, if *all* array items are multi-line.
* * Setting this to `<50` means that the majority of array items
* need to be single line before alignment is enforced for
* multi-line items in the array.
* * Setting this to `=0` is useless as in that case there are
* no multi-line items in the array anyway.
*
* This setting will respect the `ignoreNewlines` and `maxColumnn` settings.
*
* @since 0.14.0
*
* @var string|int
*/
public $alignMultilineItems = 'always';
/**
* Storage for parsed $alignMultilineItems operator part.
*
* @since 0.14.0
*
* @var string
*/
private $operator;
/**
* Storage for parsed $alignMultilineItems numeric part.
*
* Stored as a string as the comparison will be done string based.
*
* @since 0.14.0
*
* @var string
*/
private $number;
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.14.0
*
* @return array
*/
public function register() {
return array(
\T_ARRAY,
\T_OPEN_SHORT_ARRAY,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code']
&& $this->is_short_list( $stackPtr )
) {
// Short list, not short array.
return;
}
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( empty( $array_items ) ) {
return;
}
// Pass off to either the single line or multi-line array analysis.
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
return $this->process_single_line_array( $stackPtr, $array_items, $opener, $closer );
} else {
return $this->process_multi_line_array( $stackPtr, $array_items, $opener, $closer );
}
}
/**
* Process a single-line array.
*
* While the WP standard does not allow single line multi-item associative arrays,
* this sniff should function independently of that.
*
* The `WordPress.WhiteSpace.OperatorSpacing` sniff already covers checking that
* there is a space between the array key and the double arrow, but doesn't
* enforce it to be exactly one space for single line arrays.
* That is what this method covers.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $items Info array containing information on each array item.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
protected function process_single_line_array( $stackPtr, $items, $opener, $closer ) {
/*
* For single line arrays, we don't care about what level the arrow is from.
* Just find and fix them all.
*/
$next_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
( $opener + 1 ),
$closer
);
while ( false !== $next_arrow ) {
if ( \T_WHITESPACE === $this->tokens[ ( $next_arrow - 1 ) ]['code'] ) {
$space_length = $this->tokens[ ( $next_arrow - 1 ) ]['length'];
if ( 1 !== $space_length ) {
$error = 'Expected 1 space between "%s" and double arrow; %s found';
$data = array(
$this->tokens[ ( $next_arrow - 2 ) ]['content'],
$space_length,
);
$fix = $this->phpcsFile->addFixableWarning( $error, $next_arrow, 'SpaceBeforeDoubleArrow', $data );
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $next_arrow - 1 ), ' ' );
}
}
}
// Find the position of the next double arrow.
$next_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
( $next_arrow + 1 ),
$closer
);
}
// Ignore any child-arrays as the double arrows in these will already have been handled.
return ( $closer + 1 );
}
/**
* Process a multi-line array.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $items Info array containing information on each array item.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_multi_line_array( $stackPtr, $items, $opener, $closer ) {
$this->maxColumn = (int) $this->maxColumn;
$this->validate_align_multiline_items();
/*
* Determine what the spacing before the arrow should be.
*
* Will unset any array items without double arrow and with new line whitespace
* if newlines are to be ignored, so the second foreach loop only has to deal
* with items which need attention.
*
* This sniff does not take incorrect indentation of array keys into account.
* That's for the `WordPress.Arrays.ArrayIndentation` sniff to fix.
* If that would affect the alignment, a second (or third) loop of the fixer
* will correct it (again) after the indentation has been fixed.
*/
$index_end_cols = array(); // Keep track of the end column position of index keys.
$double_arrow_cols = array(); // Keep track of arrow column position and count.
$multi_line_count = 0;
$total_items = \count( $items );
foreach ( $items as $key => $item ) {
if ( strpos( $item['raw'], '=>' ) === false ) {
// Ignore items without assignment operators.
unset( $items[ $key ] );
continue;
}
// Find the position of the first double arrow.
$double_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
$item['start'],
( $item['end'] + 1 )
);
if ( false === $double_arrow ) {
// Shouldn't happen, just in case.
unset( $items[ $key ] );
continue;
}
// Make sure the arrow is for this item and not for a nested array value assignment.
$has_array_opener = $this->phpcsFile->findNext(
$this->register(),
$item['start'],
$double_arrow
);
if ( false !== $has_array_opener ) {
// Double arrow is for a nested array.
unset( $items[ $key ] );
continue;
}
// Find the end of the array key.
$last_index_token = $this->phpcsFile->findPrevious(
\T_WHITESPACE,
( $double_arrow - 1 ),
$item['start'],
true
);
if ( false === $last_index_token ) {
// Shouldn't happen, but just in case.
unset( $items[ $key ] );
continue;
}
if ( true === $this->ignoreNewlines
&& $this->tokens[ $last_index_token ]['line'] !== $this->tokens[ $double_arrow ]['line']
) {
// Ignore this item as it has a new line between the item key and the double arrow.
unset( $items[ $key ] );
continue;
}
$index_end_position = ( $this->tokens[ $last_index_token ]['column'] + ( $this->tokens[ $last_index_token ]['length'] - 1 ) );
$items[ $key ]['operatorPtr'] = $double_arrow;
$items[ $key ]['last_index_token'] = $last_index_token;
$items[ $key ]['last_index_col'] = $index_end_position;
if ( $this->tokens[ $last_index_token ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
$items[ $key ]['single_line'] = true;
} else {
$items[ $key ]['single_line'] = false;
$multi_line_count++;
}
if ( ( $index_end_position + 2 ) <= $this->maxColumn ) {
$index_end_cols[] = $index_end_position;
}
if ( ! isset( $double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] ) ) {
$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] = 1;
} else {
$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ]++;
}
}
unset( $key, $item, $double_arrow, $has_array_opener, $last_index_token );
if ( empty( $items ) || empty( $index_end_cols ) ) {
// No actionable array items found.
return;
}
/*
* Determine whether the operators for multi-line items should be aligned.
*/
if ( 'always' === $this->alignMultilineItems ) {
$alignMultilineItems = true;
} elseif ( 'never' === $this->alignMultilineItems ) {
$alignMultilineItems = false;
} else {
$percentage = (string) round( ( $multi_line_count / $total_items ) * 100, 0 );
// Bit hacky, but this is the only comparison function in PHP which allows to
// pass the comparison operator. And hey, it works ;-).
$alignMultilineItems = version_compare( $percentage, $this->number, $this->operator );
}
/*
* If necessary, rebuild the $index_end_cols and $double_arrow_cols arrays
* excluding multi-line items.
*/
if ( false === $alignMultilineItems ) {
$select_index_end_cols = array();
$double_arrow_cols = array();
foreach ( $items as $item ) {
if ( false === $item['single_line'] ) {
continue;
}
if ( ( $item['last_index_col'] + 2 ) <= $this->maxColumn ) {
$select_index_end_cols[] = $item['last_index_col'];
}
if ( ! isset( $double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] ) ) {
$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] = 1;
} else {
$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ]++;
}
}
}
/*
* Determine the expected position of the double arrows.
*/
if ( ! empty( $select_index_end_cols ) ) {
$max_index_width = max( $select_index_end_cols );
} else {
$max_index_width = max( $index_end_cols );
}
$expected_col = ( $max_index_width + 2 );
if ( false === $this->exact && ! empty( $double_arrow_cols ) ) {
/*
* If the alignment does not have to be exact, see if a majority
* group of the arrows is already at an acceptable position.
*/
arsort( $double_arrow_cols, \SORT_NUMERIC );
reset( $double_arrow_cols );
$count = current( $double_arrow_cols );
if ( $count > 1 || ( 1 === $count && \count( $items ) === 1 ) ) {
// Allow for several groups of arrows having the same $count.
$filtered_double_arrow_cols = array_keys( $double_arrow_cols, $count, true );
foreach ( $filtered_double_arrow_cols as $col ) {
if ( $col > $expected_col && $col <= $this->maxColumn ) {
$expected_col = $col;
break;
}
}
}
}
unset( $max_index_width, $count, $filtered_double_arrow_cols, $col );
/*
* Verify and correct the spacing around the double arrows.
*/
foreach ( $items as $item ) {
if ( $this->tokens[ $item['operatorPtr'] ]['column'] === $expected_col
&& $this->tokens[ $item['operatorPtr'] ]['line'] === $this->tokens[ $item['last_index_token'] ]['line']
) {
// Already correctly aligned.
continue;
}
if ( \T_WHITESPACE !== $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['code'] ) {
$before = 0;
} else {
if ( $this->tokens[ $item['last_index_token'] ]['line'] !== $this->tokens[ $item['operatorPtr'] ]['line'] ) {
$before = 'newline';
} else {
$before = $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['length'];
}
}
/*
* Deal with index sizes larger than maxColumn and with multi-line
* array items which should not be aligned.
*/
if ( ( $item['last_index_col'] + 2 ) > $this->maxColumn
|| ( false === $alignMultilineItems && false === $item['single_line'] )
) {
if ( ( $item['last_index_col'] + 2 ) === $this->tokens[ $item['operatorPtr'] ]['column']
&& $this->tokens[ $item['operatorPtr'] ]['line'] === $this->tokens[ $item['last_index_token'] ]['line']
) {
// MaxColumn/Multi-line item exception, already correctly aligned.
continue;
}
$prefix = 'LongIndex';
if ( false === $alignMultilineItems && false === $item['single_line'] ) {
$prefix = 'MultilineItem';
}
$error_code = $prefix . 'SpaceBeforeDoubleArrow';
if ( 0 === $before ) {
$error_code = $prefix . 'NoSpaceBeforeDoubleArrow';
}
$fix = $this->phpcsFile->addFixableWarning(
'Expected 1 space between "%s" and double arrow; %s found.',
$item['operatorPtr'],
$error_code,
array(
$this->tokens[ $item['last_index_token'] ]['content'],
$before,
)
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
// Remove whitespace tokens between the end of the index and the arrow, if any.
for ( $i = ( $item['last_index_token'] + 1 ); $i < $item['operatorPtr']; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
// Add the correct whitespace.
$this->phpcsFile->fixer->addContent( $item['last_index_token'], ' ' );
$this->phpcsFile->fixer->endChangeset();
}
continue;
}
/*
* Deal with the space before double arrows in all other cases.
*/
$expected_whitespace = $expected_col - ( $this->tokens[ $item['last_index_token'] ]['column'] + $this->tokens[ $item['last_index_token'] ]['length'] );
$fix = $this->phpcsFile->addFixableWarning(
'Array double arrow not aligned correctly; expected %s space(s) between "%s" and double arrow, but found %s.',
$item['operatorPtr'],
'DoubleArrowNotAligned',
array(
$expected_whitespace,
$this->tokens[ $item['last_index_token'] ]['content'],
$before,
)
);
if ( true === $fix ) {
if ( 0 === $before || 'newline' === $before ) {
$this->phpcsFile->fixer->beginChangeset();
// Remove whitespace tokens between the end of the index and the arrow, if any.
for ( $i = ( $item['last_index_token'] + 1 ); $i < $item['operatorPtr']; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
// Add the correct whitespace.
$this->phpcsFile->fixer->addContent(
$item['last_index_token'],
str_repeat( ' ', $expected_whitespace )
);
$this->phpcsFile->fixer->endChangeset();
} elseif ( $expected_whitespace > $before ) {
// Add to the existing whitespace to prevent replacing tabs with spaces.
// That's the concern of another sniff.
$this->phpcsFile->fixer->addContent(
( $item['operatorPtr'] - 1 ),
str_repeat( ' ', ( $expected_whitespace - $before ) )
);
} else {
// Too much whitespace found.
$this->phpcsFile->fixer->replaceToken(
( $item['operatorPtr'] - 1 ),
str_repeat( ' ', $expected_whitespace )
);
}
}
}
}
/**
* Validate that a valid value has been received for the alignMultilineItems property.
*
* This message may be thrown more than once if the property is being changed inline in a file.
*
* @since 0.14.0
*/
protected function validate_align_multiline_items() {
$alignMultilineItems = $this->alignMultilineItems;
if ( 'always' === $alignMultilineItems || 'never' === $alignMultilineItems ) {
return;
} else {
// Correct for a potentially added % sign.
$alignMultilineItems = rtrim( $alignMultilineItems, '%' );
if ( preg_match( '`^([=<>!]{1,2})(100|[0-9]{1,2})$`', $alignMultilineItems, $matches ) > 0 ) {
$operator = $matches[1];
$number = (int) $matches[2];
if ( \in_array( $operator, array( '<', '<=', '>', '>=', '==', '=', '!=', '<>' ), true ) === true
&& ( $number >= 0 && $number <= 100 )
) {
$this->alignMultilineItems = $alignMultilineItems;
$this->number = (string) $number;
$this->operator = $operator;
return;
}
}
}
$this->phpcsFile->addError(
'Invalid property value passed: "%s". The value for the "alignMultilineItems" property for the "WordPress.Arrays.MultipleStatementAlignment" sniff should be either "always", "never" or an comparison operator + a number between 0 and 100.',
0,
'InvalidPropertyPassed',
array( $this->alignMultilineItems )
);
// Reset to the default if an invalid value was received.
$this->alignMultilineItems = 'always';
}
}
@@ -0,0 +1,204 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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 );
}
}
}
}
@@ -0,0 +1,235 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\CodeAnalysis;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Detects variable assignments being made within conditions.
*
* This is a typical code smell and more often than not a comparison was intended.
*
* Note: this sniff does not detect variable assignments in ternaries without parentheses!
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*
* {@internal This sniff is a duplicate of the same sniff as pulled upstream.
* Once the upstream sniff has been merged and the minimum WPCS PHPCS requirement has gone up to
* the version in which the sniff was merged, this version can be safely removed.
* {@link https://github.com/squizlabs/PHP_CodeSniffer/pull/1594} }}
*/
class AssignmentInConditionSniff extends Sniff {
/**
* Assignment tokens to trigger on.
*
* Set in the register() method.
*
* @since 0.14.0
*
* @var array
*/
protected $assignment_tokens = array();
/**
* The tokens that indicate the start of a condition.
*
* @since 0.14.0
*
* @var array
*/
protected $condition_start_tokens = array();
/**
* Registers the tokens that this sniff wants to listen for.
*
* @since 0.14.0
*
* @return array
*/
public function register() {
$this->assignment_tokens = Tokens::$assignmentTokens;
unset( $this->assignment_tokens[ \T_DOUBLE_ARROW ] );
$starters = Tokens::$booleanOperators;
$starters[ \T_SEMICOLON ] = \T_SEMICOLON;
$starters[ \T_OPEN_PARENTHESIS ] = \T_OPEN_PARENTHESIS;
$starters[ \T_INLINE_ELSE ] = \T_INLINE_ELSE;
$this->condition_start_tokens = $starters;
return array(
\T_IF,
\T_ELSEIF,
\T_FOR,
\T_SWITCH,
\T_CASE,
\T_WHILE,
\T_INLINE_THEN,
);
}
/**
* 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 ) {
$token = $this->tokens[ $stackPtr ];
// Find the condition opener/closer.
if ( \T_FOR === $token['code'] ) {
if ( isset( $token['parenthesis_opener'], $token['parenthesis_closer'] ) === false ) {
return;
}
$semicolon = $this->phpcsFile->findNext( \T_SEMICOLON, ( $token['parenthesis_opener'] + 1 ), $token['parenthesis_closer'] );
if ( false === $semicolon ) {
return;
}
$opener = $semicolon;
$semicolon = $this->phpcsFile->findNext( \T_SEMICOLON, ( $opener + 1 ), $token['parenthesis_closer'] );
if ( false === $semicolon ) {
return;
}
$closer = $semicolon;
unset( $semicolon );
} elseif ( \T_CASE === $token['code'] ) {
if ( isset( $token['scope_opener'] ) === false ) {
return;
}
$opener = $stackPtr;
$closer = $token['scope_opener'];
} elseif ( \T_INLINE_THEN === $token['code'] ) {
// Check if the condition for the ternary is bracketed.
$prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
if ( false === $prev ) {
// Shouldn't happen, but in that case we don't have anything to examine anyway.
return;
}
if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $prev ]['code'] ) {
if ( ! isset( $this->tokens[ $prev ]['parenthesis_opener'] ) ) {
return;
}
$opener = $this->tokens[ $prev ]['parenthesis_opener'];
$closer = $prev;
} elseif ( isset( $token['nested_parenthesis'] ) ) {
$closer = end( $token['nested_parenthesis'] );
$opener = key( $token['nested_parenthesis'] );
$next_statement_closer = $this->phpcsFile->findEndOfStatement( $stackPtr, array( \T_COLON, \T_CLOSE_PARENTHESIS, \T_CLOSE_SQUARE_BRACKET ) );
if ( false !== $next_statement_closer && $next_statement_closer < $closer ) {
// Parentheses are unrelated to the ternary.
return;
}
$prev_statement_closer = $this->phpcsFile->findStartOfStatement( $stackPtr, array( \T_COLON, \T_OPEN_PARENTHESIS, \T_OPEN_SQUARE_BRACKET ) );
if ( false !== $prev_statement_closer && $opener < $prev_statement_closer ) {
// Parentheses are unrelated to the ternary.
return;
}
if ( $closer > $stackPtr ) {
$closer = $stackPtr;
}
} else {
// No parenthesis found, can't determine where the conditional part of the ternary starts.
return;
}
} else {
if ( isset( $token['parenthesis_opener'], $token['parenthesis_closer'] ) === false ) {
return;
}
$opener = $token['parenthesis_opener'];
$closer = $token['parenthesis_closer'];
}
$startPos = $opener;
do {
$hasAssignment = $this->phpcsFile->findNext( $this->assignment_tokens, ( $startPos + 1 ), $closer );
if ( false === $hasAssignment ) {
return;
}
// Examine whether the left side is a variable.
$hasVariable = false;
$conditionStart = $startPos;
$altConditionStart = $this->phpcsFile->findPrevious(
$this->condition_start_tokens,
( $hasAssignment - 1 ),
$startPos
);
if ( false !== $altConditionStart ) {
$conditionStart = $altConditionStart;
}
for ( $i = $hasAssignment; $i > $conditionStart; $i-- ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
// If this is a variable or array, we've seen all we need to see.
if ( \T_VARIABLE === $this->tokens[ $i ]['code']
|| \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $i ]['code']
) {
$hasVariable = true;
break;
}
// If this is a function call or something, we are OK.
if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $i ]['code'] ) {
break;
}
}
if ( true === $hasVariable ) {
$errorCode = 'Found';
if ( \T_WHILE === $token['code'] ) {
$errorCode = 'FoundInWhileCondition';
} elseif ( \T_INLINE_THEN === $token['code'] ) {
$errorCode = 'FoundInTernaryCondition';
}
$this->phpcsFile->addWarning(
'Variable assignment found within a condition. Did you mean to do a comparison?',
$hasAssignment,
$errorCode
);
} else {
$this->phpcsFile->addWarning(
'Assignment found within a condition. Did you mean to do a comparison?',
$hasAssignment,
'NonVariableAssignmentFound'
);
}
$startPos = $hasAssignment;
} while ( $startPos < $closer );
}
}
@@ -0,0 +1,161 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\CodeAnalysis;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Checks against empty statements.
*
* - Check against two semi-colons with no executable code in between.
* - Check against an empty PHP open - close tag combination.
*
* {@internal This check should at some point in the future be pulled upstream and probably
* merged into the upstream `Generic.CodeAnalysis.EmptyStatement` sniff.
* This will need to wait until the WPCS minimum requirements have gone up
* beyond PHPCS 3.x though as it is not likely that new features will be accepted
* still for the PHPCS 2.x branch.}}
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class EmptyStatementSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_SEMICOLON,
\T_CLOSE_TAG,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
switch ( $this->tokens[ $stackPtr ]['type'] ) {
/*
* Detect `something();;`.
*/
case 'T_SEMICOLON':
$prevNonEmpty = $this->phpcsFile->findPrevious(
Tokens::$emptyTokens,
( $stackPtr - 1 ),
null,
true
);
if ( false === $prevNonEmpty
|| ( \T_SEMICOLON !== $this->tokens[ $prevNonEmpty ]['code']
&& \T_OPEN_TAG !== $this->tokens[ $prevNonEmpty ]['code']
&& \T_OPEN_TAG_WITH_ECHO !== $this->tokens[ $prevNonEmpty ]['code'] )
) {
return;
}
if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
$nested = $this->tokens[ $stackPtr ]['nested_parenthesis'];
$last_closer = array_pop( $nested );
if ( isset( $this->tokens[ $last_closer ]['parenthesis_owner'] )
&& \T_FOR === $this->tokens[ $this->tokens[ $last_closer ]['parenthesis_owner'] ]['code']
) {
// Empty for() condition.
return;
}
}
$fix = $this->phpcsFile->addFixableWarning(
'Empty PHP statement detected: superfluous semi-colon.',
$stackPtr,
'SemicolonWithoutCodeDetected'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
if ( \T_OPEN_TAG === $this->tokens[ $prevNonEmpty ]['code']
|| \T_OPEN_TAG_WITH_ECHO === $this->tokens[ $prevNonEmpty ]['code']
) {
/*
* Check for superfluous whitespace after the semi-colon which will be
* removed as the `<?php ` open tag token already contains whitespace,
* either a space or a new line and in case of a new line, the indentation
* should be done via tabs, so spaces can be safely removed.
*/
if ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] ) {
$replacement = str_replace( ' ', '', $this->tokens[ ( $stackPtr + 1 ) ]['content'] );
$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), $replacement );
}
}
for ( $i = $stackPtr; $i > $prevNonEmpty; $i-- ) {
if ( \T_SEMICOLON !== $this->tokens[ $i ]['code']
&& \T_WHITESPACE !== $this->tokens[ $i ]['code']
) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
break;
/*
* Detect `<?php ?>`.
*/
case 'T_CLOSE_TAG':
$prevNonEmpty = $this->phpcsFile->findPrevious(
\T_WHITESPACE,
( $stackPtr - 1 ),
null,
true
);
if ( false === $prevNonEmpty
|| ( \T_OPEN_TAG !== $this->tokens[ $prevNonEmpty ]['code']
&& \T_OPEN_TAG_WITH_ECHO !== $this->tokens[ $prevNonEmpty ]['code'] )
) {
return;
}
$fix = $this->phpcsFile->addFixableWarning(
'Empty PHP open/close tag combination detected.',
$prevNonEmpty,
'EmptyPHPOpenCloseTagsDetected'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = $prevNonEmpty; $i <= $stackPtr; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
break;
default:
/* Deliberately left empty. */
break;
}
}
}
@@ -0,0 +1,90 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\CodeAnalysis;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Flag calls to escaping functions which look like they may have been intended
* as calls to the "translate + escape" sister-function due to the presence of
* more than one parameter.
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class EscapedNotTranslatedSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 2.2.0
*
* @var string
*/
protected $group_name = 'escapednottranslated';
/**
* List of functions to examine.
*
* @link https://developer.wordpress.org/reference/functions/esc_html/
* @link https://developer.wordpress.org/reference/functions/esc_html__/
* @link https://developer.wordpress.org/reference/functions/esc_attr/
* @link https://developer.wordpress.org/reference/functions/esc_attr__/
*
* @since 2.2.0
*
* @var array <string function_name> => <string alternative function>
*/
protected $target_functions = array(
'esc_html' => 'esc_html__',
'esc_attr' => 'esc_attr__',
);
/**
* Process the parameters of a matched function.
*
* @since 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $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;
}
/*
* We already know that there will be a valid open+close parenthesis, otherwise the sniff
* would have bowed out long before.
*/
$opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
$closer = $this->tokens[ $opener ]['parenthesis_closer'];
$data = array(
$matched_content,
$this->target_functions[ $matched_content ],
$this->phpcsFile->getTokensAsString( $stackPtr, ( $closer - $stackPtr + 1 ) ),
);
$this->phpcsFile->addWarning(
'%s() expects only one parameter. Did you mean to use %s() ? Found: %s',
$stackPtr,
'Found',
$data
);
}
}
@@ -0,0 +1,265 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Flag Database direct queries.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#direct-database-queries
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.6.0 Removed the add_unique_message() function as it is no longer needed.
* @since 0.11.0 This class now extends the WordPressCS native `Sniff` class.
* @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 DirectDatabaseQuerySniff extends Sniff {
/**
* List of custom cache get functions.
*
* @since 0.6.0
*
* @var string|string[]
*/
public $customCacheGetFunctions = array();
/**
* List of custom cache set functions.
*
* @since 0.6.0
*
* @var string|string[]
*/
public $customCacheSetFunctions = array();
/**
* List of custom cache delete functions.
*
* @since 0.6.0
*
* @var string|string[]
*/
public $customCacheDeleteFunctions = array();
/**
* Cache of previously added custom functions.
*
* Prevents having to do the same merges over and over again.
*
* @since 0.11.0
*
* @var array
*/
protected $addedCustomFunctions = array(
'cacheget' => array(),
'cacheset' => array(),
'cachedelete' => array(),
);
/**
* The lists of $wpdb methods.
*
* @since 0.6.0
* @since 0.11.0 Changed from static to non-static.
*
* @var array[]
*/
protected $methods = array(
'cachable' => array(
'delete' => true,
'get_var' => true,
'get_col' => true,
'get_row' => true,
'get_results' => true,
'query' => true,
'replace' => true,
'update' => true,
),
'noncachable' => array(
'insert' => true,
),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_VARIABLE,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
// Check for $wpdb variable.
if ( '$wpdb' !== $this->tokens[ $stackPtr ]['content'] ) {
return;
}
$is_object_call = $this->phpcsFile->findNext( \T_OBJECT_OPERATOR, ( $stackPtr + 1 ), null, false, null, true );
if ( false === $is_object_call ) {
return; // This is not a call to the wpdb object.
}
$methodPtr = $this->phpcsFile->findNext( array( \T_WHITESPACE ), ( $is_object_call + 1 ), null, true, null, true );
$method = $this->tokens[ $methodPtr ]['content'];
$this->mergeFunctionLists();
if ( ! isset( $this->methods['all'][ $method ] ) ) {
return;
}
$endOfStatement = $this->phpcsFile->findNext( \T_SEMICOLON, ( $stackPtr + 1 ), null, false, null, true );
$endOfLineComment = '';
for ( $i = ( $endOfStatement + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) {
if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $endOfStatement ]['line'] ) {
break;
}
if ( \T_COMMENT === $this->tokens[ $i ]['code'] ) {
$endOfLineComment .= $this->tokens[ $i ]['content'];
}
}
$whitelisted_db_call = false;
if ( preg_match( '/db call\W*(?:ok|pass|clear|whitelist)/i', $endOfLineComment ) ) {
$whitelisted_db_call = true;
}
// Check for Database Schema Changes.
for ( $_pos = ( $stackPtr + 1 ); $_pos < $endOfStatement; $_pos++ ) {
$_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $_pos, $endOfStatement, false, null, true );
if ( false === $_pos ) {
break;
}
if ( preg_match( '#\b(?:ALTER|CREATE|DROP)\b#i', $this->tokens[ $_pos ]['content'] ) > 0 ) {
$this->phpcsFile->addWarning( 'Attempting a database schema change is discouraged.', $_pos, 'SchemaChange' );
}
}
// Flag instance if not whitelisted.
if ( ! $whitelisted_db_call ) {
$this->phpcsFile->addWarning( 'Usage of a direct database call is discouraged.', $stackPtr, 'DirectQuery' );
}
if ( ! isset( $this->methods['cachable'][ $method ] ) ) {
return $endOfStatement;
}
$whitelisted_cache = false;
$cached = false;
$wp_cache_get = false;
if ( preg_match( '/cache\s+(?:ok|pass|clear|whitelist)/i', $endOfLineComment ) ) {
$whitelisted_cache = true;
}
if ( ! $whitelisted_cache && ! empty( $this->tokens[ $stackPtr ]['conditions'] ) ) {
$scope_function = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
if ( false === $scope_function ) {
$scope_function = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE );
}
if ( false !== $scope_function ) {
$scopeStart = $this->tokens[ $scope_function ]['scope_opener'];
$scopeEnd = $this->tokens[ $scope_function ]['scope_closer'];
for ( $i = ( $scopeStart + 1 ); $i < $scopeEnd; $i++ ) {
if ( \T_STRING === $this->tokens[ $i ]['code'] ) {
if ( isset( $this->cacheDeleteFunctions[ $this->tokens[ $i ]['content'] ] ) ) {
if ( \in_array( $method, array( 'query', 'update', 'replace', 'delete' ), true ) ) {
$cached = true;
break;
}
} elseif ( isset( $this->cacheGetFunctions[ $this->tokens[ $i ]['content'] ] ) ) {
$wp_cache_get = true;
} elseif ( isset( $this->cacheSetFunctions[ $this->tokens[ $i ]['content'] ] ) ) {
if ( $wp_cache_get ) {
$cached = true;
break;
}
}
}
}
}
}
if ( ! $cached && ! $whitelisted_cache ) {
$message = 'Direct database call without caching detected. Consider using wp_cache_get() / wp_cache_set() or wp_cache_delete().';
$this->phpcsFile->addWarning( $message, $stackPtr, 'NoCaching' );
}
return $endOfStatement;
}
/**
* Merge custom functions provided via a custom ruleset with the defaults, if we haven't already.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @return void
*/
protected function mergeFunctionLists() {
if ( ! isset( $this->methods['all'] ) ) {
$this->methods['all'] = array_merge( $this->methods['cachable'], $this->methods['noncachable'] );
}
if ( $this->customCacheGetFunctions !== $this->addedCustomFunctions['cacheget'] ) {
$this->cacheGetFunctions = $this->merge_custom_array(
$this->customCacheGetFunctions,
$this->cacheGetFunctions
);
$this->addedCustomFunctions['cacheget'] = $this->customCacheGetFunctions;
}
if ( $this->customCacheSetFunctions !== $this->addedCustomFunctions['cacheset'] ) {
$this->cacheSetFunctions = $this->merge_custom_array(
$this->customCacheSetFunctions,
$this->cacheSetFunctions
);
$this->addedCustomFunctions['cacheset'] = $this->customCacheSetFunctions;
}
if ( $this->customCacheDeleteFunctions !== $this->addedCustomFunctions['cachedelete'] ) {
$this->cacheDeleteFunctions = $this->merge_custom_array(
$this->customCacheDeleteFunctions,
$this->cacheDeleteFunctions
);
$this->addedCustomFunctions['cachedelete'] = $this->customCacheDeleteFunctions;
}
}
}
@@ -0,0 +1,661 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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'] );
}
}
@@ -0,0 +1,210 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Sniff for prepared SQL.
*
* Makes sure that variables aren't directly interpolated into SQL statements.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#formatting-sql-statements
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.8.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `WP` category to the `DB` category.
*/
class PreparedSQLSniff extends Sniff {
/**
* The lists of $wpdb methods.
*
* @since 0.8.0
* @since 0.11.0 Changed from static to non-static.
*
* @var array
*/
protected $methods = array(
'get_var' => true,
'get_col' => true,
'get_row' => true,
'get_results' => true,
'prepare' => true,
'query' => true,
);
/**
* Tokens that we don't flag when they are found in a $wpdb method call.
*
* @since 0.9.0
*
* @var array
*/
protected $ignored_tokens = array(
\T_OBJECT_OPERATOR => true,
\T_OPEN_PARENTHESIS => true,
\T_CLOSE_PARENTHESIS => true,
\T_STRING_CONCAT => true,
\T_CONSTANT_ENCAPSED_STRING => true,
\T_OPEN_SQUARE_BRACKET => true,
\T_CLOSE_SQUARE_BRACKET => true,
\T_COMMA => true,
\T_LNUMBER => true,
\T_START_HEREDOC => true,
\T_END_HEREDOC => true,
\T_START_NOWDOC => true,
\T_NOWDOC => true,
\T_END_NOWDOC => true,
\T_INT_CAST => true,
\T_DOUBLE_CAST => true,
\T_BOOL_CAST => true,
\T_NS_SEPARATOR => true,
);
/**
* A loop pointer.
*
* It is a property so that we can access it in all of our methods.
*
* @since 0.9.0
*
* @var int
*/
protected $i;
/**
* The loop end marker.
*
* It is a property so that we can access it in all of our methods.
*
* @since 0.9.0
*
* @var int
*/
protected $end;
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.8.0
*
* @return array
*/
public function register() {
$this->ignored_tokens += Tokens::$emptyTokens;
return array(
\T_VARIABLE,
\T_STRING,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.8.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
if ( ! $this->is_wpdb_method_call( $stackPtr, $this->methods ) ) {
return;
}
if ( $this->has_whitelist_comment( 'unprepared SQL', $stackPtr ) ) {
return;
}
for ( $this->i; $this->i < $this->end; $this->i++ ) {
if ( isset( $this->ignored_tokens[ $this->tokens[ $this->i ]['code'] ] ) ) {
continue;
}
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $this->i ]['code']
|| \T_HEREDOC === $this->tokens[ $this->i ]['code']
) {
$bad_variables = array_filter(
$this->get_interpolated_variables( $this->tokens[ $this->i ]['content'] ),
function ( $symbol ) {
return ( 'wpdb' !== $symbol );
}
);
foreach ( $bad_variables as $bad_variable ) {
$this->phpcsFile->addError(
'Use placeholders and $wpdb->prepare(); found interpolated variable $%s at %s',
$this->i,
'InterpolatedNotPrepared',
array(
$bad_variable,
$this->tokens[ $this->i ]['content'],
)
);
}
continue;
}
if ( \T_VARIABLE === $this->tokens[ $this->i ]['code'] ) {
if ( '$wpdb' === $this->tokens[ $this->i ]['content'] ) {
$this->is_wpdb_method_call( $this->i, $this->methods );
continue;
}
if ( $this->is_safe_casted( $this->i ) ) {
continue;
}
}
if ( \T_STRING === $this->tokens[ $this->i ]['code'] ) {
if (
isset( $this->SQLEscapingFunctions[ $this->tokens[ $this->i ]['content'] ] )
|| isset( $this->SQLAutoEscapedFunctions[ $this->tokens[ $this->i ]['content'] ] )
) {
// Find the opening parenthesis.
$opening_paren = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $this->i + 1 ), null, true, null, true );
if ( false !== $opening_paren
&& \T_OPEN_PARENTHESIS === $this->tokens[ $opening_paren ]['code']
&& isset( $this->tokens[ $opening_paren ]['parenthesis_closer'] )
) {
// Skip past the end of the function.
$this->i = $this->tokens[ $opening_paren ]['parenthesis_closer'];
continue;
}
} elseif ( isset( $this->formattingFunctions[ $this->tokens[ $this->i ]['content'] ] ) ) {
continue;
}
}
$this->phpcsFile->addError(
'Use placeholders and $wpdb->prepare(); found %s',
$this->i,
'NotPrepared',
array( $this->tokens[ $this->i ]['content'] )
);
}
return $this->end;
}
}
@@ -0,0 +1,60 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\AbstractClassRestrictionsSniff;
/**
* Verifies that no database related PHP classes are used.
*
* "Avoid touching the database directly. If there is a defined function that can get
* the data you need, use it. Database abstraction (using functions instead of queries)
* helps keep your code forward-compatible and, in cases where results are cached in memory,
* it can be many times faster."
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#database-queries
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class RestrictedClassesSniff extends AbstractClassRestrictionsSniff {
/**
* Groups of classes to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Avoid direct calls to the database.',
* 'classes' => array( 'PDO', '\Namespace\Classname' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'mysql' => array(
'type' => 'error',
'message' => 'Accessing the database directly should be avoided. Please use the $wpdb object and associated functions instead. Found: %s.',
'classes' => array(
'mysqli',
'PDO',
'PDOStatement',
),
),
);
}
}
@@ -0,0 +1,66 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DB;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Verifies that no database related PHP functions are used.
*
* "Avoid touching the database directly. If there is a defined function that can get
* the data you need, use it. Database abstraction (using functions instead of queries)
* helps keep your code forward-compatible and, in cases where results are cached in memory,
* it can be many times faster."
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#database-queries
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class RestrictedFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'mysql' => array(
'type' => 'error',
'message' => 'Accessing the database directly should be avoided. Please use the $wpdb object and associated functions instead. Found: %s.',
'functions' => array(
'mysql_*',
'mysqli_*',
'mysqlnd_ms_*',
'mysqlnd_qc_*',
'mysqlnd_uh_*',
'mysqlnd_memcache_*',
'maxdb_*',
),
'whitelist' => array(
'mysql_to_rfc3339' => true,
),
),
);
}
}
@@ -0,0 +1,86 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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;
}
}
@@ -0,0 +1,174 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DateTime;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Don't use current_time() to get a (timezone corrected) "timestamp".
*
* Disallow using the current_time() function to get "timestamps" as it
* doesn't produce a *real* timestamp, but a "WordPress timestamp", i.e.
* a Unix timestamp with current timezone offset, not a Unix timestamp ansich.
*
* @link https://developer.wordpress.org/reference/functions/current_time/
* @link https://make.wordpress.org/core/2019/09/23/date-time-improvements-wp-5-3/
* @link https://core.trac.wordpress.org/ticket/40657
* @link https://github.com/WordPress/WordPress-Coding-Standards/issues/1791
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class CurrentTimeTimestampSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 2.2.0
*
* @var string
*/
protected $group_name = 'current_time';
/**
* List of functions to examine.
*
* @since 2.2.0
*
* @var array <string function_name> => <bool always needed ?>
*/
protected $target_functions = array(
'current_time' => true,
);
/**
* Process the parameters of a matched function.
*
* @since 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $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 ) {
/*
* We already know there will be valid open & close parentheses as otherwise the parameter
* retrieval function call would have returned an empty array, so no additional checks needed.
*/
$open_parens = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, $stackPtr );
$close_parens = $this->tokens[ $open_parens ]['parenthesis_closer'];
/*
* Check whether the first parameter is a timestamp format.
*/
for ( $i = $parameters[1]['start']; $i <= $parameters[1]['end']; $i++ ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
if ( isset( Tokens::$textStringTokens[ $this->tokens[ $i ]['code'] ] ) ) {
$content_first = trim( $this->strip_quotes( $this->tokens[ $i ]['content'] ) );
if ( 'U' !== $content_first && 'timestamp' !== $content_first ) {
// Most likely valid use of current_time().
return;
}
continue;
}
if ( isset( Tokens::$heredocTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
/*
* If we're still here, we've encountered an unexpected token, like a variable or
* function call. Bow out as we can't determine the runtime value.
*/
return;
}
$gmt_true = false;
/*
* Check whether the second parameter, $gmt, is a set to `true` or `1`.
*/
if ( isset( $parameters[2] ) ) {
$content_second = '';
if ( 'true' === $parameters[2]['raw'] || '1' === $parameters[2]['raw'] ) {
$content_second = $parameters[2]['raw'];
$gmt_true = true;
} else {
// Do a more extensive parameter check.
for ( $i = $parameters[2]['start']; $i <= $parameters[2]['end']; $i++ ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
$content_second .= $this->tokens[ $i ]['content'];
}
if ( 'true' === $content_second || '1' === $content_second ) {
$gmt_true = true;
}
}
}
/*
* Non-UTC timestamp requested.
*/
if ( false === $gmt_true ) {
$this->phpcsFile->addWarning(
'Calling current_time() with a $type of "timestamp" or "U" is strongly discouraged as it will not return a Unix (UTC) timestamp. Please consider using a non-timestamp format or otherwise refactoring this code.',
$stackPtr,
'Requested'
);
return;
}
/*
* UTC timestamp requested. Should use time() instead.
*/
$has_comment = $this->phpcsFile->findNext( Tokens::$commentTokens, ( $stackPtr + 1 ), ( $close_parens + 1 ) );
$error = 'Don\'t use current_time() for retrieving a Unix (UTC) timestamp. Use time() instead. Found: %s';
$error_code = 'RequestedUTC';
$code_snippet = "current_time( '" . $content_first . "'";
if ( isset( $content_second ) ) {
$code_snippet .= ', ' . $content_second;
}
$code_snippet .= ' )';
if ( false !== $has_comment ) {
// If there are comments, we don't auto-fix as it would remove those comments.
$this->phpcsFile->addError( $error, $stackPtr, $error_code, array( $code_snippet ) );
return;
}
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code, array( $code_snippet ) );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $stackPtr + 1 ); $i < $close_parens; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->replaceToken( $stackPtr, 'time(' );
$this->phpcsFile->fixer->endChangeset();
}
}
}
@@ -0,0 +1,62 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\DateTime;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Forbids the use of various native DateTime related PHP/WP functions and suggests alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class RestrictedFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* @return array
*/
public function getGroups() {
return array(
/*
* Disallow the changing the timezone.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#manipulating-the-timezone-server-side
*/
'timezone_change' => array(
'type' => 'error',
'message' => 'Using %s() and similar isn\'t allowed, instead use WP internal timezone support.',
'functions' => array(
'date_default_timezone_set',
),
),
/*
* Use gmdate(), not date().
* Don't rely on the current PHP time zone as it might have been changed by third party code.
*
* @link https://make.wordpress.org/core/2019/09/23/date-time-improvements-wp-5-3/
* @link https://core.trac.wordpress.org/ticket/46438
* @link https://github.com/WordPress/WordPress-Coding-Standards/issues/1713
*/
'date' => array(
'type' => 'error',
'message' => '%s() is affected by runtime timezone changes which can cause date/time to be incorrectly displayed. Use gmdate() instead.',
'functions' => array(
'date',
),
),
);
}
}
@@ -0,0 +1,248 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Files;
use WordPressCS\WordPress\Sniff;
/**
* Ensures filenames do not contain underscores.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.1.0
* @since 0.11.0 - This sniff will now also check for all lowercase file names.
* - This sniff will now also verify that files containing a class start with `class-`.
* - This sniff will now also verify that files in `wp-includes` containing
* template tags end in `-template`. Based on @subpackage file DocBlock tag.
* - This sniff will now allow for underscores in file names for certain theme
* specific exceptions if the `$is_theme` property is set to `true`.
* @since 0.12.0 Now extends the WordPressCS native `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* @uses \WordPressCS\WordPress\Sniff::$custom_test_class_whitelist
*/
class FileNameSniff extends Sniff {
/**
* Regex for the theme specific exceptions.
*
* N.B. This regex currently does not allow for mimetype sublevel only file names,
* such as `plain.php`.
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#single-post
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#custom-taxonomies
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#custom-post-types
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#embeds
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#attachment
* @link https://developer.wordpress.org/themes/template-files-section/partial-and-miscellaneous-template-files/#content-slug-php
* @link https://wphierarchy.com/
* @link https://en.wikipedia.org/wiki/Media_type#Naming
*
* @since 0.11.0
*
* @var string
*/
const THEME_EXCEPTIONS_REGEX = '`
^ # Anchor to the beginning of the string.
(?:
# Template prefixes which can have exceptions.
(?:archive|category|content|embed|page|single|tag|taxonomy)
-[^\.]+ # These need to be followed by a dash and some chars.
|
(?:application|audio|example|image|message|model|multipart|text|video) #Top-level mime-types
(?:_[^\.]+)? # Optionally followed by an underscore and a sub-type.
)\.(?:php|inc)$ # End in .php (or .inc for the test files) and anchor to the end of the string.
`Dx';
/**
* Whether the codebase being sniffed is a theme.
*
* If true, it will allow for certain typical theme specific exceptions to the filename rules.
*
* @since 0.11.0
*
* @var bool
*/
public $is_theme = false;
/**
* Whether to apply strict class file name rules.
*
* If true, it demands that classes are prefixed with `class-` and that the rest of the
* file name reflects the class name.
*
* @since 0.11.0
*
* @var bool
*/
public $strict_class_file_names = true;
/**
* Historical exceptions in WP core to the class name rule.
*
* @since 0.11.0
*
* @var array
*/
private $class_exceptions = array(
'class.wp-dependencies.php' => true,
'class.wp-scripts.php' => true,
'class.wp-styles.php' => true,
);
/**
* Unit test version of the historical exceptions in WP core.
*
* @since 0.11.0
*
* @var array
*/
private $unittest_class_exceptions = array(
'class.wp-dependencies.inc' => true,
'class.wp-scripts.inc' => true,
'class.wp-styles.inc' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
if ( \defined( '\PHP_CODESNIFFER_IN_TESTS' ) ) {
$this->class_exceptions = array_merge( $this->class_exceptions, $this->unittest_class_exceptions );
}
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|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
// Usage of `strip_quotes` is to ensure `stdin_path` passed by IDEs does not include quotes.
$file = $this->strip_quotes( $this->phpcsFile->getFileName() );
if ( 'STDIN' === $file ) {
return;
}
// Respect phpcs:disable comments as long as they are not accompanied by an enable (PHPCS 3.2+).
if ( \defined( '\T_PHPCS_DISABLE' ) && \defined( '\T_PHPCS_ENABLE' ) ) {
$i = -1;
while ( $i = $this->phpcsFile->findNext( \T_PHPCS_DISABLE, ( $i + 1 ) ) ) {
if ( empty( $this->tokens[ $i ]['sniffCodes'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['WordPress'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files.FileName'] )
) {
do {
$i = $this->phpcsFile->findNext( \T_PHPCS_ENABLE, ( $i + 1 ) );
} while ( false !== $i
&& ! empty( $this->tokens[ $i ]['sniffCodes'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files.FileName'] ) );
if ( false === $i ) {
// The entire (rest of the) file is disabled.
return;
}
}
}
}
$fileName = basename( $file );
$expected = strtolower( str_replace( '_', '-', $fileName ) );
/*
* Generic check for lowercase hyphenated file names.
*/
if ( $fileName !== $expected && ( false === $this->is_theme || 1 !== preg_match( self::THEME_EXCEPTIONS_REGEX, $fileName ) ) ) {
$this->phpcsFile->addError(
'Filenames should be all lowercase with hyphens as word separators. Expected %s, but found %s.',
0,
'NotHyphenatedLowercase',
array( $expected, $fileName )
);
}
unset( $expected );
/*
* Check files containing a class for the "class-" prefix and that the rest of
* the file name reflects the class name.
*/
if ( true === $this->strict_class_file_names ) {
$has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr );
if ( false !== $has_class && false === $this->is_test_class( $has_class ) ) {
$class_name = $this->phpcsFile->getDeclarationName( $has_class );
$expected = 'class-' . strtolower( str_replace( '_', '-', $class_name ) );
if ( substr( $fileName, 0, -4 ) !== $expected && ! isset( $this->class_exceptions[ $fileName ] ) ) {
$this->phpcsFile->addError(
'Class file names should be based on the class name with "class-" prepended. Expected %s, but found %s.',
0,
'InvalidClassFileName',
array(
$expected . '.php',
$fileName,
)
);
}
unset( $expected );
}
}
/*
* Check non-class files in "wp-includes" with a "@subpackage Template" tag for a "-template" suffix.
*/
if ( false !== strpos( $file, \DIRECTORY_SEPARATOR . 'wp-includes' . \DIRECTORY_SEPARATOR ) ) {
$subpackage_tag = $this->phpcsFile->findNext( \T_DOC_COMMENT_TAG, $stackPtr, null, false, '@subpackage' );
if ( false !== $subpackage_tag ) {
$subpackage = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, $subpackage_tag );
if ( false !== $subpackage ) {
$fileName_end = substr( $fileName, -13 );
$has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr );
if ( ( 'Template' === trim( $this->tokens[ $subpackage ]['content'] )
&& $this->tokens[ $subpackage_tag ]['line'] === $this->tokens[ $subpackage ]['line'] )
&& ( ( ! \defined( '\PHP_CODESNIFFER_IN_TESTS' ) && '-template.php' !== $fileName_end )
|| ( \defined( '\PHP_CODESNIFFER_IN_TESTS' ) && '-template.inc' !== $fileName_end ) )
&& false === $has_class
) {
$this->phpcsFile->addError(
'Files containing template tags should have "-template" appended to the end of the file name. Expected %s, but found %s.',
0,
'InvalidTemplateTagFileName',
array(
substr( $fileName, 0, -4 ) . '-template.php',
$fileName,
)
);
}
}
}
}
// Only run this sniff once per file, no need to run it again.
return ( $this->phpcsFile->numTokens + 1 );
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,187 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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()`.
* @since 2.2.0 Will now ignore functions and methods which are marked as @deprecated.
*
* 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 ) {
if ( Sniff::is_function_deprecated( $phpcsFile, $stackPtr ) === true ) {
/*
* Deprecated functions don't have to comply with the naming conventions,
* otherwise functions deprecated in favour of a function with a compliant
* name would still trigger an error.
*/
return;
}
$functionName = $phpcsFile->getDeclarationName( $stackPtr );
if ( ! isset( $functionName ) ) {
// Ignore closures.
return;
}
if ( '' === ltrim( $functionName, '_' ) ) {
// Ignore special functions, like __().
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;
}
if ( Sniff::is_function_deprecated( $phpcsFile, $stackPtr ) === true ) {
/*
* Deprecated functions don't have to comply with the naming conventions,
* otherwise functions deprecated in favour of a function with a compliant
* name would still trigger an error.
*/
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 );
}
}
}
@@ -0,0 +1,289 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\NamingConventions;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* 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 functions to restrict.
*
* @since 0.11.0
*
* @return array
*/
public function getGroups() {
$this->target_functions = $this->hookInvokeFunctions;
// No need to examine the names of deprecated hooks.
unset(
$this->target_functions['do_action_deprecated'],
$this->target_functions['apply_filters_deprecated']
);
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 string $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 ) {
$regex = $this->prepare_regex();
$case_errors = 0;
$underscores = 0;
$content = array();
$expected = array();
for ( $i = $parameters[1]['start']; $i <= $parameters[1]['end']; $i++ ) {
// Skip past comment tokens.
if ( isset( Tokens::$commentTokens[ $this->tokens[ $i ]['code'] ] ) !== false ) {
continue;
}
$content[ $i ] = $this->tokens[ $i ]['content'];
$expected[ $i ] = $this->tokens[ $i ]['content'];
// Skip past potential variable array access: $var['Key'].
if ( \T_VARIABLE === $this->tokens[ $i ]['code'] ) {
do {
$open_bracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( false === $open_bracket
|| \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code']
|| ! isset( $this->tokens[ $open_bracket ]['bracket_closer'] )
) {
continue 2;
}
$i = $this->tokens[ $open_bracket ]['bracket_closer'];
} while ( isset( $this->tokens[ $i ] ) && $i <= $parameters[1]['end'] );
continue;
}
// Skip past non-string tokens.
if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) === false ) {
continue;
}
$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++;
}
}
$first_non_empty = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$parameters[1]['start'],
( $parameters[1]['end'] + 1 ),
true
);
$data = array(
trim( implode( '', $expected ) ),
trim( implode( '', $content ) ),
);
if ( $case_errors > 0 ) {
$error = 'Hook names should be lowercase. Expected: %s, but found: %s.';
$this->phpcsFile->addError( $error, $first_non_empty, 'NotLowercase', $data );
}
if ( $underscores > 0 ) {
$error = 'Words in hook names should be separated using underscores. Expected: %s, but found: %s.';
$this->phpcsFile->addWarning( $error, $first_non_empty, '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 );
}
}
@@ -0,0 +1,208 @@
<?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 PHP_CodeSniffer\Util\Tokens;
/**
* Validates post type names.
*
* Checks the post type slug for invalid characters, long function names
* and reserved names.
*
* @link https://developer.wordpress.org/reference/functions/register_post_type/
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class ValidPostTypeSlugSniff extends AbstractFunctionParameterSniff {
/**
* Max length of a post type name is limited by the SQL field.
*
* @since 2.2.0
*
* @var int
*/
const POST_TYPE_MAX_LENGTH = 20;
/**
* Regex that whitelists characters that can be used as the post type slug.
*
* @link https://developer.wordpress.org/reference/functions/register_post_type/
* @since 2.2.0
*
* @var string
*/
const POST_TYPE_CHARACTER_WHITELIST = '/^[a-z0-9_-]+$/';
/**
* Array of functions that must be checked.
*
* @since 2.2.0
*
* @var array List of function names as keys. Value irrelevant.
*/
protected $target_functions = array(
'register_post_type' => true,
);
/**
* Array of reserved post type names which can not be used by themes and plugins.
*
* @since 2.2.0
*
* @var array
*/
protected $reserved_names = array(
'post' => true,
'page' => true,
'attachment' => true,
'revision' => true,
'nav_menu_item' => true,
'custom_css' => true,
'customize_changeset' => true,
'oembed_cache' => true,
'user_request' => true,
'wp_block' => true,
'action' => true,
'author' => true,
'order' => true,
'theme' => true,
);
/**
* All valid tokens for in the first parameter of register_post_type().
*
* Set in `register()`.
*
* @since 2.2.0
*
* @var string
*/
private $valid_tokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 2.2.0
*
* @return array
*/
public function register() {
$this->valid_tokens = Tokens::$textStringTokens + Tokens::$heredocTokens + Tokens::$emptyTokens;
return parent::register();
}
/**
* Process the parameter of a matched function.
*
* Errors on invalid post type names when reserved keywords are used,
* the post type is too long, or contains invalid characters.
*
* @since 2.2.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 ) {
$string_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $parameters[1]['start'], ( $parameters[1]['end'] + 1 ) );
$has_invalid_tokens = $this->phpcsFile->findNext( $this->valid_tokens, $parameters[1]['start'], ( $parameters[1]['end'] + 1 ), true );
if ( false !== $has_invalid_tokens || false === $string_pos ) {
// Check for non string based slug parameter (we cannot determine if this is valid).
$this->phpcsFile->addWarning(
'The post type slug is not a string literal. It is not possible to automatically determine the validity of this slug. Found: %s.',
$stackPtr,
'NotStringLiteral',
array(
$parameters[1]['raw'],
),
3
);
return;
}
$post_type = $this->strip_quotes( $this->tokens[ $string_pos ]['content'] );
if ( strlen( $post_type ) === 0 ) {
// Error for using empty slug.
$this->phpcsFile->addError(
'register_post_type() called without a post type slug. The slug must be a non-empty string.',
$parameters[1]['start'],
'Empty'
);
return;
}
$data = array(
$this->tokens[ $string_pos ]['content'],
);
// Warn for dynamic parts in the slug parameter.
if ( 'T_DOUBLE_QUOTED_STRING' === $this->tokens[ $string_pos ]['type'] || ( 'T_HEREDOC' === $this->tokens[ $string_pos ]['type'] && strpos( $this->tokens[ $string_pos ]['content'], '$' ) !== false ) ) {
$this->phpcsFile->addWarning(
'The post type slug may, or may not, get too long with dynamic contents and could contain invalid characters. Found: %s.',
$string_pos,
'PartiallyDynamic',
$data
);
$post_type = $this->strip_interpolated_variables( $post_type );
}
if ( preg_match( self::POST_TYPE_CHARACTER_WHITELIST, $post_type ) === 0 ) {
// Error for invalid characters.
$this->phpcsFile->addError(
'register_post_type() called with invalid post type %s. Post type contains invalid characters. Only lowercase alphanumeric characters, dashes, and underscores are allowed.',
$string_pos,
'InvalidCharacters',
$data
);
}
if ( isset( $this->reserved_names[ $post_type ] ) ) {
// Error for using reserved slug names.
$this->phpcsFile->addError(
'register_post_type() called with reserved post type %s. Reserved post types should not be used as they interfere with the functioning of WordPress itself.',
$string_pos,
'Reserved',
$data
);
} elseif ( stripos( $post_type, 'wp_' ) === 0 ) {
// Error for using reserved slug prefix.
$this->phpcsFile->addError(
'The post type passed to register_post_type() uses a prefix reserved for WordPress itself. Found: %s.',
$string_pos,
'ReservedPrefix',
$data
);
}
// Error for slugs that are too long.
if ( strlen( $post_type ) > self::POST_TYPE_MAX_LENGTH ) {
$this->phpcsFile->addError(
'A post type slug must not exceed %d characters. Found: %s (%d characters).',
$string_pos,
'TooLong',
array(
self::POST_TYPE_MAX_LENGTH,
$this->tokens[ $string_pos ]['content'],
strlen( $post_type ),
)
);
}
}
}
@@ -0,0 +1,299 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\NamingConventions;
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff as PHPCS_AbstractVariableSniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use WordPressCS\WordPress\Sniff;
/**
* Checks the naming of variables and member variables.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.9.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 2.0.0 - Defers to the upstream `$phpReservedVars` property.
* - Now offers name suggestions for variables in violation.
*
* Last synced with base class June 2018 at commit 78ddbae97cac078f09928bf89e3ab9e53ad2ace0.
* @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php
*
* @uses PHP_CodeSniffer\Sniffs\AbstractVariableSniff::$phpReservedVars
*/
class ValidVariableNameSniff extends PHPCS_AbstractVariableSniff {
/**
* Mixed-case variables used by WordPress.
*
* @since 0.11.0
*
* @var array
*/
protected $wordpress_mixed_case_vars = array(
'EZSQL_ERROR' => true,
'GETID3_ERRORARRAY' => true,
'is_IE' => true,
'is_IIS' => true,
'is_macIE' => true,
'is_NS4' => true,
'is_winIE' => true,
'PHP_SELF' => true,
'post_ID' => true,
'tag_ID' => true,
'user_ID' => true,
);
/**
* List of member variables that can have mixed case.
*
* @since 0.9.0
* @since 0.11.0 Changed from public to protected.
*
* @var array
*/
protected $whitelisted_mixed_case_member_var_names = array(
'ID' => true,
'comment_ID' => true,
'comment_post_ID' => true,
'post_ID' => true,
'comment_author_IP' => true,
'cat_ID' => true,
);
/**
* Custom list of properties which can have mixed case.
*
* @since 0.11.0
*
* @var string|string[]
*/
public $customPropertiesWhitelist = array();
/**
* Cache of previously added custom functions.
*
* Prevents having to do the same merges over and over again.
*
* @since 0.10.0
* @since 0.11.0 - Name changed from $addedCustomVariables.
* - Changed the format from simple bool to array.
*
* @var array
*/
protected $addedCustomProperties = array(
'properties' => null,
);
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned.
* @param int $stack_ptr The position of the current token in the
* stack passed in $tokens.
*
* @return void
*/
protected function processVariable( File $phpcs_file, $stack_ptr ) {
$tokens = $phpcs_file->getTokens();
$var_name = ltrim( $tokens[ $stack_ptr ]['content'], '$' );
// If it's a php reserved var, then its ok.
if ( isset( $this->phpReservedVars[ $var_name ] ) ) {
return;
}
// Merge any custom variables with the defaults.
$this->mergeWhiteList();
// Likewise if it is a mixed-case var used by WordPress core.
if ( isset( $this->wordpress_mixed_case_vars[ $var_name ] ) ) {
return;
}
$obj_operator = $phpcs_file->findNext( Tokens::$emptyTokens, ( $stack_ptr + 1 ), null, true );
if ( \T_OBJECT_OPERATOR === $tokens[ $obj_operator ]['code'] ) {
// Check to see if we are using a variable from an object.
$var = $phpcs_file->findNext( Tokens::$emptyTokens, ( $obj_operator + 1 ), null, true );
if ( \T_STRING === $tokens[ $var ]['code'] ) {
$bracket = $phpcs_file->findNext( Tokens::$emptyTokens, ( $var + 1 ), null, true );
if ( \T_OPEN_PARENTHESIS !== $tokens[ $bracket ]['code'] ) {
$obj_var_name = $tokens[ $var ]['content'];
// There is no way for us to know if the var is public or
// private, so we have to ignore a leading underscore if there is
// one and just check the main part of the variable name.
$original_var_name = $obj_var_name;
if ( '_' === substr( $obj_var_name, 0, 1 ) ) {
$obj_var_name = substr( $obj_var_name, 1 );
}
if ( ! isset( $this->whitelisted_mixed_case_member_var_names[ $obj_var_name ] ) && self::isSnakeCase( $obj_var_name ) === false ) {
$error = 'Object property "$%s" is not in valid snake_case format, try "$%s"';
$data = array(
$original_var_name,
Sniff::get_snake_case_name_suggestion( $original_var_name ),
);
$phpcs_file->addError( $error, $var, 'UsedPropertyNotSnakeCase', $data );
}
}
}
}
$in_class = false;
$obj_operator = $phpcs_file->findPrevious( Tokens::$emptyTokens, ( $stack_ptr - 1 ), null, true );
if ( \T_DOUBLE_COLON === $tokens[ $obj_operator ]['code'] || \T_OBJECT_OPERATOR === $tokens[ $obj_operator ]['code'] ) {
// The variable lives within a class, and is referenced like
// this: MyClass::$_variable or $class->variable.
$in_class = true;
}
// There is no way for us to know if the var is public or private,
// so we have to ignore a leading underscore if there is one and just
// check the main part of the variable name.
$original_var_name = $var_name;
if ( '_' === substr( $var_name, 0, 1 ) && true === $in_class ) {
$var_name = substr( $var_name, 1 );
}
if ( self::isSnakeCase( $var_name ) === false ) {
if ( $in_class && ! isset( $this->whitelisted_mixed_case_member_var_names[ $var_name ] ) ) {
$error = 'Object property "$%s" is not in valid snake_case format, try "$%s"';
$error_name = 'UsedPropertyNotSnakeCase';
} elseif ( ! $in_class ) {
$error = 'Variable "$%s" is not in valid snake_case format, try "$%s"';
$error_name = 'VariableNotSnakeCase';
}
if ( isset( $error, $error_name ) ) {
$data = array(
$original_var_name,
Sniff::get_snake_case_name_suggestion( $original_var_name ),
);
$phpcs_file->addError( $error, $stack_ptr, $error_name, $data );
}
}
}
/**
* Processes class member variables.
*
* @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned.
* @param int $stack_ptr The position of the current token in the
* stack passed in $tokens.
*
* @return void
*/
protected function processMemberVar( File $phpcs_file, $stack_ptr ) {
$tokens = $phpcs_file->getTokens();
$var_name = ltrim( $tokens[ $stack_ptr ]['content'], '$' );
$member_props = $phpcs_file->getMemberProperties( $stack_ptr );
if ( empty( $member_props ) ) {
// Couldn't get any info about this variable, which
// generally means it is invalid or possibly has a parse
// error. Any errors will be reported by the core, so
// we can ignore it.
return;
}
// Merge any custom variables with the defaults.
$this->mergeWhiteList();
if ( ! isset( $this->whitelisted_mixed_case_member_var_names[ $var_name ] ) && false === self::isSnakeCase( $var_name ) ) {
$error = 'Member variable "$%s" is not in valid snake_case format, try "$%s"';
$data = array(
$var_name,
Sniff::get_snake_case_name_suggestion( $var_name ),
);
$phpcs_file->addError( $error, $stack_ptr, 'PropertyNotSnakeCase', $data );
}
}
/**
* Processes the variable found within a double quoted string.
*
* @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned.
* @param int $stack_ptr The position of the double quoted
* string.
*
* @return void
*/
protected function processVariableInString( File $phpcs_file, $stack_ptr ) {
$tokens = $phpcs_file->getTokens();
if ( preg_match_all( '|[^\\\]\${?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)|', $tokens[ $stack_ptr ]['content'], $matches ) > 0 ) {
// Merge any custom variables with the defaults.
$this->mergeWhiteList();
foreach ( $matches[1] as $var_name ) {
// If it's a php reserved var, then its ok.
if ( isset( $this->phpReservedVars[ $var_name ] ) ) {
continue;
}
// Likewise if it is a mixed-case var used by WordPress core.
if ( isset( $this->wordpress_mixed_case_vars[ $var_name ] ) ) {
return;
}
if ( false === self::isSnakeCase( $var_name ) ) {
$error = 'Variable "$%s" is not in valid snake_case format, try "$%s"';
$data = array(
$var_name,
Sniff::get_snake_case_name_suggestion( $var_name ),
);
$phpcs_file->addError( $error, $stack_ptr, 'InterpolatedVariableNotSnakeCase', $data );
}
}
}
}
/**
* Return whether the variable is in snake_case.
*
* @param string $var_name Variable name.
* @return bool
*/
public static function isSnakeCase( $var_name ) {
return (bool) preg_match( '/^[a-z0-9_]+$/', $var_name );
}
/**
* Merge a custom whitelist provided via a custom ruleset with the predefined whitelist,
* if we haven't already.
*
* @since 0.10.0
* @since 2.0.0 Removed unused $phpcs_file parameter.
*
* @return void
*/
protected function mergeWhiteList() {
if ( $this->customPropertiesWhitelist !== $this->addedCustomProperties['properties'] ) {
// Fix property potentially passed as comma-delimited string.
$customProperties = Sniff::merge_custom_array( $this->customPropertiesWhitelist, array(), false );
$this->whitelisted_mixed_case_member_var_names = Sniff::merge_custom_array(
$customProperties,
$this->whitelisted_mixed_case_member_var_names
);
$this->addedCustomProperties['properties'] = $this->customPropertiesWhitelist;
}
}
}
@@ -0,0 +1,66 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Restrict the use of various development functions.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class DevelopmentFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'error_log' => array(
'type' => 'warning',
'message' => '%s() found. Debug code should not normally be used in production.',
'functions' => array(
'error_log',
'var_dump',
'var_export',
'print_r',
'trigger_error',
'set_error_handler',
'debug_backtrace',
'debug_print_backtrace',
'wp_debug_backtrace_summary',
),
),
'prevent_path_disclosure' => array(
'type' => 'warning',
'message' => '%s() can lead to full path disclosure.',
'functions' => array(
'error_reporting',
'phpinfo',
),
),
);
}
}
@@ -0,0 +1,65 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Disallow the use of short ternaries.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#ternary-operator
*
* @package WPCS\WordPressCodingStandards
*
* @since 2.2.0
*/
class DisallowShortTernarySniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 2.2.0
*
* @return array
*/
public function register() {
return array( \T_INLINE_THEN );
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$nextNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( false === $nextNonEmpty ) {
// Live coding or parse error.
return;
}
if ( \T_INLINE_ELSE !== $this->tokens[ $nextNonEmpty ]['code'] ) {
return;
}
$this->phpcsFile->addError(
'Using short ternaries is not allowed',
$stackPtr,
'Found'
);
}
}
@@ -0,0 +1,103 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Discourages the use of various native PHP functions and suggests alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 `create_function` was moved to the PHP.RestrictedFunctions sniff.
*/
class DiscouragedPHPFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to discourage.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'serialize' => array(
'type' => 'warning',
'message' => '%s() found. Serialized data has known vulnerability problems with Object Injection. JSON is generally a better approach for serializing data. See https://www.owasp.org/index.php/PHP_Object_Injection',
'functions' => array(
'serialize',
'unserialize',
),
),
'urlencode' => array(
'type' => 'warning',
'message' => '%s() should only be used when dealing with legacy applications rawurlencode() should now be used instead. See http://php.net/manual/en/function.rawurlencode.php and http://www.faqs.org/rfcs/rfc3986.html',
'functions' => array(
'urlencode',
),
),
'runtime_configuration' => array(
'type' => 'warning',
'message' => '%s() found. Changing configuration values at runtime is strongly discouraged.',
'functions' => array(
'error_reporting',
'ini_restore',
'apache_setenv',
'putenv',
'set_include_path',
'restore_include_path',
// This alias was DEPRECATED in PHP 5.3.0, and REMOVED as of PHP 7.0.0.
'magic_quotes_runtime',
// Warning This function was DEPRECATED in PHP 5.3.0, and REMOVED as of PHP 7.0.0.
'set_magic_quotes_runtime',
// Warning This function was removed from most SAPIs in PHP 5.3.0, and was removed from PHP-FPM in PHP 7.0.0.
'dl',
),
),
'system_calls' => array(
'type' => 'warning',
'message' => '%s() found. PHP system calls are often disabled by server admins.',
'functions' => array(
'exec',
'passthru',
'proc_open',
'shell_exec',
'system',
'popen',
),
),
'obfuscation' => array(
'type' => 'warning',
'message' => '%s() can be used to obfuscate code which is strongly discouraged. Please verify that the function is used for benign reasons.',
'functions' => array(
'base64_decode',
'base64_encode',
'convert_uudecode',
'convert_uuencode',
'str_rot13',
),
),
);
}
}
@@ -0,0 +1,55 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Restricts the usage of extract().
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#dont-extract
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0 Previously this check was contained within the
* `WordPress.VIP.RestrictedFunctions` sniff.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `Functions` category to the `PHP` category.
*/
class DontExtractSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'extract' => array(
'type' => 'error',
'message' => '%s() usage is highly discouraged, due to the complexity and unintended issues it might cause.',
'functions' => array(
'extract',
),
),
);
}
}
@@ -0,0 +1,177 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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 string $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'],
)
);
}
}
@@ -0,0 +1,239 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Discourage the use of the PHP error silencing operator.
*
* This sniff allows the error operator to be used with a select list
* of whitelisted functions, as no amount of error checking can prevent
* PHP from throwing errors when those functions are used.
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.1.0
*/
class NoSilencedErrorsSniff extends Sniff {
/**
* Number of tokens to display in the error message to show
* the error silencing context.
*
* @since 1.1.0
*
* @var int
*/
public $context_length = 6;
/**
* Whether or not the `$function_whitelist` should be used.
*
* Defaults to true.
*
* This property only affects whether the standard function whitelist is
* used. The custom whitelist, if set, will always be respected.
*
* @since 1.1.0
*
* @var bool
*/
public $use_default_whitelist = true;
/**
* User defined whitelist.
*
* Allows users to pass a list of additional functions to whitelist
* from their custom ruleset.
*
* @since 1.1.0
*
* @var array
*/
public $custom_whitelist = array();
/**
* PHP native function whitelist.
*
* Errors caused by calls to any of these native PHP functions
* are allowed to be silenced as file system permissions and such
* can cause E_WARNINGs to be thrown which cannot be prevented via
* error checking.
*
* Note: only calls to global functions - in contrast to class methods -
* are taken into account.
*
* Only functions for which the PHP manual annotates that an
* error will be thrown on failure are accepted into this list.
*
* @since 1.1.0
*
* @var array <string function name> => <bool true>
*/
protected $function_whitelist = array(
// Directory extension.
'chdir' => true,
'opendir' => true,
'scandir' => true,
// File extension.
'file_exists' => true,
'file_get_contents' => true,
'file' => true,
'fileatime' => true,
'filectime' => true,
'filegroup' => true,
'fileinode' => true,
'filemtime' => true,
'fileowner' => true,
'fileperms' => true,
'filesize' => true,
'filetype' => true,
'fopen' => true,
'is_dir' => true,
'is_executable' => true,
'is_file' => true,
'is_link' => true,
'is_readable' => true,
'is_writable' => true,
'is_writeable' => true,
'lstat' => true,
'mkdir' => true,
'move_uploaded_file' => true,
'readfile' => true,
'readlink' => true,
'rename' => true,
'rmdir' => true,
'stat' => true,
'unlink' => true,
// FTP extension.
'ftp_chdir' => true,
'ftp_login' => true,
'ftp_rename' => true,
// Stream extension.
'stream_select' => true,
'stream_set_chunk_size' => true,
// Zlib extension.
'deflate_add' => true,
'deflate_init' => true,
'inflate_add' => true,
'inflate_init' => true,
'readgzfile' => true,
// Miscellaneous other functions.
'imagecreatefromstring' => true,
'parse_url' => true, // Pre-PHP 5.3.3 an E_WARNING was thrown when URL parsing failed.
'unserialize' => true,
);
/**
* Tokens which are regarded as empty for the purpose of determining
* the name of the called function.
*
* This property is set from within the register() method.
*
* @since 1.1.0
*
* @var array
*/
private $empty_tokens = array();
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 1.1.0
*
* @return array
*/
public function register() {
$this->empty_tokens = Tokens::$emptyTokens;
$this->empty_tokens[ \T_NS_SEPARATOR ] = \T_NS_SEPARATOR;
$this->empty_tokens[ \T_BITWISE_AND ] = \T_BITWISE_AND;
return array(
\T_ASPERAND,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 1.1.0
*
* @param int $stackPtr The position of the current token in the stack.
*/
public function process_token( $stackPtr ) {
// Handle the user-defined custom function whitelist.
$this->custom_whitelist = $this->merge_custom_array( $this->custom_whitelist, array(), false );
$this->custom_whitelist = array_map( 'strtolower', $this->custom_whitelist );
/*
* Check if the error silencing is done for one of the whitelisted functions.
*
* @internal The function call name determination is done even when there is no whitelist active
* to allow the metrics to be more informative.
*/
$next_non_empty = $this->phpcsFile->findNext( $this->empty_tokens, ( $stackPtr + 1 ), null, true, null, true );
if ( false !== $next_non_empty && \T_STRING === $this->tokens[ $next_non_empty ]['code'] ) {
$has_parenthesis = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true );
if ( false !== $has_parenthesis && \T_OPEN_PARENTHESIS === $this->tokens[ $has_parenthesis ]['code'] ) {
$function_name = strtolower( $this->tokens[ $next_non_empty ]['content'] );
if ( ( true === $this->use_default_whitelist
&& isset( $this->function_whitelist[ $function_name ] ) === true )
|| ( ! empty( $this->custom_whitelist )
&& in_array( $function_name, $this->custom_whitelist, true ) === true )
) {
$this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', 'whitelisted function call: ' . $function_name );
return;
}
}
}
$this->context_length = (int) $this->context_length;
$context_length = $this->context_length;
if ( $this->context_length <= 0 ) {
$context_length = 2;
}
// Prepare the "Found" string to display.
$end_of_statement = $this->phpcsFile->findEndOfStatement( $stackPtr, \T_COMMA );
if ( ( $end_of_statement - $stackPtr ) < $context_length ) {
$context_length = ( $end_of_statement - $stackPtr );
}
$found = $this->phpcsFile->getTokensAsString( $stackPtr, $context_length );
$found = str_replace( array( "\t", "\n", "\r" ), ' ', $found ) . '...';
$error_msg = 'Silencing errors is strongly discouraged. Use proper error checking instead.';
$data = array();
if ( $this->context_length > 0 ) {
$error_msg .= ' Found: %s';
$data[] = $found;
}
$this->phpcsFile->addWarning(
$error_msg,
$stackPtr,
'Discouraged',
$data
);
if ( isset( $function_name ) ) {
$this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', '@' . $function_name );
} else {
$this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', $found );
}
}
}
@@ -0,0 +1,76 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Perl compatible regular expressions (PCRE, preg_ functions) should be used in preference
* to their POSIX counterparts.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#regular-expressions
* @link http://php.net/manual/en/ref.regex.php
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0 Previously this check was contained within the
* `WordPress.VIP.RestrictedFunctions` and the
* `WordPress.PHP.DiscouragedPHPFunctions` sniffs.
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class POSIXFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'ereg' => array(
'type' => 'error',
'message' => '%s() has been deprecated since PHP 5.3 and removed in PHP 7.0, please use preg_match() instead.',
'functions' => array(
'ereg',
'eregi',
'sql_regcase',
),
),
'ereg_replace' => array(
'type' => 'error',
'message' => '%s() has been deprecated since PHP 5.3 and removed in PHP 7.0, please use preg_replace() instead.',
'functions' => array(
'ereg_replace',
'eregi_replace',
),
),
'split' => array(
'type' => 'error',
'message' => '%s() has been deprecated since PHP 5.3 and removed in PHP 7.0, please use explode(), str_split() or preg_split() instead.',
'functions' => array(
'split',
'spliti',
),
),
);
}
}
@@ -0,0 +1,69 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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 string $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'
);
}
}
@@ -0,0 +1,48 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Forbids the use of various native PHP functions and suggests alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*/
class RestrictedPHPFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to forbid.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'create_function' => array(
'type' => 'error',
'message' => '%s() is deprecated as of PHP 7.2, please use full fledged functions or anonymous functions instead.',
'functions' => array(
'create_function',
),
),
);
}
}
@@ -0,0 +1,56 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\Sniff;
/**
* Enforces Strict Comparison checks, based upon Squiz code.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.4.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* Last synced with base class ?[unknown date]? at commit ?[unknown commit]?.
* It is currently unclear whether this sniff is actually based on Squiz code on whether the above
* reference to it is a copy/paste oversight.
* @link Possibly: https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/Operators/ComparisonOperatorUsageSniff.php
*/
class StrictComparisonsSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_IS_EQUAL,
\T_IS_NOT_EQUAL,
);
}
/**
* 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 ) {
if ( ! $this->has_whitelist_comment( 'loose comparison', $stackPtr ) ) {
$error = 'Found: ' . $this->tokens[ $stackPtr ]['content'] . '. Use strict comparisons (=== or !==).';
$this->phpcsFile->addWarning( $error, $stackPtr, 'LooseComparison' );
}
}
}
@@ -0,0 +1,105 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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 string $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;
}
}
}
@@ -0,0 +1,99 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\Sniff;
/**
* Verifies the correct usage of type cast keywords.
*
* Type casts should be:
* - normalized, i.e. (float) not (real).
*
* Additionally, the use of the (unset) and (binary) casts is discouraged.
*
* @link https://make.wordpress.org/core/handbook/best-practices/....
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.2.0
* @since 2.0.0 No longer checks that type casts are lowercase or short form.
* Relevant PHPCS native sniffs have been included in the rulesets instead.
*/
class TypeCastsSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_DOUBLE_CAST,
\T_UNSET_CAST,
\T_STRING_CAST,
\T_BINARY_CAST,
);
}
/**
* 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_code = $this->tokens[ $stackPtr ]['code'];
$typecast = str_replace( ' ', '', $this->tokens[ $stackPtr ]['content'] );
$typecast_lc = strtolower( $typecast );
switch ( $token_code ) {
case \T_DOUBLE_CAST:
if ( '(float)' !== $typecast_lc ) {
$fix = $this->phpcsFile->addFixableError(
'Normalized type keywords must be used; expected "(float)" but found "%s"',
$stackPtr,
'DoubleRealFound',
array( $typecast )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( $stackPtr, '(float)' );
}
}
break;
case \T_UNSET_CAST:
$this->phpcsFile->addWarning(
'Using the "(unset)" cast is strongly discouraged. Use the "unset()" language construct or assign "null" as the value to the variable instead.',
$stackPtr,
'UnsetFound'
);
break;
case \T_STRING_CAST:
case \T_BINARY_CAST:
if ( \T_STRING_CAST === $token_code && '(binary)' !== $typecast_lc ) {
break;
}
$this->phpcsFile->addWarning(
'Using binary casting is strongly discouraged. Found: "%s"',
$stackPtr,
'BinaryFound',
array( $typecast )
);
break;
}
}
}
@@ -0,0 +1,125 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\PHP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Enforces Yoda conditional statements.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#yoda-conditions
*
* @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 YodaConditionsSniff extends Sniff {
/**
* The tokens that indicate the start of a condition.
*
* @since 0.12.0
*
* @var array
*/
protected $condition_start_tokens;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
$starters = Tokens::$booleanOperators;
$starters += Tokens::$assignmentTokens;
$starters[ \T_CASE ] = \T_CASE;
$starters[ \T_RETURN ] = \T_RETURN;
$starters[ \T_INLINE_THEN ] = \T_INLINE_THEN;
$starters[ \T_INLINE_ELSE ] = \T_INLINE_ELSE;
$starters[ \T_SEMICOLON ] = \T_SEMICOLON;
$starters[ \T_OPEN_PARENTHESIS ] = \T_OPEN_PARENTHESIS;
$this->condition_start_tokens = $starters;
return array(
\T_IS_EQUAL,
\T_IS_NOT_EQUAL,
\T_IS_IDENTICAL,
\T_IS_NOT_IDENTICAL,
);
}
/**
* 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 ) {
$start = $this->phpcsFile->findPrevious( $this->condition_start_tokens, $stackPtr, null, false, null, true );
$needs_yoda = false;
// Note: going backwards!
for ( $i = $stackPtr; $i > $start; $i-- ) {
// Ignore whitespace.
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
// If this is a variable or array, we've seen all we need to see.
if ( \T_VARIABLE === $this->tokens[ $i ]['code']
|| \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $i ]['code']
) {
$needs_yoda = true;
break;
}
// If this is a function call or something, we are OK.
if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $i ]['code'] ) {
return;
}
}
if ( ! $needs_yoda ) {
return;
}
// Check if this is a var to var comparison, e.g.: if ( $var1 == $var2 ).
$next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( isset( Tokens::$castTokens[ $this->tokens[ $next_non_empty ]['code'] ] ) ) {
$next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true );
}
if ( \in_array( $this->tokens[ $next_non_empty ]['code'], array( \T_SELF, \T_PARENT, \T_STATIC ), true ) ) {
$next_non_empty = $this->phpcsFile->findNext(
( Tokens::$emptyTokens + array( \T_DOUBLE_COLON => \T_DOUBLE_COLON ) ),
( $next_non_empty + 1 ),
null,
true
);
}
if ( \T_VARIABLE === $this->tokens[ $next_non_empty ]['code'] ) {
return;
}
$this->phpcsFile->addError( 'Use Yoda Condition checks, you must.', $stackPtr, 'NotYoda' );
}
}
@@ -0,0 +1,505 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Security;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Verifies that all outputted strings are escaped.
*
* @link http://codex.wordpress.org/Data_Validation Data Validation on WordPress Codex
*
* @package WPCS\WordPressCodingStandards
*
* @since 2013-06-11
* @since 0.4.0 This class now extends the WordPressCS native `Sniff` class.
* @since 0.5.0 The various function list properties which used to be contained in this class
* have been moved to the WordPressCS native `Sniff` parent class.
* @since 0.12.0 This sniff will now also check for output escaping when using shorthand
* echo tags `<?=`.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `XSS` category to the `Security` category.
*/
class EscapeOutputSniff extends Sniff {
/**
* Custom list of functions which escape values for output.
*
* @since 0.5.0
*
* @var string|string[]
*/
public $customEscapingFunctions = array();
/**
* Custom list of functions whose return values are pre-escaped for output.
*
* @since 0.3.0
*
* @var string|string[]
*/
public $customAutoEscapedFunctions = array();
/**
* Custom list of functions which print output incorporating the passed values.
*
* @since 0.4.0
*
* @var string|string[]
*/
public $customPrintingFunctions = array();
/**
* Printing functions that incorporate unsafe values.
*
* @since 0.4.0
* @since 0.11.0 Changed from public static to protected non-static.
*
* @var array
*/
protected $unsafePrintingFunctions = array(
'_e' => 'esc_html_e() or esc_attr_e()',
'_ex' => 'echo esc_html_x() or echo esc_attr_x()',
);
/**
* Cache of previously added custom functions.
*
* Prevents having to do the same merges over and over again.
*
* @since 0.4.0
* @since 0.11.0 - Changed from public static to protected non-static.
* - Changed the format from simple bool to array.
*
* @var array
*/
protected $addedCustomFunctions = array(
'escape' => array(),
'autoescape' => array(),
'sanitize' => array(),
'print' => array(),
);
/**
* List of names of the tokens representing PHP magic constants.
*
* @since 0.10.0
*
* @var array
*/
private $magic_constant_tokens = array(
'T_CLASS_C' => true, // __CLASS__
'T_DIR' => true, // __DIR__
'T_FILE' => true, // __FILE__
'T_FUNC_C' => true, // __FUNCTION__
'T_LINE' => true, // __LINE__
'T_METHOD_C' => true, // __METHOD__
'T_NS_C' => true, // __NAMESPACE__
'T_TRAIT_C' => true, // __TRAIT__
);
/**
* List of names of the native PHP constants which can be considered safe.
*
* @since 1.0.0
*
* @var array
*/
private $safe_php_constants = array(
'PHP_EOL' => true, // String.
'PHP_VERSION' => true, // Integer.
'PHP_MAJOR_VERSION' => true, // Integer.
'PHP_MINOR_VERSION' => true, // Integer.
'PHP_RELEASE_VERSION' => true, // Integer.
'PHP_VERSION_ID' => true, // Integer.
'PHP_EXTRA_VERSION' => true, // String.
'PHP_DEBUG' => true, // Integer.
);
/**
* List of tokens which can be considered as safe when directly part of the output.
*
* @since 0.12.0
*
* @var array
*/
private $safe_components = array(
'T_CONSTANT_ENCAPSED_STRING' => true,
'T_LNUMBER' => true,
'T_MINUS' => true,
'T_PLUS' => true,
'T_MULTIPLY' => true,
'T_DIVIDE' => true,
'T_MODULUS' => true,
'T_TRUE' => true,
'T_FALSE' => true,
'T_NULL' => true,
'T_DNUMBER' => true,
'T_START_NOWDOC' => true,
'T_NOWDOC' => true,
'T_END_NOWDOC' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_ECHO,
\T_PRINT,
\T_EXIT,
\T_STRING,
\T_OPEN_TAG_WITH_ECHO,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
$this->mergeFunctionLists();
$function = $this->tokens[ $stackPtr ]['content'];
// Find the opening parenthesis (if present; T_ECHO might not have it).
$open_paren = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
// If function, not T_ECHO nor T_PRINT.
if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] ) {
// Skip if it is a function but is not one of the printing functions.
if ( ! isset( $this->printingFunctions[ $this->tokens[ $stackPtr ]['content'] ] ) ) {
return;
}
if ( isset( $this->tokens[ $open_paren ]['parenthesis_closer'] ) ) {
$end_of_statement = $this->tokens[ $open_paren ]['parenthesis_closer'];
}
// These functions only need to have the first argument escaped.
if ( \in_array( $function, array( 'trigger_error', 'user_error' ), true ) ) {
$first_param = $this->get_function_call_parameter( $stackPtr, 1 );
$end_of_statement = ( $first_param['end'] + 1 );
unset( $first_param );
}
/*
* If the first param to `_deprecated_file()` follows the typical `basename( __FILE__ )`
* pattern, it doesn't need to be escaped.
*/
if ( '_deprecated_file' === $function ) {
$first_param = $this->get_function_call_parameter( $stackPtr, 1 );
// Quick check. This disregards comments.
if ( preg_match( '`^basename\s*\(\s*__FILE__\s*\)$`', $first_param['raw'] ) === 1 ) {
$stackPtr = ( $first_param['end'] + 2 );
}
unset( $first_param );
}
}
// Checking for the ignore comment, ex: //xss ok.
if ( $this->has_whitelist_comment( 'xss', $stackPtr ) ) {
return;
}
if ( isset( $this->unsafePrintingFunctions[ $function ] ) ) {
$error = $this->phpcsFile->addError(
"All output should be run through an escaping function (like %s), found '%s'.",
$stackPtr,
'UnsafePrintingFunction',
array( $this->unsafePrintingFunctions[ $function ], $function )
);
// If the error was reported, don't bother checking the function's arguments.
if ( $error ) {
return isset( $end_of_statement ) ? $end_of_statement : null;
}
}
$ternary = false;
// This is already determined if this is a function and not T_ECHO.
if ( ! isset( $end_of_statement ) ) {
$end_of_statement = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), $stackPtr );
$last_token = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $end_of_statement - 1 ), null, true );
// Check for the ternary operator. We only need to do this here if this
// echo is lacking parenthesis. Otherwise it will be handled below.
if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $open_paren ]['code'] || \T_CLOSE_PARENTHESIS !== $this->tokens[ $last_token ]['code'] ) {
$ternary = $this->phpcsFile->findNext( \T_INLINE_THEN, $stackPtr, $end_of_statement );
// If there is a ternary skip over the part before the ?. However, if
// the ternary is within parentheses, it will be handled in the loop.
if ( false !== $ternary && empty( $this->tokens[ $ternary ]['nested_parenthesis'] ) ) {
$stackPtr = $ternary;
}
}
}
// Ignore the function itself.
$stackPtr++;
$in_cast = false;
// Looping through echo'd components.
$watch = true;
for ( $i = $stackPtr; $i < $end_of_statement; $i++ ) {
// Ignore whitespaces and comments.
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
// Ignore namespace separators.
if ( \T_NS_SEPARATOR === $this->tokens[ $i ]['code'] ) {
continue;
}
if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] ) {
if ( ! isset( $this->tokens[ $i ]['parenthesis_closer'] ) ) {
// Live coding or parse error.
break;
}
if ( $in_cast ) {
// Skip to the end of a function call if it has been casted to a safe value.
$i = $this->tokens[ $i ]['parenthesis_closer'];
$in_cast = false;
} else {
// Skip over the condition part of a ternary (i.e., to after the ?).
$ternary = $this->phpcsFile->findNext( \T_INLINE_THEN, $i, $this->tokens[ $i ]['parenthesis_closer'] );
if ( false !== $ternary ) {
$next_paren = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, ( $i + 1 ), $this->tokens[ $i ]['parenthesis_closer'] );
// We only do it if the ternary isn't within a subset of parentheses.
if ( false === $next_paren || ( isset( $this->tokens[ $next_paren ]['parenthesis_closer'] ) && $ternary > $this->tokens[ $next_paren ]['parenthesis_closer'] ) ) {
$i = $ternary;
}
}
}
continue;
}
// Handle arrays for those functions that accept them.
if ( \T_ARRAY === $this->tokens[ $i ]['code'] ) {
$i++; // Skip the opening parenthesis.
continue;
}
if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $i ]['code']
|| \T_CLOSE_SHORT_ARRAY === $this->tokens[ $i ]['code']
) {
continue;
}
if ( \in_array( $this->tokens[ $i ]['code'], array( \T_DOUBLE_ARROW, \T_CLOSE_PARENTHESIS ), true ) ) {
continue;
}
// Handle magic constants for debug functions.
if ( isset( $this->magic_constant_tokens[ $this->tokens[ $i ]['type'] ] ) ) {
continue;
}
// Handle safe PHP native constants.
if ( \T_STRING === $this->tokens[ $i ]['code']
&& isset( $this->safe_php_constants[ $this->tokens[ $i ]['content'] ] )
&& $this->is_use_of_global_constant( $i )
) {
continue;
}
// Wake up on concatenation characters, another part to check.
if ( \T_STRING_CONCAT === $this->tokens[ $i ]['code'] ) {
$watch = true;
continue;
}
// Wake up after a ternary else (:).
if ( false !== $ternary && \T_INLINE_ELSE === $this->tokens[ $i ]['code'] ) {
$watch = true;
continue;
}
// Wake up for commas.
if ( \T_COMMA === $this->tokens[ $i ]['code'] ) {
$in_cast = false;
$watch = true;
continue;
}
if ( false === $watch ) {
continue;
}
// Allow T_CONSTANT_ENCAPSED_STRING eg: echo 'Some String';
// Also T_LNUMBER, e.g.: echo 45; exit -1; and booleans.
if ( isset( $this->safe_components[ $this->tokens[ $i ]['type'] ] ) ) {
continue;
}
$watch = false;
// Allow int/double/bool casted variables.
if ( isset( $this->safe_casts[ $this->tokens[ $i ]['code'] ] ) ) {
$in_cast = true;
continue;
}
// Now check that next token is a function call.
if ( \T_STRING === $this->tokens[ $i ]['code'] ) {
$ptr = $i;
$functionName = $this->tokens[ $i ]['content'];
$function_opener = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, ( $i + 1 ), null, false, null, true );
$is_formatting_function = isset( $this->formattingFunctions[ $functionName ] );
if ( false !== $function_opener ) {
if ( isset( $this->arrayWalkingFunctions[ $functionName ] ) ) {
// Get the callback parameter.
$callback = $this->get_function_call_parameter(
$ptr,
$this->arrayWalkingFunctions[ $functionName ]
);
if ( ! empty( $callback ) ) {
/*
* If this is a function callback (not a method callback array) and we're able
* to resolve the function name, do so.
*/
$mapped_function = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$callback['start'],
( $callback['end'] + 1 ),
true
);
if ( false !== $mapped_function
&& \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $mapped_function ]['code']
) {
$functionName = $this->strip_quotes( $this->tokens[ $mapped_function ]['content'] );
$ptr = $mapped_function;
}
}
}
// Skip pointer to after the function.
// If this is a formatting function we just skip over the opening
// parenthesis. Otherwise we skip all the way to the closing.
if ( $is_formatting_function ) {
$i = ( $function_opener + 1 );
$watch = true;
} else {
if ( isset( $this->tokens[ $function_opener ]['parenthesis_closer'] ) ) {
$i = $this->tokens[ $function_opener ]['parenthesis_closer'];
} else {
// Live coding or parse error.
break;
}
}
}
// If this is a safe function, we don't flag it.
if (
$is_formatting_function
|| isset( $this->autoEscapedFunctions[ $functionName ] )
|| isset( $this->escapingFunctions[ $functionName ] )
) {
continue;
}
$content = $functionName;
} else {
$content = $this->tokens[ $i ]['content'];
$ptr = $i;
}
// Make the error message a little more informative for array access variables.
if ( \T_VARIABLE === $this->tokens[ $ptr ]['code'] ) {
$array_keys = $this->get_array_access_keys( $ptr );
if ( ! empty( $array_keys ) ) {
$content .= '[' . implode( '][', $array_keys ) . ']';
}
}
$this->phpcsFile->addError(
"All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '%s'.",
$ptr,
'OutputNotEscaped',
array( $content )
);
}
return $end_of_statement;
}
/**
* Merge custom functions provided via a custom ruleset with the defaults, if we haven't already.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @return void
*/
protected function mergeFunctionLists() {
if ( $this->customEscapingFunctions !== $this->addedCustomFunctions['escape'] ) {
$customEscapeFunctions = $this->merge_custom_array( $this->customEscapingFunctions, array(), false );
$this->escapingFunctions = $this->merge_custom_array(
$customEscapeFunctions,
$this->escapingFunctions
);
$this->addedCustomFunctions['escape'] = $this->customEscapingFunctions;
}
if ( $this->customAutoEscapedFunctions !== $this->addedCustomFunctions['autoescape'] ) {
$this->autoEscapedFunctions = $this->merge_custom_array(
$this->customAutoEscapedFunctions,
$this->autoEscapedFunctions
);
$this->addedCustomFunctions['autoescape'] = $this->customAutoEscapedFunctions;
}
if ( $this->customPrintingFunctions !== $this->addedCustomFunctions['print'] ) {
$this->printingFunctions = $this->merge_custom_array(
$this->customPrintingFunctions,
$this->printingFunctions
);
$this->addedCustomFunctions['print'] = $this->customPrintingFunctions;
}
}
}
@@ -0,0 +1,178 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Security;
use WordPressCS\WordPress\Sniff;
/**
* Checks that nonce verification accompanies form processing.
*
* @link https://developer.wordpress.org/plugins/security/nonces/ Nonces on Plugin Developer Handbook
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.5.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `CSRF` category to the `Security` category.
*/
class NonceVerificationSniff extends Sniff {
/**
* Superglobals to notify about when not accompanied by an nonce check.
*
* A value of `true` results in an error. A value of `false` in a warning.
*
* @since 0.12.0
*
* @var array
*/
protected $superglobals = array(
'$_POST' => true,
'$_FILE' => true,
'$_GET' => false,
'$_REQUEST' => false,
);
/**
* Custom list of functions which verify nonces.
*
* @since 0.5.0
*
* @var string|string[]
*/
public $customNonceVerificationFunctions = array();
/**
* Custom list of functions that sanitize the values passed to them.
*
* @since 0.11.0
*
* @var string|string[]
*/
public $customSanitizingFunctions = array();
/**
* Custom sanitizing functions that implicitly unslash the values passed to them.
*
* @since 0.11.0
*
* @var string|string[]
*/
public $customUnslashingSanitizingFunctions = array();
/**
* Cache of previously added custom functions.
*
* Prevents having to do the same merges over and over again.
*
* @since 0.5.0
* @since 0.11.0 - Changed from public static to protected non-static.
* - Changed the format from simple bool to array.
*
* @var array
*/
protected $addedCustomFunctions = array(
'nonce' => array(),
'sanitize' => array(),
'unslashsanitize' => array(),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_VARIABLE,
);
}
/**
* 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 ) {
$instance = $this->tokens[ $stackPtr ];
if ( ! isset( $this->superglobals[ $instance['content'] ] ) ) {
return;
}
if ( $this->has_whitelist_comment( 'CSRF', $stackPtr ) ) {
return;
}
if ( $this->is_assignment( $stackPtr ) ) {
return;
}
$this->mergeFunctionLists();
if ( $this->has_nonce_check( $stackPtr ) ) {
return;
}
$error_code = 'Missing';
if ( false === $this->superglobals[ $instance['content'] ] ) {
$error_code = 'Recommended';
}
// If we're still here, no nonce-verification function was found.
$this->addMessage(
'Processing form data without nonce verification.',
$stackPtr,
$this->superglobals[ $instance['content'] ],
$error_code
);
}
/**
* Merge custom functions provided via a custom ruleset with the defaults, if we haven't already.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @return void
*/
protected function mergeFunctionLists() {
if ( $this->customNonceVerificationFunctions !== $this->addedCustomFunctions['nonce'] ) {
$this->nonceVerificationFunctions = $this->merge_custom_array(
$this->customNonceVerificationFunctions,
$this->nonceVerificationFunctions
);
$this->addedCustomFunctions['nonce'] = $this->customNonceVerificationFunctions;
}
if ( $this->customSanitizingFunctions !== $this->addedCustomFunctions['sanitize'] ) {
$this->sanitizingFunctions = $this->merge_custom_array(
$this->customSanitizingFunctions,
$this->sanitizingFunctions
);
$this->addedCustomFunctions['sanitize'] = $this->customSanitizingFunctions;
}
if ( $this->customUnslashingSanitizingFunctions !== $this->addedCustomFunctions['unslashsanitize'] ) {
$this->unslashingSanitizingFunctions = $this->merge_custom_array(
$this->customUnslashingSanitizingFunctions,
$this->unslashingSanitizingFunctions
);
$this->addedCustomFunctions['unslashsanitize'] = $this->customUnslashingSanitizingFunctions;
}
}
}
@@ -0,0 +1,89 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Security;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
/**
* Warn about __FILE__ for page registration.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#using-__file__-for-page-registration
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @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.
* @since 1.0.0 This sniff has been moved from the `VIP` category to the `Security` category.
*/
class PluginMenuSlugSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 0.11.0
*
* @var string
*/
protected $group_name = 'add_menu_functions';
/**
* Functions which can be used to add pages to the WP Admin menu.
*
* @since 0.3.0
* @since 0.11.0 Renamed from $add_menu_functions to $target_functions
* and changed visibility to protected.
*
* @var array <string function name> => <array target parameter positions>
*/
protected $target_functions = array(
'add_menu_page' => array( 4 ),
'add_object_page' => array( 4 ),
'add_utility_page' => array( 4 ),
'add_submenu_page' => array( 1, 5 ),
'add_dashboard_page' => array( 4 ),
'add_posts_page' => array( 4 ),
'add_media_page' => array( 4 ),
'add_links_page' => array( 4 ),
'add_pages_page' => array( 4 ),
'add_comments_page' => array( 4 ),
'add_theme_page' => array( 4 ),
'add_plugins_page' => array( 4 ),
'add_users_page' => array( 4 ),
'add_management_page' => array( 4 ),
'add_options_page' => array( 4 ),
);
/**
* Process the parameters of a matched function.
*
* @since 0.11.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $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 ) {
foreach ( $this->target_functions[ $matched_content ] as $position ) {
if ( isset( $parameters[ $position ] ) ) {
$file_constant = $this->phpcsFile->findNext( \T_FILE, $parameters[ $position ]['start'], ( $parameters[ $position ]['end'] + 1 ) );
if ( false !== $file_constant ) {
$this->phpcsFile->addWarning( 'Using __FILE__ for menu slugs risks exposing filesystem structure.', $stackPtr, 'Using__FILE__' );
}
}
}
}
}
@@ -0,0 +1,48 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Security;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Encourages use of wp_safe_redirect() to avoid open redirect vulnerabilities.
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.0.0
*/
class SafeRedirectSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'wp_redirect' => array(
'type' => 'warning',
'message' => '%s() found. Using wp_safe_redirect(), along with the allowed_redirect_hosts filter if needed, can help avoid any chances of malicious redirects within code. It is also important to remember to call exit() after a redirect so that no other unwanted code is executed.',
'functions' => array(
'wp_redirect',
),
),
);
}
}
@@ -0,0 +1,233 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Security;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Flag any non-validated/sanitized input ( _GET / _POST / etc. ).
*
* @link https://github.com/WordPress/WordPress-Coding-Standards/issues/69
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.4.0 This class now extends the WordPressCS native `Sniff` class.
* @since 0.5.0 Method getArrayIndexKey() has been moved to the WordPressCS native `Sniff` class.
* @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 `Security` category.
*/
class ValidatedSanitizedInputSniff extends Sniff {
/**
* Check for validation functions for a variable within its own parenthesis only.
*
* @var boolean
*/
public $check_validation_in_scope_only = false;
/**
* Custom list of functions that sanitize the values passed to them.
*
* @since 0.5.0
*
* @var string|string[]
*/
public $customSanitizingFunctions = array();
/**
* Custom sanitizing functions that implicitly unslash the values passed to them.
*
* @since 0.5.0
*
* @var string|string[]
*/
public $customUnslashingSanitizingFunctions = array();
/**
* Cache of previously added custom functions.
*
* Prevents having to do the same merges over and over again.
*
* @since 0.5.0
* @since 0.11.0 - Changed from static to non-static.
* - Changed the format from simple bool to array.
*
* @var array
*/
protected $addedCustomFunctions = array(
'sanitize' => array(),
'unslashsanitize' => array(),
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_VARIABLE,
\T_DOUBLE_QUOTED_STRING,
\T_HEREDOC,
);
}
/**
* 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 ) {
$superglobals = $this->input_superglobals;
// Handling string interpolation.
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $stackPtr ]['code']
|| \T_HEREDOC === $this->tokens[ $stackPtr ]['code']
) {
$interpolated_variables = array_map(
function ( $symbol ) {
return '$' . $symbol;
},
$this->get_interpolated_variables( $this->tokens[ $stackPtr ]['content'] )
);
foreach ( array_intersect( $interpolated_variables, $superglobals ) as $bad_variable ) {
$this->phpcsFile->addError( 'Detected usage of a non-sanitized, non-validated input variable %s: %s', $stackPtr, 'InputNotValidatedNotSanitized', array( $bad_variable, $this->tokens[ $stackPtr ]['content'] ) );
}
return;
}
// Check if this is a superglobal.
if ( ! \in_array( $this->tokens[ $stackPtr ]['content'], $superglobals, true ) ) {
return;
}
// If we're overriding a superglobal with an assignment, no need to test.
if ( $this->is_assignment( $stackPtr ) ) {
return;
}
// This superglobal is being validated.
if ( $this->is_in_isset_or_empty( $stackPtr ) ) {
return;
}
$array_keys = $this->get_array_access_keys( $stackPtr );
if ( empty( $array_keys ) ) {
return;
}
$error_data = array( $this->tokens[ $stackPtr ]['content'] . '[' . implode( '][', $array_keys ) . ']' );
/*
* Check for validation first.
*/
$validated = false;
for ( $i = ( $stackPtr + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $i ]['code']
&& isset( $this->tokens[ $i ]['bracket_closer'] )
) {
// Skip over array keys.
$i = $this->tokens[ $i ]['bracket_closer'];
continue;
}
if ( \T_COALESCE === $this->tokens[ $i ]['code'] ) {
$validated = true;
}
// Anything else means this is not a validation coalesce.
break;
}
if ( false === $validated ) {
$validated = $this->is_validated( $stackPtr, $array_keys, $this->check_validation_in_scope_only );
}
if ( false === $validated ) {
$this->phpcsFile->addError(
'Detected usage of a possibly undefined superglobal array index: %s. Use isset() or empty() to check the index exists before using it',
$stackPtr,
'InputNotValidated',
$error_data
);
}
if ( $this->has_whitelist_comment( 'sanitization', $stackPtr ) ) {
return;
}
// If this variable is being tested with one of the `is_..()` functions, sanitization isn't needed.
if ( $this->is_in_type_test( $stackPtr ) ) {
return;
}
// If this is a comparison ('a' == $_POST['foo']), sanitization isn't needed.
if ( $this->is_comparison( $stackPtr, false ) ) {
return;
}
// If this is a comparison using the array comparison functions, sanitization isn't needed.
if ( $this->is_in_array_comparison( $stackPtr ) ) {
return;
}
$this->mergeFunctionLists();
// Now look for sanitizing functions.
if ( ! $this->is_sanitized( $stackPtr, true ) ) {
$this->phpcsFile->addError(
'Detected usage of a non-sanitized input variable: %s',
$stackPtr,
'InputNotSanitized',
$error_data
);
}
}
/**
* Merge custom functions provided via a custom ruleset with the defaults, if we haven't already.
*
* @since 0.11.0 Split out from the `process()` method.
*
* @return void
*/
protected function mergeFunctionLists() {
if ( $this->customSanitizingFunctions !== $this->addedCustomFunctions['sanitize'] ) {
$this->sanitizingFunctions = $this->merge_custom_array(
$this->customSanitizingFunctions,
$this->sanitizingFunctions
);
$this->addedCustomFunctions['sanitize'] = $this->customSanitizingFunctions;
}
if ( $this->customUnslashingSanitizingFunctions !== $this->addedCustomFunctions['unslashsanitize'] ) {
$this->unslashingSanitizingFunctions = $this->merge_custom_array(
$this->customUnslashingSanitizingFunctions,
$this->unslashingSanitizingFunctions
);
$this->addedCustomFunctions['unslashsanitize'] = $this->customUnslashingSanitizingFunctions;
}
}
}
@@ -0,0 +1,731 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\Utils;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Comprehensive I18n text domain fixer tool.
*
* This sniff can:
* - Add missing text domains.
* - Replace text domains based on an array of `old` values to a `new` value.
*
* Note: Without a user-defined configuration in a custom ruleset, this sniff will be ignored.
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.2.0
*/
class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff {
/**
* A list of tokenizers this sniff supports.
*
* @since 1.2.0
*
* @var array
*/
public $supportedTokenizers = array(
'PHP',
'CSS',
);
/**
* Old text domain(s) to replace.
*
* @since 1.2.0
*
* @var string[]|string
*/
public $old_text_domain;
/**
* New text domain.
*
* @since 1.2.0
*
* @var string
*/
public $new_text_domain = '';
/**
* The group name for this group of functions.
*
* @since 1.2.0
*
* @var string
*/
protected $group_name = 'i18nfixer';
/**
* The WP Internationalization related functions to target for the replacements.
*
* @since 1.2.0
*
* @var array <string function name> => <int parameter position>
*/
protected $target_functions = array(
'load_textdomain' => 1,
'load_plugin_textdomain' => 1,
'load_muplugin_textdomain' => 1,
'load_theme_textdomain' => 1,
'load_child_theme_textdomain' => 1,
'unload_textdomain' => 1,
'__' => 2,
'_e' => 2,
'_x' => 3,
'_ex' => 3,
'_n' => 4,
'_nx' => 5,
'_n_noop' => 3,
'_nx_noop' => 4,
'translate_nooped_plural' => 3,
'_c' => 2, // Deprecated.
'_nc' => 4, // Deprecated.
'__ngettext' => 4, // Deprecated.
'__ngettext_noop' => 3, // Deprecated.
'translate_with_context' => 2, // Deprecated.
'esc_html__' => 2,
'esc_html_e' => 2,
'esc_html_x' => 3,
'esc_attr__' => 2,
'esc_attr_e' => 2,
'esc_attr_x' => 3,
'is_textdomain_loaded' => 1,
'get_translations_for_domain' => 1,
// Shouldn't be used by plugins/themes.
'translate' => 2,
'translate_with_gettext_context' => 3,
// WP private functions. Shouldn't be used by plugins/themes.
'_load_textdomain_just_in_time' => 1,
'_get_path_to_translation_from_lang_dir' => 1,
'_get_path_to_translation' => 1,
);
/**
* Whether a valid new text domain was found.
*
* @since 1.2.0
*
* @var bool
*/
private $is_valid = false;
/**
* The new text domain as validated.
*
* @since 1.2.0
*
* @var string
*/
private $validated_textdomain = '';
/**
* Whether the plugin/theme header has been seen and fixed yet.
*
* @since 1.2.0
*
* @var bool
*/
private $header_found = false;
/**
* Possible headers for a theme.
*
* @link https://developer.wordpress.org/themes/basics/main-stylesheet-style-css/
*
* @since 1.2.0
*
* @var array Array key is the header name, the value indicated whether it is a
* required (true) or optional (false) header.
*/
private $theme_headers = array(
'Theme Name' => true,
'Theme URI' => false,
'Author' => true,
'Author URI' => false,
'Description' => true,
'Version' => true,
'License' => true,
'License URI' => true,
'Tags' => false,
'Text Domain' => true,
'Domain Path' => false,
);
/**
* Possible headers for a plugin.
*
* @link https://developer.wordpress.org/plugins/the-basics/header-requirements/
*
* @since 1.2.0
*
* @var array Array key is the header name, the value indicated whether it is a
* required (true) or optional (false) header.
*/
private $plugin_headers = array(
'Plugin Name' => true,
'Plugin URI' => false,
'Description' => false,
'Version' => false,
'Author' => false,
'Author URI' => false,
'License' => false,
'License URI' => false,
'Text Domain' => false,
'Domain Path' => false,
'Network' => false,
);
/**
* Regex template to match theme/plugin headers.
*
* @since 1.2.0
*
* @var string
*/
private $header_regex_template = '`^(?:\s*(?:(?:\*|//)\s*)?)?(%s)\s*:\s*([^\r\n]+)`';
/**
* Regex to match theme headers.
*
* Set from within the register() method.
*
* @since 1.2.0
*
* @var string
*/
private $theme_header_regex;
/**
* Regex to match plugin headers.
*
* Set from within the register() method.
*
* @since 1.2.0
*
* @var string
*/
private $plugin_header_regex;
/**
* The --tab-width CLI value that is being used.
*
* @since 1.2.0
*
* @var integer
*/
private $tab_width = null;
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 1.2.0
*
* @return array
*/
public function register() {
$headers = array_map(
'preg_quote',
array_keys( $this->theme_headers ),
array_fill( 0, \count( $this->theme_headers ), '`' )
);
$this->theme_header_regex = sprintf( $this->header_regex_template, implode( '|', $headers ) );
$headers = array_map(
'preg_quote',
array_keys( $this->plugin_headers ),
array_fill( 0, \count( $this->plugin_headers ), '`' )
);
$this->plugin_header_regex = sprintf( $this->header_regex_template, implode( '|', $headers ) );
$targets = parent::register();
$targets[] = \T_DOC_COMMENT_OPEN_TAG;
$targets[] = \T_COMMENT;
return $targets;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 1.2.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 ) {
// Check if the old/new properties are correctly set. If not, bow out.
if ( ! is_string( $this->new_text_domain )
|| '' === $this->new_text_domain
) {
return ( $this->phpcsFile->numTokens + 1 );
}
if ( isset( $this->old_text_domain ) ) {
$this->old_text_domain = $this->merge_custom_array( $this->old_text_domain, array(), false );
if ( ! is_array( $this->old_text_domain )
|| array() === $this->old_text_domain
) {
return ( $this->phpcsFile->numTokens + 1 );
}
}
// Only validate and throw warning about the text domain once.
if ( $this->new_text_domain !== $this->validated_textdomain ) {
$this->is_valid = false;
$this->validated_textdomain = $this->new_text_domain;
$this->header_found = false;
if ( 'default' === $this->new_text_domain ) {
$this->phpcsFile->addWarning(
'The "default" text domain is reserved for WordPress core use and should not be used by plugins or themes',
0,
'ReservedNewDomain',
array( $this->new_text_domain )
);
return ( $this->phpcsFile->numTokens + 1 );
}
if ( preg_match( '`^[a-z0-9-]+$`', $this->new_text_domain ) !== 1 ) {
$this->phpcsFile->addWarning(
'The text domain should be a simple lowercase text string with words separated by dashes. "%s" appears invalid',
0,
'InvalidNewDomain',
array( $this->new_text_domain )
);
return ( $this->phpcsFile->numTokens + 1 );
}
// If the text domain passed both validations, it should be considered valid.
$this->is_valid = true;
} elseif ( false === $this->is_valid ) {
return ( $this->phpcsFile->numTokens + 1 );
}
if ( isset( $this->tab_width ) === false ) {
if ( isset( $this->phpcsFile->config->tabWidth ) === false
|| 0 === $this->phpcsFile->config->tabWidth
) {
// We have no idea how wide tabs are, so assume 4 spaces for fixing.
$this->tab_width = 4;
} else {
$this->tab_width = $this->phpcsFile->config->tabWidth;
}
}
if ( \T_DOC_COMMENT_OPEN_TAG === $this->tokens[ $stackPtr ]['code']
|| \T_COMMENT === $this->tokens[ $stackPtr ]['code']
) {
// Examine for plugin/theme file header.
return $this->process_comments( $stackPtr );
} elseif ( 'CSS' !== $this->phpcsFile->tokenizerType ) {
// Examine a T_STRING token in a PHP file as a function call.
return parent::process_token( $stackPtr );
}
}
/**
* Process the parameters of a matched function.
*
* @since 1.2.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $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 ) {
$target_param = $this->target_functions[ $matched_content ];
if ( isset( $parameters[ $target_param ] ) === false && 1 !== $target_param ) {
$error_msg = 'Missing $domain arg';
$error_code = 'MissingArgDomain';
if ( isset( $parameters[ ( $target_param - 1 ) ] ) ) {
$fix = $this->phpcsFile->addFixableError( $error_msg, $stackPtr, $error_code );
if ( true === $fix ) {
$start_previous = $parameters[ ( $target_param - 1 ) ]['start'];
$end_previous = $parameters[ ( $target_param - 1 ) ]['end'];
if ( \T_WHITESPACE === $this->tokens[ $start_previous ]['code']
&& $this->tokens[ $start_previous ]['content'] === $this->phpcsFile->eolChar
) {
// Replicate the new line + indentation of the previous item.
$replacement = ',';
for ( $i = $start_previous; $i <= $end_previous; $i++ ) {
if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
break;
}
if ( isset( $this->tokens[ $i ]['orig_content'] ) ) {
$replacement .= $this->tokens[ $i ]['orig_content'];
} else {
$replacement .= $this->tokens[ $i ]['content'];
}
}
$replacement .= "'{$this->new_text_domain}'";
} else {
$replacement = ", '{$this->new_text_domain}'";
}
if ( \T_WHITESPACE === $this->tokens[ $end_previous ]['code'] ) {
$this->phpcsFile->fixer->addContentBefore( $end_previous, $replacement );
} else {
$this->phpcsFile->fixer->addContent( $end_previous, $replacement );
}
}
} else {
$error_msg .= ' and preceding argument(s)';
$error_code = 'MissingArgs';
// Expected preceeding param also missing, just throw the warning.
$this->phpcsFile->addWarning( $error_msg, $stackPtr, $error_code );
}
return;
}
// Target parameter found. Let's examine it.
$domain_param_start = $parameters[ $target_param ]['start'];
$domain_param_end = $parameters[ $target_param ]['end'];
$domain_token = null;
for ( $i = $domain_param_start; $i <= $domain_param_end; $i++ ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
continue;
}
if ( \T_CONSTANT_ENCAPSED_STRING !== $this->tokens[ $i ]['code'] ) {
// Unexpected token found, not our concern. This is handled by the I18n sniff.
return;
}
if ( isset( $domain_token ) ) {
// More than one T_CONSTANT_ENCAPSED_STRING found, not our concern. This is handled by the I18n sniff.
return;
}
$domain_token = $i;
}
// If we're still here, this means only one T_CONSTANT_ENCAPSED_STRING was found.
$old_domain = $this->strip_quotes( $this->tokens[ $domain_token ]['content'] );
if ( ! \in_array( $old_domain, $this->old_text_domain, true ) ) {
// Not a text domain targetted for replacement, ignore.
return;
}
$fix = $this->phpcsFile->addFixableError(
'Mismatched text domain. Expected \'%s\' but found \'%s\'',
$domain_token,
'TextDomainMismatch',
array( $this->new_text_domain, $old_domain )
);
if ( true === $fix ) {
$replacement = str_replace( $old_domain, $this->new_text_domain, $this->tokens[ $domain_token ]['content'] );
$this->phpcsFile->fixer->replaceToken( $domain_token, $replacement );
}
}
/**
* Process the function if no parameters were found.
*
* @since 1.2.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
*
* @return void
*/
public function process_no_parameters( $stackPtr, $group_name, $matched_content ) {
$target_param = $this->target_functions[ $matched_content ];
if ( 1 !== $target_param ) {
// Only process the no param case as fixable if the text domain is expected to be the first parameter.
$this->phpcsFile->addWarning( 'Missing $domain arg and preceding argument(s)', $stackPtr, 'MissingArgs' );
return;
}
$opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $opener ]['code']
|| isset( $this->tokens[ $opener ]['parenthesis_closer'] ) === false
) {
// Parse error or live coding.
return;
}
$fix = $this->phpcsFile->addFixableError( 'Missing $domain arg', $stackPtr, 'MissingArgDomain' );
if ( true === $fix ) {
$closer = $this->tokens[ $opener ]['parenthesis_closer'];
$replacement = " '{$this->new_text_domain}' ";
if ( $this->tokens[ $opener ]['line'] !== $this->tokens[ $closer ]['line'] ) {
$replacement = trim( $replacement );
$addBefore = ( $closer - 1 );
if ( \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
&& $this->tokens[ $closer - 1 ]['line'] === $this->tokens[ $closer ]['line']
) {
if ( isset( $this->tokens[ ( $closer - 1 ) ]['orig_content'] ) ) {
$replacement = $this->tokens[ ( $closer - 1 ) ]['orig_content']
. "\t"
. $replacement;
} else {
$replacement = $this->tokens[ ( $closer - 1 ) ]['content']
. str_repeat( ' ', $this->tab_width )
. $replacement;
}
--$addBefore;
} else {
// We don't know whether the code uses tabs or spaces, so presume WPCS, i.e. tabs.
$replacement = "\t" . $replacement;
}
$replacement = $this->phpcsFile->eolChar . $replacement;
$this->phpcsFile->fixer->addContentBefore( $addBefore, $replacement );
} elseif ( \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code'] ) {
$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), $replacement );
} else {
$this->phpcsFile->fixer->addContentBefore( $closer, $replacement );
}
}
}
/**
* Process comments to find the plugin/theme headers.
*
* @since 1.2.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_comments( $stackPtr ) {
if ( true === $this->header_found && ! defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) {
return;
}
$regex = $this->plugin_header_regex;
$headers = $this->plugin_headers;
$type = 'plugin';
$skip_to = $stackPtr;
$file = $this->strip_quotes( $this->phpcsFile->getFileName() );
if ( 'STDIN' === $file ) {
return;
}
$file_name = basename( $file );
if ( 'CSS' === $this->phpcsFile->tokenizerType ) {
if ( 'style.css' !== $file_name && ! defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) {
// CSS files only need to be examined for the file header.
return ( $this->phpcsFile->numTokens + 1 );
}
$regex = $this->theme_header_regex;
$headers = $this->theme_headers;
$type = 'theme';
}
$comment_details = array(
'required_header_found' => false,
'headers_found' => 0,
'text_domain_ptr' => false,
'text_domain_found' => '',
'last_header_ptr' => false,
'last_header_matches' => array(),
);
if ( \T_COMMENT === $this->tokens[ $stackPtr ]['code'] ) {
$block_comment = false;
if ( substr( $this->tokens[ $stackPtr ]['content'], 0, 2 ) === '/*' ) {
$block_comment = true;
}
$current = $stackPtr;
do {
if ( false === $comment_details['text_domain_ptr']
|| false === $comment_details['required_header_found']
|| $comment_details['headers_found'] < 3
) {
$comment_details = $this->examine_comment_line( $current, $regex, $headers, $comment_details );
}
if ( true === $block_comment && substr( $this->tokens[ $current ]['content'], -2 ) === '*/' ) {
++$current;
break;
}
++$current;
} while ( isset( $this->tokens[ $current ] ) && \T_COMMENT === $this->tokens[ $current ]['code'] );
$skip_to = $current;
} else {
if ( ! isset( $this->tokens[ $stackPtr ]['comment_closer'] ) ) {
return;
}
$closer = $this->tokens[ $stackPtr ]['comment_closer'];
$current = $stackPtr;
while ( ( $current = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, ( $current + 1 ), $closer ) ) !== false ) {
$comment_details = $this->examine_comment_line( $current, $regex, $headers, $comment_details );
if ( false !== $comment_details['text_domain_ptr']
&& true === $comment_details['required_header_found']
&& $comment_details['headers_found'] >= 3
) {
// No need to look at the rest of the docblock.
break;
}
}
$skip_to = $closer;
}
// So, was this the plugin/theme header ?
if ( true === $comment_details['required_header_found']
&& $comment_details['headers_found'] >= 3
) {
$this->header_found = true;
$text_domain_ptr = $comment_details['text_domain_ptr'];
$text_domain_found = $comment_details['text_domain_found'];
if ( false !== $text_domain_ptr ) {
if ( $this->new_text_domain !== $text_domain_found
&& ( \in_array( $text_domain_found, $this->old_text_domain, true ) )
) {
$fix = $this->phpcsFile->addFixableError(
'Mismatched text domain in %s header. Expected \'%s\' but found \'%s\'',
$text_domain_ptr,
'TextDomainHeaderMismatch',
array(
$type,
$this->new_text_domain,
$text_domain_found,
)
);
if ( true === $fix ) {
if ( isset( $this->tokens[ $text_domain_ptr ]['orig_content'] ) ) {
$replacement = $this->tokens[ $text_domain_ptr ]['orig_content'];
} else {
$replacement = $this->tokens[ $text_domain_ptr ]['content'];
}
$replacement = str_replace( $text_domain_found, $this->new_text_domain, $replacement );
$this->phpcsFile->fixer->replaceToken( $text_domain_ptr, $replacement );
}
}
} else {
$last_header_ptr = $comment_details['last_header_ptr'];
$last_header_matches = $comment_details['last_header_matches'];
$fix = $this->phpcsFile->addFixableError(
'Missing "Text Domain" in %s header',
$last_header_ptr,
'MissingTextDomainHeader',
array( $type )
);
if ( true === $fix ) {
if ( isset( $this->tokens[ $last_header_ptr ]['orig_content'] ) ) {
$replacement = $this->tokens[ $last_header_ptr ]['orig_content'];
} else {
$replacement = $this->tokens[ $last_header_ptr ]['content'];
}
$replacement = str_replace( $last_header_matches[1], 'Text Domain', $replacement );
$replacement = str_replace( $last_header_matches[2], $this->new_text_domain, $replacement );
if ( \T_DOC_COMMENT_OPEN_TAG === $this->tokens[ $stackPtr ]['code'] ) {
for ( $i = ( $last_header_ptr - 1 ); ; $i-- ) {
if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $last_header_ptr ]['line'] ) {
++$i;
break;
}
}
$replacement = $this->phpcsFile->eolChar
. $this->phpcsFile->getTokensAsString( $i, ( $last_header_ptr - $i ), true )
. $replacement;
}
$this->phpcsFile->fixer->addContent( $comment_details['last_header_ptr'], $replacement );
}
}
}
return $skip_to;
}
/**
* Examine an individual token in a larger comment for plugin/theme headers.
*
* @since 1.2.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $regex The regex to use to examine the comment line.
* @param array $headers Valid headers for a plugin or theme.
* @param array $comment_details The information collected so far.
*
* @return array Adjusted $comment_details array
*/
protected function examine_comment_line( $stackPtr, $regex, $headers, $comment_details ) {
if ( preg_match( $regex, $this->tokens[ $stackPtr ]['content'], $matches ) === 1 ) {
++$comment_details['headers_found'];
if ( true === $headers[ $matches[1] ] ) {
$comment_details['required_header_found'] = true;
}
if ( 'Text Domain' === $matches[1] ) {
$comment_details['text_domain_ptr'] = $stackPtr;
$comment_details['text_domain_found'] = trim( $matches[2] );
}
$comment_details['last_header_ptr'] = $stackPtr;
$comment_details['last_header_matches'] = $matches;
}
return $comment_details;
}
}
@@ -0,0 +1,316 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Discourages the use of various functions and suggests (WordPress) alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 - Takes the minimum supported WP version into account.
* - Takes exceptions based on passed parameters into account.
*
* @uses \WordPressCS\WordPress\Sniff::$minimum_supported_version
*/
class AlternativeFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Local input streams which should not be flagged for the file system function checks.
*
* @link http://php.net/manual/en/wrappers.php.php
*
* @var array
*/
protected $allowed_local_streams = array(
'php://input' => true,
'php://output' => true,
'php://stdin' => true,
'php://stdout' => true,
'php://stderr' => true,
);
/**
* Local input streams which should not be flagged for the file system function checks if
* the $filename starts with them.
*
* @link http://php.net/manual/en/wrappers.php.php
*
* @var array
*/
protected $allowed_local_stream_partials = array(
'php://temp/',
'php://fd/',
);
/**
* Local input stream constants which should not be flagged for the file system function checks.
*
* @link http://php.net/manual/en/wrappers.php.php
*
* @var array
*/
protected $allowed_local_stream_constants = array(
'STDIN' => true,
'STDOUT' => true,
'STDERR' => true,
);
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'since' => '4.9.0', //=> the WP version in which the alternative became available.
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'curl' => array(
'type' => 'warning',
'message' => 'Using cURL functions is highly discouraged. Use wp_remote_get() instead.',
'since' => '2.7.0',
'functions' => array(
'curl_*',
),
'whitelist' => array(
'curl_version' => true,
),
),
'parse_url' => array(
'type' => 'warning',
'message' => '%s() is discouraged because of inconsistency in the output across PHP versions; use wp_parse_url() instead.',
'since' => '4.4.0',
'functions' => array(
'parse_url',
),
),
'json_encode' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use wp_json_encode() instead.',
'since' => '4.1.0',
'functions' => array(
'json_encode',
),
),
'file_get_contents' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use wp_remote_get() for remote URLs instead.',
'since' => '2.7.0',
'functions' => array(
'file_get_contents',
),
),
'file_system_read' => array(
'type' => 'warning',
'message' => 'File operations should use WP_Filesystem methods instead of direct PHP filesystem calls. Found: %s()',
'since' => '2.5.0',
'functions' => array(
'readfile',
'fclose',
'fopen',
'fread',
'fwrite',
'file_put_contents',
'fsockopen',
'pfsockopen',
),
),
'strip_tags' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use the more comprehensive wp_strip_all_tags() instead.',
'since' => '2.9.0',
'functions' => array(
'strip_tags',
),
),
'rand_seeding' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Rand seeding is not necessary when using the wp_rand() function (as you should).',
'since' => '2.6.2',
'functions' => array(
'srand',
'mt_srand',
),
),
'rand' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use the far less predictable wp_rand() instead.',
'since' => '2.6.2',
'functions' => array(
'rand',
'mt_rand',
),
),
);
}
/**
* Process a matched token.
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
$this->get_wp_version_from_cl();
/*
* Deal with exceptions.
*/
switch ( $matched_content ) {
case 'strip_tags':
/*
* The function `wp_strip_all_tags()` is only a valid alternative when
* only the first parameter is passed to `strip_tags()`.
*/
if ( $this->get_function_call_parameter_count( $stackPtr ) !== 1 ) {
return;
}
break;
case 'wp_parse_url':
/*
* Before WP 4.7.0, the function `wp_parse_url()` was only a valid alternative
* if no second param was passed to `parse_url()`.
*
* @see https://developer.wordpress.org/reference/functions/wp_parse_url/#changelog
*/
if ( $this->get_function_call_parameter_count( $stackPtr ) !== 1
&& version_compare( $this->minimum_supported_version, '4.7.0', '<' )
) {
return;
}
break;
case 'file_get_contents':
/*
* Using `wp_remote_get()` will only work for remote URLs.
* See if we can determine is this function call is for a local file and if so, bow out.
*/
$params = $this->get_function_call_parameters( $stackPtr );
if ( isset( $params[2] ) && 'true' === $params[2]['raw'] ) {
// Setting `$use_include_path` to `true` is only relevant for local files.
return;
}
if ( isset( $params[1] ) === false ) {
// If the file to get is not set, this is a non-issue anyway.
return;
}
if ( strpos( $params[1]['raw'], 'http:' ) !== false
|| strpos( $params[1]['raw'], 'https:' ) !== false
) {
// Definitely a URL, throw notice.
break;
}
if ( preg_match( '`\b(?:ABSPATH|WP_(?:CONTENT|PLUGIN)_DIR|WPMU_PLUGIN_DIR|TEMPLATEPATH|STYLESHEETPATH|(?:MU)?PLUGINDIR)\b`', $params[1]['raw'] ) === 1 ) {
// Using any of the constants matched in this regex is an indicator of a local file.
return;
}
if ( preg_match( '`(?:get_home_path|plugin_dir_path|get_(?:stylesheet|template)_directory|wp_upload_dir)\s*\(`i', $params[1]['raw'] ) === 1 ) {
// Using any of the functions matched in the regex is an indicator of a local file.
return;
}
if ( $this->is_local_data_stream( $params[1]['raw'] ) === true ) {
// Local data stream.
return;
}
unset( $params );
break;
case 'readfile':
case 'fopen':
case 'file_put_contents':
/*
* Allow for handling raw data streams from the request body.
*/
$first_param = $this->get_function_call_parameter( $stackPtr, 1 );
if ( false === $first_param ) {
// If the file to work with is not set, local data streams don't come into play.
break;
}
if ( $this->is_local_data_stream( $first_param['raw'] ) === true ) {
// Local data stream.
return;
}
unset( $first_param );
break;
}
if ( ! isset( $this->groups[ $group_name ]['since'] ) ) {
return parent::process_matched_token( $stackPtr, $group_name, $matched_content );
}
// Verify if the alternative is available in the minimum supported WP version.
if ( version_compare( $this->groups[ $group_name ]['since'], $this->minimum_supported_version, '<=' ) ) {
return parent::process_matched_token( $stackPtr, $group_name, $matched_content );
}
}
/**
* Determine based on the "raw" parameter value, whether a file parameter points to
* a local data stream.
*
* @param string $raw_param_value Raw parameter value.
*
* @return bool True if this is a local data stream. False otherwise.
*/
protected function is_local_data_stream( $raw_param_value ) {
$raw_stripped = $this->strip_quotes( $raw_param_value );
if ( isset( $this->allowed_local_streams[ $raw_stripped ] )
|| isset( $this->allowed_local_stream_constants[ $raw_param_value ] )
) {
return true;
}
foreach ( $this->allowed_local_stream_partials as $partial ) {
if ( strpos( $raw_stripped, $partial ) === 0 ) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,292 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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/test/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|test|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;
}
}
}
// Ignore constant declarations via define().
if ( $this->is_in_function_call( $stackPtr, array( 'define' => true ), true, true ) ) {
return;
}
// Ignore constant declarations using the const keyword.
$stop_points = array(
\T_CONST,
\T_SEMICOLON,
\T_OPEN_TAG,
\T_CLOSE_TAG,
\T_OPEN_CURLY_BRACKET,
);
$maybe_const = $this->phpcsFile->findPrevious( $stop_points, ( $stackPtr - 1 ) );
if ( false !== $maybe_const && \T_CONST === $this->tokens[ $maybe_const ]['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( '`(?:\?|&amp;|&)[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;
}
}
@@ -0,0 +1,235 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Flag cron schedules less than 15 minutes.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#cron-schedules-less-than-15-minutes-or-expensive-events
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.11.0 - Extends the WordPressCS native `Sniff` class.
* - Now deals correctly with WP time constants.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 The minimum cron interval tested against is now configurable.
* @since 1.0.0 This sniff has been moved from the `VIP` category to the `WP` category.
*/
class CronIntervalSniff extends Sniff {
/**
* Minimum allowed cron interval in seconds.
*
* Defaults to 900 (= 15 minutes), which is the requirement for the VIP platform.
*
* @since 0.14.0
*
* @var int
*/
public $min_interval = 900;
/**
* Known WP Time constant names and their value.
*
* @since 0.11.0
*
* @var array
*/
protected $wp_time_constants = array(
'MINUTE_IN_SECONDS' => 60,
'HOUR_IN_SECONDS' => 3600,
'DAY_IN_SECONDS' => 86400,
'WEEK_IN_SECONDS' => 604800,
'MONTH_IN_SECONDS' => 2592000,
'YEAR_IN_SECONDS' => 31536000,
);
/**
* Function within which the hook should be found.
*
* @var array
*/
protected $valid_functions = array(
'add_filter' => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_CONSTANT_ENCAPSED_STRING,
\T_DOUBLE_QUOTED_STRING,
);
}
/**
* 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 ( 'cron_schedules' !== $this->strip_quotes( $token['content'] ) ) {
return;
}
// If within add_filter.
$functionPtr = $this->is_in_function_call( $stackPtr, $this->valid_functions );
if ( false === $functionPtr ) {
return;
}
$callback = $this->get_function_call_parameter( $functionPtr, 2 );
if ( false === $callback ) {
return;
}
if ( $stackPtr >= $callback['start'] ) {
// "cron_schedules" found in the second parameter, not the first.
return;
}
// Detect callback function name.
$callbackArrayPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $callback['start'], ( $callback['end'] + 1 ), true );
// If callback is array, get second element.
if ( false !== $callbackArrayPtr
&& ( \T_ARRAY === $this->tokens[ $callbackArrayPtr ]['code']
|| \T_OPEN_SHORT_ARRAY === $this->tokens[ $callbackArrayPtr ]['code'] )
) {
$callback = $this->get_function_call_parameter( $callbackArrayPtr, 2 );
if ( false === $callback ) {
$this->confused( $stackPtr );
return;
}
}
unset( $functionPtr );
// Search for the function in tokens.
$callbackFunctionPtr = $this->phpcsFile->findNext( array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING, \T_CLOSURE ), $callback['start'], ( $callback['end'] + 1 ) );
if ( false === $callbackFunctionPtr ) {
$this->confused( $stackPtr );
return;
}
if ( \T_CLOSURE === $this->tokens[ $callbackFunctionPtr ]['code'] ) {
$functionPtr = $callbackFunctionPtr;
} else {
$functionName = $this->strip_quotes( $this->tokens[ $callbackFunctionPtr ]['content'] );
for ( $ptr = 0; $ptr < $this->phpcsFile->numTokens; $ptr++ ) {
if ( \T_FUNCTION === $this->tokens[ $ptr ]['code'] ) {
$foundName = $this->phpcsFile->getDeclarationName( $ptr );
if ( $foundName === $functionName ) {
$functionPtr = $ptr;
break;
} elseif ( isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
// Skip to the end of the function definition.
$ptr = $this->tokens[ $ptr ]['scope_closer'];
}
}
}
}
if ( ! isset( $functionPtr ) ) {
$this->confused( $stackPtr );
return;
}
if ( ! isset( $this->tokens[ $functionPtr ]['scope_opener'], $this->tokens[ $functionPtr ]['scope_closer'] ) ) {
return;
}
$opening = $this->tokens[ $functionPtr ]['scope_opener'];
$closing = $this->tokens[ $functionPtr ]['scope_closer'];
for ( $i = $opening; $i <= $closing; $i++ ) {
if ( \in_array( $this->tokens[ $i ]['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) {
if ( 'interval' === $this->strip_quotes( $this->tokens[ $i ]['content'] ) ) {
$operator = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $i, null, false, null, true );
if ( false === $operator ) {
$this->confused( $stackPtr );
return;
}
$valueStart = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $operator + 1 ), null, true, null, true );
$valueEnd = $this->phpcsFile->findNext( array( \T_COMMA, \T_CLOSE_PARENTHESIS ), ( $valueStart + 1 ) );
$value = '';
for ( $j = $valueStart; $j < $valueEnd; $j++ ) {
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $j ]['code'] ] ) ) {
continue;
}
$value .= $this->tokens[ $j ]['content'];
}
if ( is_numeric( $value ) ) {
$interval = $value;
break;
}
// Deal correctly with WP time constants.
$value = str_replace( array_keys( $this->wp_time_constants ), array_values( $this->wp_time_constants ), $value );
// If all digits and operators, eval!
if ( preg_match( '#^[\s\d+*/-]+$#', $value ) > 0 ) {
$interval = eval( "return ( $value );" ); // phpcs:ignore Squiz.PHP.Eval -- No harm here.
break;
}
$this->confused( $stackPtr );
return;
}
}
}
$this->min_interval = (int) $this->min_interval;
if ( isset( $interval ) && $interval < $this->min_interval ) {
$minutes = round( ( $this->min_interval / 60 ), 1 );
$this->phpcsFile->addWarning(
'Scheduling crons at %s sec ( less than %s minutes ) is discouraged.',
$stackPtr,
'CronSchedulesInterval',
array(
$interval,
$minutes,
)
);
return;
}
}
/**
* Add warning about unclear cron schedule change.
*
* @param int $stackPtr The position of the current token in the stack.
*/
public function confused( $stackPtr ) {
$this->phpcsFile->addWarning(
'Detected changing of cron_schedules, but could not detect the interval value.',
$stackPtr,
'ChangeDetected'
);
}
}
@@ -0,0 +1,120 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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',
),
// WP 4.9.0.
'Customize_New_Menu_Section' => array(
'version' => '4.9.0',
),
'WP_Customize_New_Menu_Control' => array(
'version' => '4.9.0',
),
// WP 5.3.0.
'Services_JSON' => array(
'alt' => 'The PHP native JSON extension',
'version' => '5.3.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 string $group_name The name of the group which was matched. Will
* always be 'deprecated_classes'.
* @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
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,216 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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 string $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
);
}
}
@@ -0,0 +1,338 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
/**
* Check for usage of deprecated parameters in WP functions and suggest alternative based on the parameter passed.
*
* This sniff will throw an error when usage of deprecated parameters is
* detected if the parameter 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 DeprecatedParametersSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 0.12.0
*
* @var string
*/
protected $group_name = 'wp_deprecated_parameters';
/**
* Array of function, argument, and default value for deprecated argument.
*
* The functions are ordered alphabetically.
* Last updated for WordPress 4.8.0.
*
* @since 0.12.0
*
* @var array Multidimensional array with parameter details.
* $target_functions = array(
* (string) Function name. => array(
* (int) Target parameter position, 1-based. => array(
* 'value' => (mixed) Expected default value for the
* deprecated parameter. Currently the default
* values: true, false, null, empty arrays and
* both empty and non-empty strings can be
* handled correctly by the process_parameters()
* method. When an additional default value is
* added, the relevant code in the
* process_parameters() method will need to be
* adjusted.
* 'version' => (int) The WordPress version when deprecated.
* )
* )
* );
*/
protected $target_functions = array(
'add_option' => array(
3 => array(
'value' => '',
'version' => '2.3.0',
),
),
'comments_link' => array(
1 => array(
'value' => '',
'version' => '0.72',
),
2 => array(
'value' => '',
'version' => '1.3.0',
),
),
'comments_number' => array(
4 => array(
'value' => '',
'version' => '1.3.0',
),
),
'convert_chars' => array(
2 => array(
'value' => '',
'version' => '0.71',
),
),
'discover_pingback_server_uri' => array(
2 => array(
'value' => '',
'version' => '2.7.0',
),
),
'get_category_parents' => array(
5 => array(
'value' => array(),
'version' => '4.8.0',
),
),
'get_delete_post_link' => array(
2 => array(
'value' => '',
'version' => '3.0.0',
),
),
'get_last_updated' => array(
1 => array(
'value' => '',
'version' => '3.0.0', // Was previously part of MU.
),
),
'get_the_author' => array(
1 => array(
'value' => '',
'version' => '2.1.0',
),
),
'get_user_option' => array(
3 => array(
'value' => '',
'version' => '2.3.0',
),
),
'get_wp_title_rss' => array(
1 => array(
'value' => '&#8211;',
'version' => '4.4.0',
),
),
'is_email' => array(
2 => array(
'value' => false,
'version' => '3.0.0',
),
),
'load_plugin_textdomain' => array(
2 => array(
'value' => false,
'version' => '2.7.0',
),
),
'safecss_filter_attr' => array(
2 => array(
'value' => '',
'version' => '2.8.1',
),
),
'the_attachment_link' => array(
3 => array(
'value' => false,
'version' => '2.5.0',
),
),
'the_author' => array(
1 => array(
'value' => '',
'version' => '2.1.0',
),
2 => array(
'value' => true,
'version' => '1.5.0',
),
),
'the_author_posts_link' => array(
1 => array(
'value' => '',
'version' => '2.1.0',
),
),
'trackback_rdf' => array(
1 => array(
'value' => '',
'version' => '2.5.0',
),
),
'trackback_url' => array(
1 => array(
'value' => true,
'version' => '2.5.0',
),
),
'update_blog_option' => array(
4 => array(
'value' => null,
'version' => '3.1.0',
),
),
'update_blog_status' => array(
4 => array(
'value' => null,
'version' => '3.1.0',
),
),
'update_user_status' => array(
4 => array(
'value' => null,
'version' => '3.0.2',
),
),
'unregister_setting' => array(
4 => array(
'value' => '',
'version' => '4.7.0',
),
),
'wp_get_http_headers' => array(
2 => array(
'value' => false,
'version' => '2.7.0',
),
),
'wp_get_sidebars_widgets' => array(
1 => array(
'value' => true,
'version' => '2.8.1',
),
),
'wp_install' => array(
5 => array(
'value' => '',
'version' => '2.6.0',
),
),
'wp_new_user_notification' => array(
2 => array(
'value' => null,
'version' => '4.3.1',
),
),
'wp_notify_postauthor' => array(
2 => array(
'value' => null,
'version' => '3.8.0',
),
),
'wp_title_rss' => array(
1 => array(
'value' => '&#8211;',
'version' => '4.4.0',
),
),
'wp_upload_bits' => array(
2 => array(
'value' => null,
'version' => '2.0.0',
),
),
'xfn_check' => array(
3 => array(
'value' => '',
'version' => '2.5.0',
),
),
);
/**
* Process the parameters of a matched function.
*
* @since 0.12.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $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();
$paramCount = \count( $parameters );
foreach ( $this->target_functions[ $matched_content ] as $position => $parameter_args ) {
// Check that number of parameters defined is not less than the position to check.
if ( $position > $paramCount ) {
break;
}
// The list will need to updated if the default value is not supported.
switch ( $parameters[ $position ]['raw'] ) {
case 'true':
$matched_parameter = true;
break;
case 'false':
$matched_parameter = false;
break;
case 'null':
$matched_parameter = null;
break;
case 'array()':
case '[]':
$matched_parameter = array();
break;
default:
$matched_parameter = $this->strip_quotes( $parameters[ $position ]['raw'] );
break;
}
if ( $parameter_args['value'] === $matched_parameter ) {
continue;
}
$message = 'The parameter "%s" at position #%s of %s() has been deprecated since WordPress version %s.';
$is_error = version_compare( $parameter_args['version'], $this->minimum_supported_version, '<' );
$code = $this->string_to_errorcode( ucfirst( $matched_content ) . 'Param' . $position . 'Found' );
$data = array(
$parameters[ $position ]['raw'],
$position,
$matched_content,
$parameter_args['version'],
);
if ( isset( $parameter_args['value'] ) && $position < $paramCount ) {
$message .= ' Use "%s" instead.';
$data[] = (string) $parameter_args['value'];
} else {
$message .= ' Instead do not pass the parameter.';
}
$this->addMessage( $message, $stackPtr, $is_error, $code, $data, 0 );
}
}
}
@@ -0,0 +1,217 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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 string $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 ],
)
);
}
}
}
@@ -0,0 +1,57 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
/**
* Discourages the use of various WordPress functions and suggests alternatives.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.11.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class DiscouragedFunctionsSniff extends AbstractFunctionRestrictionsSniff {
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'query_posts' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use WP_Query instead.',
'functions' => array(
'query_posts',
),
),
'wp_reset_query' => array(
'type' => 'warning',
'message' => '%s() is discouraged. Use wp_reset_postdata() instead.',
'functions' => array(
'wp_reset_query',
),
),
);
}
}
@@ -0,0 +1,223 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* This checks the enqueued 4th and 5th parameters to make sure the version and in_footer are set.
*
* If a source ($src) value is passed, then version ($ver) needs to have non-falsy value.
* If a source ($src) value is passed a check for in footer ($in_footer), warn the user if the value is falsy.
*
* @link https://developer.wordpress.org/reference/functions/wp_register_script/
* @link https://developer.wordpress.org/reference/functions/wp_enqueue_script/
* @link https://developer.wordpress.org/reference/functions/wp_register_style/
* @link https://developer.wordpress.org/reference/functions/wp_enqueue_style/
*
* @package WPCS\WordPressCodingStandards
*
* @since 1.0.0
*/
class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSniff {
/**
* The group name for this group of functions.
*
* @since 1.0.0
*
* @var string
*/
protected $group_name = 'Enqueued';
/**
* List of enqueued functions that need to be checked for use of the in_footer and version arguments.
*
* @since 1.0.0
*
* @var array <string function_name> => <bool true>
*/
protected $target_functions = array(
'wp_register_script' => true,
'wp_enqueue_script' => true,
'wp_register_style' => true,
'wp_enqueue_style' => true,
);
/**
* False + the empty tokens array.
*
* This array is enriched with the $emptyTokens array in the register() method.
*
* @var array
*/
private $false_tokens = array(
\T_FALSE => \T_FALSE,
);
/**
* Token codes which are "safe" to accept to determine whether a version would evaluate to `false`.
*
* This array is enriched with the several of the PHPCS token arrays in the register() method.
*
* @var array
*/
private $safe_tokens = array(
\T_NULL => \T_NULL,
\T_FALSE => \T_FALSE,
\T_TRUE => \T_TRUE,
\T_LNUMBER => \T_LNUMBER,
\T_DNUMBER => \T_DNUMBER,
\T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
\T_START_NOWDOC => \T_START_NOWDOC,
\T_NOWDOC => \T_NOWDOC,
\T_END_NOWDOC => \T_END_NOWDOC,
\T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
\T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS,
\T_STRING_CONCAT => \T_STRING_CONCAT,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* Overloads and calls the parent method to allow for adding additional tokens to the $safe_tokens property.
*
* @return array
*/
public function register() {
$this->false_tokens += Tokens::$emptyTokens;
$this->safe_tokens += Tokens::$emptyTokens;
$this->safe_tokens += Tokens::$assignmentTokens;
$this->safe_tokens += Tokens::$comparisonTokens;
$this->safe_tokens += Tokens::$operators;
$this->safe_tokens += Tokens::$booleanOperators;
$this->safe_tokens += Tokens::$castTokens;
return parent::register();
}
/**
* Process the parameters of a matched function.
*
* @since 1.0.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param string $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 to see if a source ($src) is specified.
if ( ! isset( $parameters[2] ) ) {
return;
}
/*
* Version Check: Check to make sure the version is set explicitly.
*/
if ( ! isset( $parameters[4] ) || 'null' === $parameters[4]['raw'] ) {
$type = 'script';
if ( strpos( $matched_content, '_style' ) !== false ) {
$type = 'style';
}
$this->phpcsFile->addError(
'Resource version not set in call to %s(). This means new versions of the %s will not always be loaded due to browser caching.',
$stackPtr,
'MissingVersion',
array( $matched_content, $type )
);
} else {
// The version argument should have a non-false value.
if ( $this->is_falsy( $parameters[4]['start'], $parameters[4]['end'] ) ) {
$this->phpcsFile->addError(
'Version parameter is not explicitly set or has been set to an equivalent of "false" for %s; ' .
'This means that the WordPress core version will be used which is not recommended for plugin or theme development.',
$stackPtr,
'NoExplicitVersion',
array( $matched_content )
);
}
}
/*
* In footer Check
*
* Check to make sure that $in_footer is set to true.
* It will warn the user to make sure it is intended.
*
* Only wp_register_script and wp_enqueue_script need this check,
* as this parameter is not available to wp_register_style and wp_enqueue_style.
*/
if ( 'wp_register_script' !== $matched_content && 'wp_enqueue_script' !== $matched_content ) {
return;
}
if ( ! isset( $parameters[5] ) ) {
// If in footer is not set, throw a warning about the default.
$this->phpcsFile->addWarning(
'In footer ($in_footer) is not set explicitly %s; ' .
'It is recommended to load scripts in the footer. Please set this value to `true` to load it in the footer, or explicitly `false` if it should be loaded in the header.',
$stackPtr,
'NotInFooter',
array( $matched_content )
);
}
}
/**
* Determine if a range has a falsy value.
*
* @param int $start The position to start looking from.
* @param int $end The position to stop looking (inclusive).
*
* @return bool True if the parameter is falsy.
* False if the parameter is not falsy or when it
* couldn't be reliably determined.
*/
protected function is_falsy( $start, $end ) {
// Find anything excluding the false tokens.
$has_non_false = $this->phpcsFile->findNext( $this->false_tokens, $start, ( $end + 1 ), true );
// If no non-false tokens are found, we are good.
if ( false === $has_non_false ) {
return true;
}
$code_string = '';
for ( $i = $start; $i <= $end; $i++ ) {
if ( isset( $this->safe_tokens[ $this->tokens[ $i ]['code'] ] ) === false ) {
// Function call/variable or other token which makes it neigh impossible
// to determine whether the actual value would evaluate to false.
return false;
}
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) {
continue;
}
$code_string .= $this->tokens[ $i ]['content'];
}
if ( '' === $code_string ) {
return false;
}
// Evaluate the argument to figure out the outcome is false or not.
// phpcs:ignore Squiz.PHP.Eval -- No harm here.
return ( false === eval( "return (bool) $code_string;" ) );
}
}
@@ -0,0 +1,64 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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'
);
}
}
}
@@ -0,0 +1,466 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Warns about overwriting WordPress native global variables.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.4.0 This class now extends the WordPressCS native `Sniff` class.
* @since 0.12.0 The $wp_globals property has been moved to the `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This sniff has been moved from the `Variables` category to the `WP`
* category and renamed from `GlobalVariables` to `GlobalVariablesOverride`.
* @since 1.1.0 The sniff now also detects variables being overriden in the global namespace.
* @since 2.2.0 The sniff now also detects variable assignments via the list() construct.
*
* @uses \WordPressCS\WordPress\Sniff::$custom_test_class_whitelist
*/
class GlobalVariablesOverrideSniff extends Sniff {
/**
* Whether to treat all files as if they were included from
* within a function.
*
* This is mostly useful for projects containing views which are being
* included from within a function in another file, like themes.
*
* Note: enabling this is discouraged as there is no guarantee that
* the file will *never* be included from the global scope.
*
* @since 1.1.0
*
* @var bool
*/
public $treat_files_as_scoped = false;
/**
* Whitelist select variables from the Sniff::$wp_globals array.
*
* A few select variables in WP Core are _intended_ to be overwritten
* by themes/plugins. This sniff should not throw an error for those.
*
* @since 2.2.0
*
* @var array
*/
protected $override_allowed = array(
'content_width' => true,
'wp_cockneyreplace' => true,
);
/**
* Scoped object and function structures to skip over as
* variables will have a different scope within those.
*
* @since 1.1.0
*
* @var array
*/
private $skip_over = array(
\T_FUNCTION => true,
\T_CLOSURE => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.3.0
* @since 1.1.0 Added class tokens for improved test classes skipping.
*
* @return array
*/
public function register() {
// Add the OO scope tokens to the $skip_over property.
$this->skip_over += Tokens::$ooScopeTokens;
$targets = array(
\T_GLOBAL,
\T_VARIABLE,
\T_LIST,
\T_OPEN_SHORT_ARRAY,
);
// Only used to skip over test classes.
$targets += Tokens::$ooScopeTokens;
return $targets;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.3.0
* @since 1.1.0 Split the token specific logic off into separate methods.
*
* @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 ) {
$token = $this->tokens[ $stackPtr ];
// Ignore variable overrides in test classes.
if ( isset( Tokens::$ooScopeTokens[ $token['code'] ] ) ) {
if ( true === $this->is_test_class( $stackPtr )
&& $token['scope_condition'] === $stackPtr
&& isset( $token['scope_closer'] )
) {
// Skip forward to end of test class.
return $token['scope_closer'];
}
// Otherwise ignore the tokens as they were only registered to enable skipping over test classes.
return;
}
/*
* Examine variables within a function scope based on a `global` statement in the
* function.
* Examine variables not within a function scope, but within a list construct, based
* on that.
* Examine variables not within a function scope and access to the `$GLOBALS`
* variable based on the variable token.
*/
$in_function_scope = $this->phpcsFile->hasCondition( $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) );
if ( ( \T_LIST === $token['code'] || \T_OPEN_SHORT_ARRAY === $token['code'] )
&& false === $in_function_scope
&& false === $this->treat_files_as_scoped
) {
return $this->process_list_assignment( $stackPtr );
} elseif ( \T_VARIABLE === $token['code']
&& ( '$GLOBALS' === $token['content']
|| ( false === $in_function_scope && false === $this->treat_files_as_scoped ) )
) {
return $this->process_variable_assignment( $stackPtr );
} elseif ( \T_GLOBAL === $token['code']
&& ( true === $in_function_scope || true === $this->treat_files_as_scoped )
) {
return $this->process_global_statement( $stackPtr, $in_function_scope );
}
}
/**
* Check that global variables declared via a list construct are prefixed.
*
* @internal No need to take special measures for nested lists. Nested or not,
* each list part can only contain one variable being written to.
*
* @since 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
protected function process_list_assignment( $stackPtr ) {
$list_open_close = $this->find_list_open_close( $stackPtr );
if ( false === $list_open_close ) {
// Short array, not short list.
return;
}
$var_pointers = $this->get_list_variables( $stackPtr, $list_open_close );
foreach ( $var_pointers as $ptr ) {
$this->process_variable_assignment( $ptr, true );
}
// No need to re-examine these variables.
return $list_open_close['closer'];
}
/**
* Check that defined global variables are prefixed.
*
* @since 1.1.0 Logic was previously contained in the process_token() method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param bool $in_list Whether or not this is a variable in a list assignment.
* Defaults to false.
*
* @return void
*/
protected function process_variable_assignment( $stackPtr, $in_list = false ) {
if ( $this->has_whitelist_comment( 'override', $stackPtr ) === true ) {
return;
}
$token = $this->tokens[ $stackPtr ];
$var_name = substr( $token['content'], 1 ); // Strip the dollar sign.
$data = array();
// Determine the variable name for `$GLOBALS['array_key']`.
if ( 'GLOBALS' === $var_name ) {
$bracketPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
if ( false === $bracketPtr || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $bracketPtr ]['code'] || ! isset( $this->tokens[ $bracketPtr ]['bracket_closer'] ) ) {
return;
}
// Retrieve the array key and avoid getting tripped up by some simple obfuscation.
$var_name = '';
$start = ( $bracketPtr + 1 );
for ( $ptr = $start; $ptr < $this->tokens[ $bracketPtr ]['bracket_closer']; $ptr++ ) {
/*
* If the globals array key contains a variable, constant, function call
* or interpolated variable, bow out.
*/
if ( \T_VARIABLE === $this->tokens[ $ptr ]['code']
|| \T_STRING === $this->tokens[ $ptr ]['code']
|| \T_DOUBLE_QUOTED_STRING === $this->tokens[ $ptr ]['code']
) {
return;
}
if ( \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $ptr ]['code'] ) {
$var_name .= $this->strip_quotes( $this->tokens[ $ptr ]['content'] );
}
}
if ( '' === $var_name ) {
// Shouldn't happen, but just in case.
return;
}
// Set up the data for the error message.
$data[] = '$GLOBALS[\'' . $var_name . '\']';
}
/*
* Is this one of the WP global variables ?
*/
if ( isset( $this->wp_globals[ $var_name ] ) === false ) {
return;
}
/*
* Is this one of the WP global variables which are allowed to be overwritten ?
*/
if ( isset( $this->override_allowed[ $var_name ] ) === true ) {
return;
}
/*
* Check if the variable value is being changed.
*/
if ( false === $in_list
&& false === $this->is_assignment( $stackPtr )
&& false === $this->is_foreach_as( $stackPtr )
) {
return;
}
/*
* Function parameters with the same name as a WP global variable are fine,
* including when they are being assigned a default value.
*/
if ( false === $in_list && isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
foreach ( $this->tokens[ $stackPtr ]['nested_parenthesis'] as $opener => $closer ) {
if ( isset( $this->tokens[ $opener ]['parenthesis_owner'] )
&& ( \T_FUNCTION === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code']
|| \T_CLOSURE === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code'] )
) {
return;
}
}
unset( $opener, $closer );
}
/*
* Class property declarations with the same name as WP global variables are fine.
*/
if ( false === $in_list && true === $this->is_class_property( $stackPtr ) ) {
return;
}
// Still here ? In that case, the WP global variable is being tampered with.
$this->add_error( $stackPtr, $data );
}
/**
* Check that global variables imported into a function scope using a global statement
* are not being overruled.
*
* @since 1.1.0 Logic was previously contained in the process_token() method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param bool $in_function_scope Whether the global statement is within a scoped function/closure.
*
* @return void
*/
protected function process_global_statement( $stackPtr, $in_function_scope ) {
/*
* Collect the variables to watch for.
*/
$search = array();
$ptr = ( $stackPtr + 1 );
while ( isset( $this->tokens[ $ptr ] ) ) {
$var = $this->tokens[ $ptr ];
// Halt the loop at end of statement.
if ( \T_SEMICOLON === $var['code'] ) {
break;
}
if ( \T_VARIABLE === $var['code'] ) {
$var_name = substr( $var['content'], 1 );
if ( isset( $this->wp_globals[ $var_name ] )
&& isset( $this->override_allowed[ $var_name ] ) === false
) {
$search[] = $var['content'];
}
}
$ptr++;
}
unset( $var );
if ( empty( $search ) ) {
return;
}
/*
* Search for assignments to the imported global variables within the relevant scope.
*/
$start = $ptr;
if ( true === $in_function_scope ) {
$function_cond = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION );
$closure_cond = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE );
$scope_cond = max( $function_cond, $closure_cond ); // If false, it will evaluate as zero, so this is fine.
if ( isset( $this->tokens[ $scope_cond ]['scope_closer'] ) === false ) {
// Live coding or parse error.
return;
}
$end = $this->tokens[ $scope_cond ]['scope_closer'];
} else {
// Global statement in the global namespace with file is being treated as scoped.
$end = $this->phpcsFile->numTokens;
}
for ( $ptr = $start; $ptr < $end; $ptr++ ) {
// Skip over nested functions, classes and the likes.
if ( isset( $this->skip_over[ $this->tokens[ $ptr ]['code'] ] ) ) {
if ( ! isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
// Live coding or parse error.
break;
}
$ptr = $this->tokens[ $ptr ]['scope_closer'];
continue;
}
// Make sure to recognize assignments to variables in a list construct.
if ( \T_LIST === $this->tokens[ $ptr ]['code']
|| \T_OPEN_SHORT_ARRAY === $this->tokens[ $ptr ]['code']
) {
$list_open_close = $this->find_list_open_close( $ptr );
if ( false === $list_open_close ) {
// Short array, not short list.
continue;
}
$var_pointers = $this->get_list_variables( $ptr, $list_open_close );
foreach ( $var_pointers as $ptr ) {
$var_name = $this->tokens[ $ptr ]['content'];
if ( '$GLOBALS' === $var_name ) {
$var_name = '$' . $this->strip_quotes( $this->get_array_access_key( $ptr ) );
}
if ( \in_array( $var_name, $search, true ) ) {
$this->process_variable_assignment( $ptr, true );
}
}
// No need to re-examine these variables.
$ptr = $list_open_close['closer'];
continue;
}
if ( \T_VARIABLE !== $this->tokens[ $ptr ]['code'] ) {
continue;
}
if ( \in_array( $this->tokens[ $ptr ]['content'], $search, true ) === false ) {
// Not one of the variables we're interested in.
continue;
}
// Don't throw false positives for static class properties.
if ( $this->is_class_object_call( $ptr ) === true ) {
continue;
}
if ( true === $this->is_assignment( $ptr ) ) {
$this->maybe_add_error( $ptr );
continue;
}
// Check if this is a variable assignment within a `foreach()` declaration.
if ( $this->is_foreach_as( $ptr ) === true ) {
$this->maybe_add_error( $ptr );
}
}
}
/**
* Add the error if there is no whitelist comment present.
*
* @since 0.11.0
* @since 1.1.0 - Visibility changed from public to protected.
* - Check for being in a test class moved to the process_token() method.
*
* @param int $stackPtr The position of the token to throw the error for.
*
* @return void
*/
protected function maybe_add_error( $stackPtr ) {
if ( $this->has_whitelist_comment( 'override', $stackPtr ) === false ) {
$this->add_error( $stackPtr );
}
}
/**
* Add the error.
*
* @since 1.1.0
*
* @param int $stackPtr The position of the token to throw the error for.
* @param array $data Optional. Array containing one entry holding the
* name of the variable being overruled.
* Defaults to the 'content' of the $stackPtr token.
*
* @return void
*/
protected function add_error( $stackPtr, $data = array() ) {
if ( empty( $data ) ) {
$data[] = $this->tokens[ $stackPtr ]['content'];
}
$this->phpcsFile->addError(
'Overriding WordPress globals is prohibited. Found assignment to %s',
$stackPtr,
'Prohibited',
$data
);
}
}
@@ -0,0 +1,789 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff;
use WordPressCS\WordPress\PHPCSHelper;
use PHP_CodeSniffer\Util\Tokens;
/**
* Makes sure WP internationalization functions are used properly.
*
* @link https://make.wordpress.org/core/handbook/best-practices/internationalization/
* @link https://developer.wordpress.org/plugins/internationalization/
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.10.0
* @since 0.11.0 - Now also checks for translators comments.
* - Now has the ability to handle text domain set via the command-line
* as a comma-delimited list.
* `phpcs --runtime-set text_domain my-slug,default`
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 1.0.0 This class now extends the WordPressCS native
* `AbstractFunctionRestrictionSniff` class.
* The parent `exclude` property is, however, disabled as it
* would disable the whole sniff.
*/
class I18nSniff extends AbstractFunctionRestrictionsSniff {
/**
* These Regexes copied from http://php.net/manual/en/function.sprintf.php#93552
* and adjusted for better precision and updated specs.
*/
const SPRINTF_PLACEHOLDER_REGEX = '/(?:
(?<!%) # Don\'t match a literal % (%%).
(
% # 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.
)
[bcdeEfFgGosuxX] # Type specifier.
)
)/x';
/**
* "Unordered" means there's no position specifier: '%s', not '%2$s'.
*/
const UNORDERED_SPRINTF_PLACEHOLDER_REGEX = '/(?:
(?<!%) # Don\'t match a literal % (%%).
% # Start of placeholder.
[+-]? # 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.
)
[bcdeEfFgGosuxX] # Type specifier.
)/x';
/**
* Text domain.
*
* @todo Eventually this should be able to be auto-supplied via looking at $this->phpcsFile->getFilename()
* @link https://youtrack.jetbrains.com/issue/WI-17740
*
* @var string[]|string
*/
public $text_domain;
/**
* The I18N functions in use in WP.
*
* @since 0.10.0
* @since 0.11.0 Changed visibility from public to protected.
*
* @var array <string function name> => <string function type>
*/
protected $i18n_functions = array(
'translate' => 'simple',
'__' => 'simple',
'esc_attr__' => 'simple',
'esc_html__' => 'simple',
'_e' => 'simple',
'esc_attr_e' => 'simple',
'esc_html_e' => 'simple',
'translate_with_gettext_context' => 'context',
'_x' => 'context',
'_ex' => 'context',
'esc_attr_x' => 'context',
'esc_html_x' => 'context',
'_n' => 'number',
'_nx' => 'number_context',
'_n_noop' => 'noopnumber',
'_nx_noop' => 'noopnumber_context',
);
/**
* Toggle whether or not to check for translators comments for text string containing placeholders.
*
* Intended to make this part of the sniff unit testable, but can be used by end-users too,
* though they can just as easily disable this via the sniff code.
*
* @since 0.11.0
*
* @var bool
*/
public $check_translator_comments = true;
/**
* Whether or not the `default` text domain is one of the allowed text domains.
*
* @since 0.14.0
*
* @var bool
*/
private $text_domain_contains_default = false;
/**
* Whether or not the `default` text domain is the only allowed text domain.
*
* @since 0.14.0
*
* @var bool
*/
private $text_domain_is_default = false;
/**
* Groups of functions to restrict.
*
* Example: groups => array(
* 'lambda' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Use anonymous functions instead please!',
* 'functions' => array( 'file_get_contents', 'create_function' ),
* )
* )
*
* @return array
*/
public function getGroups() {
return array(
'i18n' => array(
'functions' => array_keys( $this->i18n_functions ),
),
'typos' => array(
'functions' => array(
'_',
),
),
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 1.0.0 Defers to the abstractFunctionRestriction sniff for determining
* whether something is a function call. The logic after that has
* been split off to the `process_matched_token()` method.
*
* @param int $stack_ptr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stack_ptr ) {
// Reset defaults.
$this->text_domain_contains_default = false;
$this->text_domain_is_default = false;
// Allow overruling the text_domain set in a ruleset via the command line.
$cl_text_domain = trim( PHPCSHelper::get_config_data( 'text_domain' ) );
if ( ! empty( $cl_text_domain ) ) {
$this->text_domain = array_filter( array_map( 'trim', explode( ',', $cl_text_domain ) ) );
}
$this->text_domain = $this->merge_custom_array( $this->text_domain, array(), false );
if ( ! empty( $this->text_domain ) ) {
if ( \in_array( 'default', $this->text_domain, true ) ) {
$this->text_domain_contains_default = true;
if ( \count( $this->text_domain ) === 1 ) {
$this->text_domain_is_default = true;
}
}
}
// Prevent exclusion of the i18n group.
$this->exclude = array();
parent::process_token( $stack_ptr );
}
/**
* Process a matched token.
*
* @since 1.0.0 Logic split off from the `process_token()` method.
*
* @param int $stack_ptr The position of the current token in the stack.
* @param string $group_name The name of the group which was matched.
* @param string $matched_content The token content (function name) which was matched.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_matched_token( $stack_ptr, $group_name, $matched_content ) {
$func_open_paren_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stack_ptr + 1 ), null, true );
if ( false === $func_open_paren_token
|| \T_OPEN_PARENTHESIS !== $this->tokens[ $func_open_paren_token ]['code']
|| ! isset( $this->tokens[ $func_open_paren_token ]['parenthesis_closer'] )
) {
// Live coding, parse error or not a function call.
return;
}
if ( 'typos' === $group_name && '_' === $matched_content ) {
$this->phpcsFile->addError( 'Found single-underscore "_()" function when double-underscore expected.', $stack_ptr, 'SingleUnderscoreGetTextFunction' );
return;
}
if ( \in_array( $matched_content, array( 'translate', 'translate_with_gettext_context' ), true ) ) {
$this->phpcsFile->addWarning( 'Use of the "%s()" function is reserved for low-level API usage.', $stack_ptr, 'LowLevelTranslationFunction', array( $matched_content ) );
}
$arguments_tokens = array();
$argument_tokens = array();
$tokens = $this->tokens;
// Look at arguments.
for ( $i = ( $func_open_paren_token + 1 ); $i < $this->tokens[ $func_open_paren_token ]['parenthesis_closer']; $i++ ) {
$this_token = $this->tokens[ $i ];
$this_token['token_index'] = $i;
if ( isset( Tokens::$emptyTokens[ $this_token['code'] ] ) ) {
continue;
}
if ( \T_COMMA === $this_token['code'] ) {
$arguments_tokens[] = $argument_tokens;
$argument_tokens = array();
continue;
}
// Merge consecutive single or double quoted strings (when they span multiple lines).
if ( isset( Tokens::$textStringTokens[ $this_token['code'] ] ) ) {
for ( $j = ( $i + 1 ); $j < $this->tokens[ $func_open_paren_token ]['parenthesis_closer']; $j++ ) {
if ( $this_token['code'] === $this->tokens[ $j ]['code'] ) {
$this_token['content'] .= $this->tokens[ $j ]['content'];
$i = $j;
} else {
break;
}
}
}
$argument_tokens[] = $this_token;
// Include everything up to and including the parenthesis_closer if this token has one.
if ( ! empty( $this_token['parenthesis_closer'] ) ) {
for ( $j = ( $i + 1 ); $j <= $this_token['parenthesis_closer']; $j++ ) {
$tokens[ $j ]['token_index'] = $j;
$argument_tokens[] = $tokens[ $j ];
}
$i = $this_token['parenthesis_closer'];
}
}
if ( ! empty( $argument_tokens ) ) {
$arguments_tokens[] = $argument_tokens;
}
unset( $argument_tokens );
$argument_assertions = array();
if ( 'simple' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'text',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'context' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'text',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'context',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'number' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'single',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'plural',
'tokens' => array_shift( $arguments_tokens ),
);
array_shift( $arguments_tokens );
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'number_context' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'single',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'plural',
'tokens' => array_shift( $arguments_tokens ),
);
array_shift( $arguments_tokens );
$argument_assertions[] = array(
'arg_name' => 'context',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'noopnumber' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'single',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'plural',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
} elseif ( 'noopnumber_context' === $this->i18n_functions[ $matched_content ] ) {
$argument_assertions[] = array(
'arg_name' => 'single',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'plural',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'context',
'tokens' => array_shift( $arguments_tokens ),
);
$argument_assertions[] = array(
'arg_name' => 'domain',
'tokens' => array_shift( $arguments_tokens ),
);
}
if ( ! empty( $arguments_tokens ) ) {
$this->phpcsFile->addError( 'Too many arguments for function "%s".', $func_open_paren_token, 'TooManyFunctionArgs', array( $matched_content ) );
}
foreach ( $argument_assertions as $argument_assertion_context ) {
if ( empty( $argument_assertion_context['tokens'][0] ) ) {
$argument_assertion_context['stack_ptr'] = $func_open_paren_token;
} else {
$argument_assertion_context['stack_ptr'] = $argument_assertion_context['tokens'][0]['token_index'];
}
$this->check_argument_tokens( $argument_assertion_context );
}
/*
* For _n*() calls, compare the singular and plural strings.
* If either of the arguments is missing, empty or has more than 1 token, skip out.
* An error for that will already have been reported via the `check_argument_tokens()` method.
*/
if ( false !== strpos( $this->i18n_functions[ $matched_content ], 'number' )
&& isset( $argument_assertions[0]['tokens'], $argument_assertions[1]['tokens'] )
&& count( $argument_assertions[0]['tokens'] ) === 1
&& count( $argument_assertions[1]['tokens'] ) === 1
) {
$single_context = $argument_assertions[0];
$plural_context = $argument_assertions[1];
$this->compare_single_and_plural_arguments( $stack_ptr, $single_context, $plural_context );
}
if ( true === $this->check_translator_comments ) {
$this->check_for_translator_comment( $stack_ptr, $argument_assertions );
}
}
/**
* Check if supplied tokens represent a translation text string literal.
*
* @param array $context Context (@todo needs better description).
* @return bool
*/
protected function check_argument_tokens( $context ) {
$stack_ptr = $context['stack_ptr'];
$tokens = $context['tokens'];
$arg_name = $context['arg_name'];
$is_error = empty( $context['warning'] );
$content = isset( $tokens[0] ) ? $tokens[0]['content'] : '';
if ( empty( $tokens ) || 0 === \count( $tokens ) ) {
$code = $this->string_to_errorcode( 'MissingArg' . ucfirst( $arg_name ) );
if ( 'domain' !== $arg_name ) {
$this->addMessage( 'Missing $%s arg.', $stack_ptr, $is_error, $code, array( $arg_name ) );
return false;
}
// Ok, we're examining a text domain, now deal correctly with the 'default' text domain.
if ( true === $this->text_domain_is_default ) {
return true;
}
if ( true === $this->text_domain_contains_default ) {
$this->phpcsFile->addWarning(
'Missing $%s arg. If this text string is supposed to use a WP Core translation, use the "default" text domain.',
$stack_ptr,
$code . 'Default',
array( $arg_name )
);
} elseif ( ! empty( $this->text_domain ) ) {
$this->addMessage( 'Missing $%s arg.', $stack_ptr, $is_error, $code, array( $arg_name ) );
}
return false;
}
if ( \count( $tokens ) > 1 ) {
$contents = '';
foreach ( $tokens as $token ) {
$contents .= $token['content'];
}
$code = $this->string_to_errorcode( 'NonSingularStringLiteral' . ucfirst( $arg_name ) );
$this->addMessage( 'The $%s arg must be a single string literal, not "%s".', $stack_ptr, $is_error, $code, array( $arg_name, $contents ) );
return false;
}
if ( \in_array( $arg_name, array( 'text', 'single', 'plural' ), true ) ) {
$this->check_text( $context );
}
if ( \T_DOUBLE_QUOTED_STRING === $tokens[0]['code'] || \T_HEREDOC === $tokens[0]['code'] ) {
$interpolated_variables = $this->get_interpolated_variables( $content );
foreach ( $interpolated_variables as $interpolated_variable ) {
$code = $this->string_to_errorcode( 'InterpolatedVariable' . ucfirst( $arg_name ) );
$this->addMessage( 'The $%s arg must not contain interpolated variables. Found "$%s".', $stack_ptr, $is_error, $code, array( $arg_name, $interpolated_variable ) );
}
if ( ! empty( $interpolated_variables ) ) {
return false;
}
}
if ( isset( Tokens::$textStringTokens[ $tokens[0]['code'] ] ) ) {
if ( 'domain' === $arg_name && ! empty( $this->text_domain ) ) {
$stripped_content = $this->strip_quotes( $content );
if ( ! \in_array( $stripped_content, $this->text_domain, true ) ) {
$this->addMessage(
'Mismatched text domain. Expected \'%s\' but got %s.',
$stack_ptr,
$is_error,
'TextDomainMismatch',
array( implode( "' or '", $this->text_domain ), $content )
);
return false;
}
if ( true === $this->text_domain_is_default && 'default' === $stripped_content ) {
$fixable = false;
$error = 'No need to supply the text domain when the only accepted text domain is "default".';
$error_code = 'SuperfluousDefaultTextDomain';
if ( $tokens[0]['token_index'] === $stack_ptr ) {
$prev = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $stack_ptr - 1 ), null, true );
if ( false !== $prev && \T_COMMA === $this->tokens[ $prev ]['code'] ) {
$fixable = true;
}
}
if ( false === $fixable ) {
$this->phpcsFile->addWarning( $error, $stack_ptr, $error_code );
return false;
}
$fix = $this->phpcsFile->addFixableWarning( $error, $stack_ptr, $error_code );
if ( true === $fix ) {
// Remove preceeding comma, whitespace and the text domain token.
$this->phpcsFile->fixer->beginChangeset();
for ( $i = $prev; $i <= $stack_ptr; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
}
return false;
}
}
return true;
}
$code = $this->string_to_errorcode( 'NonSingularStringLiteral' . ucfirst( $arg_name ) );
$this->addMessage( 'The $%s arg must be a single string literal, not "%s".', $stack_ptr, $is_error, $code, array( $arg_name, $content ) );
return false;
}
/**
* Check for inconsistencies between single and plural arguments.
*
* @param int $stack_ptr The position of the current token in the stack.
* @param array $single_context Single context (@todo needs better description).
* @param array $plural_context Plural context (@todo needs better description).
* @return void
*/
protected function compare_single_and_plural_arguments( $stack_ptr, $single_context, $plural_context ) {
$single_content = $single_context['tokens'][0]['content'];
$plural_content = $plural_context['tokens'][0]['content'];
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $single_content, $single_placeholders );
$single_placeholders = $single_placeholders[0];
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $plural_content, $plural_placeholders );
$plural_placeholders = $plural_placeholders[0];
// English conflates "singular" with "only one", described in the codex:
// https://codex.wordpress.org/I18n_for_WordPress_Developers#Plurals .
if ( \count( $single_placeholders ) < \count( $plural_placeholders ) ) {
$error_string = 'Missing singular placeholder, needed for some languages. See https://codex.wordpress.org/I18n_for_WordPress_Developers#Plurals';
$single_index = $single_context['tokens'][0]['token_index'];
$this->phpcsFile->addError( $error_string, $single_index, 'MissingSingularPlaceholder' );
}
// Reordering is fine, but mismatched placeholders is probably wrong.
sort( $single_placeholders );
sort( $plural_placeholders );
if ( $single_placeholders !== $plural_placeholders ) {
$this->phpcsFile->addWarning( 'Mismatched placeholders is probably an error', $stack_ptr, 'MismatchedPlaceholders' );
}
}
/**
* Check the string itself for problems.
*
* @param array $context Context (@todo needs better description).
* @return void
*/
protected function check_text( $context ) {
$stack_ptr = $context['stack_ptr'];
$arg_name = $context['arg_name'];
$content = $context['tokens'][0]['content'];
$is_error = empty( $context['warning'] );
// UnorderedPlaceholders: Check for multiple unordered placeholders.
$unordered_matches_count = preg_match_all( self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $content, $unordered_matches );
$unordered_matches = $unordered_matches[0];
$all_matches_count = preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $content, $all_matches );
if ( $unordered_matches_count > 0 && $unordered_matches_count !== $all_matches_count && $all_matches_count > 1 ) {
$code = $this->string_to_errorcode( 'MixedOrderedPlaceholders' . ucfirst( $arg_name ) );
$this->phpcsFile->addError(
'Multiple placeholders should be ordered. Mix of ordered and non-ordered placeholders found. Found: %s.',
$stack_ptr,
$code,
array( implode( ', ', $all_matches[0] ) )
);
} elseif ( $unordered_matches_count >= 2 ) {
$code = $this->string_to_errorcode( 'UnorderedPlaceholders' . ucfirst( $arg_name ) );
$suggestions = array();
$replace_regexes = array();
$replacements = array();
for ( $i = 0; $i < $unordered_matches_count; $i++ ) {
$to_insert = ( $i + 1 );
$to_insert .= ( '"' !== $content[0] ) ? '$' : '\$';
$suggestions[ $i ] = substr_replace( $unordered_matches[ $i ], $to_insert, 1, 0 );
// Prepare the strings for use a regex.
$replace_regexes[ $i ] = '`\Q' . $unordered_matches[ $i ] . '\E`';
// Note: the initial \\ is a literal \, the four \ in the replacement translate to also to a literal \.
$replacements[ $i ] = str_replace( '\\', '\\\\', $suggestions[ $i ] );
// Note: the $ needs escaping to prevent numeric sequences after the $ being interpreted as match replacements.
$replacements[ $i ] = str_replace( '$', '\\$', $replacements[ $i ] );
}
$fix = $this->addFixableMessage(
'Multiple placeholders should be ordered. Expected \'%s\', but got %s.',
$stack_ptr,
$is_error,
$code,
array( implode( ', ', $suggestions ), implode( ', ', $unordered_matches ) )
);
if ( true === $fix ) {
$fixed_str = preg_replace( $replace_regexes, $replacements, $content, 1 );
$this->phpcsFile->fixer->replaceToken( $stack_ptr, $fixed_str );
}
}
/*
* NoEmptyStrings.
*
* Strip placeholders and surrounding quotes.
*/
$content_without_quotes = trim( $this->strip_quotes( $content ) );
$non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $content_without_quotes );
if ( '' === $non_placeholder_content ) {
$this->phpcsFile->addError( 'Strings should have translatable content', $stack_ptr, 'NoEmptyStrings' );
return;
}
/*
* NoHtmlWrappedStrings
*
* Strip surrounding quotes.
*/
$reader = new \XMLReader();
$reader->XML( $content_without_quotes, 'UTF-8', LIBXML_NOERROR | LIBXML_ERR_NONE | LIBXML_NOWARNING );
// Is the first node an HTML element?
if ( ! $reader->read() || \XMLReader::ELEMENT !== $reader->nodeType ) {
return;
}
// If the opening HTML element includes placeholders in its attributes, we don't warn.
// E.g. '<option id="%1$s" value="%2$s">Translatable option name</option>'.
$i = 0;
while ( $attr = $reader->getAttributeNo( $i ) ) {
if ( preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $attr ) === 1 ) {
return;
}
++$i;
}
// We don't flag strings wrapped in `<a href="...">...</a>`, as the link target might actually need localization.
if ( 'a' === $reader->name && $reader->getAttribute( 'href' ) ) {
return;
}
// Does the entire string only consist of this HTML node?
if ( $reader->readOuterXml() === $content_without_quotes ) {
$this->phpcsFile->addWarning( 'Strings should not be wrapped in HTML', $stack_ptr, 'NoHtmlWrappedStrings' );
}
}
/**
* Check for the presence of a translators comment if one of the text strings contains a placeholder.
*
* @param int $stack_ptr The position of the gettext call token in the stack.
* @param array $args The function arguments.
* @return void
*/
protected function check_for_translator_comment( $stack_ptr, $args ) {
foreach ( $args as $arg ) {
if ( false === \in_array( $arg['arg_name'], array( 'text', 'single', 'plural' ), true ) ) {
continue;
}
if ( empty( $arg['tokens'] ) ) {
continue;
}
foreach ( $arg['tokens'] as $token ) {
if ( empty( $token['content'] ) ) {
continue;
}
if ( preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $token['content'], $placeholders ) < 1 ) {
// No placeholders found.
continue;
}
$previous_comment = $this->phpcsFile->findPrevious( Tokens::$commentTokens, ( $stack_ptr - 1 ) );
if ( false !== $previous_comment ) {
/*
* Check that the comment is either on the line before the gettext call or
* if it's not, that there is only whitespace between.
*/
$correctly_placed = false;
if ( ( $this->tokens[ $previous_comment ]['line'] + 1 ) === $this->tokens[ $stack_ptr ]['line'] ) {
$correctly_placed = true;
} else {
$next_non_whitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $previous_comment + 1 ), $stack_ptr, true );
if ( false === $next_non_whitespace || $this->tokens[ $next_non_whitespace ]['line'] === $this->tokens[ $stack_ptr ]['line'] ) {
// No non-whitespace found or next non-whitespace is on same line as gettext call.
$correctly_placed = true;
}
unset( $next_non_whitespace );
}
/*
* Check that the comment starts with 'translators:'.
*/
if ( true === $correctly_placed ) {
if ( \T_COMMENT === $this->tokens[ $previous_comment ]['code'] ) {
$comment_text = trim( $this->tokens[ $previous_comment ]['content'] );
// If it's multi-line /* */ comment, collect all the parts.
if ( '*/' === substr( $comment_text, -2 ) && '/*' !== substr( $comment_text, 0, 2 ) ) {
for ( $i = ( $previous_comment - 1 ); 0 <= $i; $i-- ) {
if ( \T_COMMENT !== $this->tokens[ $i ]['code'] ) {
break;
}
$comment_text = trim( $this->tokens[ $i ]['content'] ) . $comment_text;
}
}
if ( true === $this->is_translators_comment( $comment_text ) ) {
// Comment is ok.
return;
}
} elseif ( \T_DOC_COMMENT_CLOSE_TAG === $this->tokens[ $previous_comment ]['code'] ) {
// If it's docblock comment (wrong style) make sure that it's a translators comment.
$db_start = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_OPEN_TAG, ( $previous_comment - 1 ) );
$db_first_text = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, ( $db_start + 1 ), $previous_comment );
if ( true === $this->is_translators_comment( $this->tokens[ $db_first_text ]['content'] ) ) {
$this->phpcsFile->addWarning(
'A "translators:" comment must be a "/* */" style comment. Docblock comments will not be picked up by the tools to generate a ".pot" file.',
$stack_ptr,
'TranslatorsCommentWrongStyle'
);
return;
}
}
}
}
// Found placeholders but no translators comment.
$this->phpcsFile->addWarning(
'A gettext call containing placeholders was found, but was not accompanied by a "translators:" comment on the line above to clarify the meaning of the placeholders.',
$stack_ptr,
'MissingTranslatorsComment'
);
return;
}
}
}
/**
* Check if a (collated) comment string starts with 'translators:'.
*
* @since 0.11.0
*
* @param string $content Comment string content.
* @return bool
*/
private function is_translators_comment( $content ) {
if ( preg_match( '`^(?:(?://|/\*{1,2}) )?translators:`i', $content, $matches ) === 1 ) {
return true;
}
return false;
}
}
@@ -0,0 +1,78 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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;
}
}
@@ -0,0 +1,88 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WP;
use WordPressCS\WordPress\Sniffs\DateTime\RestrictedFunctionsSniff;
/**
* Disallow the changing of timezone.
*
* @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#manipulating-the-timezone-server-side
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.11.0 Extends the WordPressCS native `AbstractFunctionRestrictionsSniff`
* class instead of the upstream `Generic.PHP.ForbiddenFunctions` sniff.
* @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 `WP` category.
*
* @deprecated 2.2.0 Use the `WordPress.DateTime.RestrictedFunctions` sniff instead.
* This `WordPress.WP.TimezoneChange` sniff will be removed in WPCS 3.0.0.
*/
class TimezoneChangeSniff extends RestrictedFunctionsSniff {
/**
* Keep track of whether the warnings have been thrown to prevent
* the messages being thrown for every token triggering the sniff.
*
* @since 2.2.0
*
* @var array
*/
private $thrown = array(
'DeprecatedSniff' => false,
'FoundPropertyForDeprecatedSniff' => false,
);
/**
* Don't use.
*
* @deprecated 2.2.0
*
* @return array
*/
public function getGroups() {
$groups = parent::getGroups();
return array( 'timezone_change' => $groups['timezone_change'] );
}
/**
* Don't use.
*
* @since 2.2.0 Added to allow for throwing the deprecation notices.
* @deprecated 2.2.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void|int
*/
public function process_token( $stackPtr ) {
if ( false === $this->thrown['DeprecatedSniff'] ) {
$this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning(
'The "WordPress.WP.TimezoneChange" sniff has been deprecated. Use the "WordPress.DateTime.RestrictedFunctions" sniff instead. Please update your custom ruleset.',
0,
'DeprecatedSniff'
);
}
if ( ! empty( $this->exclude )
&& false === $this->thrown['FoundPropertyForDeprecatedSniff']
) {
$this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning(
'The "WordPress.WP.TimezoneChange" sniff has been deprecated. Use the "WordPress.DateTime.RestrictedFunctions" sniff instead. "exclude" property setting found. Please update your custom ruleset.',
0,
'FoundPropertyForDeprecatedSniff'
);
}
return parent::process_token( $stackPtr );
}
}
@@ -0,0 +1,62 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WhiteSpace;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Ensure cast statements are preceded by whitespace.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.11.0 This sniff now has the ability to fix the issues it flags.
* @since 0.11.0 The error level for all errors thrown by this sniff has been raised from warning to error.
* @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.
* @since 1.2.0 Removed the `NoSpaceAfterCloseParenthesis` error code in favour of the
* upstream `Generic.Formatting.SpaceAfterCast.NoSpace` error.
* @since 2.2.0 Added exception for whitespace between spread operator and cast.
*/
class CastStructureSpacingSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return Tokens::$castTokens;
}
/**
* 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 ) {
if ( \T_WHITESPACE !== $this->tokens[ ( $stackPtr - 1 ) ]['code']
&& \T_ELLIPSIS !== $this->tokens[ ( $stackPtr - 1 ) ]['code']
) {
$error = 'No space before opening casting parenthesis is prohibited';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceBeforeOpenParenthesis' );
if ( true === $fix ) {
$this->phpcsFile->fixer->addContentBefore( $stackPtr, ' ' );
}
}
}
}
@@ -0,0 +1,572 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WhiteSpace;
use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Enforces spacing around logical operators and assignments, based upon Squiz code.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.1.0
* @since 2013-06-11 This sniff no longer supports JS.
* @since 0.3.0 This sniff now has the ability to fix most errors it flags.
* @since 0.7.0 This class now extends the WordPressCS native `Sniff` class.
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* Last synced with base class 2017-01-15 at commit b024ad84656c37ef5733c6998ebc1e60957b2277.
* Note: This class has diverged quite far from the original. All the same, checking occasionally
* to see if there are upstream fixes made from which this sniff can benefit, is warranted.
* @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php
*/
class ControlStructureSpacingSniff extends Sniff {
/**
* Check for blank lines on start/end of control structures.
*
* @var boolean
*/
public $blank_line_check = false;
/**
* Check for blank lines after control structures.
*
* @var boolean
*/
public $blank_line_after_check = true;
/**
* Require for space before T_COLON when using the alternative syntax for control structures.
*
* @var string one of 'required', 'forbidden', 'optional'
*/
public $space_before_colon = 'required';
/**
* How many spaces should be between a T_CLOSURE and T_OPEN_PARENTHESIS.
*
* `function[*]() {...}`
*
* @since 0.7.0
*
* @var int
*/
public $spaces_before_closure_open_paren = -1;
/**
* Tokens for which to ignore extra space on the inside of parenthesis.
*
* For functions, this is already checked by the Squiz.Functions.FunctionDeclarationArgumentSpacing sniff.
* For do / else / try, there are no parenthesis, so skip it.
*
* @since 0.11.0
*
* @var array
*/
private $ignore_extra_space_after_open_paren = array(
\T_FUNCTION => true,
\T_CLOSURE => true,
\T_DO => true,
\T_ELSE => true,
\T_TRY => true,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_IF,
\T_WHILE,
\T_FOREACH,
\T_FOR,
\T_SWITCH,
\T_DO,
\T_ELSE,
\T_ELSEIF,
\T_FUNCTION,
\T_CLOSURE,
\T_USE,
\T_TRY,
\T_CATCH,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$this->spaces_before_closure_open_paren = (int) $this->spaces_before_closure_open_paren;
if ( isset( $this->tokens[ ( $stackPtr + 1 ) ] ) && \T_WHITESPACE !== $this->tokens[ ( $stackPtr + 1 ) ]['code']
&& ! ( \T_ELSE === $this->tokens[ $stackPtr ]['code'] && \T_COLON === $this->tokens[ ( $stackPtr + 1 ) ]['code'] )
&& ! ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code']
&& 0 >= $this->spaces_before_closure_open_paren )
) {
$error = 'Space after opening control structure is required';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceAfterStructureOpen' );
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $stackPtr, ' ' );
}
}
if ( ! isset( $this->tokens[ $stackPtr ]['scope_closer'] ) ) {
if ( \T_USE === $this->tokens[ $stackPtr ]['code'] && 'closure' === $this->get_use_type( $stackPtr ) ) {
$scopeOpener = $this->phpcsFile->findNext( \T_OPEN_CURLY_BRACKET, ( $stackPtr + 1 ) );
$scopeCloser = $this->tokens[ $scopeOpener ]['scope_closer'];
} elseif ( \T_WHILE !== $this->tokens[ $stackPtr ]['code'] ) {
return;
}
} else {
$scopeOpener = $this->tokens[ $stackPtr ]['scope_opener'];
$scopeCloser = $this->tokens[ $stackPtr ]['scope_closer'];
}
// Alternative syntax.
if ( isset( $scopeOpener ) && \T_COLON === $this->tokens[ $scopeOpener ]['code'] ) {
if ( 'required' === $this->space_before_colon ) {
if ( \T_WHITESPACE !== $this->tokens[ ( $scopeOpener - 1 ) ]['code'] ) {
$error = 'Space between opening control structure and T_COLON is required';
$fix = $this->phpcsFile->addFixableError( $error, $scopeOpener, 'NoSpaceBetweenStructureColon' );
if ( true === $fix ) {
$this->phpcsFile->fixer->addContentBefore( $scopeOpener, ' ' );
}
}
} elseif ( 'forbidden' === $this->space_before_colon ) {
if ( \T_WHITESPACE === $this->tokens[ ( $scopeOpener - 1 ) ]['code'] ) {
$error = 'Extra space between opening control structure and T_COLON found';
$fix = $this->phpcsFile->addFixableError( $error, ( $scopeOpener - 1 ), 'SpaceBetweenStructureColon' );
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $scopeOpener - 1 ), '' );
}
}
}
}
$parenthesisOpener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
// If this is a function declaration.
if ( \T_FUNCTION === $this->tokens[ $stackPtr ]['code'] ) {
if ( \T_STRING === $this->tokens[ $parenthesisOpener ]['code'] ) {
$function_name_ptr = $parenthesisOpener;
} elseif ( \T_BITWISE_AND === $this->tokens[ $parenthesisOpener ]['code'] ) {
// This function returns by reference (function &function_name() {}).
$parenthesisOpener = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
( $parenthesisOpener + 1 ),
null,
true
);
$function_name_ptr = $parenthesisOpener;
}
if ( isset( $function_name_ptr ) ) {
$parenthesisOpener = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
( $parenthesisOpener + 1 ),
null,
true
);
// Checking this: function my_function[*](...) {}.
if ( ( $function_name_ptr + 1 ) !== $parenthesisOpener ) {
$error = 'Space between function name and opening parenthesis is prohibited.';
$fix = $this->phpcsFile->addFixableError(
$error,
$stackPtr,
'SpaceBeforeFunctionOpenParenthesis',
$this->tokens[ ( $function_name_ptr + 1 ) ]['content']
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $function_name_ptr + 1 ), '' );
}
}
}
} elseif ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code'] ) {
// Check if there is a use () statement.
if ( isset( $this->tokens[ $parenthesisOpener ]['parenthesis_closer'] ) ) {
$usePtr = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
( $this->tokens[ $parenthesisOpener ]['parenthesis_closer'] + 1 ),
null,
true,
null,
true
);
// If it is, we set that as the "scope opener".
if ( \T_USE === $this->tokens[ $usePtr ]['code'] ) {
$scopeOpener = $usePtr;
}
}
}
if ( \T_COLON !== $this->tokens[ $parenthesisOpener ]['code']
&& \T_FUNCTION !== $this->tokens[ $stackPtr ]['code']
) {
if ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code']
&& 0 === $this->spaces_before_closure_open_paren
) {
if ( ( $stackPtr + 1 ) !== $parenthesisOpener ) {
// Checking this: function[*](...) {}.
$error = 'Space before closure opening parenthesis is prohibited';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpaceBeforeClosureOpenParenthesis' );
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), '' );
}
}
} elseif (
(
\T_CLOSURE !== $this->tokens[ $stackPtr ]['code']
|| 1 === $this->spaces_before_closure_open_paren
)
&& ( $stackPtr + 1 ) === $parenthesisOpener
) {
// Checking this: if[*](...) {}.
$error = 'No space before opening parenthesis is prohibited';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceBeforeOpenParenthesis' );
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $stackPtr, ' ' );
}
}
}
if ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code']
&& ' ' !== $this->tokens[ ( $stackPtr + 1 ) ]['content']
) {
// Checking this: if [*](...) {}.
$error = 'Expected exactly one space before opening parenthesis; "%s" found.';
$fix = $this->phpcsFile->addFixableError(
$error,
$stackPtr,
'ExtraSpaceBeforeOpenParenthesis',
$this->tokens[ ( $stackPtr + 1 ) ]['content']
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), ' ' );
}
}
if ( \T_CLOSE_PARENTHESIS !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['code'] ) {
if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['code'] ) {
// Checking this: $value = my_function([*]...).
$error = 'No space after opening parenthesis is prohibited';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceAfterOpenParenthesis' );
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $parenthesisOpener, ' ' );
}
} elseif ( ( ' ' !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['content']
&& "\n" !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['content']
&& "\r\n" !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['content'] )
&& ! isset( $this->ignore_extra_space_after_open_paren[ $this->tokens[ $stackPtr ]['code'] ] )
) {
// Checking this: if ([*]...) {}.
$error = 'Expected exactly one space after opening parenthesis; "%s" found.';
$fix = $this->phpcsFile->addFixableError(
$error,
$stackPtr,
'ExtraSpaceAfterOpenParenthesis',
$this->tokens[ ( $parenthesisOpener + 1 ) ]['content']
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $parenthesisOpener + 1 ), ' ' );
}
}
}
if ( isset( $this->tokens[ $parenthesisOpener ]['parenthesis_closer'] ) ) {
$parenthesisCloser = $this->tokens[ $parenthesisOpener ]['parenthesis_closer'];
if ( \T_CLOSE_PARENTHESIS !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['code'] ) {
// Checking this: if (...[*]) {}.
if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisCloser - 1 ) ]['code'] ) {
$error = 'No space before closing parenthesis is prohibited';
$fix = $this->phpcsFile->addFixableError( $error, $parenthesisCloser, 'NoSpaceBeforeCloseParenthesis' );
if ( true === $fix ) {
$this->phpcsFile->fixer->addContentBefore( $parenthesisCloser, ' ' );
}
} elseif ( ' ' !== $this->tokens[ ( $parenthesisCloser - 1 ) ]['content'] ) {
$prevNonEmpty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $parenthesisCloser - 1 ), null, true );
if ( $this->tokens[ ( $parenthesisCloser ) ]['line'] === $this->tokens[ ( $prevNonEmpty + 1 ) ]['line'] ) {
$error = 'Expected exactly one space before closing parenthesis; "%s" found.';
$fix = $this->phpcsFile->addFixableError(
$error,
$stackPtr,
'ExtraSpaceBeforeCloseParenthesis',
$this->tokens[ ( $parenthesisCloser - 1 ) ]['content']
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $parenthesisCloser - 1 ), ' ' );
}
}
}
if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisCloser + 1 ) ]['code']
&& ! ( // Do NOT flag : immediately following ) for return types declarations.
\T_COLON === $this->tokens[ ( $parenthesisCloser + 1 ) ]['code']
&& ( isset( $this->tokens[ $parenthesisCloser ]['parenthesis_owner'] ) === false
|| in_array( $this->tokens[ $this->tokens[ $parenthesisCloser ]['parenthesis_owner'] ]['code'], array( \T_FUNCTION, \T_CLOSURE ), true ) )
)
&& ( isset( $scopeOpener ) && \T_COLON !== $this->tokens[ $scopeOpener ]['code'] )
) {
$error = 'Space between opening control structure and closing parenthesis is required';
$fix = $this->phpcsFile->addFixableError( $error, $scopeOpener, 'NoSpaceAfterCloseParenthesis' );
if ( true === $fix ) {
$this->phpcsFile->fixer->addContentBefore( $scopeOpener, ' ' );
}
}
}
// Ignore this for function declarations. Handled by the OpeningFunctionBraceKernighanRitchie sniff.
if ( \T_FUNCTION !== $this->tokens[ $stackPtr ]['code']
&& \T_CLOSURE !== $this->tokens[ $stackPtr ]['code']
&& isset( $this->tokens[ $parenthesisOpener ]['parenthesis_owner'] )
&& ( isset( $scopeOpener )
&& $this->tokens[ $parenthesisCloser ]['line'] !== $this->tokens[ $scopeOpener ]['line'] )
) {
$error = 'Opening brace should be on the same line as the declaration';
$fix = $this->phpcsFile->addFixableError( $error, $parenthesisOpener, 'OpenBraceNotSameLine' );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $parenthesisCloser + 1 ); $i < $scopeOpener; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->addContent( $parenthesisCloser, ' ' );
$this->phpcsFile->fixer->endChangeset();
}
return;
} elseif ( \T_WHITESPACE === $this->tokens[ ( $parenthesisCloser + 1 ) ]['code']
&& ' ' !== $this->tokens[ ( $parenthesisCloser + 1 ) ]['content']
) {
// Checking this: if (...) [*]{}.
$error = 'Expected exactly one space between closing parenthesis and opening control structure; "%s" found.';
$fix = $this->phpcsFile->addFixableError(
$error,
$stackPtr,
'ExtraSpaceAfterCloseParenthesis',
$this->tokens[ ( $parenthesisCloser + 1 ) ]['content']
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $parenthesisCloser + 1 ), ' ' );
}
}
}
if ( false !== $this->blank_line_check && isset( $scopeOpener ) ) {
$firstContent = $this->phpcsFile->findNext( \T_WHITESPACE, ( $scopeOpener + 1 ), null, true );
// We ignore spacing for some structures that tend to have their own rules.
$ignore = array(
\T_FUNCTION => true,
\T_CLOSURE => true,
\T_DOC_COMMENT_OPEN_TAG => true,
\T_CLOSE_TAG => true,
\T_COMMENT => true,
);
$ignore += Tokens::$ooScopeTokens;
if ( ! isset( $ignore[ $this->tokens[ $firstContent ]['code'] ] )
&& $this->tokens[ $firstContent ]['line'] > ( $this->tokens[ $scopeOpener ]['line'] + 1 )
) {
$error = 'Blank line found at start of control structure';
$fix = $this->phpcsFile->addFixableError( $error, $scopeOpener, 'BlankLineAfterStart' );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $scopeOpener + 1 ); $i < $firstContent; $i++ ) {
if ( $this->tokens[ $i ]['line'] === $this->tokens[ $firstContent ]['line'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->addNewline( $scopeOpener );
$this->phpcsFile->fixer->endChangeset();
}
}
if ( $firstContent !== $scopeCloser ) {
$lastContent = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $scopeCloser - 1 ), null, true );
$lastNonEmptyContent = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $scopeCloser - 1 ), null, true );
$checkToken = $lastContent;
if ( isset( $this->tokens[ $lastNonEmptyContent ]['scope_condition'] ) ) {
$checkToken = $this->tokens[ $lastNonEmptyContent ]['scope_condition'];
}
if ( ! isset( $ignore[ $this->tokens[ $checkToken ]['code'] ] )
&& $this->tokens[ $lastContent ]['line'] <= ( $this->tokens[ $scopeCloser ]['line'] - 2 )
) {
for ( $i = ( $scopeCloser - 1 ); $i > $lastContent; $i-- ) {
if ( $this->tokens[ $i ]['line'] < $this->tokens[ $scopeCloser ]['line']
&& \T_OPEN_TAG !== $this->tokens[ $firstContent ]['code']
) {
// TODO: Reporting error at empty line won't highlight it in IDE.
$error = 'Blank line found at end of control structure';
$fix = $this->phpcsFile->addFixableError( $error, $i, 'BlankLineBeforeEnd' );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $j = ( $lastContent + 1 ); $j < $scopeCloser; $j++ ) {
if ( $this->tokens[ $j ]['line'] === $this->tokens[ $scopeCloser ]['line'] ) {
break;
}
$this->phpcsFile->fixer->replaceToken( $j, '' );
}
/*
* PHPCS annotations, like normal inline comments, are tokenized including
* the new line at the end, so don't add any extra as it would cause a fixer
* conflict.
*/
if ( \T_COMMENT !== $this->tokens[ $lastContent ]['code']
&& ! isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $lastContent ]['code'] ] ) ) {
$this->phpcsFile->fixer->addNewlineBefore( $j );
}
$this->phpcsFile->fixer->endChangeset();
}
break;
}
}
}
}
unset( $ignore );
}
if ( ! isset( $scopeCloser ) || true !== $this->blank_line_after_check ) {
return;
}
// {@internal This is just for the blank line check. Only whitespace should be considered,
// not "other" empty tokens.}}
$trailingContent = $this->phpcsFile->findNext( \T_WHITESPACE, ( $scopeCloser + 1 ), null, true );
if ( false === $trailingContent ) {
return;
}
if ( \T_COMMENT === $this->tokens[ $trailingContent ]['code']
|| isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $trailingContent ]['code'] ] )
) {
// Special exception for code where the comment about
// an ELSE or ELSEIF is written between the control structures.
$nextCode = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $scopeCloser + 1 ), null, true );
if ( \T_ELSE === $this->tokens[ $nextCode ]['code'] || \T_ELSEIF === $this->tokens[ $nextCode ]['code'] ) {
$trailingContent = $nextCode;
}
// Move past end comments.
if ( $this->tokens[ $trailingContent ]['line'] === $this->tokens[ $scopeCloser ]['line'] ) {
if ( preg_match( '`^//[ ]?end`i', $this->tokens[ $trailingContent ]['content'], $matches ) > 0 ) {
$scopeCloser = $trailingContent;
$trailingContent = $this->phpcsFile->findNext( \T_WHITESPACE, ( $trailingContent + 1 ), null, true );
}
}
}
if ( \T_ELSE === $this->tokens[ $trailingContent ]['code'] && \T_IF === $this->tokens[ $stackPtr ]['code'] ) {
// IF with ELSE.
return;
}
if ( \T_WHILE === $this->tokens[ $trailingContent ]['code'] && \T_DO === $this->tokens[ $stackPtr ]['code'] ) {
// DO with WHILE.
return;
}
if ( \T_CLOSE_TAG === $this->tokens[ $trailingContent ]['code'] ) {
// At the end of the script or embedded code.
return;
}
if ( isset( $this->tokens[ $trailingContent ]['scope_condition'] )
&& \T_CLOSE_CURLY_BRACKET === $this->tokens[ $trailingContent ]['code']
) {
// Another control structure's closing brace.
$owner = $this->tokens[ $trailingContent ]['scope_condition'];
if ( \in_array( $this->tokens[ $owner ]['code'], array( \T_FUNCTION, \T_CLOSURE, \T_CLASS, \T_ANON_CLASS, \T_INTERFACE, \T_TRAIT ), true ) ) {
// The next content is the closing brace of a function, class, interface or trait
// so normal function/class rules apply and we can ignore it.
return;
}
if ( ( $this->tokens[ $scopeCloser ]['line'] + 1 ) !== $this->tokens[ $trailingContent ]['line'] ) {
// TODO: Won't cover following case: "} echo 'OK';".
$error = 'Blank line found after control structure';
$fix = $this->phpcsFile->addFixableError( $error, $scopeCloser, 'BlankLineAfterEnd' );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
$i = ( $scopeCloser + 1 );
while ( $this->tokens[ $i ]['line'] !== $this->tokens[ $trailingContent ]['line'] ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
$i++;
}
// TODO: Instead a separate error should be triggered when content comes right after closing brace.
if ( \T_COMMENT !== $this->tokens[ $scopeCloser ]['code']
&& isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $scopeCloser ]['code'] ] ) === false
) {
$this->phpcsFile->fixer->addNewlineBefore( $trailingContent );
}
$this->phpcsFile->fixer->endChangeset();
}
}
}
}
}
@@ -0,0 +1,104 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WhiteSpace;
use WordPressCS\WordPress\Sniff;
use WordPressCS\WordPress\PHPCSHelper;
/**
* Enforces using spaces for mid-line alignment.
*
* @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 DisallowInlineTabsSniff extends Sniff {
/**
* 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,
);
}
/**
* 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 );
}
$check_tokens = array(
\T_WHITESPACE => true,
\T_DOC_COMMENT_WHITESPACE => true,
\T_DOC_COMMENT_STRING => true,
);
for ( $i = ( $stackPtr + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) {
// Skip all non-whitespace tokens and skip whitespace at the start of a new line.
if ( ! isset( $check_tokens[ $this->tokens[ $i ]['code'] ] ) || 1 === $this->tokens[ $i ]['column'] ) {
continue;
}
// If tabs are being converted to spaces by the tokenizer, the
// original content should be checked instead of the converted content.
if ( isset( $this->tokens[ $i ]['orig_content'] ) ) {
$content = $this->tokens[ $i ]['orig_content'];
} else {
$content = $this->tokens[ $i ]['content'];
}
if ( '' === $content || strpos( $content, "\t" ) === false ) {
continue;
}
$fix = $this->phpcsFile->addFixableError(
'Spaces must be used for mid-line alignment; tabs are not allowed',
$i,
'NonIndentTabsUsed'
);
if ( true === $fix ) {
if ( isset( $this->tokens[ $i ]['orig_content'] ) ) {
// Use the replacement that PHPCS has already done.
$this->phpcsFile->fixer->replaceToken( $i, $this->tokens[ $i ]['content'] );
} else {
// Replace tabs with spaces, using an indent of $tab_width.
// Other sniffs can then correct the indent if they need to.
$spaces = str_repeat( ' ', $this->tab_width );
$newContent = str_replace( "\t", $spaces, $this->tokens[ $i ]['content'] );
$this->phpcsFile->fixer->replaceToken( $i, $newContent );
}
}
}
// Ignore the rest of the file.
return ( $this->phpcsFile->numTokens + 1 );
}
}
@@ -0,0 +1,64 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Sniffs\WhiteSpace;
use PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\OperatorSpacingSniff as PHPCS_Squiz_OperatorSpacingSniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Verify operator spacing, uses the Squiz sniff, but additionally also sniffs for the `!` (boolean not) operator.
*
* "Always put spaces after commas, and on both sides of logical, comparison, string and assignment operators."
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.1.0
* @since 0.3.0 This sniff now has the ability to fix the issues it flags.
* @since 0.12.0 This sniff used to be a copy of a very old and outdated version of the
* upstream sniff.
* Now, the sniff defers completely to the upstream sniff, adding just the
* T_BOOLEAN_NOT and the logical operators (`&&` and the like) - via the
* registration method and changing the value of the customizable
* $ignoreNewlines property.
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* Last synced with base class June 2017 at commit 41127aa4764536f38f504fb3f7b8831f05919c89.
* @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php
*/
class OperatorSpacingSniff extends PHPCS_Squiz_OperatorSpacingSniff {
/**
* Allow newlines instead of spaces.
*
* N.B.: The upstream sniff defaults to `false`.
*
* @var boolean
*/
public $ignoreNewlines = true;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
$tokens = parent::register();
$tokens[ \T_BOOLEAN_NOT ] = \T_BOOLEAN_NOT;
$tokens[ \T_INSTANCEOF ] = \T_INSTANCEOF;
$logical_operators = Tokens::$booleanOperators;
// Using array union to auto-dedup.
return $tokens + $logical_operators;
}
}
@@ -0,0 +1,199 @@
<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/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 );
}
}