402 lines
12 KiB
PHP
402 lines
12 KiB
PHP
<?php declare(strict_types = 1);
|
|
|
|
namespace SlevomatCodingStandard\Sniffs\Classes;
|
|
|
|
use PHP_CodeSniffer\Files\File;
|
|
use PHP_CodeSniffer\Sniffs\Sniff;
|
|
use SlevomatCodingStandard\Helpers\FixerHelper;
|
|
use SlevomatCodingStandard\Helpers\PropertyHelper;
|
|
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
|
|
use SlevomatCodingStandard\Helpers\TokenHelper;
|
|
use SlevomatCodingStandard\Helpers\TypeHintHelper;
|
|
use UnexpectedValueException;
|
|
use function array_key_exists;
|
|
use function array_keys;
|
|
use function array_map;
|
|
use function asort;
|
|
use function count;
|
|
use function implode;
|
|
use function in_array;
|
|
use function preg_split;
|
|
use function sprintf;
|
|
use function strtolower;
|
|
use const T_AS;
|
|
use const T_CONST;
|
|
use const T_DOUBLE_COLON;
|
|
use const T_FUNCTION;
|
|
use const T_NULLABLE;
|
|
use const T_PRIVATE;
|
|
use const T_PROTECTED;
|
|
use const T_PUBLIC;
|
|
use const T_READONLY;
|
|
use const T_STATIC;
|
|
use const T_VAR;
|
|
use const T_VARIABLE;
|
|
use const T_WHITESPACE;
|
|
|
|
class PropertyDeclarationSniff implements Sniff
|
|
{
|
|
|
|
public const CODE_NO_SPACE_BEFORE_NULLABILITY_SYMBOL = 'NoSpaceBeforeNullabilitySymbol';
|
|
|
|
public const CODE_MULTIPLE_SPACES_BEFORE_NULLABILITY_SYMBOL = 'MultipleSpacesBeforeNullabilitySymbol';
|
|
|
|
public const CODE_MULTIPLE_SPACES_BEFORE_TYPE_HINT = 'MultipleSpacesBeforeTypeHint';
|
|
|
|
public const CODE_NO_SPACE_BETWEEN_TYPE_HINT_AND_PROPERTY = 'NoSpaceBetweenTypeHintAndProperty';
|
|
|
|
public const CODE_MULTIPLE_SPACES_BETWEEN_TYPE_HINT_AND_PROPERTY = 'MultipleSpacesBetweenTypeHintAndProperty';
|
|
|
|
public const CODE_WHITESPACE_AFTER_NULLABILITY_SYMBOL = 'WhitespaceAfterNullabilitySymbol';
|
|
|
|
public const CODE_INCORRECT_ORDER_OF_MODIFIERS = 'IncorrectOrderOfModifiers';
|
|
|
|
public const CODE_MULTIPLE_SPACES_BETWEEN_MODIFIERS = 'MultipleSpacesBetweenModifiers';
|
|
|
|
/** @var list<string>|null */
|
|
public $modifiersOrder = [];
|
|
|
|
/** @var bool */
|
|
public $checkPromoted = false;
|
|
|
|
/** @var bool */
|
|
public $enableMultipleSpacesBetweenModifiersCheck = false;
|
|
|
|
/** @var array<int, array<int, (int|string)>>|null */
|
|
private $normalizedModifiersOrder = null;
|
|
|
|
/**
|
|
* @return array<int, (int|string)>
|
|
*/
|
|
public function register(): array
|
|
{
|
|
return TokenHelper::$propertyModifiersTokenCodes;
|
|
}
|
|
|
|
/**
|
|
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
|
|
* @param int $modifierPointer
|
|
*/
|
|
public function process(File $phpcsFile, $modifierPointer): void
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
$asPointer = TokenHelper::findPreviousEffective($phpcsFile, $modifierPointer - 1);
|
|
if ($tokens[$asPointer]['code'] === T_AS) {
|
|
return;
|
|
}
|
|
|
|
$nextPointer = TokenHelper::findNextEffective($phpcsFile, $modifierPointer + 1);
|
|
if (in_array($tokens[$nextPointer]['code'], TokenHelper::$propertyModifiersTokenCodes, true)) {
|
|
// We don't want to report the same property twice
|
|
return;
|
|
}
|
|
|
|
if ($tokens[$nextPointer]['code'] === T_DOUBLE_COLON) {
|
|
// Ignore static::
|
|
return;
|
|
}
|
|
|
|
$propertyPointer = TokenHelper::findNext($phpcsFile, [T_FUNCTION, T_CONST, T_VARIABLE], $modifierPointer + 1);
|
|
|
|
if ($propertyPointer === null || $tokens[$propertyPointer]['code'] !== T_VARIABLE) {
|
|
return;
|
|
}
|
|
|
|
if (!PropertyHelper::isProperty($phpcsFile, $propertyPointer, $this->checkPromoted)) {
|
|
return;
|
|
}
|
|
|
|
$firstModifierPointer = $modifierPointer;
|
|
do {
|
|
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $firstModifierPointer - 1);
|
|
if (!in_array($tokens[$previousPointer]['code'], TokenHelper::$propertyModifiersTokenCodes, true)) {
|
|
break;
|
|
}
|
|
|
|
$firstModifierPointer = $previousPointer;
|
|
} while (true);
|
|
|
|
$this->checkModifiersOrder($phpcsFile, $propertyPointer, $firstModifierPointer, $modifierPointer);
|
|
$this->checkSpacesBetweenModifiers($phpcsFile, $propertyPointer, $firstModifierPointer, $modifierPointer);
|
|
$this->checkTypeHintSpacing($phpcsFile, $propertyPointer, $modifierPointer);
|
|
}
|
|
|
|
private function checkModifiersOrder(File $phpcsFile, int $propertyPointer, int $firstModifierPointer, int $lastModifierPointer): void
|
|
{
|
|
$modifiersPointers = TokenHelper::findNextAll(
|
|
$phpcsFile,
|
|
TokenHelper::$propertyModifiersTokenCodes,
|
|
$firstModifierPointer,
|
|
$lastModifierPointer + 1
|
|
);
|
|
|
|
if (count($modifiersPointers) < 2) {
|
|
return;
|
|
}
|
|
|
|
$tokens = $phpcsFile->getTokens();
|
|
$modifiersGroups = $this->getNormalizedModifiersOrder();
|
|
|
|
$expectedModifiersPositions = [];
|
|
foreach ($modifiersPointers as $modifierPointer) {
|
|
for ($i = 0; $i < count($modifiersGroups); $i++) {
|
|
if (in_array($tokens[$modifierPointer]['code'], $modifiersGroups[$i], true)) {
|
|
$expectedModifiersPositions[$modifierPointer] = $i;
|
|
continue 2;
|
|
}
|
|
}
|
|
|
|
// Modifier position is not defined so add it to the end
|
|
$expectedModifiersPositions[$modifierPointer] = count($modifiersGroups);
|
|
}
|
|
|
|
$error = false;
|
|
|
|
for ($i = 1; $i < count($modifiersPointers); $i++) {
|
|
for ($j = 0; $j < $i; $j++) {
|
|
if ($expectedModifiersPositions[$modifiersPointers[$i]] < $expectedModifiersPositions[$modifiersPointers[$j]]) {
|
|
$error = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$error) {
|
|
return;
|
|
}
|
|
|
|
$actualModifiers = array_map(static function (int $modifierPointer) use ($tokens): string {
|
|
return $tokens[$modifierPointer]['content'];
|
|
}, $modifiersPointers);
|
|
$actualModifiersFormatted = implode(' ', $actualModifiers);
|
|
|
|
asort($expectedModifiersPositions);
|
|
$expectedModifiers = array_map(static function (int $modifierPointer) use ($tokens): string {
|
|
return $tokens[$modifierPointer]['content'];
|
|
}, array_keys($expectedModifiersPositions));
|
|
$expectedModifiersFormatted = implode(' ', $expectedModifiers);
|
|
|
|
$fix = $phpcsFile->addFixableError(
|
|
sprintf(
|
|
'Incorrect order of modifiers "%s" of property %s, expected "%s".',
|
|
$actualModifiersFormatted,
|
|
$tokens[$propertyPointer]['content'],
|
|
$expectedModifiersFormatted
|
|
),
|
|
$firstModifierPointer,
|
|
self::CODE_INCORRECT_ORDER_OF_MODIFIERS
|
|
);
|
|
if (!$fix) {
|
|
return;
|
|
}
|
|
|
|
$phpcsFile->fixer->beginChangeset();
|
|
|
|
FixerHelper::change($phpcsFile, $firstModifierPointer, $lastModifierPointer, $expectedModifiersFormatted);
|
|
|
|
$phpcsFile->fixer->endChangeset();
|
|
}
|
|
|
|
private function checkSpacesBetweenModifiers(
|
|
File $phpcsFile,
|
|
int $propertyPointer,
|
|
int $firstModifierPointer,
|
|
int $lastModifierPointer
|
|
): void
|
|
{
|
|
if (!$this->enableMultipleSpacesBetweenModifiersCheck) {
|
|
return;
|
|
}
|
|
|
|
$modifiersPointers = TokenHelper::findNextAll(
|
|
$phpcsFile,
|
|
TokenHelper::$propertyModifiersTokenCodes,
|
|
$firstModifierPointer,
|
|
$lastModifierPointer + 1
|
|
);
|
|
|
|
if (count($modifiersPointers) < 2) {
|
|
return;
|
|
}
|
|
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
$error = false;
|
|
for ($i = 0; $i < count($modifiersPointers) - 1; $i++) {
|
|
$whitespace = TokenHelper::getContent($phpcsFile, $modifiersPointers[$i] + 1, $modifiersPointers[$i + 1] - 1);
|
|
if ($whitespace !== ' ') {
|
|
$error = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$error) {
|
|
return;
|
|
}
|
|
|
|
$fix = $phpcsFile->addFixableError(
|
|
sprintf('There must be exactly one space between modifiers of property %s.', $tokens[$propertyPointer]['content']),
|
|
$firstModifierPointer,
|
|
self::CODE_MULTIPLE_SPACES_BETWEEN_MODIFIERS
|
|
);
|
|
if (!$fix) {
|
|
return;
|
|
}
|
|
|
|
$expectedModifiers = array_map(static function (int $modifierPointer) use ($tokens): string {
|
|
return $tokens[$modifierPointer]['content'];
|
|
}, $modifiersPointers);
|
|
$expectedModifiersFormatted = implode(' ', $expectedModifiers);
|
|
|
|
$phpcsFile->fixer->beginChangeset();
|
|
|
|
FixerHelper::change($phpcsFile, $firstModifierPointer, $lastModifierPointer, $expectedModifiersFormatted);
|
|
|
|
$phpcsFile->fixer->endChangeset();
|
|
}
|
|
|
|
private function checkTypeHintSpacing(File $phpcsFile, int $propertyPointer, int $lastModifierPointer): void
|
|
{
|
|
$typeHintEndPointer = TokenHelper::findPrevious(
|
|
$phpcsFile,
|
|
TokenHelper::getTypeHintTokenCodes(),
|
|
$propertyPointer - 1,
|
|
$lastModifierPointer
|
|
);
|
|
if ($typeHintEndPointer === null) {
|
|
return;
|
|
}
|
|
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
$typeHintStartPointer = TypeHintHelper::getStartPointer($phpcsFile, $typeHintEndPointer);
|
|
|
|
$previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $typeHintStartPointer - 1, $lastModifierPointer);
|
|
$nullabilitySymbolPointer = $previousPointer !== null && $tokens[$previousPointer]['code'] === T_NULLABLE ? $previousPointer : null;
|
|
|
|
$propertyName = $tokens[$propertyPointer]['content'];
|
|
|
|
if ($tokens[$lastModifierPointer + 1]['code'] !== T_WHITESPACE) {
|
|
$errorMessage = sprintf('There must be exactly one space before type hint nullability symbol of property %s.', $propertyName);
|
|
$errorCode = self::CODE_NO_SPACE_BEFORE_NULLABILITY_SYMBOL;
|
|
|
|
$fix = $phpcsFile->addFixableError($errorMessage, $typeHintEndPointer, $errorCode);
|
|
if ($fix) {
|
|
$phpcsFile->fixer->beginChangeset();
|
|
$phpcsFile->fixer->addContent($lastModifierPointer, ' ');
|
|
$phpcsFile->fixer->endChangeset();
|
|
}
|
|
} elseif ($tokens[$lastModifierPointer + 1]['content'] !== ' ') {
|
|
if ($nullabilitySymbolPointer !== null) {
|
|
$errorMessage = sprintf(
|
|
'There must be exactly one space before type hint nullability symbol of property %s.',
|
|
$propertyName
|
|
);
|
|
$errorCode = self::CODE_MULTIPLE_SPACES_BEFORE_NULLABILITY_SYMBOL;
|
|
} else {
|
|
$errorMessage = sprintf('There must be exactly one space before type hint of property %s.', $propertyName);
|
|
$errorCode = self::CODE_MULTIPLE_SPACES_BEFORE_TYPE_HINT;
|
|
}
|
|
|
|
$fix = $phpcsFile->addFixableError($errorMessage, $lastModifierPointer, $errorCode);
|
|
if ($fix) {
|
|
$phpcsFile->fixer->beginChangeset();
|
|
$phpcsFile->fixer->replaceToken($lastModifierPointer + 1, ' ');
|
|
$phpcsFile->fixer->endChangeset();
|
|
}
|
|
}
|
|
|
|
if ($tokens[$typeHintEndPointer + 1]['code'] !== T_WHITESPACE) {
|
|
$fix = $phpcsFile->addFixableError(
|
|
sprintf('There must be exactly one space between type hint and property %s.', $propertyName),
|
|
$typeHintEndPointer,
|
|
self::CODE_NO_SPACE_BETWEEN_TYPE_HINT_AND_PROPERTY
|
|
);
|
|
if ($fix) {
|
|
$phpcsFile->fixer->beginChangeset();
|
|
$phpcsFile->fixer->addContent($typeHintEndPointer, ' ');
|
|
$phpcsFile->fixer->endChangeset();
|
|
}
|
|
} elseif ($tokens[$typeHintEndPointer + 1]['content'] !== ' ') {
|
|
$fix = $phpcsFile->addFixableError(
|
|
sprintf('There must be exactly one space between type hint and property %s.', $propertyName),
|
|
$typeHintEndPointer,
|
|
self::CODE_MULTIPLE_SPACES_BETWEEN_TYPE_HINT_AND_PROPERTY
|
|
);
|
|
if ($fix) {
|
|
$phpcsFile->fixer->beginChangeset();
|
|
$phpcsFile->fixer->replaceToken($typeHintEndPointer + 1, ' ');
|
|
$phpcsFile->fixer->endChangeset();
|
|
}
|
|
}
|
|
|
|
if ($nullabilitySymbolPointer === null) {
|
|
return;
|
|
}
|
|
|
|
if ($nullabilitySymbolPointer + 1 === $typeHintStartPointer) {
|
|
return;
|
|
}
|
|
|
|
$fix = $phpcsFile->addFixableError(
|
|
sprintf('There must be no whitespace between type hint nullability symbol and type hint of property %s.', $propertyName),
|
|
$typeHintStartPointer,
|
|
self::CODE_WHITESPACE_AFTER_NULLABILITY_SYMBOL
|
|
);
|
|
if (!$fix) {
|
|
return;
|
|
}
|
|
|
|
$phpcsFile->fixer->beginChangeset();
|
|
$phpcsFile->fixer->replaceToken($nullabilitySymbolPointer + 1, '');
|
|
$phpcsFile->fixer->endChangeset();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<int, (int|string)>>
|
|
*/
|
|
private function getNormalizedModifiersOrder(): array
|
|
{
|
|
if ($this->normalizedModifiersOrder === null) {
|
|
$modifiersGroups = SniffSettingsHelper::normalizeArray($this->modifiersOrder);
|
|
|
|
if ($modifiersGroups === []) {
|
|
$modifiersGroups = [
|
|
'var, public, protected, private',
|
|
'static, readonly',
|
|
];
|
|
}
|
|
|
|
$this->normalizedModifiersOrder = [];
|
|
|
|
$mapping = [
|
|
'var' => T_VAR,
|
|
'public' => T_PUBLIC,
|
|
'protected' => T_PROTECTED,
|
|
'private' => T_PRIVATE,
|
|
'static' => T_STATIC,
|
|
'readonly' => T_READONLY,
|
|
];
|
|
|
|
foreach ($modifiersGroups as $modifiersGroupNo => $modifiersGroup) {
|
|
$this->normalizedModifiersOrder[$modifiersGroupNo] = [];
|
|
|
|
/** @var list<string> $modifiers */
|
|
$modifiers = preg_split('~\\s*,\\s*~', strtolower($modifiersGroup));
|
|
|
|
foreach ($modifiers as $modifier) {
|
|
if (!array_key_exists($modifier, $mapping)) {
|
|
throw new UnexpectedValueException(sprintf('Unknown property modifier "%s".', $modifier));
|
|
}
|
|
|
|
$this->normalizedModifiersOrder[$modifiersGroupNo][] = $mapping[$modifier];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->normalizedModifiersOrder;
|
|
}
|
|
|
|
}
|