fuc-new/vendor/slevomat/coding-standard/SlevomatCodingStandard/Sniffs/Classes/ClassStructureSniff.php
2026-05-02 16:20:44 +08:00

667 lines
22 KiB
PHP

<?php declare(strict_types = 1);
namespace SlevomatCodingStandard\Sniffs\Classes;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\ClassHelper;
use SlevomatCodingStandard\Helpers\DocCommentHelper;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\FunctionHelper;
use SlevomatCodingStandard\Helpers\PropertyHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_diff;
use function array_filter;
use function array_flip;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_values;
use function assert;
use function implode;
use function in_array;
use function preg_replace;
use function preg_split;
use function sprintf;
use function str_repeat;
use function strtolower;
use const T_ABSTRACT;
use const T_CLOSE_CURLY_BRACKET;
use const T_CONST;
use const T_ENUM_CASE;
use const T_FINAL;
use const T_FUNCTION;
use const T_OPEN_CURLY_BRACKET;
use const T_PROTECTED;
use const T_PUBLIC;
use const T_SEMICOLON;
use const T_STATIC;
use const T_USE;
use const T_VARIABLE;
use const T_WHITESPACE;
class ClassStructureSniff implements Sniff
{
public const CODE_INCORRECT_GROUP_ORDER = 'IncorrectGroupOrder';
private const GROUP_USES = 'uses';
private const GROUP_PUBLIC_CONSTANTS = 'public constants';
private const GROUP_PROTECTED_CONSTANTS = 'protected constants';
private const GROUP_PRIVATE_CONSTANTS = 'private constants';
private const GROUP_PUBLIC_PROPERTIES = 'public properties';
private const GROUP_PUBLIC_STATIC_PROPERTIES = 'public static properties';
private const GROUP_PROTECTED_PROPERTIES = 'protected properties';
private const GROUP_PROTECTED_STATIC_PROPERTIES = 'protected static properties';
private const GROUP_PRIVATE_PROPERTIES = 'private properties';
private const GROUP_PRIVATE_STATIC_PROPERTIES = 'private static properties';
private const GROUP_CONSTRUCTOR = 'constructor';
private const GROUP_STATIC_CONSTRUCTORS = 'static constructors';
private const GROUP_DESTRUCTOR = 'destructor';
private const GROUP_MAGIC_METHODS = 'magic methods';
private const GROUP_PUBLIC_METHODS = 'public methods';
private const GROUP_PUBLIC_ABSTRACT_METHODS = 'public abstract methods';
private const GROUP_PUBLIC_FINAL_METHODS = 'public final methods';
private const GROUP_PUBLIC_STATIC_METHODS = 'public static methods';
private const GROUP_PUBLIC_STATIC_ABSTRACT_METHODS = 'public static abstract methods';
private const GROUP_PUBLIC_STATIC_FINAL_METHODS = 'public static final methods';
private const GROUP_PROTECTED_METHODS = 'protected methods';
private const GROUP_PROTECTED_ABSTRACT_METHODS = 'protected abstract methods';
private const GROUP_PROTECTED_FINAL_METHODS = 'protected final methods';
private const GROUP_PROTECTED_STATIC_METHODS = 'protected static methods';
private const GROUP_PROTECTED_STATIC_ABSTRACT_METHODS = 'protected static abstract methods';
private const GROUP_PROTECTED_STATIC_FINAL_METHODS = 'protected static final methods';
private const GROUP_PRIVATE_METHODS = 'private methods';
private const GROUP_PRIVATE_STATIC_METHODS = 'private static methods';
private const GROUP_ENUM_CASES = 'enum cases';
private const GROUP_SHORTCUT_CONSTANTS = 'constants';
private const GROUP_SHORTCUT_PROPERTIES = 'properties';
private const GROUP_SHORTCUT_STATIC_PROPERTIES = 'static properties';
private const GROUP_SHORTCUT_METHODS = 'methods';
private const GROUP_SHORTCUT_PUBLIC_METHODS = 'all public methods';
private const GROUP_SHORTCUT_PROTECTED_METHODS = 'all protected methods';
private const GROUP_SHORTCUT_PRIVATE_METHODS = 'all private methods';
private const GROUP_SHORTCUT_STATIC_METHODS = 'static methods';
private const GROUP_SHORTCUT_ABSTRACT_METHODS = 'abstract methods';
private const GROUP_SHORTCUT_FINAL_METHODS = 'final methods';
private const SHORTCUTS = [
self::GROUP_SHORTCUT_CONSTANTS => [
self::GROUP_PUBLIC_CONSTANTS,
self::GROUP_PROTECTED_CONSTANTS,
self::GROUP_PRIVATE_CONSTANTS,
],
self::GROUP_SHORTCUT_STATIC_PROPERTIES => [
self::GROUP_PUBLIC_STATIC_PROPERTIES,
self::GROUP_PROTECTED_STATIC_PROPERTIES,
self::GROUP_PRIVATE_STATIC_PROPERTIES,
],
self::GROUP_SHORTCUT_PROPERTIES => [
self::GROUP_SHORTCUT_STATIC_PROPERTIES,
self::GROUP_PUBLIC_PROPERTIES,
self::GROUP_PROTECTED_PROPERTIES,
self::GROUP_PRIVATE_PROPERTIES,
],
self::GROUP_SHORTCUT_PUBLIC_METHODS => [
self::GROUP_PUBLIC_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PUBLIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_METHODS,
self::GROUP_PUBLIC_METHODS,
],
self::GROUP_SHORTCUT_PROTECTED_METHODS => [
self::GROUP_PROTECTED_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_METHODS,
self::GROUP_PROTECTED_METHODS,
],
self::GROUP_SHORTCUT_PRIVATE_METHODS => [
self::GROUP_PRIVATE_STATIC_METHODS,
self::GROUP_PRIVATE_METHODS,
],
self::GROUP_SHORTCUT_FINAL_METHODS => [
self::GROUP_PUBLIC_FINAL_METHODS,
self::GROUP_PROTECTED_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
],
self::GROUP_SHORTCUT_ABSTRACT_METHODS => [
self::GROUP_PUBLIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
],
self::GROUP_SHORTCUT_STATIC_METHODS => [
self::GROUP_STATIC_CONSTRUCTORS,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_METHODS,
self::GROUP_PROTECTED_STATIC_METHODS,
self::GROUP_PRIVATE_STATIC_METHODS,
],
self::GROUP_SHORTCUT_METHODS => [
self::GROUP_SHORTCUT_FINAL_METHODS,
self::GROUP_SHORTCUT_ABSTRACT_METHODS,
self::GROUP_SHORTCUT_STATIC_METHODS,
self::GROUP_CONSTRUCTOR,
self::GROUP_DESTRUCTOR,
self::GROUP_PUBLIC_METHODS,
self::GROUP_PROTECTED_METHODS,
self::GROUP_PRIVATE_METHODS,
self::GROUP_MAGIC_METHODS,
],
];
private const SPECIAL_METHODS = [
'__construct' => self::GROUP_CONSTRUCTOR,
'__destruct' => self::GROUP_DESTRUCTOR,
'__call' => self::GROUP_MAGIC_METHODS,
'__callstatic' => self::GROUP_MAGIC_METHODS,
'__get' => self::GROUP_MAGIC_METHODS,
'__set' => self::GROUP_MAGIC_METHODS,
'__isset' => self::GROUP_MAGIC_METHODS,
'__unset' => self::GROUP_MAGIC_METHODS,
'__sleep' => self::GROUP_MAGIC_METHODS,
'__wakeup' => self::GROUP_MAGIC_METHODS,
'__serialize' => self::GROUP_MAGIC_METHODS,
'__unserialize' => self::GROUP_MAGIC_METHODS,
'__tostring' => self::GROUP_MAGIC_METHODS,
'__invoke' => self::GROUP_MAGIC_METHODS,
'__set_state' => self::GROUP_MAGIC_METHODS,
'__clone' => self::GROUP_MAGIC_METHODS,
'__debuginfo' => self::GROUP_MAGIC_METHODS,
];
/** @var list<string> */
public $groups = [];
/** @var array<string, int>|null */
private $normalizedGroups;
/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return array_values(Tokens::$ooScopeTokens);
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $pointer
*/
public function process(File $phpcsFile, $pointer): int
{
$tokens = $phpcsFile->getTokens();
/** @var array{scope_closer: int, level: int} $rootScopeToken */
$rootScopeToken = $tokens[$pointer];
$groupsOrder = $this->getNormalizedGroups();
$groupLastMemberPointer = $rootScopeToken['scope_opener'];
$expectedGroup = null;
$groupsFirstMembers = [];
while (true) {
$nextGroup = $this->findNextGroup($phpcsFile, $groupLastMemberPointer, $rootScopeToken);
if ($nextGroup === null) {
break;
}
[$groupFirstMemberPointer, $groupLastMemberPointer, $group] = $nextGroup;
if ($groupsOrder[$group] >= ($groupsOrder[$expectedGroup] ?? 0)) {
$groupsFirstMembers[$group] = $groupFirstMemberPointer;
$expectedGroup = $group;
continue;
}
$expectedGroups = array_filter(
$groupsOrder,
static function (int $order) use ($groupsOrder, $expectedGroup): bool {
return $order >= $groupsOrder[$expectedGroup];
}
);
$fix = $phpcsFile->addFixableError(
sprintf(
'The placement of "%s" group is invalid. Last group was "%s" and one of these is expected after it: %s',
$group,
$expectedGroup,
implode(', ', array_keys($expectedGroups))
),
$groupFirstMemberPointer,
self::CODE_INCORRECT_GROUP_ORDER
);
if (!$fix) {
continue;
}
foreach ($groupsFirstMembers as $memberGroup => $firstMemberPointer) {
if ($groupsOrder[$memberGroup] <= $groupsOrder[$group]) {
continue;
}
$this->fixIncorrectGroupOrder($phpcsFile, $groupFirstMemberPointer, $groupLastMemberPointer, $firstMemberPointer);
// run the sniff again to fix the rest of the groups
return $pointer - 1;
}
}
return $pointer + 1;
}
/**
* @param array{scope_closer: int, level: int} $rootScopeToken
* @return array{int, int, string}|null
*/
private function findNextGroup(File $phpcsFile, int $pointer, array $rootScopeToken): ?array
{
$tokens = $phpcsFile->getTokens();
$groupTokenTypes = [T_USE, T_ENUM_CASE, T_CONST, T_VARIABLE, T_FUNCTION];
$currentTokenPointer = $pointer;
while (true) {
$currentTokenPointer = TokenHelper::findNext(
$phpcsFile,
$groupTokenTypes,
($currentToken['scope_closer'] ?? $currentTokenPointer) + 1,
$rootScopeToken['scope_closer']
);
if ($currentTokenPointer === null) {
break;
}
$currentToken = $tokens[$currentTokenPointer];
if ($currentToken['code'] === T_VARIABLE && !PropertyHelper::isProperty($phpcsFile, $currentTokenPointer)) {
continue;
}
if ($currentToken['level'] - $rootScopeToken['level'] !== 1) {
continue;
}
$group = $this->getGroupForToken($phpcsFile, $currentTokenPointer);
if (!isset($currentGroup)) {
$currentGroup = $group;
$groupFirstMemberPointer = $currentTokenPointer;
}
if ($group !== $currentGroup) {
break;
}
$groupLastMemberPointer = $currentTokenPointer;
}
if (!isset($currentGroup)) {
return null;
}
assert(isset($groupFirstMemberPointer) === true);
assert(isset($groupLastMemberPointer) === true);
return [$groupFirstMemberPointer, $groupLastMemberPointer, $currentGroup];
}
private function getGroupForToken(File $phpcsFile, int $pointer): string
{
$tokens = $phpcsFile->getTokens();
switch ($tokens[$pointer]['code']) {
case T_USE:
return self::GROUP_USES;
case T_ENUM_CASE:
return self::GROUP_ENUM_CASES;
case T_CONST:
switch ($this->getVisibilityForToken($phpcsFile, $pointer)) {
case T_PUBLIC:
return self::GROUP_PUBLIC_CONSTANTS;
case T_PROTECTED:
return self::GROUP_PROTECTED_CONSTANTS;
}
return self::GROUP_PRIVATE_CONSTANTS;
case T_FUNCTION:
$name = strtolower(FunctionHelper::getName($phpcsFile, $pointer));
if (array_key_exists($name, self::SPECIAL_METHODS)) {
return self::SPECIAL_METHODS[$name];
}
$visibility = $this->getVisibilityForToken($phpcsFile, $pointer);
$isStatic = $this->isMemberStatic($phpcsFile, $pointer);
$isFinal = $this->isMethodFinal($phpcsFile, $pointer);
if ($this->isMethodAbstract($phpcsFile, $pointer)) {
if ($visibility === T_PUBLIC) {
return $isStatic ? self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS : self::GROUP_PUBLIC_ABSTRACT_METHODS;
}
return $isStatic ? self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS : self::GROUP_PROTECTED_ABSTRACT_METHODS;
}
if ($isStatic && $visibility === T_PUBLIC && $this->isStaticConstructor($phpcsFile, $pointer)) {
return self::GROUP_STATIC_CONSTRUCTORS;
}
switch ($visibility) {
case T_PUBLIC:
if ($isFinal) {
return $isStatic ? self::GROUP_PUBLIC_STATIC_FINAL_METHODS : self::GROUP_PUBLIC_FINAL_METHODS;
}
return $isStatic ? self::GROUP_PUBLIC_STATIC_METHODS : self::GROUP_PUBLIC_METHODS;
case T_PROTECTED:
if ($isFinal) {
return $isStatic ? self::GROUP_PROTECTED_STATIC_FINAL_METHODS : self::GROUP_PROTECTED_FINAL_METHODS;
}
return $isStatic ? self::GROUP_PROTECTED_STATIC_METHODS : self::GROUP_PROTECTED_METHODS;
}
return $isStatic ? self::GROUP_PRIVATE_STATIC_METHODS : self::GROUP_PRIVATE_METHODS;
default:
$isStatic = $this->isMemberStatic($phpcsFile, $pointer);
$visibility = $this->getVisibilityForToken($phpcsFile, $pointer);
switch ($visibility) {
case T_PUBLIC:
return $isStatic ? self::GROUP_PUBLIC_STATIC_PROPERTIES : self::GROUP_PUBLIC_PROPERTIES;
case T_PROTECTED:
return $isStatic
? self::GROUP_PROTECTED_STATIC_PROPERTIES
: self::GROUP_PROTECTED_PROPERTIES;
default:
return $isStatic ? self::GROUP_PRIVATE_STATIC_PROPERTIES : self::GROUP_PRIVATE_PROPERTIES;
}
}
}
private function getVisibilityForToken(File $phpcsFile, int $pointer): int
{
$tokens = $phpcsFile->getTokens();
$previousPointer = TokenHelper::findPrevious(
$phpcsFile,
array_merge(Tokens::$scopeModifiers, [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON]),
$pointer - 1
);
/** @var int $visibilityPointer */
$visibilityPointer = in_array($tokens[$previousPointer]['code'], Tokens::$scopeModifiers, true)
? $tokens[$previousPointer]['code']
: T_PUBLIC;
return $visibilityPointer;
}
private function isMemberStatic(File $phpcsFile, int $pointer): bool
{
$previousPointer = TokenHelper::findPrevious(
$phpcsFile,
[T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON, T_STATIC],
$pointer - 1
);
return $phpcsFile->getTokens()[$previousPointer]['code'] === T_STATIC;
}
private function isMethodFinal(File $phpcsFile, int $pointer): bool
{
$previousPointer = TokenHelper::findPrevious(
$phpcsFile,
[T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON, T_FINAL],
$pointer - 1
);
return $phpcsFile->getTokens()[$previousPointer]['code'] === T_FINAL;
}
private function isMethodAbstract(File $phpcsFile, int $pointer): bool
{
$previousPointer = TokenHelper::findPrevious(
$phpcsFile,
[T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON, T_ABSTRACT],
$pointer - 1
);
return $phpcsFile->getTokens()[$previousPointer]['code'] === T_ABSTRACT;
}
private function isStaticConstructor(File $phpcsFile, int $pointer): bool
{
$parentClassName = $this->getParentClassName($phpcsFile, $pointer);
$returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $pointer);
if ($returnTypeHint !== null) {
return in_array($returnTypeHint->getTypeHintWithoutNullabilitySymbol(), ['self', $parentClassName], true);
}
$returnAnnotation = FunctionHelper::findReturnAnnotation($phpcsFile, $pointer);
if ($returnAnnotation === null) {
return false;
}
return in_array((string) $returnAnnotation->getValue()->type, ['static', 'self', $parentClassName], true);
}
private function getParentClassName(File $phpcsFile, int $pointer): string
{
$classPointer = TokenHelper::findPrevious($phpcsFile, Tokens::$ooScopeTokens, $pointer - 1);
assert($classPointer !== null);
return ClassHelper::getName($phpcsFile, $classPointer);
}
private function fixIncorrectGroupOrder(
File $file,
int $groupFirstMemberPointer,
int $groupLastMemberPointer,
int $nextGroupMemberPointer
): void
{
$previousMemberEndPointer = $this->findPreviousMemberEndPointer($file, $groupFirstMemberPointer);
$groupStartPointer = $this->findGroupStartPointer($file, $groupFirstMemberPointer, $previousMemberEndPointer);
$groupEndPointer = $this->findGroupEndPointer($file, $groupLastMemberPointer);
$groupContent = TokenHelper::getContent($file, $groupStartPointer, $groupEndPointer);
$nextGroupMemberStartPointer = $this->findGroupStartPointer($file, $nextGroupMemberPointer);
$file->fixer->beginChangeset();
FixerHelper::removeBetweenIncluding($file, $groupStartPointer, $groupEndPointer);
$linesBetween = $this->removeBlankLinesAfterMember($file, $previousMemberEndPointer, $groupStartPointer);
$newLines = str_repeat($file->eolChar, $linesBetween);
$file->fixer->addContentBefore($nextGroupMemberStartPointer, $groupContent . $newLines);
$file->fixer->endChangeset();
}
private function findPreviousMemberEndPointer(File $phpcsFile, int $memberPointer): int
{
$endTypes = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON];
$previousMemberEndPointer = TokenHelper::findPrevious($phpcsFile, $endTypes, $memberPointer - 1);
assert($previousMemberEndPointer !== null);
return $previousMemberEndPointer;
}
private function findGroupStartPointer(File $phpcsFile, int $memberPointer, ?int $previousMemberEndPointer = null): int
{
$startPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $memberPointer - 1);
if ($startPointer === null) {
if ($previousMemberEndPointer === null) {
$previousMemberEndPointer = $this->findPreviousMemberEndPointer($phpcsFile, $memberPointer);
}
$startPointer = TokenHelper::findNextEffective($phpcsFile, $previousMemberEndPointer + 1);
assert($startPointer !== null);
}
$types = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON];
return (int) $phpcsFile->findFirstOnLine($types, $startPointer, true);
}
private function findGroupEndPointer(File $phpcsFile, int $memberPointer): int
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$memberPointer]['code'] === T_FUNCTION && !FunctionHelper::isAbstract($phpcsFile, $memberPointer)) {
$endPointer = $tokens[$memberPointer]['scope_closer'];
} elseif ($tokens[$memberPointer]['code'] === T_USE && array_key_exists('scope_closer', $tokens[$memberPointer])) {
$endPointer = $tokens[$memberPointer]['scope_closer'];
} else {
$endPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $memberPointer + 1);
assert($endPointer !== null);
}
return $endPointer;
}
private function removeBlankLinesAfterMember(File $phpcsFile, int $memberEndPointer, int $endPointer): int
{
$whitespacePointer = $memberEndPointer;
$linesToRemove = 0;
while (true) {
$whitespacePointer = TokenHelper::findNext($phpcsFile, T_WHITESPACE, $whitespacePointer, $endPointer);
if ($whitespacePointer === null) {
break;
}
$linesToRemove++;
$phpcsFile->fixer->replaceToken($whitespacePointer, '');
$whitespacePointer++;
}
return $linesToRemove;
}
/**
* @return array<string, int>
*/
private function getNormalizedGroups(): array
{
if ($this->normalizedGroups === null) {
$supportedGroups = [
self::GROUP_USES,
self::GROUP_ENUM_CASES,
self::GROUP_PUBLIC_CONSTANTS,
self::GROUP_PROTECTED_CONSTANTS,
self::GROUP_PRIVATE_CONSTANTS,
self::GROUP_PUBLIC_PROPERTIES,
self::GROUP_PUBLIC_STATIC_PROPERTIES,
self::GROUP_PROTECTED_PROPERTIES,
self::GROUP_PROTECTED_STATIC_PROPERTIES,
self::GROUP_PRIVATE_PROPERTIES,
self::GROUP_PRIVATE_STATIC_PROPERTIES,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_FINAL_METHODS,
self::GROUP_PUBLIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_FINAL_METHODS,
self::GROUP_PROTECTED_ABSTRACT_METHODS,
self::GROUP_CONSTRUCTOR,
self::GROUP_STATIC_CONSTRUCTORS,
self::GROUP_DESTRUCTOR,
self::GROUP_PUBLIC_METHODS,
self::GROUP_PUBLIC_STATIC_METHODS,
self::GROUP_PROTECTED_METHODS,
self::GROUP_PROTECTED_STATIC_METHODS,
self::GROUP_PRIVATE_METHODS,
self::GROUP_PRIVATE_STATIC_METHODS,
self::GROUP_MAGIC_METHODS,
];
$normalizedGroupsWithShortcuts = [];
$order = 1;
foreach (SniffSettingsHelper::normalizeArray($this->groups) as $groupsString) {
/** @var list<string> $groups */
$groups = preg_split('~\\s*,\\s*~', strtolower($groupsString));
foreach ($groups as $groupOrShortcut) {
$groupOrShortcut = preg_replace('~\\s+~', ' ', $groupOrShortcut);
if (
!in_array($groupOrShortcut, $supportedGroups, true)
&& !array_key_exists($groupOrShortcut, self::SHORTCUTS)
) {
throw new UnsupportedClassGroupException($groupOrShortcut);
}
$normalizedGroupsWithShortcuts[$groupOrShortcut] = $order;
}
$order++;
}
$normalizedGroups = [];
foreach ($normalizedGroupsWithShortcuts as $groupOrShortcut => $groupOrder) {
if (in_array($groupOrShortcut, $supportedGroups, true)) {
$normalizedGroups[$groupOrShortcut] = $groupOrder;
} else {
foreach ($this->unpackShortcut($groupOrShortcut, $supportedGroups) as $group) {
if (
array_key_exists($group, $normalizedGroupsWithShortcuts)
|| array_key_exists($group, $normalizedGroups)
) {
continue;
}
$normalizedGroups[$group] = $groupOrder;
}
}
}
if ($normalizedGroups === []) {
$normalizedGroups = array_flip($supportedGroups);
} else {
$missingGroups = array_diff($supportedGroups, array_keys($normalizedGroups));
if ($missingGroups !== []) {
throw new MissingClassGroupsException($missingGroups);
}
}
$this->normalizedGroups = $normalizedGroups;
}
return $this->normalizedGroups;
}
/**
* @param array<int, string> $supportedGroups
* @return array<int, string>
*/
private function unpackShortcut(string $shortcut, array $supportedGroups): array
{
$groups = [];
foreach (self::SHORTCUTS[$shortcut] as $groupOrShortcut) {
if (in_array($groupOrShortcut, $supportedGroups, true)) {
$groups[] = $groupOrShortcut;
} elseif (
!array_key_exists($groupOrShortcut, self::SHORTCUTS)
&& in_array($groupOrShortcut, self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS], true)
) {
// Nothing
} else {
$groups = array_merge($groups, $this->unpackShortcut($groupOrShortcut, $supportedGroups));
}
}
return $groups;
}
}