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 [];
+ }
+}