wishthis/vendor/phpcsstandards/phpcsutils/PHPCSUtils/Fixers/SpacesFixer.php
2023-09-20 13:52:46 +02:00

246 lines
10 KiB
PHP

<?php
/**
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
*
* @package PHPCSUtils
* @copyright 2019-2020 PHPCSUtils Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSUtils
*/
namespace PHPCSUtils\Fixers;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Utils\Numbers;
/**
* Utility to check and, if necessary, fix the whitespace between two tokens.
*
* @since 1.0.0
*/
final class SpacesFixer
{
/**
* Check the whitespace between two tokens, throw an error if it doesn't match the
* expected whitespace and if relevant, fix it.
*
* Note:
* - This method will not auto-fix if there is anything but whitespace between the two
* tokens. In that case, it will throw a non-fixable error/warning.
* - If `'newline'` is expected and _no_ new line is encountered, a new line will be added,
* but no assumptions will be made about the intended indentation of the code.
* This should be handled by a (separate) indentation sniff.
* - If `'newline'` is expected and multiple new lines are encountered, this will be accepted
* as valid.
* No assumptions are made about whether additional blank lines are allowed or not.
* If _exactly_ one line is desired, combine this fixer with the {@see \PHPCSUtils\Fixers\BlankLineFixer}
* (upcoming).
* - The fixer will not leave behind any trailing spaces on the original line when fixing
* to `'newline'`, but it will not correct _existing_ trailing spaces when there already
* is a new line in place.
* - This method can optionally record a metric for this check which will be displayed
* when the end-user requests the "info" report.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the token which should be used
* when reporting an issue.
* @param int $secondPtr The stack pointer to the second token.
* This token can be before or after the `$stackPtr`,
* but should only be separated from the `$stackPtr`
* by whitespace and/or comments/annotations.
* @param string|int $expectedSpaces Number of spaces to enforce.
* Valid values:
* - (int) Number of spaces. Must be `0` or more.
* - (string) `'newline'`.
* @param string $errorTemplate Error message template.
* Note: _The placeholder replacement phrase will be
* in human readable English and include "spaces"/
* "new line", so no need to include that in the template._
* This string should contain two placeholders:
* - `%1$s` = expected spaces phrase.
* - `%2$s` = found spaces phrase.
* @param string $errorCode A violation code unique to the sniff message.
* Defaults to `"Found"`.
* It is strongly recommended to change this if
* this fixer is used for different errors in the
* same sniff.
* @param string $errorType Optional. Whether to report the issue as a
* `"warning"` or an `"error"`. Defaults to `"error"`.
* @param int $errorSeverity Optional. The severity level for this message.
* A value of `0` will be converted into the default
* severity level.
* @param string $metricName Optional. The name of the metric to record.
* This can be a short description phrase.
* Leave empty to not record metrics.
*
* @return void
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the tokens passed do not exist or are whitespace
* tokens.
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If `$expectedSpaces` is not a valid value.
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the tokens passed are separated by more than just
* empty (whitespace + comments/annotations) tokens.
*/
public static function checkAndFix(
File $phpcsFile,
$stackPtr,
$secondPtr,
$expectedSpaces,
$errorTemplate,
$errorCode = 'Found',
$errorType = 'error',
$errorSeverity = 0,
$metricName = ''
) {
$tokens = $phpcsFile->getTokens();
/*
* Validate the received function input.
*/
if (isset($tokens[$stackPtr], $tokens[$secondPtr]) === false
|| $tokens[$stackPtr]['code'] === \T_WHITESPACE
|| $tokens[$secondPtr]['code'] === \T_WHITESPACE
) {
throw new RuntimeException('The $stackPtr and the $secondPtr token must exist and not be whitespace');
}
$expected = false;
if ($expectedSpaces === 'newline') {
$expected = $expectedSpaces;
} elseif (\is_int($expectedSpaces) === true && $expectedSpaces >= 0) {
$expected = $expectedSpaces;
} elseif (\is_string($expectedSpaces) === true && Numbers::isDecimalInt($expectedSpaces) === true) {
$expected = (int) $expectedSpaces;
}
if ($expected === false) {
throw new RuntimeException(
'The $expectedSpaces setting should be either "newline", 0 or a positive integer'
);
}
$ptrA = $stackPtr;
$ptrB = $secondPtr;
if ($stackPtr > $secondPtr) {
$ptrA = $secondPtr;
$ptrB = $stackPtr;
}
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($ptrA + 1), null, true);
if ($nextNonEmpty !== false && $nextNonEmpty < $ptrB) {
throw new RuntimeException(
'The $stackPtr and the $secondPtr token must be adjacent tokens separated only'
. ' by whitespace and/or comments'
);
}
/*
* Determine how many spaces are between the two tokens.
*/
$found = 0;
$foundPhrase = 'no spaces';
if ($tokens[$ptrA]['line'] !== $tokens[$ptrB]['line']) {
$found = 'newline';
$foundPhrase = 'a new line';
if (($tokens[$ptrA]['line'] + 1) !== $tokens[$ptrB]['line']) {
$foundPhrase = 'multiple new lines';
}
} elseif (($ptrA + 1) !== $ptrB) {
if ($tokens[($ptrA + 1)]['code'] === \T_WHITESPACE) {
$found = $tokens[($ptrA + 1)]['length'];
$foundPhrase = $found . (($found === 1) ? ' space' : ' spaces');
} else {
$found = 'non-whitespace tokens';
$foundPhrase = 'non-whitespace tokens';
}
}
if ($metricName !== '') {
$phpcsFile->recordMetric($stackPtr, $metricName, $foundPhrase);
}
if ($found === $expected) {
return;
}
/*
* Handle the violation message.
*/
$expectedPhrase = 'no space';
if ($expected === 'newline') {
$expectedPhrase = 'a new line';
} elseif ($expected === 1) {
$expectedPhrase = $expected . ' space';
} elseif ($expected > 1) {
$expectedPhrase = $expected . ' spaces';
}
$fixable = true;
$nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($ptrA + 1), null, true);
if ($nextNonWhitespace !== $ptrB) {
// Comment found between the tokens and we don't know where it should go, so don't auto-fix.
$fixable = false;
}
if ($found === 'newline'
&& $tokens[$ptrA]['code'] === \T_COMMENT
&& \substr($tokens[$ptrA]['content'], -2) !== '*/'
) {
/*
* $ptrA is a slash-style trailing comment, removing the new line would comment out
* the code, so don't auto-fix.
*/
$fixable = false;
}
$method = 'add';
$method .= ($fixable === true) ? 'Fixable' : '';
$method .= ($errorType === 'error') ? 'Error' : 'Warning';
$recorded = $phpcsFile->$method(
$errorTemplate,
$stackPtr,
$errorCode,
[$expectedPhrase, $foundPhrase],
$errorSeverity
);
if ($fixable === false || $recorded === false) {
return;
}
/*
* Fix the violation.
*/
$phpcsFile->fixer->beginChangeset();
/*
* Remove existing whitespace. No need to check if it's whitespace as otherwise the fixer
* wouldn't have kicked in.
*/
for ($i = ($ptrA + 1); $i < $ptrB; $i++) {
$phpcsFile->fixer->replaceToken($i, '');
}
// If necessary: add the correct amount whitespace.
if ($expected !== 0) {
if ($expected === 'newline') {
$phpcsFile->fixer->addContent($ptrA, $phpcsFile->eolChar);
} else {
$replacement = $tokens[$ptrA]['content'] . \str_repeat(' ', $expected);
$phpcsFile->fixer->replaceToken($ptrA, $replacement);
}
}
$phpcsFile->fixer->endChangeset();
}
}