-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #294 from PHPCSStandards/universal/new-concatposit…
…ion-sniff ✨ New `Universal.Operators.ConcatPosition` sniff
- Loading branch information
Showing
5 changed files
with
540 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?xml version="1.0"?> | ||
<documentation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="https://phpcsstandards.github.io/PHPCSDevTools/phpcsdocs.xsd" | ||
title="Concatenation position" | ||
> | ||
<standard> | ||
<![CDATA[ | ||
Enforces that the concatenation operator for multi-line concatenations is in a preferred position, either always at the start of the next line or always at the end of the previous line. | ||
The preferred position is configurable and defaults to "start" for _start of the next line_. | ||
Note: mid-line concatenation is still allowed and will not be flagged by this sniff. | ||
]]> | ||
</standard> | ||
<code_comparison> | ||
<code title="Valid: multi-line concatenation with the concatenation operator at the start of each line."> | ||
<![CDATA[ | ||
$var = 'text' . $a | ||
<em>.</em> $b . 'text' | ||
<em>.</em> $c; | ||
]]> | ||
</code> | ||
<code title="Invalid: multi-line concatenation with the concatenation operator not consistently at the start of each line."> | ||
<![CDATA[ | ||
$var = 'text' . $a <em>.</em> | ||
$b . 'text' | ||
<em>.</em> $c; | ||
]]> | ||
</code> | ||
</code_comparison> | ||
</documentation> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
<?php | ||
/** | ||
* PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer. | ||
* | ||
* @package PHPCSExtra | ||
* @copyright 2023 PHPCSExtra Contributors | ||
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3 | ||
* @link https://github.com/PHPCSStandards/PHPCSExtra | ||
*/ | ||
|
||
namespace PHPCSExtra\Universal\Sniffs\Operators; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
use PHP_CodeSniffer\Util\Tokens; | ||
|
||
/** | ||
* Enforces that the concatenation operator in multi-line concatenations is in a preferred position, | ||
* either always at the start of the next line or always at the end of the previous line. | ||
* | ||
* Note: this sniff has no opinion on spacing before/after the concatenation operator. | ||
* It will normalize based on the "one space before/after" PSR-12 industry standard. | ||
* If different spacing is preferred, use the `Squiz.Strings.ConcatenationSpacing` to enforce/correct that. | ||
* | ||
* @since 1.2.0 | ||
*/ | ||
final class ConcatPositionSniff implements Sniff | ||
{ | ||
|
||
/** | ||
* The phrase to use for the metric recorded by this sniff. | ||
* | ||
* @since 1.2.0 | ||
* | ||
* @var string | ||
*/ | ||
const METRIC_NAME = 'Multi-line concatenation operator position'; | ||
|
||
/** | ||
* Position indication: start of next line. | ||
* | ||
* @since 1.2.0 | ||
* | ||
* @var string | ||
*/ | ||
const POSITION_START = 'start'; | ||
|
||
/** | ||
* Position indication: end of previous line. | ||
* | ||
* @since 1.2.0 | ||
* | ||
* @var string | ||
*/ | ||
const POSITION_END = 'end'; | ||
|
||
/** | ||
* Position indication: neither start of next line nor end of previous line. | ||
* | ||
* @since 1.2.0 | ||
* | ||
* @var string | ||
*/ | ||
const POSITION_STANDALONE = 'stand-alone'; | ||
|
||
/** | ||
* Preferred position for the concatenation operator. | ||
* | ||
* Valid values are: 'start' and 'end'. | ||
* Defaults to 'start'. | ||
* | ||
* @since 1.2.0 | ||
* | ||
* @var string | ||
*/ | ||
public $allowOnly = self::POSITION_START; | ||
|
||
/** | ||
* Returns an array of tokens this test wants to listen for. | ||
* | ||
* @since 1.2.0 | ||
* | ||
* @return array<int|string> | ||
*/ | ||
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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
<?php | ||
|
||
/* | ||
* Not our targets. | ||
*/ | ||
|
||
// Same line. | ||
$a = 'text' . 'text' . $b; | ||
|
||
/* | ||
* Always prefer start of line (default). | ||
* | ||
* phpcs:set Universal.Operators.ConcatPosition allowOnly start | ||
*/ | ||
|
||
// OK. | ||
$a = 'start' | ||
. 'start with space' | ||
. $b; | ||
|
||
$a = 'start' | ||
.'start no space' | ||
./*comment*/$b; | ||
|
||
$a = 'start' | ||
// Comment on own line. | ||
. $b; | ||
|
||
// Not OK. | ||
$a = 'mixed' . | ||
|
||
|
||
'mixed' | ||
. $b; | ||
|
||
$a = 'end with space' . | ||
'end with space and comment' . // Comment. | ||
$b; | ||
|
||
$a = 'end with space' . | ||
/* comment */ 'end with comment before'; | ||
|
||
$a = 'end no space'. | ||
'end no space and comment'/*comment*/.//Comment | ||
$b; | ||
|
||
$a = 'end with space' . | ||
// Comment on own line. | ||
'end with space with comment above' . | ||
// Comment on own line. | ||
$b; | ||
|
||
$a = 'stand-alone' | ||
. | ||
'stand-alone'; | ||
|
||
/* | ||
* Always prefer end of line (via setting). | ||
* | ||
* phpcs:set Universal.Operators.ConcatPosition allowOnly end | ||
*/ | ||
|
||
// OK. | ||
$a = 'end with space' . | ||
'end with space and comment' . // Comment. | ||
$b; | ||
|
||
$a = 'end no space'. | ||
'end no space and comment'/*comment*/.//Comment | ||
$b; | ||
|
||
$a = 'end no space'. | ||
// Comment on own line. | ||
$b; | ||
|
||
// Not OK. | ||
$a = 'mixed' . | ||
'mixed' | ||
|
||
|
||
. $b; | ||
|
||
$a = 'start' | ||
. 'start with space' | ||
. $b; | ||
|
||
$a = 'start' | ||
.'start no space' | ||
./*comment*/$b; | ||
|
||
$a = 'start' | ||
// Comment on own line. | ||
.'start with comment above' | ||
// Comment on own line. | ||
.$b; | ||
|
||
$a = 'stand-alone' | ||
. | ||
'stand-alone'; | ||
|
||
/* | ||
* Invalid setting will use the default (start of line). | ||
* | ||
* phpcs:set Universal.Operators.ConcatPosition allowOnly mixed | ||
*/ | ||
|
||
// OK. | ||
$a = 'start' | ||
. 'start with space' | ||
. $b; | ||
|
||
// Not OK. | ||
$a = 'mixed' . | ||
'mixed' | ||
. $b; | ||
|
||
// Reset to the default value. | ||
// phpcs:set Universal.Operators.ConcatPosition allowOnly start | ||
|
||
// Intentional parse error/live coding. | ||
// This needs to be the last test in the file. | ||
$a = 'text' . |
Oops, something went wrong.