diff --git a/Universal/Docs/Operators/ConcatPositionStandard.xml b/Universal/Docs/Operators/ConcatPositionStandard.xml new file mode 100644 index 00000000..4d2761af --- /dev/null +++ b/Universal/Docs/Operators/ConcatPositionStandard.xml @@ -0,0 +1,31 @@ + + + + + + + + . $b . 'text' + . $c; + ]]> + + + . + $b . 'text' + . $c; + ]]> + + + diff --git a/Universal/Sniffs/Operators/ConcatPositionSniff.php b/Universal/Sniffs/Operators/ConcatPositionSniff.php new file mode 100644 index 00000000..093785ad --- /dev/null +++ b/Universal/Sniffs/Operators/ConcatPositionSniff.php @@ -0,0 +1,204 @@ + + */ + public function register() + { + return [\T_STRING_CONCAT]; + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @since 1.2.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. + */ + public function process(File $phpcsFile, $stackPtr) + { + /* + * Validate the setting. + */ + if ($this->allowOnly !== self::POSITION_END) { + // Use the default. + $this->allowOnly = self::POSITION_START; + } + + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + + if ($nextNonEmpty === false) { + // Parse error/live coding. + return; + } + + $tokens = $phpcsFile->getTokens(); + if ($tokens[$prevNonEmpty]['line'] === $tokens[$nextNonEmpty]['line']) { + // Not multi-line concatenation. Not our target. + return; + } + + $position = self::POSITION_STANDALONE; + if ($tokens[$prevNonEmpty]['line'] === $tokens[$stackPtr]['line']) { + $position = self::POSITION_END; + } elseif ($tokens[$nextNonEmpty]['line'] === $tokens[$stackPtr]['line']) { + $position = self::POSITION_START; + } + + // Record metric. + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, $position); + + if ($this->allowOnly === $position) { + // All okay. + return; + } + + $fix = $phpcsFile->addFixableError( + 'The concatenation operator for multi-line concatenations should always be at the %s of a line.', + $stackPtr, + 'Incorrect', + [$this->allowOnly] + ); + + if ($fix === true) { + if ($this->allowOnly === self::POSITION_END) { + $phpcsFile->fixer->beginChangeset(); + + // Move the concat operator. + $phpcsFile->fixer->replaceToken($stackPtr, ''); + $phpcsFile->fixer->addContent($prevNonEmpty, ' .'); + + if ($position === self::POSITION_START + && $tokens[($stackPtr + 1)]['code'] === \T_WHITESPACE + ) { + // Remove trailing space. + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } elseif ($position === self::POSITION_STANDALONE) { + // Remove potential indentation space. + if ($tokens[($stackPtr - 1)]['code'] === \T_WHITESPACE) { + $phpcsFile->fixer->replaceToken(($stackPtr - 1), ''); + } + + // Remove new line. + if ($tokens[($stackPtr + 1)]['code'] === \T_WHITESPACE) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } + } + + $phpcsFile->fixer->endChangeset(); + return; + } + + // Fixer for allowOnly === self::POSITION_START. + $phpcsFile->fixer->beginChangeset(); + + // Move the concat operator. + $phpcsFile->fixer->replaceToken($stackPtr, ''); + $phpcsFile->fixer->addContentBefore($nextNonEmpty, '. '); + + if ($position === self::POSITION_END + && $tokens[($stackPtr - 1)]['code'] === \T_WHITESPACE + ) { + // Remove trailing space. + $phpcsFile->fixer->replaceToken(($stackPtr - 1), ''); + } elseif ($position === self::POSITION_STANDALONE) { + // Remove potential indentation space. + if ($tokens[($stackPtr - 1)]['code'] === \T_WHITESPACE) { + $phpcsFile->fixer->replaceToken(($stackPtr - 1), ''); + } + + // Remove new line. + if ($tokens[($stackPtr + 1)]['code'] === \T_WHITESPACE) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } + } + + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/Universal/Tests/Operators/ConcatPositionUnitTest.inc b/Universal/Tests/Operators/ConcatPositionUnitTest.inc new file mode 100644 index 00000000..24569e7a --- /dev/null +++ b/Universal/Tests/Operators/ConcatPositionUnitTest.inc @@ -0,0 +1,122 @@ + Key is the line number, value is the number of expected errors. + */ + public function getErrorList() + { + return [ + 30 => 1, + 36 => 1, + 37 => 1, + 40 => 1, + 43 => 1, + 44 => 1, + 47 => 1, + 49 => 1, + 54 => 1, + 81 => 1, + 84 => 1, + 85 => 1, + 88 => 1, + 89 => 1, + 93 => 1, + 95 => 1, + 98 => 1, + 113 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array Key is the line number, value is the number of expected warnings. + */ + public function getWarningList() + { + return []; + } +}