<?php /* * This file is part of PHP CS Fixer. * * (c) Fabien Potencier <fabien@symfony.com> * Dariusz Rumiński <dariusz.ruminski@gmail.com> * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Symfony\CS\Fixer\Contrib; use Symfony\CS\AbstractFixer; use Symfony\CS\Tokenizer\Token; use Symfony\CS\Tokenizer\Tokens; /** * @author Sebastiaan Stok <s.stok@rollerscapes.net> * @author Dariusz Rumiński <dariusz.ruminski@gmail.com> * @author SpacePossum */ class OrderedUseFixer extends AbstractFixer { const IMPORT_TYPE_CLASS = 1; const IMPORT_TYPE_CONST = 2; const IMPORT_TYPE_FUNCTION = 3; /** * {@inheritdoc} */ public function fix(\SplFileInfo $file, $content) { $tokens = Tokens::fromCode($content); $namespacesImports = $tokens->getImportUseIndexes(true); if (0 === count($namespacesImports)) { return $content; } $usesOrder = array(); foreach ($namespacesImports as $uses) { $usesOrder = array_replace($usesOrder, $this->getNewOrder(array_reverse($uses), $tokens)); } // First clean the old content // This must be done first as the indexes can be scattered foreach ($usesOrder as $use) { $tokens->clearRange($use['startIndex'], $use['endIndex']); } $usesOrder = array_reverse($usesOrder, true); // Now insert the new tokens, starting from the end foreach ($usesOrder as $index => $use) { $declarationTokens = Tokens::fromCode('<?php use '.$use['namespace'].';'); $declarationTokens->clearRange(0, 2); // clear `<?php use ` $declarationTokens[count($declarationTokens) - 1]->clear(); // clear `;` $declarationTokens->clearEmptyTokens(); $tokens->insertAt($index, $declarationTokens); if ($use['group']) { // a group import must start with `use` and cannot be part of comma separated import list $prev = $tokens->getPrevMeaningfulToken($index); if ($tokens[$prev]->equals(',')) { $tokens[$prev]->setContent(';'); $tokens->insertAt($prev + 1, new Token(array(T_USE, 'use'))); if (!$tokens[$prev + 2]->isWhitespace()) { $tokens->insertAt($prev + 2, new Token(array(T_WHITESPACE, ' '))); } } } } return $tokens->generateCode(); } /** * {@inheritdoc} */ public function getDescription() { return 'Ordering use statements.'; } /** * {@inheritdoc} */ public function getPriority() { // should be run after the RemoveLeadingSlashUseFixer return -30; } /** * This method is used for sorting the uses in a namespace. * * @param string[] $first * @param string[] $second * * @return int * * @internal */ public static function sortingCallBack(array $first, array $second) { if ($first['importType'] !== $second['importType']) { return $first['importType'] > $second['importType'] ? 1 : -1; } $firstNamespace = trim(preg_replace('%/\*(.*)\*/%s', '', $first['namespace'])); $secondNamespace = trim(preg_replace('%/\*(.*)\*/%s', '', $second['namespace'])); // Replace backslashes by spaces before sorting for correct sort order return strcasecmp( str_replace('\\', ' ', $firstNamespace), str_replace('\\', ' ', $secondNamespace) ); } private function getNewOrder(array $uses, Tokens $tokens) { $indexes = array(); $originalIndexes = array(); for ($i = count($uses) - 1; $i >= 0; --$i) { $index = $uses[$i]; $startIndex = $tokens->getTokenNotOfKindSibling($index + 1, 1, array(array(T_WHITESPACE))); $endIndex = $tokens->getNextTokenOfKind($startIndex, array(';', array(T_CLOSE_TAG))); $previous = $tokens->getPrevMeaningfulToken($endIndex); $group = $tokens[$previous]->equals('}'); if ($tokens[$startIndex]->isGivenKind(array(T_CONST))) { $type = self::IMPORT_TYPE_CONST; } elseif ($tokens[$startIndex]->isGivenKind(array(T_FUNCTION))) { $type = self::IMPORT_TYPE_FUNCTION; } else { $type = self::IMPORT_TYPE_CLASS; } $namespaceTokens = array(); $index = $startIndex; while ($index <= $endIndex) { $token = $tokens[$index]; if ($index === $endIndex || (!$group && $token->equals(','))) { if ($group) { // if group import, sort the items within the group definition // figure out where the list of namespace parts within the group def. starts $namespaceTokensCount = count($namespaceTokens) - 1; $namespace = ''; for ($k = 0; $k < $namespaceTokensCount; ++$k) { if ($namespaceTokens[$k]->equals('{')) { $namespace .= '{'; break; } $namespace .= $namespaceTokens[$k]->getContent(); } // fetch all parts, split up in an array of strings, move comments to the end $parts = array(); for ($k1 = $k + 1; $k1 < $namespaceTokensCount; ++$k1) { $comment = ''; $namespacePart = ''; for ($k2 = $k1; ; ++$k2) { if ($namespaceTokens[$k2]->equalsAny(array(',', '}'))) { break; } if ($namespaceTokens[$k2]->isComment()) { $comment .= $namespaceTokens[$k2]->getContent(); continue; } $namespacePart .= $namespaceTokens[$k2]->getContent(); } $namespacePart = trim($namespacePart); $comment = trim($comment); if ('' !== $comment) { $namespacePart .= ' '.$comment; } $parts[] = $namespacePart.', '; $k1 = $k2; } $sortedParts = $parts; sort($parts); // check if the order needs to be updated, otherwise don't touch as we might change valid CS (to other valid CS). if ($sortedParts === $parts) { $namespace = Tokens::fromArray($namespaceTokens)->generateCode(); } else { $namespace .= substr(implode('', $parts), 0, -2).'}'; } } else { $namespace = Tokens::fromArray($namespaceTokens)->generateCode(); } $indexes[$startIndex] = array( 'namespace' => $namespace, 'startIndex' => $startIndex, 'endIndex' => $index - 1, 'importType' => $type, 'group' => $group, ); $originalIndexes[] = $startIndex; if ($index === $endIndex) { break; } $namespaceTokens = array(); $nextPartIndex = $tokens->getTokenNotOfKindSibling($index, 1, array(array(','), array(T_WHITESPACE))); $startIndex = $nextPartIndex; $index = $nextPartIndex; continue; } $namespaceTokens[] = $token; ++$index; } } uasort($indexes, 'self::sortingCallBack'); $index = -1; $usesOrder = array(); // Loop trough the index but use original index order foreach ($indexes as $v) { $usesOrder[$originalIndexes[++$index]] = $v; } return $usesOrder; } }