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 Collections::arrayOpenTokensBC(); } /** * 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 = Helper::getTabWidth( $this->phpcsFile ); } if ( isset( Collections::shortArrayListOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) && Arrays::isShortArray( $this->phpcsFile, $stackPtr ) === false ) { // Short list, not short array. return; } /* * Determine the array opener & closer. */ $array_open_close = Arrays::getOpenClose( $this->phpcsFile, $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 = PassedParameters::getParameters( $this->phpcsFile, $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'] ) { return $closer; } // Ignore this item if there is anything but whitespace before the start of the next item. if ( 1 !== $this->tokens[ $first_content ]['column'] ) { // Go to the start of the line. $i = $first_content; while ( 1 !== $this->tokens[ --$i ]['column'] ); if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) { $end_of_previous_item = $end_of_this_item; continue; } } $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 ( isset( Tokens::$stringTokens[ $token_code ] ) === true ) { // 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. * * @return void */ 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. * * @return void */ 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 ); } } }