基础代码
This commit is contained in:
+468
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+542
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+192
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+313
@@ -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, ' ' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+617
@@ -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';
|
||||
}
|
||||
|
||||
}
|
||||
+204
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+235
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
+161
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+90
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+265
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+661
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -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',
|
||||
),
|
||||
),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+174
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+1046
File diff suppressed because it is too large
Load Diff
Vendored
+187
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+289
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+208
@@ -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 ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+299
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+103
@@ -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'],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+239
@@ -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',
|
||||
),
|
||||
),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+69
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+48
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+125
@@ -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' );
|
||||
}
|
||||
|
||||
}
|
||||
+505
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+178
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -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__' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+48
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+233
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+731
@@ -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;
|
||||
}
|
||||
}
|
||||
+316
@@ -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( '`(?:\?|&|&)[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'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+120
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+1422
File diff suppressed because it is too large
Load Diff
+216
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+338
@@ -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' => '–',
|
||||
'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' => '–',
|
||||
'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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+217
@@ -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 ],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+57
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+223
@@ -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;" ) );
|
||||
}
|
||||
}
|
||||
+64
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+466
@@ -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 );
|
||||
}
|
||||
}
|
||||
+62
@@ -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, ' ' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+572
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+104
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+199
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user