|null */ public $modifiersOrder = []; /** @var bool */ public $checkPromoted = false; /** @var bool */ public $enableMultipleSpacesBetweenModifiersCheck = false; /** @var array>|null */ private $normalizedModifiersOrder = null; /** * @return array */ 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> */ 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 $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; } }