From 41fe1f493b2a07616c15d06583104675cb0e9bdc Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Sat, 9 Jun 2018 10:50:31 +0000 Subject: [PATCH] GlobalNamespaceImportFixer - Introduction --- README.rst | 13 + .../NativeConstantInvocationFixer.php | 9 + .../NativeFunctionInvocationFixer.php | 9 + .../Import/GlobalNamespaceImportFixer.php | 753 ++++++++++++++ src/Tokenizer/Analyzer/ClassyAnalyzer.php | 83 ++ tests/AutoReview/FixerFactoryTest.php | 4 + .../Import/GlobalNamespaceImportFixerTest.php | 953 ++++++++++++++++++ ...al_namespace_import,no_unused_imports.test | 20 + ...obal_namespace_import,ordered_imports.test | 22 + ...nt_invocation,global_namespace_import.test | 18 + ...on_invocation,global_namespace_import.test | 18 + .../Tokenizer/Analyzer/ClassyAnalyzerTest.php | 182 ++++ 12 files changed, 2084 insertions(+) create mode 100644 src/Fixer/Import/GlobalNamespaceImportFixer.php create mode 100644 src/Tokenizer/Analyzer/ClassyAnalyzer.php create mode 100644 tests/Fixer/Import/GlobalNamespaceImportFixerTest.php create mode 100644 tests/Fixtures/Integration/priority/global_namespace_import,no_unused_imports.test create mode 100644 tests/Fixtures/Integration/priority/global_namespace_import,ordered_imports.test create mode 100644 tests/Fixtures/Integration/priority/native_constant_invocation,global_namespace_import.test create mode 100644 tests/Fixtures/Integration/priority/native_function_invocation,global_namespace_import.test create mode 100644 tests/Tokenizer/Analyzer/ClassyAnalyzerTest.php diff --git a/README.rst b/README.rst index 3c902bacd63..796a3973f3e 100644 --- a/README.rst +++ b/README.rst @@ -743,6 +743,19 @@ Choose from the list of available rules: - ``annotations`` (``array``): list of annotations to remove, e.g. ``["author"]``; defaults to ``[]`` +* **global_namespace_import** + + Imports or fully qualifies global classes/functions/constants. + + Configuration options: + + - ``import_classes`` (``false``, ``null``, ``true``): whether to import, not import or + ignore global classes; defaults to ``true`` + - ``import_constants`` (``false``, ``null``, ``true``): whether to import, not import or + ignore global constants; defaults to ``null`` + - ``import_functions`` (``false``, ``null``, ``true``): whether to import, not import or + ignore global functions; defaults to ``null`` + * **hash_to_slash_comment** Single line comments should use double slashes ``//`` and not hash ``#``. diff --git a/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php b/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php index 7885fe470ab..6cdd56e80f5 100644 --- a/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php +++ b/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php @@ -92,6 +92,15 @@ public function getDefinition() ); } + /** + * {@inheritdoc} + */ + public function getPriority() + { + // must be run before GlobalNamespaceImportFixer + return 10; + } + /** * {@inheritdoc} */ diff --git a/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.php b/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.php index be2e2128f65..3e1401b3e0a 100644 --- a/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.php +++ b/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.php @@ -160,6 +160,15 @@ function baz($options) ); } + /** + * {@inheritdoc} + */ + public function getPriority() + { + // must be run before GlobalNamespaceImportFixer + return 10; + } + /** * {@inheritdoc} */ diff --git a/src/Fixer/Import/GlobalNamespaceImportFixer.php b/src/Fixer/Import/GlobalNamespaceImportFixer.php new file mode 100644 index 00000000000..a16ed590f3d --- /dev/null +++ b/src/Fixer/Import/GlobalNamespaceImportFixer.php @@ -0,0 +1,753 @@ + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\Fixer\Import; + +use PhpCsFixer\AbstractFixer; +use PhpCsFixer\DocBlock\Annotation; +use PhpCsFixer\DocBlock\DocBlock; +use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface; +use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface; +use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; +use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; +use PhpCsFixer\FixerDefinition\CodeSample; +use PhpCsFixer\FixerDefinition\FixerDefinition; +use PhpCsFixer\Preg; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis; +use PhpCsFixer\Tokenizer\Analyzer\ClassyAnalyzer; +use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer; +use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer; +use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer; +use PhpCsFixer\Tokenizer\CT; +use PhpCsFixer\Tokenizer\Token; +use PhpCsFixer\Tokenizer\Tokens; +use PhpCsFixer\Tokenizer\TokensAnalyzer; + +/** + * @author Gregor Harlan + */ +final class GlobalNamespaceImportFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface, WhitespacesAwareFixerInterface +{ + /** + * {@inheritdoc} + */ + public function getDefinition() + { + return new FixerDefinition( + 'Imports or fully qualifies global classes/functions/constants.', + [ + new CodeSample( + ' true, 'import_constants' => true, 'import_functions' => true] + ), + new CodeSample( + ' false, 'import_constants' => false, 'import_functions' => false] + ), + ] + ); + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + // must be run after NativeConstantInvocationFixer, NativeFunctionInvocationFixer + // must be run before NoUnusedImportsFixer, OrderedImportsFixer + return 0; + } + + /** + * {@inheritdoc} + */ + public function isCandidate(Tokens $tokens) + { + return $tokens->isAnyTokenKindsFound([T_USE, T_NS_SEPARATOR]) + && (Tokens::isLegacyMode() || $tokens->countTokenKind(T_NAMESPACE) < 2) + && $tokens->isMonolithicPhp(); + } + + /** + * {@inheritdoc} + */ + protected function applyFix(\SplFileInfo $file, Tokens $tokens) + { + if (Tokens::isLegacyMode() && $tokens->isTokenKindFound(T_NAMESPACE) && \count((new NamespacesAnalyzer())->getDeclarations($tokens)) > 1) { + return; + } + + $useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens); + + $newImports = []; + + if (true === $this->configuration['import_constants']) { + $newImports['const'] = $this->importConstants($tokens, $useDeclarations); + } elseif (false === $this->configuration['import_constants']) { + $this->fullyQualifyConstants($tokens, $useDeclarations); + } + + if (true === $this->configuration['import_functions']) { + $newImports['function'] = $this->importFunctions($tokens, $useDeclarations); + } elseif (false === $this->configuration['import_functions']) { + $this->fullyQualifyFunctions($tokens, $useDeclarations); + } + + if (true === $this->configuration['import_classes']) { + $newImports['class'] = $this->importClasses($tokens, $useDeclarations); + } elseif (false === $this->configuration['import_classes']) { + $this->fullyQualifyClasses($tokens, $useDeclarations); + } + + $newImports = array_filter($newImports); + + if ($newImports) { + $this->insertImports($tokens, $newImports, $useDeclarations); + } + } + + protected function createConfigurationDefinition() + { + return new FixerConfigurationResolver([ + (new FixerOptionBuilder('import_constants', 'Whether to import, not import or ignore global constants.')) + ->setDefault(null) + ->setAllowedValues([true, false, null]) + ->getOption(), + (new FixerOptionBuilder('import_functions', 'Whether to import, not import or ignore global functions.')) + ->setDefault(null) + ->setAllowedValues([true, false, null]) + ->getOption(), + (new FixerOptionBuilder('import_classes', 'Whether to import, not import or ignore global classes.')) + ->setDefault(true) + ->setAllowedValues([true, false, null]) + ->getOption(), + ]); + } + + /** + * @param Tokens $tokens + * @param NamespaceUseAnalysis[] $useDeclarations + * + * @return array + */ + private function importConstants(Tokens $tokens, array $useDeclarations) + { + list($global, $other) = $this->filterUseDeclarations($useDeclarations, static function (NamespaceUseAnalysis $declaration) { + return $declaration->isConstant(); + }, true); + + // find namespaced const declarations (`const FOO = 1`) + // and add them to the not importable names (already used) + for ($index = 0, $count = $tokens->count(); $index < $count; ++$index) { + $token = $tokens[$index]; + + if ($token->isClassy()) { + $index = $tokens->getNextTokenOfKind($index, ['{']); + $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index); + + continue; + } + + if (!$token->isGivenKind(T_CONST)) { + continue; + } + + $index = $tokens->getNextMeaningfulToken($index); + $other[$tokens[$index]->getContent()] = true; + } + + $analyzer = new TokensAnalyzer($tokens); + + $indexes = []; + + for ($index = $tokens->count() - 1; $index >= 0; --$index) { + $token = $tokens[$index]; + + if (!$token->isGivenKind(T_STRING)) { + continue; + } + + $name = $token->getContent(); + + if (isset($other[$name])) { + continue; + } + + if (!$analyzer->isConstantInvocation($index)) { + continue; + } + + $nsSeparatorIndex = $tokens->getPrevMeaningfulToken($index); + if (!$tokens[$nsSeparatorIndex]->isGivenKind(T_NS_SEPARATOR)) { + if (!isset($global[$name])) { + // found an unqualified constant invocation + // add it to the not importable names (already used) + $other[$name] = true; + } + + continue; + } + + $prevIndex = $tokens->getPrevMeaningfulToken($nsSeparatorIndex); + if ($tokens[$prevIndex]->isGivenKind([CT::T_NAMESPACE_OPERATOR, T_STRING])) { + continue; + } + + $indexes[] = $index; + } + + return $this->prepareImports($tokens, $indexes, $global, $other, true); + } + + /** + * @param Tokens $tokens + * @param NamespaceUseAnalysis[] $useDeclarations + * + * @return array + */ + private function importFunctions(Tokens $tokens, array $useDeclarations) + { + list($global, $other) = $this->filterUseDeclarations($useDeclarations, static function (NamespaceUseAnalysis $declaration) { + return $declaration->isFunction(); + }, false); + + // find function declarations + // and add them to the not importable names (already used) + foreach ($this->findFunctionDeclarations($tokens, 0, $tokens->count() - 1) as $name) { + $other[strtolower($name)] = true; + } + + $analyzer = new FunctionsAnalyzer(); + + $indexes = []; + + for ($index = $tokens->count() - 1; $index >= 0; --$index) { + $token = $tokens[$index]; + + if (!$token->isGivenKind(T_STRING)) { + continue; + } + + $name = strtolower($token->getContent()); + + if (isset($other[$name])) { + continue; + } + + if (!$analyzer->isGlobalFunctionCall($tokens, $index)) { + continue; + } + + $nsSeparatorIndex = $tokens->getPrevMeaningfulToken($index); + if (!$tokens[$nsSeparatorIndex]->isGivenKind(T_NS_SEPARATOR)) { + if (!isset($global[$name])) { + $other[$name] = true; + } + + continue; + } + + $indexes[] = $index; + } + + return $this->prepareImports($tokens, $indexes, $global, $other, false); + } + + /** + * @param Tokens $tokens + * @param NamespaceUseAnalysis[] $useDeclarations + * + * @return array + */ + private function importClasses(Tokens $tokens, array $useDeclarations) + { + list($global, $other) = $this->filterUseDeclarations($useDeclarations, static function (NamespaceUseAnalysis $declaration) { + return $declaration->isClass(); + }, false); + + /** @var DocBlock[] $docBlocks */ + $docBlocks = []; + + // find class declarations and class usages in docblocks + // and add them to the not importable names (already used) + for ($index = 0, $count = $tokens->count(); $index < $count; ++$index) { + $token = $tokens[$index]; + + if ($token->isGivenKind(T_DOC_COMMENT)) { + $docBlocks[$index] = new DocBlock($token->getContent()); + + $this->traverseDocBlockTypes($docBlocks[$index], static function ($type) use (&$other) { + if (false === strpos($type, '\\')) { + $other[strtolower($type)] = true; + } + }); + } + + if (!$token->isClassy()) { + continue; + } + + $index = $tokens->getNextMeaningfulToken($index); + + if ($tokens[$index]->isGivenKind(T_STRING)) { + $other[strtolower($tokens[$index]->getContent())] = true; + } + } + + $analyzer = new ClassyAnalyzer(); + + $indexes = []; + + for ($index = $tokens->count() - 1; $index >= 0; --$index) { + $token = $tokens[$index]; + + if (!$token->isGivenKind(T_STRING)) { + continue; + } + + $name = strtolower($token->getContent()); + + if (isset($other[$name])) { + continue; + } + + if (!$analyzer->isClassyInvocation($tokens, $index)) { + continue; + } + + $nsSeparatorIndex = $tokens->getPrevMeaningfulToken($index); + if (!$tokens[$nsSeparatorIndex]->isGivenKind(T_NS_SEPARATOR)) { + if (!isset($global[$name])) { + $other[$name] = true; + } + + continue; + } + + if ($tokens[$tokens->getPrevMeaningfulToken($nsSeparatorIndex)]->isGivenKind([CT::T_NAMESPACE_OPERATOR, T_STRING])) { + continue; + } + + $indexes[] = $index; + } + + $imports = []; + + foreach ($docBlocks as $index => $docBlock) { + $changed = $this->traverseDocBlockTypes($docBlock, static function ($type) use ($global, $other, &$imports) { + if ('\\' !== $type[0]) { + return $type; + } + + $name = substr($type, 1); + $checkName = strtolower($name); + + if (false !== strpos($checkName, '\\') || isset($other[$checkName])) { + return $type; + } + + if (isset($global[$checkName])) { + return \is_string($global[$checkName]) ? $global[$checkName] : $name; + } + + $imports[$checkName] = $name; + + return $name; + }); + + if ($changed) { + $tokens[$index] = new Token([T_DOC_COMMENT, $docBlock->getContent()]); + } + } + + return $imports + $this->prepareImports($tokens, $indexes, $global, $other, false); + } + + /** + * Removes the leading slash at the given indexes (when the name is not already used). + * + * @param Tokens $tokens + * @param int[] $indexes + * @param array $global + * @param array $other + * @param bool $caseSensitive + * + * @return array array keys contain the names that must be imported + */ + private function prepareImports(Tokens $tokens, array $indexes, array $global, array $other, $caseSensitive) + { + $imports = []; + + foreach ($indexes as $index) { + $name = $tokens[$index]->getContent(); + $checkName = $caseSensitive ? $name : strtolower($name); + + if (isset($other[$checkName])) { + continue; + } + + if (!isset($global[$checkName])) { + $imports[$checkName] = $name; + } elseif (\is_string($global[$checkName])) { + $tokens[$index] = new Token([T_STRING, $global[$checkName]]); + } + + $tokens->clearAt($tokens->getPrevMeaningfulToken($index)); + } + + return $imports; + } + + /** + * @param Tokens $tokens + * @param array $imports + * @param NamespaceUseAnalysis[] $useDeclarations + */ + private function insertImports(Tokens $tokens, array $imports, array $useDeclarations) + { + if ($useDeclarations) { + $useDeclaration = end($useDeclarations); + $index = $useDeclaration->getEndIndex() + 1; + } else { + $namespace = (new NamespacesAnalyzer())->getDeclarations($tokens)[0]; + $index = $namespace->getEndIndex() + 1; + } + + $lineEnding = $this->whitespacesConfig->getLineEnding(); + + if (!$tokens[$index]->isWhitespace() || false === strpos($tokens[$index]->getContent(), "\n")) { + $tokens->insertAt($index, new Token([T_WHITESPACE, $lineEnding])); + } + + foreach ($imports as $type => $typeImports) { + foreach ($typeImports as $name) { + $items = [ + new Token([T_WHITESPACE, $lineEnding]), + new Token([T_USE, 'use']), + new Token([T_WHITESPACE, ' ']), + ]; + + if ('const' === $type) { + $items[] = new Token([CT::T_CONST_IMPORT, 'const']); + $items[] = new Token([T_WHITESPACE, ' ']); + } elseif ('function' === $type) { + $items[] = new Token([CT::T_FUNCTION_IMPORT, 'function']); + $items[] = new Token([T_WHITESPACE, ' ']); + } + + $items[] = new Token([T_STRING, $name]); + $items[] = new Token(';'); + + $tokens->insertAt($index, $items); + } + } + } + + /** + * @param Tokens $tokens + * @param NamespaceUseAnalysis[] $useDeclarations + */ + private function fullyQualifyConstants(Tokens $tokens, array $useDeclarations) + { + if (!$tokens->isTokenKindFound(CT::T_CONST_IMPORT)) { + return; + } + + list($global) = $this->filterUseDeclarations($useDeclarations, static function (NamespaceUseAnalysis $declaration) { + return $declaration->isConstant() && !$declaration->isAliased(); + }, true); + + if (!$global) { + return; + } + + $analyzer = new TokensAnalyzer($tokens); + + for ($index = $tokens->count() - 1; $index >= 0; --$index) { + $token = $tokens[$index]; + + if (!$token->isGivenKind(T_STRING)) { + continue; + } + + if (!isset($global[$token->getContent()])) { + continue; + } + + if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_NS_SEPARATOR)) { + continue; + } + + if (!$analyzer->isConstantInvocation($index)) { + continue; + } + + $tokens->insertAt($index, new Token([T_NS_SEPARATOR, '\\'])); + } + } + + /** + * @param Tokens $tokens + * @param NamespaceUseAnalysis[] $useDeclarations + */ + private function fullyQualifyFunctions(Tokens $tokens, array $useDeclarations) + { + if (!$tokens->isTokenKindFound(CT::T_FUNCTION_IMPORT)) { + return; + } + + list($global) = $this->filterUseDeclarations($useDeclarations, static function (NamespaceUseAnalysis $declaration) { + return $declaration->isFunction() && !$declaration->isAliased(); + }, false); + + if (!$global) { + return; + } + + $analyzer = new FunctionsAnalyzer(); + + for ($index = $tokens->count() - 1; $index >= 0; --$index) { + $token = $tokens[$index]; + + if (!$token->isGivenKind(T_STRING)) { + continue; + } + + if (!isset($global[strtolower($token->getContent())])) { + continue; + } + + if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_NS_SEPARATOR)) { + continue; + } + + if (!$analyzer->isGlobalFunctionCall($tokens, $index)) { + continue; + } + + $tokens->insertAt($index, new Token([T_NS_SEPARATOR, '\\'])); + } + } + + /** + * @param Tokens $tokens + * @param NamespaceUseAnalysis[] $useDeclarations + */ + private function fullyQualifyClasses(Tokens $tokens, array $useDeclarations) + { + if (!$tokens->isTokenKindFound(T_USE)) { + return; + } + + list($global) = $this->filterUseDeclarations($useDeclarations, static function (NamespaceUseAnalysis $declaration) { + return $declaration->isClass() && !$declaration->isAliased(); + }, false); + + if (!$global) { + return; + } + + $analyzer = new ClassyAnalyzer(); + + for ($index = $tokens->count() - 1; $index >= 0; --$index) { + $token = $tokens[$index]; + + if ($token->isGivenKind(T_DOC_COMMENT)) { + $doc = new DocBlock($token->getContent()); + + $changed = $this->traverseDocBlockTypes($doc, static function ($type) use ($global) { + if (!isset($global[strtolower($type)])) { + return $type; + } + + return '\\'.$type; + }); + + if ($changed) { + $tokens[$index] = new Token([T_DOC_COMMENT, $doc->getContent()]); + } + + continue; + } + + if (!$token->isGivenKind(T_STRING)) { + continue; + } + + if (!isset($global[strtolower($token->getContent())])) { + continue; + } + + if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_NS_SEPARATOR)) { + continue; + } + + if (!$analyzer->isClassyInvocation($tokens, $index)) { + continue; + } + + $tokens->insertAt($index, new Token([T_NS_SEPARATOR, '\\'])); + } + } + + /** + * @param NamespaceUseAnalysis[] $declarations + * @param callable $callback + * @param bool $caseSensitive + * + * @return array + */ + private function filterUseDeclarations(array $declarations, callable $callback, $caseSensitive) + { + $global = []; + $other = []; + + foreach ($declarations as $declaration) { + if (!$callback($declaration)) { + continue; + } + + $fullName = ltrim($declaration->getFullName(), '\\'); + + if (false !== strpos($fullName, '\\')) { + $name = $caseSensitive ? $declaration->getShortName() : strtolower($declaration->getShortName()); + $other[$name] = true; + + continue; + } + + $checkName = $caseSensitive ? $fullName : strtolower($fullName); + $alias = $declaration->getShortName(); + $global[$checkName] = $alias === $fullName ? true : $alias; + } + + return [$global, $other]; + } + + private function findFunctionDeclarations(Tokens $tokens, $start, $end) + { + for ($index = $start; $index <= $end; ++$index) { + $token = $tokens[$index]; + + if ($token->isClassy()) { + $classStart = $tokens->getNextTokenOfKind($index, ['{']); + $classEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classStart); + + for ($index = $classStart; $index <= $classEnd; ++$index) { + if (!$tokens[$index]->isGivenKind(T_FUNCTION)) { + continue; + } + + $methodStart = $tokens->getNextTokenOfKind($index, ['{', ';']); + + if ($tokens[$methodStart]->equals(';')) { + $index = $methodStart; + + continue; + } + + $methodEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $methodStart); + + foreach ($this->findFunctionDeclarations($tokens, $methodStart, $methodEnd) as $function) { + yield $function; + } + + $index = $methodEnd; + } + + continue; + } + + if (!$token->isGivenKind(T_FUNCTION)) { + continue; + } + + $index = $tokens->getNextMeaningfulToken($index); + + if ($tokens[$index]->isGivenKind(CT::T_RETURN_REF)) { + $index = $tokens->getNextMeaningfulToken($index); + } + + if ($tokens[$index]->isGivenKind(T_STRING)) { + yield $tokens[$index]->getContent(); + } + } + } + + private function traverseDocBlockTypes(DocBlock $doc, callable $callback) + { + $annotations = $doc->getAnnotationsOfType(Annotation::getTagsWithTypes()); + + if (!$annotations) { + return false; + } + + $changed = false; + + foreach ($annotations as $annotation) { + $types = $new = $annotation->getTypes(); + + foreach ($types as $i => $fullType) { + $newFullType = $fullType; + + Preg::matchAll('/[\\\\\w]+/', $fullType, $matches, PREG_OFFSET_CAPTURE); + + foreach (array_reverse($matches[0]) as list($type, $offset)) { + $newType = $callback($type); + + if (null !== $newType && $type !== $newType) { + $newFullType = substr_replace($newFullType, $newType, $offset, \strlen($type)); + } + } + + $new[$i] = $newFullType; + } + + if ($types !== $new) { + $annotation->setTypes($new); + $changed = true; + } + } + + return $changed; + } +} diff --git a/src/Tokenizer/Analyzer/ClassyAnalyzer.php b/src/Tokenizer/Analyzer/ClassyAnalyzer.php new file mode 100644 index 00000000000..6091568c99b --- /dev/null +++ b/src/Tokenizer/Analyzer/ClassyAnalyzer.php @@ -0,0 +1,83 @@ + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\Tokenizer\Analyzer; + +use PhpCsFixer\Tokenizer\CT; +use PhpCsFixer\Tokenizer\Tokens; + +/** + * @internal + */ +final class ClassyAnalyzer +{ + /** + * @param Tokens $tokens + * @param int $index + * + * @return bool + */ + public function isClassyInvocation(Tokens $tokens, $index) + { + $token = $tokens[$index]; + + if (!$token->isGivenKind(T_STRING)) { + throw new \LogicException(sprintf('No T_STRING at given index %d, got %s.', $index, $tokens[$index]->getName())); + } + + if (\in_array(strtolower($token->getContent()), ['bool', 'float', 'int', 'iterable', 'object', 'parent', 'self', 'string', 'void'], true)) { + return false; + } + + $next = $tokens->getNextMeaningfulToken($index); + $nextToken = $tokens[$next]; + + if ($nextToken->isGivenKind(T_NS_SEPARATOR)) { + return false; + } + + if ($nextToken->isGivenKind([T_DOUBLE_COLON, T_ELLIPSIS, CT::T_TYPE_ALTERNATION, T_VARIABLE])) { + return true; + } + + $prev = $tokens->getPrevMeaningfulToken($index); + + while ($tokens[$prev]->isGivenKind([CT::T_NAMESPACE_OPERATOR, T_NS_SEPARATOR, T_STRING])) { + $prev = $tokens->getPrevMeaningfulToken($prev); + } + + $prevToken = $tokens[$prev]; + + if ($prevToken->isGivenKind([T_EXTENDS, T_INSTANCEOF, T_INSTEADOF, T_IMPLEMENTS, T_NEW, CT::T_NULLABLE_TYPE, CT::T_TYPE_ALTERNATION, CT::T_TYPE_COLON, CT::T_USE_TRAIT])) { + return true; + } + + // `Foo & $bar` could be: + // - function reference parameter: function baz(Foo & $bar) {} + // - bit operator: $x = Foo & $bar; + if ($nextToken->equals('&') && $tokens[$tokens->getNextMeaningfulToken($next)]->isGivenKind(T_VARIABLE)) { + $checkIndex = $tokens->getPrevTokenOfKind($prev + 1, [';', '{', '}', [T_FUNCTION], [T_OPEN_TAG], [T_OPEN_TAG_WITH_ECHO]]); + + return $tokens[$checkIndex]->isGivenKind(T_FUNCTION); + } + + if (!$prevToken->equals(',')) { + return false; + } + + do { + $prev = $tokens->getPrevMeaningfulToken($prev); + } while ($tokens[$prev]->equalsAny([',', [T_NS_SEPARATOR], [T_STRING], [CT::T_NAMESPACE_OPERATOR]])); + + return $tokens[$prev]->isGivenKind([T_IMPLEMENTS, CT::T_USE_TRAIT]); + } +} diff --git a/tests/AutoReview/FixerFactoryTest.php b/tests/AutoReview/FixerFactoryTest.php index 6b22dd64508..6b0cc07a0cb 100644 --- a/tests/AutoReview/FixerFactoryTest.php +++ b/tests/AutoReview/FixerFactoryTest.php @@ -102,6 +102,8 @@ public function provideFixersPriorityCases() [$fixers['general_phpdoc_annotation_remove'], $fixers['phpdoc_trim']], [$fixers['general_phpdoc_annotation_remove'], $fixers['no_empty_phpdoc']], [$fixers['general_phpdoc_annotation_remove'], $fixers['phpdoc_line_span']], + [$fixers['global_namespace_import'], $fixers['no_unused_imports']], + [$fixers['global_namespace_import'], $fixers['ordered_imports']], [$fixers['indentation_type'], $fixers['phpdoc_indent']], [$fixers['implode_call'], $fixers['method_argument_space']], [$fixers['is_null'], $fixers['yoda_style']], @@ -138,6 +140,8 @@ public function provideFixersPriorityCases() [$fixers['no_multiline_whitespace_around_double_arrow'], $fixers['binary_operator_spaces']], [$fixers['no_multiline_whitespace_around_double_arrow'], $fixers['trailing_comma_in_multiline_array']], [$fixers['multiline_whitespace_before_semicolons'], $fixers['space_after_semicolon']], + [$fixers['native_constant_invocation'], $fixers['global_namespace_import']], + [$fixers['native_function_invocation'], $fixers['global_namespace_import']], [$fixers['no_php4_constructor'], $fixers['ordered_class_elements']], [$fixers['no_short_bool_cast'], $fixers['cast_spaces']], [$fixers['no_short_echo_tag'], $fixers['no_mixed_echo_print']], diff --git a/tests/Fixer/Import/GlobalNamespaceImportFixerTest.php b/tests/Fixer/Import/GlobalNamespaceImportFixerTest.php new file mode 100644 index 00000000000..a5ae84dfad1 --- /dev/null +++ b/tests/Fixer/Import/GlobalNamespaceImportFixerTest.php @@ -0,0 +1,953 @@ + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\Tests\Fixer\Import; + +use PhpCsFixer\Tests\Test\AbstractFixerTestCase; + +/** + * @author Gregor Harlan + * + * @internal + * + * @covers \PhpCsFixer\Fixer\Import\GlobalNamespaceImportFixer + */ +final class GlobalNamespaceImportFixerTest extends AbstractFixerTestCase +{ + /** + * @param string $expected + * @param null|string $input + * + * @dataProvider provideFixImportConstantsCases + */ + public function testFixImportConstants($expected, $input = null) + { + $this->fixer->configure(['import_constants' => true]); + $this->doTest($expected, $input); + } + + public function provideFixImportConstantsCases() + { + return [ + 'non-global names' => [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' +fixer->configure(['import_functions' => true]); + $this->doTest($expected, $input); + } + + public function provideFixImportFunctionsCases() + { + return [ + 'non-global names' => [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' +fixer->configure(['import_functions' => true]); + $this->doTest($expected, $input); + } + + public function provideFixImportFunctions70Cases() + { + return [ + 'name already used' => [ + <<<'EXPECTED' +fixer->configure(['import_classes' => true]); + $this->doTest($expected, $input); + } + + public function provideFixImportClassesCases() + { + return [ + 'non-global names' => [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + */ +function x() {} + +/** @var \Foo $foo */ +$foo = new \Foo(); +EXPECTED + ], + 'without namespace / only import once' => [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + */ +function x() {} +EXPECTED + , + <<<'INPUT' + */ +function x() {} +INPUT + ], + 'with namespace with {} syntax' => [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' +fixer->configure(['import_classes' => true]); + $this->doTest($expected, $input); + } + + public function provideFixImportClasses71Cases() + { + return [ + 'handle typehints' => [ + <<<'EXPECTED' +fixer->configure(['import_constants' => false]); + $this->doTest($expected, $input); + } + + public function provideFixFullyQualifyConstantsCases() + { + return [ + 'already fqn or sub namespace' => [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' +fixer->configure(['import_functions' => false]); + $this->doTest($expected, $input); + } + + public function provideFixFullyQualifyFunctionsCases() + { + return [ + 'already fqn or sub namespace' => [ + <<<'EXPECTED' + [ + <<<'EXPECTED' + [ + <<<'EXPECTED' +fixer->configure(['import_classes' => false]); + $this->doTest($expected, $input); + } + + public function provideFixFullyQualifyClassesCases() + { + return [ + 'already fqn or sub namespace' => [ + <<<'EXPECTED' + [ + <<<'EXPECTED' +>|null + */ +function abc($foo, \Bar $bar = null) {} +EXPECTED + , + <<<'INPUT' +>|null + */ +function abc($foo, Bar $bar = null) {} +INPUT + ], + 'ignore other imports and non-imported names' => [ + <<<'EXPECTED' + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\Tests\Tokenizer\Analyzer; + +use PhpCsFixer\Tests\TestCase; +use PhpCsFixer\Tokenizer\Analyzer\ClassyAnalyzer; +use PhpCsFixer\Tokenizer\Tokens; + +/** + * @internal + * + * @covers \PhpCsFixer\Tokenizer\Analyzer\ClassyAnalyzer + */ +final class ClassyAnalyzerTest extends TestCase +{ + /** + * @param string $source + * + * @dataProvider provideIsClassyInvocationCases + */ + public function testIsClassyInvocation($source, array $expected) + { + $tokens = Tokens::fromCode($source); + $analyzer = new ClassyAnalyzer(); + + foreach ($expected as $index => $isClassy) { + static::assertSame($isClassy, $analyzer->isClassyInvocation($tokens, $index), 'Token at index '.$index.' should match the expected value.'); + } + } + + public function provideIsClassyInvocationCases() + { + return [ + [ + ' true], + ], + [ + ' true], + ], + [ + ' false, 5 => true], + ], + [ + ' true], + ], + [ + ' true, 3 => false], + ], + [ + ' true, 4 => false], + ], + [ + ' false, 3 => true, 5 => false], + ], + [ + ' true], + ], + [ + ' false, 8 => true], + ], + [ + ' false, 7 => true, 10 => false, 12 => true, 16 => true, 19 => true], + ], + [ + ' false, 9 => true, 12 => false, 14 => true, 18 => true, 21 => true, 25 => true, 32 => true], + ], + [ + ' false, 5 => true, 10 => true, 17 => true, 23 => false, 25 => true], + ], + [ + ' false, 9 => false, 15 => false, 17 => false, 22 => false, 24 => false, 33 => false], + ], + [ + ' false, 7 => false], + ], + [ + ' false], + ], + [ + ' false, 7 => false], + ], + ]; + } + + /** + * @param string $source + * + * @dataProvider provideIsClassyInvocation70Cases + * @requires PHP 7.0 + */ + public function testIsClassyInvocation70($source, array $expected) + { + $tokens = Tokens::fromCode($source); + $analyzer = new ClassyAnalyzer(); + + foreach ($expected as $index => $isClassy) { + static::assertSame($isClassy, $analyzer->isClassyInvocation($tokens, $index), 'Token at index '.$index.' should match the expected value.'); + } + } + + public function provideIsClassyInvocation70Cases() + { + return [ + [ + ' false, 5 => false, 10 => false, 17 => false], + ], + [ + ' false, 8 => true], + ], + [ + ' false, 9 => true], + ], + ]; + } + + /** + * @param string $source + * + * @dataProvider provideIsClassyInvocation71Cases + * @requires PHP 7.1 + */ + public function testIsClassyInvocation71($source, array $expected) + { + $tokens = Tokens::fromCode($source); + $analyzer = new ClassyAnalyzer(); + + foreach ($expected as $index => $isClassy) { + static::assertSame($isClassy, $analyzer->isClassyInvocation($tokens, $index), 'Token at index '.$index.' should match the expected value.'); + } + } + + public function provideIsClassyInvocation71Cases() + { + return [ + [ + ' false, 9 => true], + ], + [ + ' false, 6 => true, 12 => false, 14 => true, 22 => true], + ], + [ + ' false, 5 => false, 11 => false], + ], + [ + ' false, 6 => false, 13 => false], + ], + ]; + } +}