[ 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 */ public $groups = []; /** @var array|null */ private $normalizedGroups; /** * @return array */ 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 */ 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 $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 $supportedGroups * @return array */ 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; } }