Introducing coding standards validation.

This commit is contained in:
Marcos Schratzenstaller
2019-04-22 17:28:46 -03:00
committed by Nathan Rice
parent 6e40c921ab
commit fd504d41ac
1624 changed files with 190339 additions and 262 deletions
@@ -0,0 +1,64 @@
<?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 WordPress\Sniffs\Arrays;
use WordPress\AbstractArrayAssignmentRestrictionsSniff;
/**
* Restricts array assignment of certain keys.
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* @deprecated 0.10.0 The functionality which used to be contained in this class has been moved to
* the WordPress_AbstractArrayAssignmentRestrictionsSniff class.
* This class is left here to prevent backward-compatibility breaks for
* custom sniffs extending the old class and references to this
* sniff from custom phpcs.xml files.
* This file is also still used to unit test the abstract class.
* @see \WordPress\AbstractArrayAssignmentRestrictionsSniff
*/
class ArrayAssignmentRestrictionsSniff extends AbstractArrayAssignmentRestrictionsSniff {
/**
* Groups of variables to restrict.
*
* Example: groups => array(
* 'groupname' => array(
* 'type' => 'error' | 'warning',
* 'message' => 'Dont use this one please!',
* 'keys' => array( 'key1', 'another_key' ),
* 'callback' => array( 'class', 'method' ), // Optional.
* )
* )
*
* @return array
*/
public function getGroups() {
return array();
}
/**
* 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 ) {
return true;
}
}
@@ -0,0 +1,61 @@
<?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 WordPress\Sniffs\Arrays;
use PHP_CodeSniffer_File as File;
/**
* Enforces WordPress array format, based upon Squiz code.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.1.0
* @since 0.5.0 Now extends `Squiz_Sniffs_Arrays_ArrayDeclarationSniff`.
* @since 0.11.0 The additional single-line array checks have been moved to their own
* sniff WordPress.Arrays.ArrayDeclarationSpacing.
* This class now only contains a slimmed down version of the upstream sniff.
* @since 0.13.0 Class name changed: this class is now namespaced.
*
* @deprecated 0.13.0 This sniff has now been deprecated. Most checks which were previously
* contained herein had recently been excluded in favour of dedicated
* sniffs with higher precision. The last remaining checks which were not
* already covered elsewhere have been moved to the `ArrayDeclarationSpacing`
* sniff.
* This class is left here to prevent breaking custom rulesets which refer
* to this sniff.
*/
class ArrayDeclarationSniff {
/**
* Don't use.
*
* @deprecated 0.13.0
*
* @return int[]
*/
public function register() {
return array();
}
/**
* Don't use.
*
* @deprecated 0.13.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile A PHP_CodeSniffer file.
* @param int $stackPtr The position of the token.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {}
}
@@ -0,0 +1,442 @@
<?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 WordPress\Sniffs\Arrays;
use WordPress\Sniff;
use PHP_CodeSniffer_Tokens as 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_Sniffs_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 `WordPress_Sniff` instead.
* @since 0.13.0 Added the last remaining checks from the `ArrayDeclaration` sniff
* which were not covered elsewhere. The `ArrayDeclaration` sniff has
* now been deprecated.
* @since 0.13.0 Class name changed: this class is now namespaced.
* @since 0.14.0 Single item associative arrays are now by default exempt from the
* "must be multi-line" rule. This behaviour can be changed using the
* `allow_single_item_single_line_associative_arrays` property.
*/
class ArrayDeclarationSpacingSniff extends Sniff {
/**
* Whether or not to allow single item associative arrays to be single line.
*
* @since 0.14.0
*
* @var bool Defaults to true.
*/
public $allow_single_item_single_line_associative_arrays = true;
/**
* Token this sniff targets.
*
* Also used for distinguishing between the array and an array value
* which is also an array.
*
* @since 0.12.0
*
* @var array
*/
private $targets = array(
\T_ARRAY => \T_ARRAY,
\T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY,
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.12.0
*
* @return array
*/
public function register() {
return $this->targets;
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.12.0 The actual checks contained in this method used to
* be in the `processSingleLineArray()` method.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
unset( $array_open_close );
/*
* Long arrays only: Check for space between the array keyword and the open parenthesis.
*/
if ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] ) {
if ( ( $stackPtr + 1 ) !== $opener ) {
$error = 'There must be no space between the "array" keyword and the opening parenthesis';
$error_code = 'SpaceAfterKeyword';
$nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), ( $opener + 1 ), true );
if ( $nextNonWhitespace !== $opener ) {
// Don't auto-fix: Something other than whitespace found between keyword and open parenthesis.
$this->phpcsFile->addError( $error, $stackPtr, $error_code );
} else {
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code );
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $stackPtr + 1 ); $i < $opener; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
unset( $i );
}
}
unset( $error, $error_code, $nextNonWhitespace, $fix );
}
}
/*
* Check for empty arrays.
*/
$nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $opener + 1 ), ( $closer + 1 ), true );
if ( $nextNonWhitespace === $closer ) {
if ( ( $opener + 1 ) !== $closer ) {
$fix = $this->phpcsFile->addFixableError(
'Empty array declaration must have no space between the parentheses',
$stackPtr,
'SpaceInEmptyArray'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
for ( $i = ( $opener + 1 ); $i < $closer; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
$this->phpcsFile->fixer->endChangeset();
unset( $i );
}
}
// This array is empty, so the below checks aren't necessary.
return;
}
unset( $nextNonWhitespace );
// Pass off to either the single line or multi-line array analysis.
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
$this->process_single_line_array( $stackPtr, $opener, $closer );
} else {
$this->process_multi_line_array( $stackPtr, $opener, $closer );
}
}
/**
* Process a single-line array.
*
* @since 0.13.0 The actual checks contained in this method used to
* be in the `process()` method.
*
* @param int $stackPtr The position of the current token in the stack.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_single_line_array( $stackPtr, $opener, $closer ) {
/*
* Check that associative arrays are always multi-line.
*/
$array_has_keys = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $opener, $closer );
if ( false !== $array_has_keys ) {
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( ( false === $this->allow_single_item_single_line_associative_arrays
&& ! empty( $array_items ) )
|| ( true === $this->allow_single_item_single_line_associative_arrays
&& \count( $array_items ) > 1 )
) {
/*
* Make sure the double arrow is for *this* array, not for a nested one.
*/
$array_has_keys = false; // Reset before doing more detailed check.
foreach ( $array_items as $item ) {
for ( $ptr = $item['start']; $ptr <= $item['end']; $ptr++ ) {
if ( \T_DOUBLE_ARROW === $this->tokens[ $ptr ]['code'] ) {
$array_has_keys = true;
break 2;
}
// Skip passed any nested arrays.
if ( isset( $this->targets[ $this->tokens[ $ptr ]['code'] ] ) ) {
$nested_array_open_close = $this->find_array_open_close( $ptr );
if ( false === $nested_array_open_close ) {
// Nested array open/close could not be determined.
continue;
}
$ptr = $nested_array_open_close['closer'];
}
}
}
if ( true === $array_has_keys ) {
$phrase = 'an';
if ( true === $this->allow_single_item_single_line_associative_arrays ) {
$phrase = 'a multi-item';
}
$fix = $this->phpcsFile->addFixableError(
'When %s array uses associative keys, each value should start on a new line.',
$closer,
'AssociativeArrayFound',
array( $phrase )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
foreach ( $array_items as $item ) {
/*
* Add a line break before the first non-empty token in the array item.
* Prevents extraneous whitespace at the start of the line which could be
* interpreted as alignment whitespace.
*/
$first_non_empty = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$item['start'],
( $item['end'] + 1 ),
true
);
if ( false === $first_non_empty ) {
continue;
}
if ( $item['start'] <= ( $first_non_empty - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $first_non_empty - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $first_non_empty - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $first_non_empty );
}
$this->phpcsFile->fixer->endChangeset();
}
// No need to check for spacing around opener/closer as this array should be multi-line.
return;
}
}
}
/*
* Check that there is a single space after the array opener and before the array closer.
*/
if ( \T_WHITESPACE !== $this->tokens[ ( $opener + 1 ) ]['code'] ) {
$fix = $this->phpcsFile->addFixableError(
'Missing space after array opener.',
$opener,
'NoSpaceAfterArrayOpener'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $opener, ' ' );
}
} elseif ( ' ' !== $this->tokens[ ( $opener + 1 ) ]['content'] ) {
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space after array opener, found %s.',
$opener,
'SpaceAfterArrayOpener',
array( \strlen( $this->tokens[ ( $opener + 1 ) ]['content'] ) )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $opener + 1 ), ' ' );
}
}
if ( \T_WHITESPACE !== $this->tokens[ ( $closer - 1 ) ]['code'] ) {
$fix = $this->phpcsFile->addFixableError(
'Missing space before array closer.',
$closer,
'NoSpaceBeforeArrayCloser'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContentBefore( $closer, ' ' );
}
} elseif ( ' ' !== $this->tokens[ ( $closer - 1 ) ]['content'] ) {
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space before array closer, found %s.',
$closer,
'SpaceBeforeArrayCloser',
array( \strlen( $this->tokens[ ( $closer - 1 ) ]['content'] ) )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), ' ' );
}
}
}
/**
* Process a multi-line array.
*
* @since 0.13.0 The actual checks contained in this method used to
* be in the `ArrayDeclaration` sniff.
*
* @param int $stackPtr The position of the current token in the stack.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_multi_line_array( $stackPtr, $opener, $closer ) {
/*
* Check that the closing bracket is on a new line.
*/
$last_content = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $closer - 1 ), $opener, true );
if ( false !== $last_content
&& $this->tokens[ $last_content ]['line'] === $this->tokens[ $closer ]['line']
) {
$fix = $this->phpcsFile->addFixableError(
'Closing parenthesis of array declaration must be on a new line',
$closer,
'CloseBraceNewLine'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
if ( $last_content < ( $closer - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $closer );
$this->phpcsFile->fixer->endChangeset();
}
}
/*
* Check that each array item starts on a new line.
*/
$array_items = $this->get_function_call_parameters( $stackPtr );
$end_of_last_item = $opener;
foreach ( $array_items as $item ) {
$end_of_this_item = ( $item['end'] + 1 );
// Find the line on which the item starts.
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$item['start'],
$end_of_this_item,
true
);
// Ignore comments after array items if the next real content starts on a new line.
if ( \T_COMMENT === $this->tokens[ $first_content ]['code']
|| isset( $this->phpcsCommentTokens[ $this->tokens[ $first_content ]['type'] ] )
) {
$next = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
( $first_content + 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 ( $item['start'] <= ( $first_content - 1 )
&& \T_WHITESPACE === $this->tokens[ ( $first_content - 1 ) ]['code']
) {
// Remove whitespace which would otherwise becoming trailing
// (as it gives problems with the fixed file).
$this->phpcsFile->fixer->replaceToken( ( $first_content - 1 ), '' );
}
$this->phpcsFile->fixer->addNewlineBefore( $first_content );
$this->phpcsFile->fixer->endChangeset();
}
}
$end_of_last_item = $end_of_this_item;
}
}
}
@@ -0,0 +1,534 @@
<?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 WordPress\Sniffs\Arrays;
use WordPress\Sniff;
use WordPress\PHPCSHelper;
use PHP_CodeSniffer_Tokens as 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 void
*/
public function process_token( $stackPtr ) {
if ( ! isset( $this->tab_width ) ) {
$this->tab_width = PHPCSHelper::get_tab_width( $this->phpcsFile );
}
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
// Not interested in single line arrays.
return;
}
/*
* Check the closing bracket is lined up with the start of the content on the line
* containing the array opener.
*/
$opener_line_spaces = $this->get_indentation_size( $opener );
$closer_line_spaces = ( $this->tokens[ $closer ]['column'] - 1 );
if ( $closer_line_spaces !== $opener_line_spaces ) {
$error = 'Array closer not aligned correctly; expected %s space(s) but found %s';
$error_code = 'CloseBraceNotAligned';
/*
* Report & fix the issue if the close brace is on its own line with
* nothing or only indentation whitespace before it.
*/
if ( 0 === $closer_line_spaces
|| ( \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
&& 1 === $this->tokens[ ( $closer - 1 ) ]['column'] )
) {
$this->add_array_alignment_error(
$closer,
$error,
$error_code,
$opener_line_spaces,
$closer_line_spaces,
$this->get_indentation_string( $opener_line_spaces )
);
} else {
/*
* Otherwise, only report the error, don't try and fix it (yet).
*
* It will get corrected in a future loop of the fixer once the closer
* has been moved to its own line by the `ArrayDeclarationSpacing` sniff.
*/
$this->phpcsFile->addError(
$error,
$closer,
$error_code,
array( $opener_line_spaces, $closer_line_spaces )
);
}
unset( $error, $error_code );
}
/*
* Verify & correct the array item indentation.
*/
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( empty( $array_items ) ) {
// Strange, no array items found.
return;
}
$expected_spaces = ( $opener_line_spaces + $this->tab_width );
$expected_indent = $this->get_indentation_string( $expected_spaces );
$end_of_previous_item = $opener;
foreach ( $array_items as $item ) {
$end_of_this_item = ( $item['end'] + 1 );
// Find the line on which the item starts.
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$item['start'],
$end_of_this_item,
true
);
// Deal with trailing comments.
if ( false !== $first_content
&& \T_COMMENT === $this->tokens[ $first_content ]['code']
&& $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_previous_item ]['line']
) {
$first_content = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE, \T_COMMENT ),
( $first_content + 1 ),
$end_of_this_item,
true
);
}
if ( false === $first_content ) {
$end_of_previous_item = $end_of_this_item;
continue;
}
// Bow out from reporting and fixing mixed multi-line/single-line arrays.
// That is handled by the ArrayDeclarationSpacingSniff.
if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_previous_item ]['line']
|| ( 1 !== $this->tokens[ $first_content ]['column']
&& \T_WHITESPACE !== $this->tokens[ ( $first_content - 1 ) ]['code'] )
) {
return $closer;
}
$found_spaces = ( $this->tokens[ $first_content ]['column'] - 1 );
if ( $found_spaces !== $expected_spaces ) {
$this->add_array_alignment_error(
$first_content,
'Array item not aligned correctly; expected %s spaces but found %s',
'ItemNotAligned',
$expected_spaces,
$found_spaces,
$expected_indent
);
}
// No need for further checking if this is a one-line array item.
if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
$end_of_previous_item = $end_of_this_item;
continue;
}
/*
* Multi-line array items.
*
* Verify & if needed, correct the indentation of subsequent lines.
* Subsequent lines may be indented more or less than the mimimum expected indent,
* but the "first line after" should be indented - at least - as much as the very first line
* of the array item.
* Indentation correction for subsequent lines will be based on that diff.
*/
// Find first token on second line of the array item.
// If the second line is a heredoc/nowdoc, continue on until we find a line with a different token.
// Same for the second line of a multi-line text string.
for ( $ptr = ( $first_content + 1 ); $ptr <= $item['end']; $ptr++ ) {
if ( $this->tokens[ $first_content ]['line'] !== $this->tokens[ $ptr ]['line']
&& 1 === $this->tokens[ $ptr ]['column']
&& false === $this->ignore_token( $ptr )
) {
break;
}
}
$first_content_on_line2 = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$ptr,
$end_of_this_item,
true
);
if ( false === $first_content_on_line2 ) {
/*
* Apparently there were only tokens in the ignore list on subsequent lines.
*
* In that case, the comma after the array item might be on a line by itself,
* so check its placement.
*/
if ( $this->tokens[ $item['end'] ]['line'] !== $this->tokens[ $end_of_this_item ]['line']
&& \T_COMMA === $this->tokens[ $end_of_this_item ]['code']
&& ( $this->tokens[ $end_of_this_item ]['column'] - 1 ) !== $expected_spaces
) {
$this->add_array_alignment_error(
$end_of_this_item,
'Comma after multi-line array item not aligned correctly; expected %s spaces, but found %s',
'MultiLineArrayItemCommaNotAligned',
$expected_spaces,
( $this->tokens[ $end_of_this_item ]['column'] - 1 ),
$expected_indent
);
}
$end_of_previous_item = $end_of_this_item;
continue;
}
$found_spaces_on_line2 = $this->get_indentation_size( $first_content_on_line2 );
$expected_spaces_on_line2 = $expected_spaces;
if ( $found_spaces < $found_spaces_on_line2 ) {
$expected_spaces_on_line2 += ( $found_spaces_on_line2 - $found_spaces );
}
if ( $found_spaces_on_line2 !== $expected_spaces_on_line2 ) {
$fix = $this->phpcsFile->addFixableError(
'Multi-line array item not aligned correctly; expected %s spaces, but found %s',
$first_content_on_line2,
'MultiLineArrayItemNotAligned',
array(
$expected_spaces_on_line2,
$found_spaces_on_line2,
)
);
if ( true === $fix ) {
$expected_indent_on_line2 = $this->get_indentation_string( $expected_spaces_on_line2 );
$this->phpcsFile->fixer->beginChangeset();
// Fix second line for the array item.
if ( 1 === $this->tokens[ $first_content_on_line2 ]['column']
&& \T_COMMENT === $this->tokens[ $first_content_on_line2 ]['code']
) {
$actual_comment = ltrim( $this->tokens[ $first_content_on_line2 ]['content'] );
$replacement = $expected_indent_on_line2 . $actual_comment;
$this->phpcsFile->fixer->replaceToken( $first_content_on_line2, $replacement );
} else {
$this->fix_alignment_error( $first_content_on_line2, $expected_indent_on_line2 );
}
// Fix subsequent lines.
for ( $i = ( $first_content_on_line2 + 1 ); $i <= $item['end']; $i++ ) {
// We're only interested in the first token on each line.
if ( 1 !== $this->tokens[ $i ]['column'] ) {
if ( $this->tokens[ $i ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
// We might as well quit if we're past the first token on the last line.
break;
}
continue;
}
$first_content_on_line = $this->phpcsFile->findNext(
array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ),
$i,
$end_of_this_item,
true
);
if ( false === $first_content_on_line ) {
break;
}
// Ignore lines with heredoc and nowdoc tokens and subsequent lines in multi-line strings.
if ( true === $this->ignore_token( $first_content_on_line ) ) {
$i = $first_content_on_line;
continue;
}
$found_spaces_on_line = $this->get_indentation_size( $first_content_on_line );
$expected_spaces_on_line = ( $expected_spaces_on_line2 + ( $found_spaces_on_line - $found_spaces_on_line2 ) );
$expected_spaces_on_line = max( $expected_spaces_on_line, 0 ); // Can't be below 0.
$expected_indent_on_line = $this->get_indentation_string( $expected_spaces_on_line );
if ( $found_spaces_on_line !== $expected_spaces_on_line ) {
if ( 1 === $this->tokens[ $first_content_on_line ]['column']
&& \T_COMMENT === $this->tokens[ $first_content_on_line ]['code']
) {
$actual_comment = ltrim( $this->tokens[ $first_content_on_line ]['content'] );
$replacement = $expected_indent_on_line . $actual_comment;
$this->phpcsFile->fixer->replaceToken( $first_content_on_line, $replacement );
} else {
$this->fix_alignment_error( $first_content_on_line, $expected_indent_on_line );
}
}
// Move past any potential empty lines between the previous non-empty line and this one.
// No need to do the fixes twice.
$i = $first_content_on_line;
}
/*
* Check the placement of the comma after the array item as it might be on a line by itself.
*/
if ( $this->tokens[ $item['end'] ]['line'] !== $this->tokens[ $end_of_this_item ]['line']
&& \T_COMMA === $this->tokens[ $end_of_this_item ]['code']
&& ( $this->tokens[ $end_of_this_item ]['column'] - 1 ) !== $expected_spaces
) {
$this->add_array_alignment_error(
$end_of_this_item,
'Comma after array item not aligned correctly; expected %s spaces, but found %s',
'MultiLineArrayItemCommaNotAligned',
$expected_spaces,
( $this->tokens[ $end_of_this_item ]['column'] - 1 ),
$expected_indent
);
}
$this->phpcsFile->fixer->endChangeset();
}
}
$end_of_previous_item = $end_of_this_item;
}
}
/**
* Should the token be ignored ?
*
* This method is only intended to be used with the first token on a line
* for subsequent lines in an multi-line array item.
*
* @param int $ptr Stack pointer to the first token on a line.
*
* @return bool
*/
protected function ignore_token( $ptr ) {
$token_code = $this->tokens[ $ptr ]['code'];
if ( isset( $this->ignore_tokens[ $token_code ] ) ) {
return true;
}
/*
* If it's a subsequent line of a multi-line sting, it will not start with a quote
* character, nor just *be* a quote character.
*/
if ( \T_CONSTANT_ENCAPSED_STRING === $token_code
|| \T_DOUBLE_QUOTED_STRING === $token_code
) {
// Deal with closing quote of a multi-line string being on its own line.
if ( "'" === $this->tokens[ $ptr ]['content']
|| '"' === $this->tokens[ $ptr ]['content']
) {
return true;
}
// Deal with subsequent lines of a multi-line string where the token is broken up per line.
if ( "'" !== $this->tokens[ $ptr ]['content'][0]
&& '"' !== $this->tokens[ $ptr ]['content'][0]
) {
return true;
}
}
return false;
}
/**
* Determine the line indentation whitespace.
*
* @param int $ptr Stack pointer to an arbitrary token on a line.
*
* @return int Nr of spaces found. Where necessary, tabs are translated to spaces.
*/
protected function get_indentation_size( $ptr ) {
// Find the first token on the line.
for ( ; $ptr >= 0; $ptr-- ) {
if ( 1 === $this->tokens[ $ptr ]['column'] ) {
break;
}
}
$whitespace = '';
if ( \T_WHITESPACE === $this->tokens[ $ptr ]['code']
|| \T_DOC_COMMENT_WHITESPACE === $this->tokens[ $ptr ]['code']
) {
return $this->tokens[ $ptr ]['length'];
}
/*
* Special case for multi-line, non-docblock comments.
* Only applicable for subsequent lines in an array item.
*
* First/Single line is tokenized as T_WHITESPACE + T_COMMENT
* Subsequent lines are tokenized as T_COMMENT including the indentation whitespace.
*/
if ( \T_COMMENT === $this->tokens[ $ptr ]['code'] ) {
$content = $this->tokens[ $ptr ]['content'];
$actual_comment = ltrim( $content );
$whitespace = str_replace( $actual_comment, '', $content );
}
return \strlen( $whitespace );
}
/**
* Create an indentation string.
*
* @param int $nr Number of spaces the indentation should be.
*
* @return string
*/
protected function get_indentation_string( $nr ) {
if ( 0 >= $nr ) {
return '';
}
// Space-based indentation.
if ( false === $this->tabIndent ) {
return str_repeat( ' ', $nr );
}
// Tab-based indentation.
$num_tabs = (int) floor( $nr / $this->tab_width );
$remaining = ( $nr % $this->tab_width );
$tab_indent = str_repeat( "\t", $num_tabs );
$tab_indent .= str_repeat( ' ', $remaining );
return $tab_indent;
}
/**
* Throw an error and fix incorrect array alignment.
*
* @param int $ptr Stack pointer to the first content on the line.
* @param string $error Error message.
* @param string $error_code Error code.
* @param int $expected Expected nr of spaces (tabs translated to space value).
* @param int $found Found nr of spaces (tabs translated to space value).
* @param string $new_indent Whitespace indent replacement content.
*/
protected function add_array_alignment_error( $ptr, $error, $error_code, $expected, $found, $new_indent ) {
$fix = $this->phpcsFile->addFixableError( $error, $ptr, $error_code, array( $expected, $found ) );
if ( true === $fix ) {
$this->fix_alignment_error( $ptr, $new_indent );
}
}
/**
* Fix incorrect array alignment.
*
* @param int $ptr Stack pointer to the first content on the line.
* @param string $new_indent Whitespace indent replacement content.
*/
protected function fix_alignment_error( $ptr, $new_indent ) {
if ( 1 === $this->tokens[ $ptr ]['column'] ) {
$this->phpcsFile->fixer->addContentBefore( $ptr, $new_indent );
} else {
$this->phpcsFile->fixer->replaceToken( ( $ptr - 1 ), $new_indent );
}
}
}
@@ -0,0 +1,90 @@
<?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 WordPress\Sniffs\Arrays;
use WordPress\Sniff;
/**
* Check for proper spacing in array key references.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.3.0
* @since 0.7.0 This sniff now has the ability to fix a number of the issues it flags.
* @since 0.12.0 This class now extends WordPress_Sniff.
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class ArrayKeySpacingRestrictionsSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_OPEN_SQUARE_BRACKET,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
$token = $this->tokens[ $stackPtr ];
if ( ! isset( $token['bracket_closer'] ) ) {
$this->phpcsFile->addWarning( 'Missing bracket closer.', $stackPtr, 'MissingBracketCloser' );
return;
}
$need_spaces = $this->phpcsFile->findNext(
array( \T_CONSTANT_ENCAPSED_STRING, \T_LNUMBER, \T_WHITESPACE, \T_MINUS ),
( $stackPtr + 1 ),
$token['bracket_closer'],
true
);
$spaced1 = ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] );
$spaced2 = ( \T_WHITESPACE === $this->tokens[ ( $token['bracket_closer'] - 1 ) ]['code'] );
// It should have spaces unless if it only has strings or numbers as the key.
if ( false !== $need_spaces && ! ( $spaced1 && $spaced2 ) ) {
$error = 'Array keys must be surrounded by spaces unless they contain a string or an integer.';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpacesAroundArrayKeys' );
if ( true === $fix ) {
if ( ! $spaced1 ) {
$this->phpcsFile->fixer->addContentBefore( ( $stackPtr + 1 ), ' ' );
}
if ( ! $spaced2 ) {
$this->phpcsFile->fixer->addContentBefore( $token['bracket_closer'], ' ' );
}
}
} elseif ( false === $need_spaces && ( $spaced1 || $spaced2 ) ) {
$error = 'Array keys must NOT be surrounded by spaces if they only contain a string or an integer.';
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpacesAroundArrayKeys' );
if ( true === $fix ) {
if ( $spaced1 ) {
$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), '' );
}
if ( $spaced2 ) {
$this->phpcsFile->fixer->replaceToken( ( $token['bracket_closer'] - 1 ), '' );
}
}
}
}
}
@@ -0,0 +1,291 @@
<?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 WordPress\Sniffs\Arrays;
use WordPress\Sniff;
use PHP_CodeSniffer_Tokens as Tokens;
/**
* Enforces a comma after each array item and the spacing around it.
*
* Rules:
* - For multi-line arrays, a comma is needed after each array item.
* - Same for single-line arrays, but no comma is allowed after the last array item.
* - There should be no space between the comma and the end of the array item.
* - There should be exactly one space between the comma and the start of the
* next array item for single-line items.
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.12.0
* @since 0.13.0 Class name changed: this class is now namespaced.
*/
class CommaAfterArrayItemSniff extends Sniff {
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return array(
\T_ARRAY,
\T_OPEN_SHORT_ARRAY,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return void
*/
public function process_token( $stackPtr ) {
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
unset( $array_open_close );
// This array is empty, so the below checks aren't necessary.
if ( ( $opener + 1 ) === $closer ) {
return;
}
$single_line = true;
if ( $this->tokens[ $opener ]['line'] !== $this->tokens[ $closer ]['line'] ) {
$single_line = false;
}
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( empty( $array_items ) ) {
// Strange, no array items found.
return;
}
$array_item_count = \count( $array_items );
// Note: $item_index is 1-based and the array items are split on the commas!
foreach ( $array_items as $item_index => $item ) {
$maybe_comma = ( $item['end'] + 1 );
$is_comma = false;
if ( isset( $this->tokens[ $maybe_comma ] ) && \T_COMMA === $this->tokens[ $maybe_comma ]['code'] ) {
$is_comma = true;
}
/*
* Check if this is a comma at the end of the last item in a single line array.
*/
if ( true === $single_line && $item_index === $array_item_count ) {
if ( true === $is_comma ) {
$fix = $this->phpcsFile->addFixableError(
'Comma not allowed after last value in single-line array declaration',
$maybe_comma,
'CommaAfterLast'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( $maybe_comma, '' );
}
}
/*
* No need to do the spacing checks for the last item in a single line array.
* This is handled by another sniff checking the spacing before the array closer.
*/
continue;
}
$last_content = $this->phpcsFile->findPrevious(
Tokens::$emptyTokens,
$item['end'],
$item['start'],
true
);
if ( false === $last_content ) {
// Shouldn't be able to happen, but just in case, ignore this array item.
continue;
}
/**
* Make sure every item in a multi-line array has a comma at the end.
*
* Should in reality only be triggered by the last item in a multi-line array
* as otherwise we'd have a parse error already.
*/
if ( false === $is_comma && false === $single_line ) {
$fix = $this->phpcsFile->addFixableError(
'Each array item in a multi-line array declaration must end in a comma',
$last_content,
'NoComma'
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $last_content, ',' );
}
}
if ( false === $is_comma ) {
// Can't check spacing around the comma if there is no comma.
continue;
}
/*
* Check for whitespace at the end of the array item.
*/
if ( $last_content !== $item['end']
// Ignore whitespace at the end of a multi-line item if it is the end of a heredoc/nowdoc.
&& ( true === $single_line
|| ! isset( Tokens::$heredocTokens[ $this->tokens[ $last_content ]['code'] ] ) )
) {
$newlines = 0;
$spaces = 0;
for ( $i = $item['end']; $i > $last_content; $i-- ) {
if ( \T_WHITESPACE === $this->tokens[ $i ]['code'] ) {
if ( $this->tokens[ $i ]['content'] === $this->phpcsFile->eolChar ) {
$newlines++;
} else {
$spaces += $this->tokens[ $i ]['length'];
}
} elseif ( \T_COMMENT === $this->tokens[ $i ]['code']
|| isset( $this->phpcsCommentTokens[ $this->tokens[ $i ]['type'] ] )
) {
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( $this->phpcsCommentTokens[ $this->tokens[ $i ]['type'] ] )
) {
// 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( $this->phpcsCommentTokens[ $this->tokens[ $next_non_whitespace ]['type'] ] ) ) )
) {
continue;
}
$space_length = $next_token['length'];
if ( 1 === $space_length ) {
continue;
}
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space between comma and "%s"; %s found',
$maybe_comma,
'SpaceAfterComma',
array(
$this->tokens[ $next_non_whitespace ]['content'],
$space_length,
)
);
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $maybe_comma + 1 ), ' ' );
}
} else {
// This is either a comment or a mixed single/multi-line array.
// Just add a space and let other sniffs sort out the array layout.
$fix = $this->phpcsFile->addFixableError(
'Expected 1 space between comma and "%s"; 0 found',
$maybe_comma,
'NoSpaceAfterComma',
array( $next_token['content'] )
);
if ( true === $fix ) {
$this->phpcsFile->fixer->addContent( $maybe_comma, ' ' );
}
}
}
}
}
@@ -0,0 +1,609 @@
<?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 WordPress\Sniffs\Arrays;
use WordPress\Sniff;
/**
* Enforces alignment of the double arrow assignment operator for multi-item, multi-line arrays.
*
* - Align the double arrow operator to the same column for each item in a multi-item array.
* - Allows for setting a maxColumn property to aid in managing line-length.
* - Allows for new line(s) before a double arrow (configurable).
* - Allows for handling multi-line array items differently if so desired (configurable).
*
* @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation
*
* @package WPCS\WordPressCodingStandards
*
* @since 0.14.0
*
* {@internal This sniff should eventually be pulled upstream as part of a solution
* for https://github.com/squizlabs/PHP_CodeSniffer/issues/582 }}
*/
class MultipleStatementAlignmentSniff extends Sniff {
/**
* Whether or not to ignore an array item for the purpose of alignment
* when a new line is found between the array key and the double arrow.
*
* @since 0.14.0
*
* @var bool
*/
public $ignoreNewlines = true;
/**
* Whether the alignment should be exact.
*
* Exact in this context means "largest index key + 1 space".
* When `false`, that is seen as the minimum alignment.
*
* @since 0.14.0
*
* @var bool
*/
public $exact = true;
/**
* The maximum column on which the double arrow alignment should be set.
*
* This property allows for limiting the whitespace padding to prevent
* overly long lines.
*
* If this value is set to, for instance, 60, it will:
* - if the expected column < 60, align at the expected column.
* - if the expected column >= 60, align at column 60.
* - for the outliers, i.e. the array indexes where the end position
* goes past column 60, it will not align the arrow, the sniff will
* just make sure there is only one space between the end of the
* array index and the double arrow.
*
* The column value is regarded as a hard value, i.e. includes indentation,
* so setting it very low is not a good idea.
*
* @since 0.14.0
*
* @var int
*/
public $maxColumn = 1000;
/**
* Whether or not to align the arrow operator for multi-line array items.
*
* Whether or not an item is regarded as multi-line is based on the **value**
* of the item, not the key.
*
* Valid values are:
* - 'always': Default. Align all arrays items regardless of single/multi-line.
* - 'never': Never align array items which span multiple lines.
* This will enforce one space between the array index and the
* double arrow operator for multi-line array items, independently
* of the alignment of the rest of the array items.
* Multi-line items where the arrow is already aligned with the
* "expected" alignment, however, will be left alone.
* - operator : Only align the operator for multi-line arrays items if the
* + number percentage of multi-line items passes the comparison.
* - As it is a percentage, the number has to be between 0 and 100.
* - Supported operators: <, <=, >, >=, ==, =, !=, <>
* - The percentage is calculated against all array items
* (with and without assignment operator).
* - The (new) expected alignment will be calculated based only
* on the items being aligned.
* - Multi-line items where the arrow is already aligned with the
* (new) "expected" alignment, however, will be left alone.
* Examples:
* * Setting this to `!=100` or `<100` means that alignment will
* be enforced, unless *all* array items are multi-line.
* This is probably the most commonly desired situation.
* * Setting this to `=100` means that alignment will only
* be enforced, if *all* array items are multi-line.
* * Setting this to `<50` means that the majority of array items
* need to be single line before alignment is enforced for
* multi-line items in the array.
* * Setting this to `=0` is useless as in that case there are
* no multi-line items in the array anyway.
*
* This setting will respect the `ignoreNewlines` and `maxColumnn` settings.
*
* @since 0.14.0
*
* @var string|int
*/
public $alignMultilineItems = 'always';
/**
* Storage for parsed $alignMultilineItems operator part.
*
* @since 0.14.0
*
* @var string
*/
private $operator;
/**
* Storage for parsed $alignMultilineItems numeric part.
*
* Stored as a string as the comparison will be done string based.
*
* @since 0.14.0
*
* @var string
*/
private $number;
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 0.14.0
*
* @return array
*/
public function register() {
return array(
\T_ARRAY,
\T_OPEN_SHORT_ARRAY,
);
}
/**
* Processes this test, when one of its tokens is encountered.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
/*
* Determine the array opener & closer.
*/
$array_open_close = $this->find_array_open_close( $stackPtr );
if ( false === $array_open_close ) {
// Array open/close could not be determined.
return;
}
$opener = $array_open_close['opener'];
$closer = $array_open_close['closer'];
$array_items = $this->get_function_call_parameters( $stackPtr );
if ( empty( $array_items ) ) {
return;
}
// Pass off to either the single line or multi-line array analysis.
if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) {
return $this->process_single_line_array( $stackPtr, $array_items, $opener, $closer );
} else {
return $this->process_multi_line_array( $stackPtr, $array_items, $opener, $closer );
}
}
/**
* Process a single-line array.
*
* While the WP standard does not allow single line multi-item associative arrays,
* this sniff should function independently of that.
*
* The `WordPress.WhiteSpace.OperatorSpacing` sniff already covers checking that
* there is a space between the array key and the double arrow, but doesn't
* enforce it to be exactly one space for single line arrays.
* That is what this method covers.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $items Info array containing information on each array item.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
protected function process_single_line_array( $stackPtr, $items, $opener, $closer ) {
/*
* For single line arrays, we don't care about what level the arrow is from.
* Just find and fix them all.
*/
$next_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
( $opener + 1 ),
$closer
);
while ( false !== $next_arrow ) {
if ( \T_WHITESPACE === $this->tokens[ ( $next_arrow - 1 ) ]['code'] ) {
$space_length = $this->tokens[ ( $next_arrow - 1 ) ]['length'];
if ( 1 !== $space_length ) {
$error = 'Expected 1 space between "%s" and double arrow; %s found';
$data = array(
$this->tokens[ ( $next_arrow - 2 ) ]['content'],
$space_length,
);
$fix = $this->phpcsFile->addFixableWarning( $error, $next_arrow, 'SpaceBeforeDoubleArrow', $data );
if ( true === $fix ) {
$this->phpcsFile->fixer->replaceToken( ( $next_arrow - 1 ), ' ' );
}
}
}
// Find the position of the next double arrow.
$next_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
( $next_arrow + 1 ),
$closer
);
}
// Ignore any child-arrays as the double arrows in these will already have been handled.
return ( $closer + 1 );
}
/**
* Process a multi-line array.
*
* @since 0.14.0
*
* @param int $stackPtr The position of the current token in the stack.
* @param array $items Info array containing information on each array item.
* @param int $opener The position of the array opener.
* @param int $closer The position of the array closer.
*
* @return void
*/
protected function process_multi_line_array( $stackPtr, $items, $opener, $closer ) {
$this->maxColumn = (int) $this->maxColumn;
$this->validate_align_multiline_items();
/*
* Determine what the spacing before the arrow should be.
*
* Will unset any array items without double arrow and with new line whitespace
* if newlines are to be ignored, so the second foreach loop only has to deal
* with items which need attention.
*
* This sniff does not take incorrect indentation of array keys into account.
* That's for the `WordPress.Arrays.ArrayIndentation` sniff to fix.
* If that would affect the alignment, a second (or third) loop of the fixer
* will correct it (again) after the indentation has been fixed.
*/
$index_end_cols = array(); // Keep track of the end column position of index keys.
$double_arrow_cols = array(); // Keep track of arrow column position and count.
$multi_line_count = 0;
$total_items = \count( $items );
foreach ( $items as $key => $item ) {
if ( strpos( $item['raw'], '=>' ) === false ) {
// Ignore items without assignment operators.
unset( $items[ $key ] );
continue;
}
// Find the position of the first double arrow.
$double_arrow = $this->phpcsFile->findNext(
\T_DOUBLE_ARROW,
$item['start'],
( $item['end'] + 1 )
);
if ( false === $double_arrow ) {
// Shouldn't happen, just in case.
unset( $items[ $key ] );
continue;
}
// Make sure the arrow is for this item and not for a nested array value assignment.
$has_array_opener = $this->phpcsFile->findNext(
$this->register(),
$item['start'],
$double_arrow
);
if ( false !== $has_array_opener ) {
// Double arrow is for a nested array.
unset( $items[ $key ] );
continue;
}
// Find the end of the array key.
$last_index_token = $this->phpcsFile->findPrevious(
\T_WHITESPACE,
( $double_arrow - 1 ),
$item['start'],
true
);
if ( false === $last_index_token ) {
// Shouldn't happen, but just in case.
unset( $items[ $key ] );
continue;
}
if ( true === $this->ignoreNewlines
&& $this->tokens[ $last_index_token ]['line'] !== $this->tokens[ $double_arrow ]['line']
) {
// Ignore this item as it has a new line between the item key and the double arrow.
unset( $items[ $key ] );
continue;
}
$index_end_position = ( $this->tokens[ $last_index_token ]['column'] + ( $this->tokens[ $last_index_token ]['length'] - 1 ) );
$items[ $key ]['operatorPtr'] = $double_arrow;
$items[ $key ]['last_index_token'] = $last_index_token;
$items[ $key ]['last_index_col'] = $index_end_position;
if ( $this->tokens[ $last_index_token ]['line'] === $this->tokens[ $item['end'] ]['line'] ) {
$items[ $key ]['single_line'] = true;
} else {
$items[ $key ]['single_line'] = false;
$multi_line_count++;
}
if ( ( $index_end_position + 2 ) <= $this->maxColumn ) {
$index_end_cols[] = $index_end_position;
}
if ( ! isset( $double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] ) ) {
$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] = 1;
} else {
$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ]++;
}
}
unset( $key, $item, $double_arrow, $has_array_opener, $last_index_token );
if ( empty( $items ) || empty( $index_end_cols ) ) {
// No actionable array items found.
return;
}
/*
* Determine whether the operators for multi-line items should be aligned.
*/
if ( 'always' === $this->alignMultilineItems ) {
$alignMultilineItems = true;
} elseif ( 'never' === $this->alignMultilineItems ) {
$alignMultilineItems = false;
} else {
$percentage = (string) round( ( $multi_line_count / $total_items ) * 100, 0 );
// Bit hacky, but this is the only comparison function in PHP which allows to
// pass the comparison operator. And hey, it works ;-).
$alignMultilineItems = version_compare( $percentage, $this->number, $this->operator );
}
/*
* If necessary, rebuild the $index_end_cols and $double_arrow_cols arrays
* excluding multi-line items.
*/
if ( false === $alignMultilineItems ) {
$select_index_end_cols = array();
$double_arrow_cols = array();
foreach ( $items as $item ) {
if ( false === $item['single_line'] ) {
continue;
}
if ( ( $item['last_index_col'] + 2 ) <= $this->maxColumn ) {
$select_index_end_cols[] = $item['last_index_col'];
}
if ( ! isset( $double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] ) ) {
$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] = 1;
} else {
$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ]++;
}
}
}
/*
* Determine the expected position of the double arrows.
*/
if ( ! empty( $select_index_end_cols ) ) {
$max_index_width = max( $select_index_end_cols );
} else {
$max_index_width = max( $index_end_cols );
}
$expected_col = ( $max_index_width + 2 );
if ( false === $this->exact && ! empty( $double_arrow_cols ) ) {
/*
* If the alignment does not have to be exact, see if a majority
* group of the arrows is already at an acceptable position.
*/
arsort( $double_arrow_cols, \SORT_NUMERIC );
reset( $double_arrow_cols );
$count = current( $double_arrow_cols );
if ( $count > 1 || ( 1 === $count && \count( $items ) === 1 ) ) {
// Allow for several groups of arrows having the same $count.
$filtered_double_arrow_cols = array_keys( $double_arrow_cols, $count, true );
foreach ( $filtered_double_arrow_cols as $col ) {
if ( $col > $expected_col && $col <= $this->maxColumn ) {
$expected_col = $col;
break;
}
}
}
}
unset( $max_index_width, $count, $filtered_double_arrow_cols, $col );
/*
* Verify and correct the spacing around the double arrows.
*/
foreach ( $items as $item ) {
if ( $this->tokens[ $item['operatorPtr'] ]['column'] === $expected_col
&& $this->tokens[ $item['operatorPtr'] ]['line'] === $this->tokens[ $item['last_index_token'] ]['line']
) {
// Already correctly aligned.
continue;
}
if ( \T_WHITESPACE !== $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['code'] ) {
$before = 0;
} else {
if ( $this->tokens[ $item['last_index_token'] ]['line'] !== $this->tokens[ $item['operatorPtr'] ]['line'] ) {
$before = 'newline';
} else {
$before = $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['length'];
}
}
/*
* Deal with index sizes larger than maxColumn and with multi-line
* array items which should not be aligned.
*/
if ( ( $item['last_index_col'] + 2 ) > $this->maxColumn
|| ( false === $alignMultilineItems && false === $item['single_line'] )
) {
if ( ( $item['last_index_col'] + 2 ) === $this->tokens[ $item['operatorPtr'] ]['column']
&& $this->tokens[ $item['operatorPtr'] ]['line'] === $this->tokens[ $item['last_index_token'] ]['line']
) {
// MaxColumn/Multi-line item exception, already correctly aligned.
continue;
}
$prefix = 'LongIndex';
if ( false === $alignMultilineItems && false === $item['single_line'] ) {
$prefix = 'MultilineItem';
}
$error_code = $prefix . 'SpaceBeforeDoubleArrow';
if ( 0 === $before ) {
$error_code = $prefix . 'NoSpaceBeforeDoubleArrow';
}
$fix = $this->phpcsFile->addFixableWarning(
'Expected 1 space between "%s" and double arrow; %s found.',
$item['operatorPtr'],
$error_code,
array(
$this->tokens[ $item['last_index_token'] ]['content'],
$before,
)
);
if ( true === $fix ) {
$this->phpcsFile->fixer->beginChangeset();
// Remove whitespace tokens between the end of the index and the arrow, if any.
for ( $i = ( $item['last_index_token'] + 1 ); $i < $item['operatorPtr']; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
// Add the correct whitespace.
$this->phpcsFile->fixer->addContent( $item['last_index_token'], ' ' );
$this->phpcsFile->fixer->endChangeset();
}
continue;
}
/*
* Deal with the space before double arrows in all other cases.
*/
$expected_whitespace = $expected_col - ( $this->tokens[ $item['last_index_token'] ]['column'] + $this->tokens[ $item['last_index_token'] ]['length'] );
$fix = $this->phpcsFile->addFixableWarning(
'Array double arrow not aligned correctly; expected %s space(s) between "%s" and double arrow, but found %s.',
$item['operatorPtr'],
'DoubleArrowNotAligned',
array(
$expected_whitespace,
$this->tokens[ $item['last_index_token'] ]['content'],
$before,
)
);
if ( true === $fix ) {
if ( 0 === $before || 'newline' === $before ) {
$this->phpcsFile->fixer->beginChangeset();
// Remove whitespace tokens between the end of the index and the arrow, if any.
for ( $i = ( $item['last_index_token'] + 1 ); $i < $item['operatorPtr']; $i++ ) {
$this->phpcsFile->fixer->replaceToken( $i, '' );
}
// Add the correct whitespace.
$this->phpcsFile->fixer->addContent(
$item['last_index_token'],
str_repeat( ' ', $expected_whitespace )
);
$this->phpcsFile->fixer->endChangeset();
} elseif ( $expected_whitespace > $before ) {
// Add to the existing whitespace to prevent replacing tabs with spaces.
// That's the concern of another sniff.
$this->phpcsFile->fixer->addContent(
( $item['operatorPtr'] - 1 ),
str_repeat( ' ', ( $expected_whitespace - $before ) )
);
} else {
// Too much whitespace found.
$this->phpcsFile->fixer->replaceToken(
( $item['operatorPtr'] - 1 ),
str_repeat( ' ', $expected_whitespace )
);
}
}
}
}
/**
* Validate that a valid value has been received for the alignMultilineItems property.
*
* This message may be thrown more than once if the property is being changed inline in a file.
*
* @since 0.14.0
*/
protected function validate_align_multiline_items() {
$alignMultilineItems = $this->alignMultilineItems;
if ( 'always' === $alignMultilineItems || 'never' === $alignMultilineItems ) {
return;
} else {
// Correct for a potentially added % sign.
$alignMultilineItems = rtrim( $alignMultilineItems, '%' );
if ( preg_match( '`^([=<>!]{1,2})(100|[0-9]{1,2})$`', $alignMultilineItems, $matches ) > 0 ) {
$operator = $matches[1];
$number = (int) $matches[2];
if ( \in_array( $operator, array( '<', '<=', '>', '>=', '==', '=', '!=', '<>' ), true ) === true
&& ( $number >= 0 && $number <= 100 )
) {
$this->alignMultilineItems = $alignMultilineItems;
$this->number = (string) $number;
$this->operator = $operator;
return;
}
}
}
$this->phpcsFile->addError(
'Invalid property value passed: "%s". The value for the "alignMultilineItems" property for the "WordPress.Arrays.MultipleStatementAlignment" sniff should be either "always", "never" or an comparison operator + a number between 0 and 100.',
0,
'InvalidPropertyPassed',
array( $this->alignMultilineItems )
);
// Reset to the default if an invalid value was received.
$this->alignMultilineItems = 'always';
}
}