refactor ImportFullyQualifiedNamesRector to allow non-namespaced elements

This commit is contained in:
Tomas Votruba 2019-05-19 23:44:38 +02:00
parent 5757e867e0
commit 53cd941dcd
23 changed files with 908 additions and 353 deletions

View File

@ -66,9 +66,7 @@ parameters:
PhpCsFixer\Fixer\Phpdoc\PhpdocTypesFixer:
- 'packages/Php/src/Rector/Double/RealToFloatTypeCastRector.php'
Symplify\CodingStandard\Sniffs\CleanCode\ForbiddenReferenceSniff:
# 3rd party contract
- 'src/PhpParser/Printer/BetterStandardPrinter.php'
Symplify\CodingStandard\Sniffs\CleanCode\ForbiddenReferenceSniff: ~
Symplify\CodingStandard\Fixer\Php\ClassStringToClassConstantFixer:
# classes might not exist
@ -146,3 +144,4 @@ parameters:
Symplify\CodingStandard\Sniffs\DependencyInjection\NoClassInstantiationSniff:
# 3rd party api
- 'src/PhpParser/Node/Value/ValueResolver.php'

View File

@ -3,7 +3,11 @@
namespace Rector\CodeQuality\Rector\FuncCall;
use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
@ -64,13 +68,11 @@ CODE_SAMPLE
return null;
}
$array = new Node\Expr\Array_();
$array = new Array_();
foreach ($node->args as $arg) {
$variableName = $this->getValue($arg->value);
$array->items[] = new Node\Expr\ArrayItem(new Node\Expr\Variable($variableName), new Node\Scalar\String_(
$variableName
));
$array->items[] = new ArrayItem(new Variable($variableName), new String_($variableName));
}
return $array;

View File

@ -0,0 +1,201 @@
<?php declare(strict_types=1);
namespace Rector\CodingStyle\Application;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Namespace_;
use Rector\CodingStyle\Imports\UsedImportsResolver;
use Rector\CodingStyle\Naming\ClassNaming;
use Rector\Contract\PhpParser\Node\CommanderInterface;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\Node\BetterNodeFinder;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
final class UseAddingCommander implements CommanderInterface
{
/**
* @var string[][]
*/
private $useImportsInFilePath = [];
/**
* @var string[][]
*/
private $functionUseImportsInFilePath = [];
/**
* @var UseImportsAdder
*/
private $useImportsAdder;
/**
* @var ClassNaming
*/
private $classNaming;
/**
* @var UsedImportsResolver
*/
private $usedImportsResolver;
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
public function __construct(
UseImportsAdder $useImportsAdder,
ClassNaming $classNaming,
UsedImportsResolver $usedImportsResolver,
BetterNodeFinder $betterNodeFinder
) {
$this->useImportsAdder = $useImportsAdder;
$this->classNaming = $classNaming;
$this->usedImportsResolver = $usedImportsResolver;
$this->betterNodeFinder = $betterNodeFinder;
}
public function addUseImport(Node $node, string $useImport): void
{
/** @var SmartFileInfo|null $fileInfo */
$fileInfo = $node->getAttribute(AttributeKey::FILE_INFO);
if ($fileInfo === null) {
return;
}
$this->useImportsInFilePath[$fileInfo->getRealPath()][] = $useImport;
}
public function addFunctionUseImport(Node $node, string $functionUseImport): void
{
/** @var SmartFileInfo $fileInfo */
$fileInfo = $node->getAttribute(AttributeKey::FILE_INFO);
$this->functionUseImportsInFilePath[$fileInfo->getRealPath()][] = $functionUseImport;
}
/**
* @param Stmt[] $nodes
* @return Stmt[]
*/
public function traverseNodes(array $nodes): array
{
// no nodes → just return
if (! isset($nodes[0])) {
return $nodes;
}
$filePath = $this->getRealPathFromNode($nodes[0]);
$useImports = $this->useImportsInFilePath[$filePath] ?? [];
$functionUseImports = $this->functionUseImportsInFilePath[$filePath] ?? [];
// nothing to import
if ($useImports === [] && $functionUseImports === []) {
return $nodes;
}
// clear applied imports, so isActive() doesn't return any false positives
unset($this->useImportsInFilePath[$filePath], $this->functionUseImportsInFilePath[$filePath]);
// A. has namespace? add under it
$namespace = $this->betterNodeFinder->findFirstInstanceOf($nodes, Namespace_::class);
if ($namespace instanceof Namespace_) {
$this->useImportsAdder->addImportsToNamespace($namespace, $useImports, $functionUseImports);
return $nodes;
}
// B. no namespace? add in the top
return $this->useImportsAdder->addImportsToStmts($nodes, $useImports, $functionUseImports);
}
public function isActive(): bool
{
return count($this->useImportsInFilePath) > 0 || count($this->functionUseImportsInFilePath) > 0;
}
public function isShortImported(Node $node, string $fullyQualifiedName): bool
{
$filePath = $this->getRealPathFromNode($node);
$shortName = $this->classNaming->getShortName($fullyQualifiedName);
$fileUseImports = $this->useImportsInFilePath[$filePath] ?? [];
foreach ($fileUseImports as $fileUseImport) {
$shortFileUseImport = $this->classNaming->getShortName($fileUseImport);
if ($shortFileUseImport === $shortName) {
return true;
}
}
$fileFunctionUseImports = $this->functionUseImportsInFilePath[$filePath] ?? [];
foreach ($fileFunctionUseImports as $fileFunctionUseImport) {
$shortFileFunctionUseImport = $this->classNaming->getShortName($fileFunctionUseImport);
if ($shortFileFunctionUseImport === $shortName) {
return true;
}
}
return false;
}
public function isImportShortable(Node $node, string $fullyQualifiedName): bool
{
$filePath = $this->getRealPathFromNode($node);
$fileUseImports = $this->useImportsInFilePath[$filePath] ?? [];
if (in_array($fullyQualifiedName, $fileUseImports, true)) {
return true;
}
$functionUseImports = $this->functionUseImportsInFilePath[$filePath] ?? [];
if (in_array($fullyQualifiedName, $functionUseImports, true)) {
return true;
}
return false;
}
public function analyseFileInfoUseStatements(Node $node): void
{
$filePath = $this->getRealPathFromNode($node);
$usedImports = $this->usedImportsResolver->resolveForNode($node);
foreach ($usedImports as $usedImport) {
$this->useImportsInFilePath[$filePath][] = $usedImport;
}
}
public function hasImport(Name $name, string $fullyQualifiedName): bool
{
$filePath = $this->getRealPathFromNode($name);
return in_array($fullyQualifiedName, $this->useImportsInFilePath[$filePath] ?? [], true);
}
public function canImportBeAdded(Name $name, string $import): bool
{
$shortImport = $this->classNaming->getShortName($import);
$filePath = $this->getRealPathFromNode($name);
foreach ($this->useImportsInFilePath[$filePath] ?? [] as $importsInClass) {
$shortImportInClass = $this->classNaming->getShortName($importsInClass);
if ($importsInClass !== $import && $shortImportInClass === $shortImport) {
return true;
}
}
return false;
}
private function getRealPathFromNode(Node $node): ?string
{
/** @var SmartFileInfo|null $fileInfo */
$fileInfo = $node->getAttribute(AttributeKey::FILE_INFO);
return $fileInfo->getRealPath();
}
}

View File

@ -0,0 +1,121 @@
<?php declare(strict_types=1);
namespace Rector\CodingStyle\Application;
use Nette\Utils\Strings;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Stmt\UseUse;
use Rector\CodingStyle\Imports\UsedImportsResolver;
final class UseImportsAdder
{
/**
* @var UsedImportsResolver
*/
private $usedImportsResolver;
public function __construct(UsedImportsResolver $usedImportsResolver)
{
$this->usedImportsResolver = $usedImportsResolver;
}
/**
* @param Stmt[] $stmts
* @param string[] $useImports
* @param string[] $functionUseImports
* @return Stmt[]
*/
public function addImportsToStmts(array $stmts, array $useImports, array $functionUseImports): array
{
$existingUseImports = $this->usedImportsResolver->resolveForStmts($stmts);
$existingFunctionUseImports = $this->usedImportsResolver->resolveFunctionImportsForStmts($stmts);
$useImports = array_unique($useImports);
$functionUseImports = array_unique($functionUseImports);
$useImports = array_diff($useImports, $existingUseImports);
$functionUseImports = array_diff($functionUseImports, $existingFunctionUseImports);
$newUses = $this->createUses($useImports, $functionUseImports, null);
return array_merge($newUses, $stmts);
}
/**
* @param string[] $useImports
* @param string[] $functionUseImports
*/
public function addImportsToNamespace(Namespace_ $namespace, array $useImports, array $functionUseImports): void
{
$namespaceName = $this->getNamespaceName($namespace);
$existingUseImports = $this->usedImportsResolver->resolveForNode($namespace);
$existingFunctionUseImports = $this->usedImportsResolver->resolveFunctionImportsForNode($namespace);
$useImports = array_unique($useImports);
$functionUseImports = array_unique($functionUseImports);
$useImports = array_diff($useImports, $existingUseImports);
$functionUseImports = array_diff($functionUseImports, $existingFunctionUseImports);
$newUses = $this->createUses($useImports, $functionUseImports, $namespaceName);
$namespace->stmts = array_merge($newUses, $namespace->stmts);
}
private function getNamespaceName(Namespace_ $namespace): ?string
{
if ($namespace->name === null) {
return null;
}
return $namespace->name->toString();
}
private function isCurrentNamespace(?string $namespaceName, string $useImports): bool
{
if ($namespaceName === null) {
return false;
}
$afterCurrentNamespace = Strings::after($useImports, $namespaceName . '\\');
if (! $afterCurrentNamespace) {
return false;
}
return ! Strings::contains($afterCurrentNamespace, '\\');
}
/**
* @param string[] $useImports
* @param string[] $functionUseImports
* @return Use_[]
*/
private function createUses(array $useImports, array $functionUseImports, ?string $namespaceName): array
{
$newUses = [];
foreach ($useImports as $useImport) {
if ($this->isCurrentNamespace($namespaceName, $useImport)) {
continue;
}
// already imported in previous cycle
$useUse = new UseUse(new Name($useImport));
$newUses[] = new Use_([$useUse]);
}
foreach ($functionUseImports as $functionUseImport) {
if ($this->isCurrentNamespace($namespaceName, $functionUseImport)) {
continue;
}
// already imported in previous cycle
$useUse = new UseUse(new Name($functionUseImport), null, Use_::TYPE_FUNCTION);
$newUses[] = new Use_([$useUse]);
}
return $newUses;
}
}

View File

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace Rector\CodingStyle\Imports;
use PhpParser\Node;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\UseUse;
final class AliasUsesResolver
{
/**
* @var UseImportsTraverser
*/
private $useImportsTraverser;
public function __construct(UseImportsTraverser $useImportsTraverser)
{
$this->useImportsTraverser = $useImportsTraverser;
}
/**
* @return string[]
*/
public function resolveForNode(Node $node): array
{
if ($node instanceof Namespace_) {
return $this->resolveForNamespace($node);
}
return [];
}
/**
* @return string[]
*/
private function resolveForNamespace(Namespace_ $node): array
{
$aliasedUses = [];
$this->useImportsTraverser->traverserStmts($node->stmts, function (
UseUse $useUse,
string $name
) use (&$aliasedUses): void {
if ($useUse->alias === null) {
return;
}
$aliasedUses[] = $name;
});
return $aliasedUses;
}
}

View File

@ -1,60 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\CodingStyle\Imports;
use Rector\CodingStyle\Naming\ClassNaming;
final class ImportsInClassCollection
{
/**
* @var string[]
*/
private $importsInClass = [];
/**
* @var ClassNaming
*/
private $classNaming;
public function __construct(ClassNaming $classNaming)
{
$this->classNaming = $classNaming;
}
public function addImport(string $import): void
{
$this->importsInClass[] = $import;
}
public function hasImport(string $import): bool
{
return in_array($import, $this->importsInClass, true);
}
public function canImportBeAdded(string $import): bool
{
$shortImport = $this->classNaming->getShortName($import);
foreach ($this->importsInClass as $importsInClass) {
$shortImportInClass = $this->classNaming->getShortName($importsInClass);
if ($importsInClass !== $import && $shortImportInClass === $shortImport) {
return true;
}
}
return false;
}
public function reset(): void
{
$this->importsInClass = [];
}
/**
* @return string[]
*/
public function get(): array
{
return $this->importsInClass;
}
}

View File

@ -0,0 +1,67 @@
<?php declare(strict_types=1);
namespace Rector\CodingStyle\Imports;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Namespace_;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;
final class ShortNameResolver
{
/**
* @var CallableNodeTraverser
*/
private $callableNodeTraverser;
public function __construct(CallableNodeTraverser $callableNodeTraverser)
{
$this->callableNodeTraverser = $callableNodeTraverser;
}
/**
* @return string[]
*/
public function resolveForNode(Node $node): array
{
$namespace = $node->getAttribute(AttributeKey::NAMESPACE_NODE);
if ($namespace instanceof Namespace_) {
return $this->resolveForNamespace($namespace);
}
return [];
}
/**
* @return string[]
*/
private function resolveForNamespace(Namespace_ $node): array
{
$shortNames = [];
$this->callableNodeTraverser->traverseNodesWithCallable($node->stmts, function (Node $node) use (
&$shortNames
): void {
if (! $node instanceof Name) {
return;
}
$originalName = $node->getAttribute('originalName');
if (! $originalName instanceof Name) {
return;
}
// already short
if (Strings::contains($originalName->toString(), '\\')) {
return;
}
$shortNames[$originalName->toString()] = $node->toString();
});
return $shortNames;
}
}

View File

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace Rector\CodingStyle\Imports;
use PhpParser\Node;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Use_;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;
final class UseImportsTraverser
{
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var CallableNodeTraverser
*/
private $callableNodeTraverser;
public function __construct(NameResolver $nameResolver, CallableNodeTraverser $callableNodeTraverser)
{
$this->nameResolver = $nameResolver;
$this->callableNodeTraverser = $callableNodeTraverser;
}
/**
* @param Stmt[] $stmts
*/
public function traverserStmts(array $stmts, callable $callable, int $type = Use_::TYPE_NORMAL): void
{
$this->callableNodeTraverser->traverseNodesWithCallable($stmts, function (Node $node) use ($callable, $type) {
if (! $node instanceof Use_) {
return null;
}
// only import uses
if ($node->type !== $type) {
return null;
}
foreach ($node->uses as $useUse) {
$name = $this->nameResolver->resolve($useUse);
$callable($useUse, $name);
}
});
}
}

View File

@ -0,0 +1,117 @@
<?php declare(strict_types=1);
namespace Rector\CodingStyle\Imports;
use PhpParser\Node;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Stmt\UseUse;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\PhpParser\Node\Resolver\NameResolver;
final class UsedImportsResolver
{
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var UseImportsTraverser
*/
private $useImportsTraverser;
public function __construct(
BetterNodeFinder $betterNodeFinder,
NameResolver $nameResolver,
UseImportsTraverser $useImportsTraverser
) {
$this->betterNodeFinder = $betterNodeFinder;
$this->nameResolver = $nameResolver;
$this->useImportsTraverser = $useImportsTraverser;
}
/**
* @return string[]
*/
public function resolveForNode(Node $node): array
{
$namespace = $node->getAttribute(AttributeKey::NAMESPACE_NODE);
if ($namespace instanceof Namespace_) {
return $this->resolveForNamespace($namespace);
}
return [];
}
/**
* @param Stmt[] $stmts
* @return string[]
*/
public function resolveForStmts(array $stmts): array
{
$usedImports = [];
/** @var Class_|null $class */
$class = $this->betterNodeFinder->findFirstInstanceOf($stmts, Class_::class);
// add class itself
if ($class !== null) {
$className = $this->nameResolver->resolve($class);
if ($className !== null) {
$usedImports[] = $className;
}
}
$this->useImportsTraverser->traverserStmts($stmts, function (
UseUse $useUse,
string $name
) use (&$usedImports): void {
$usedImports[] = $name;
});
return $usedImports;
}
/**
* @param Stmt[] $stmts
* @return string[]
*/
public function resolveFunctionImportsForStmts(array $stmts): array
{
$usedFunctionImports = [];
$this->useImportsTraverser->traverserStmts($stmts, function (
UseUse $useUse,
string $name
) use (&$usedFunctionImports): void {
$usedFunctionImports[] = $name;
}, Use_::TYPE_FUNCTION);
return $usedFunctionImports;
}
/**
* @return string[]
*/
public function resolveFunctionImportsForNode(Namespace_ $namespace): array
{
return $this->resolveFunctionImportsForStmts($namespace->stmts);
}
/**
* @return string[]
*/
private function resolveForNamespace(Namespace_ $node): array
{
return $this->resolveForStmts($node->stmts);
}
}

View File

@ -6,41 +6,24 @@ use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Stmt\UseUse;
use Rector\CodingStyle\Imports\ImportsInClassCollection;
use Rector\CodingStyle\Application\UseAddingCommander;
use Rector\CodingStyle\Imports\AliasUsesResolver;
use Rector\CodingStyle\Imports\ShortNameResolver;
use Rector\CodingStyle\Naming\ClassNaming;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
final class ImportFullyQualifiedNamesRector extends AbstractRector
{
/**
* @var CallableNodeTraverser
*/
private $callableNodeTraverser;
/**
* @var string[]
*/
private $alreadyUsedShortNames = [];
/**
* @var string[]
*/
private $newUseStatements = [];
/**
* @var string[]
*/
private $newFunctionUseStatements = [];
private $shortNames = [];
/**
* @var string[]
@ -52,11 +35,6 @@ final class ImportFullyQualifiedNamesRector extends AbstractRector
*/
private $docBlockManipulator;
/**
* @var ImportsInClassCollection
*/
private $importsInClassCollection;
/**
* @var ClassNaming
*/
@ -67,18 +45,35 @@ final class ImportFullyQualifiedNamesRector extends AbstractRector
*/
private $shouldImportDocBlocks = true;
/**
* @var AliasUsesResolver
*/
private $aliasUsesResolver;
/**
* @var UseAddingCommander
*/
private $useAddingCommander;
/**
* @var ShortNameResolver
*/
private $shortNameResolver;
public function __construct(
CallableNodeTraverser $callableNodeTraverser,
DocBlockManipulator $docBlockManipulator,
ImportsInClassCollection $importsInClassCollection,
ClassNaming $classNaming,
AliasUsesResolver $aliasUsesResolver,
UseAddingCommander $useAddingCommander,
ShortNameResolver $shortNameResolver,
bool $shouldImportDocBlocks = true
) {
$this->callableNodeTraverser = $callableNodeTraverser;
$this->docBlockManipulator = $docBlockManipulator;
$this->importsInClassCollection = $importsInClassCollection;
$this->classNaming = $classNaming;
$this->shouldImportDocBlocks = $shouldImportDocBlocks;
$this->useAddingCommander = $useAddingCommander;
$this->aliasUsesResolver = $aliasUsesResolver;
$this->shortNameResolver = $shortNameResolver;
}
public function getDefinition(): RectorDefinition
@ -115,191 +110,98 @@ CODE_SAMPLE
*/
public function getNodeTypes(): array
{
return [Namespace_::class];
return [Name::class, Node::class];
}
/**
* @param Namespace_ $node
* @param Name|Node $node
*/
public function refactor(Node $node): ?Node
{
$this->resetCollectedNames();
$this->useAddingCommander->analyseFileInfoUseStatements($node);
$this->resolveAlreadyImportedUses($node);
if ($node instanceof Name) {
if (! $this->canBeNameImported($node)) {
return null;
}
// "new X" or "X::static()"
$this->resolveAlreadyUsedShortNames($node);
$this->aliasedUses = $this->aliasUsesResolver->resolveForNode($node);
$newUseStatements = $this->importNamesAndCollectNewUseStatements($node);
$this->addNewUseStatements($node, $newUseStatements);
// "new X" or "X::static()"
$this->shortNames = $this->shortNameResolver->resolveForNode($node);
return $node;
return $this->importNamesAndCollectNewUseStatements($node);
}
// process every doc block node
if ($this->shouldImportDocBlocks) {
$useImports = $this->docBlockManipulator->importNames($node);
foreach ($useImports as $useImport) {
$this->useAddingCommander->addUseImport($node, $useImport);
}
return $node;
}
return null;
}
private function resolveAlreadyImportedUses(Namespace_ $namespace): void
private function importNamesAndCollectNewUseStatements(Name $name): ?Name
{
/** @var Class_|null $class */
$class = $this->betterNodeFinder->findFirstInstanceOf($namespace->stmts, Class_::class);
$originalName = $name->getAttribute('originalName');
// add class itself
if ($class !== null) {
$className = $this->getName($class);
if ($className !== null) {
$this->importsInClassCollection->addImport($className);
}
}
$this->callableNodeTraverser->traverseNodesWithCallable($namespace->stmts, function (Node $node) {
if (! $node instanceof Use_) {
if ($originalName instanceof Name) {
// already short
if (! Strings::contains($originalName->toString(), '\\')) {
return null;
}
// only import uses
if ($node->type !== Use_::TYPE_NORMAL) {
return null;
}
foreach ($node->uses as $useUse) {
$name = $this->getName($useUse);
if ($name === null) {
throw new ShouldNotHappenException();
}
if ($useUse->alias !== null) {
// alias workaround
$this->aliasedUses[] = $name;
}
$this->importsInClassCollection->addImport($name);
}
});
}
/**
* @param string[] $newUseStatements
*/
private function addNewUseStatements(Namespace_ $namespace, array $newUseStatements): void
{
if ($newUseStatements === [] && $this->newFunctionUseStatements === []) {
return;
} else {
// not sure what to do
return null;
}
$newUses = [];
$newUseStatements = array_unique($newUseStatements);
$namespaceName = $this->getName($namespace);
if ($namespaceName === null) {
throw new ShouldNotHappenException();
// the short name is already used, skip it
// @todo this is duplicated check of - $this->useAddingCommander->isShortImported?
$shortName = $this->classNaming->getShortName($name->toString());
if ($this->isShortNameAlreadyUsedForDifferentFqn($name, $shortName)) {
return null;
}
foreach ($newUseStatements as $newUseStatement) {
if ($this->isCurrentNamespace($namespaceName, $newUseStatement)) {
continue;
}
$fullyQualifiedName = $this->getName($name);
// already imported in previous cycle
$useUse = new UseUse(new Name($newUseStatement));
$newUses[] = new Use_([$useUse]);
$this->importsInClassCollection->addImport($newUseStatement);
// the similar end is already imported → skip
if ($this->shouldSkipName($name, $fullyQualifiedName)) {
return null;
}
foreach ($this->newFunctionUseStatements as $newFunctionUseStatement) {
if ($this->isCurrentNamespace($namespaceName, $newFunctionUseStatement)) {
continue;
}
// already imported in previous cycle
$useUse = new UseUse(new Name($newFunctionUseStatement), null, Use_::TYPE_FUNCTION);
$newUses[] = new Use_([$useUse]);
$this->importsInClassCollection->addImport($newFunctionUseStatement);
}
$namespace->stmts = array_merge($newUses, $namespace->stmts);
}
/**
* @return string[]
*/
private function importNamesAndCollectNewUseStatements(Namespace_ $node): array
{
$this->newUseStatements = [];
$this->newFunctionUseStatements = [];
$this->callableNodeTraverser->traverseNodesWithCallable($node->stmts, function (Node $node): ?Name {
if (! $node instanceof Name) {
return null;
}
$name = $node->getAttribute('originalName');
if ($name instanceof Name) {
// already short
if (! Strings::contains($name->toString(), '\\')) {
return null;
}
} else {
return null;
}
// the short name is already used, skip it
$shortName = $this->classNaming->getShortName($name->toString());
if ($this->isShortNameAlreadyUsedForDifferentFqn($node, $shortName)) {
return null;
}
if ($this->getName($node) === $node->toString()) {
$fullyQualifiedName = $this->getName($node);
// the similar end is already imported → skip
if ($this->shouldSkipName($fullyQualifiedName)) {
return null;
}
$shortName = $this->classNaming->getShortName($fullyQualifiedName);
if (isset($this->newUseStatements[$shortName]) || isset($this->newFunctionUseStatements[$shortName])) {
if ($fullyQualifiedName === $this->newUseStatements[$shortName] || $fullyQualifiedName === $this->newFunctionUseStatements[$shortName]) {
return new Name($shortName);
}
return null;
}
if (! $this->importsInClassCollection->hasImport($fullyQualifiedName)) {
if ($node->getAttribute(AttributeKey::PARENT_NODE) instanceof FuncCall) {
$this->newFunctionUseStatements[$shortName] = $fullyQualifiedName;
} else {
$this->newUseStatements[$shortName] = $fullyQualifiedName;
}
}
// possibly aliased
if (in_array($fullyQualifiedName, $this->aliasedUses, true)) {
return null;
}
$this->importsInClassCollection->addImport($fullyQualifiedName);
$shortName = $this->classNaming->getShortName($fullyQualifiedName);
if ($this->useAddingCommander->isShortImported($name, $fullyQualifiedName)) {
if ($this->useAddingCommander->isImportShortable($name, $fullyQualifiedName)) {
return new Name($shortName);
}
return null;
});
if ($this->shouldImportDocBlocks) {
// for doc blocks
$this->callableNodeTraverser->traverseNodesWithCallable($node->stmts, function (Node $node): void {
$importedDocUseStatements = $this->docBlockManipulator->importNames($node);
$this->newUseStatements = array_merge($this->newUseStatements, $importedDocUseStatements);
});
}
return $this->newUseStatements;
if (! $this->useAddingCommander->hasImport($name, $fullyQualifiedName)) {
if ($name->getAttribute(AttributeKey::PARENT_NODE) instanceof FuncCall) {
$this->useAddingCommander->addFunctionUseImport($name, $fullyQualifiedName);
} else {
$this->useAddingCommander->addUseImport($name, $fullyQualifiedName);
}
}
// possibly aliased
if (in_array($fullyQualifiedName, $this->aliasedUses, true)) {
return null;
}
return new Name($shortName);
}
// 1. name is fully qualified → import it
private function shouldSkipName(string $fullyQualifiedName): bool
private function shouldSkipName(Name $name, string $fullyQualifiedName): bool
{
// not namespaced class
if (! Strings::contains($fullyQualifiedName, '\\')) {
@ -313,60 +215,27 @@ CODE_SAMPLE
return true;
}
return $this->importsInClassCollection->canImportBeAdded($fullyQualifiedName);
}
private function resolveAlreadyUsedShortNames(Namespace_ $namespace): void
{
if ($namespace->name instanceof Name) {
$this->alreadyUsedShortNames[$namespace->name->toString()] = $namespace->name->toString();
}
$this->callableNodeTraverser->traverseNodesWithCallable((array) $namespace->stmts, function (Node $node): void {
if (! $node instanceof Name) {
return;
}
$name = $node->getAttribute('originalName');
if (! $name instanceof Name) {
return;
}
// already short
if (Strings::contains($name->toString(), '\\')) {
return;
}
$this->alreadyUsedShortNames[$name->toString()] = $node->toString();
});
}
private function isCurrentNamespace(string $namespaceName, string $newUseStatement): bool
{
$afterCurrentNamespace = Strings::after($newUseStatement, $namespaceName . '\\');
if (! $afterCurrentNamespace) {
return false;
}
return ! Strings::contains($afterCurrentNamespace, '\\');
}
private function resetCollectedNames(): void
{
$this->newUseStatements = [];
$this->newFunctionUseStatements = [];
$this->alreadyUsedShortNames = [];
$this->importsInClassCollection->reset();
$this->docBlockManipulator->resetImportedNames();
return $this->useAddingCommander->canImportBeAdded($name, $fullyQualifiedName);
}
// is already used
private function isShortNameAlreadyUsedForDifferentFqn(Name $name, string $shortName): bool
{
if (! isset($this->alreadyUsedShortNames[$shortName])) {
if (! isset($this->shortNames[$shortName])) {
return false;
}
return $this->alreadyUsedShortNames[$shortName] !== $this->getName($name);
return $this->shortNames[$shortName] !== $this->getName($name);
}
private function canBeNameImported(Name $name): bool
{
$parentNode = $name->getAttribute(AttributeKey::PARENT_NODE);
if ($parentNode instanceof Namespace_) {
return false;
}
return ! $parentNode instanceof UseUse;
}
}

View File

@ -0,0 +1,19 @@
<?php
use Foo\Bar;
function importFromThisFunctionToo() {
$baz = new \Foo\Bar;
}
?>
-----
<?php
use Foo\Bar;
function importFromThisFunctionToo() {
$baz = new Bar;
}
?>

View File

@ -0,0 +1,27 @@
<?php
use function Safe\count;
class ImportFunctionWithoutNamespace
{
public function run()
{
return \Safe\count([1]);
}
}
?>
-----
<?php
use function Safe\count;
class ImportFunctionWithoutNamespace
{
public function run()
{
return count([1]);
}
}
?>

View File

@ -0,0 +1,14 @@
<?php
function importFromThisFunction() {
$baz = new \Foo\Bar;
}
?>
-----
<?php
use Foo\Bar;
function importFromThisFunction() {
$baz = new Bar;
}

View File

@ -7,7 +7,6 @@ use PhpParser\Node\Arg;
final class CallableThisArrayToAnonymousFunctionRector
{
/**
* @param Node\Param[] $params
* @return Node\Arg[]
@ -29,7 +28,6 @@ use PhpParser\Node\Arg;
final class CallableThisArrayToAnonymousFunctionRector
{
/**
* @param Param[] $params
* @return Arg[]

View File

@ -0,0 +1,22 @@
<?php
namespace Rector\CodingStyle\Tests\Rector\Namespace_\ImportFullyQualifiedNamesRector\Fixture;
function noClass()
{
return new \SomeAnother\AnotherClass;
}
?>
-----
<?php
namespace Rector\CodingStyle\Tests\Rector\Namespace_\ImportFullyQualifiedNamesRector\Fixture;
use SomeAnother\AnotherClass;
function noClass()
{
return new AnotherClass;
}
?>

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace Rector\CodingStyle\Tests\Rector\Namespace_\ImportFullyQualifiedNamesRector;
use Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class ImportFullyQualifiedNamesRectorNonNamespacedTest extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/NonNamespaced/simple.php.inc',
__DIR__ . '/Fixture/NonNamespaced/already_imported.php.inc',
__DIR__ . '/Fixture/NonNamespaced/function_import.php.inc',
]);
}
protected function getRectorClass(): string
{
return ImportFullyQualifiedNamesRector::class;
}
}

View File

@ -2,42 +2,53 @@
namespace Rector\CodingStyle\Tests\Rector\Namespace_\ImportFullyQualifiedNamesRector;
use Iterator;
use Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class ImportFullyQualifiedNamesRectorTest extends AbstractRectorTestCase
{
public function test(): void
/**
* @dataProvider provideNamespacedClasses()
* @dataProvider provideFunctions()
*/
public function test(string $file): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/double_import.php.inc',
__DIR__ . '/Fixture/double_import_with_existing.php.inc',
__DIR__ . '/Fixture/already_with_use.php.inc',
__DIR__ . '/Fixture/already_class_name.php.inc',
$this->doTestFile($file);
}
// keep
__DIR__ . '/Fixture/keep.php.inc',
__DIR__ . '/Fixture/keep_same_end.php.inc',
__DIR__ . '/Fixture/keep_trait_use.php.inc',
public function provideFunctions(): Iterator
{
yield [__DIR__ . '/Fixture/import_function.php.inc'];
yield [__DIR__ . '/Fixture/import_function_no_class.php.inc'];
yield [__DIR__ . '/Fixture/import_return_doc.php.inc'];
}
// php doc
__DIR__ . '/Fixture/import_param_doc.php.inc',
__DIR__ . '/Fixture/import_return_doc.php.inc',
__DIR__ . '/Fixture/doc_combined.php.inc',
__DIR__ . '/Fixture/conflicting_endings.php.inc',
__DIR__ . '/Fixture/already_class_name_in_param_doc.php.inc',
public function provideNamespacedClasses(): Iterator
{
yield [__DIR__ . '/Fixture/fixture.php.inc'];
yield [__DIR__ . '/Fixture/double_import.php.inc'];
yield [__DIR__ . '/Fixture/double_import_with_existing.php.inc'];
yield [__DIR__ . '/Fixture/already_with_use.php.inc'];
yield [__DIR__ . '/Fixture/already_class_name.php.inc'];
yield [__DIR__ . '/Fixture/no_class.php.inc'];
// buggy
__DIR__ . '/Fixture/many_imports.php.inc',
__DIR__ . '/Fixture/keep_static_method.php.inc',
__DIR__ . '/Fixture/keep_various_request.php.inc',
__DIR__ . '/Fixture/instance_of.php.inc',
// keep
yield [__DIR__ . '/Fixture/keep.php.inc'];
yield [__DIR__ . '/Fixture/keep_same_end.php.inc'];
yield [__DIR__ . '/Fixture/keep_trait_use.php.inc'];
// function
__DIR__ . '/Fixture/import_function.php.inc',
__DIR__ . '/Fixture/import_function_no_class.php.inc',
]);
// php doc
yield [__DIR__ . '/Fixture/import_param_doc.php.inc'];
yield [__DIR__ . '/Fixture/doc_combined.php.inc'];
yield [__DIR__ . '/Fixture/conflicting_endings.php.inc'];
yield [__DIR__ . '/Fixture/already_class_name_in_param_doc.php.inc'];
// buggy
yield [__DIR__ . '/Fixture/many_imports.php.inc'];
yield [__DIR__ . '/Fixture/keep_static_method.php.inc'];
yield [__DIR__ . '/Fixture/keep_various_request.php.inc'];
yield [__DIR__ . '/Fixture/instance_of.php.inc'];
}
protected function getRectorClass(): string

View File

@ -5,6 +5,7 @@ namespace Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer;
use Nette\Utils\Strings;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Name;
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
@ -32,7 +33,7 @@ use Rector\BetterPhpDocParser\NodeDecorator\StringsTypePhpDocNodeDecorator;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\Printer\PhpDocInfoPrinter;
use Rector\CodingStyle\Imports\ImportsInClassCollection;
use Rector\CodingStyle\Application\UseAddingCommander;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Exception\MissingTagException;
use Rector\NodeTypeResolver\Php\ParamTypeInfo;
@ -78,9 +79,9 @@ final class DocBlockManipulator
private $importedNames = [];
/**
* @var ImportsInClassCollection
* @var UseAddingCommander
*/
private $importsInClassCollection;
private $useAddingCommander;
public function __construct(
PhpDocInfoFactory $phpDocInfoFactory,
@ -89,7 +90,7 @@ final class DocBlockManipulator
AttributeAwareNodeFactory $attributeAwareNodeFactory,
StringsTypePhpDocNodeDecorator $stringsTypePhpDocNodeDecorator,
NodeTraverser $nodeTraverser,
ImportsInClassCollection $importsInClassCollection
UseAddingCommander $useAddingCommander
) {
$this->phpDocInfoFactory = $phpDocInfoFactory;
$this->phpDocInfoPrinter = $phpDocInfoPrinter;
@ -97,7 +98,7 @@ final class DocBlockManipulator
$this->attributeAwareNodeFactory = $attributeAwareNodeFactory;
$this->stringsTypePhpDocNodeDecorator = $stringsTypePhpDocNodeDecorator;
$this->nodeTraverser = $nodeTraverser;
$this->importsInClassCollection = $importsInClassCollection;
$this->useAddingCommander = $useAddingCommander;
}
public function hasTag(Node $node, string $name): bool
@ -411,22 +412,22 @@ final class DocBlockManipulator
$phpDocNode = $phpDocInfo->getPhpDocNode();
$this->nodeTraverser->traverseWithCallable($phpDocNode, function (
AttributeAwareNodeInterface $node
): AttributeAwareNodeInterface {
if (! $node instanceof IdentifierTypeNode) {
return $node;
AttributeAwareNodeInterface $docNode
) use ($node): AttributeAwareNodeInterface {
if (! $docNode instanceof IdentifierTypeNode) {
return $docNode;
}
// is class without namespaced name → skip
$name = ltrim($node->name, '\\');
$name = ltrim($docNode->name, '\\');
if (! Strings::contains($name, '\\')) {
return $node;
return $docNode;
}
$fullyQualifiedName = $this->getFullyQualifiedName($node);
$fullyQualifiedName = $this->getFullyQualifiedName($docNode);
$shortName = $this->getShortName($name);
return $this->processFqnNameImport($node, $shortName, $fullyQualifiedName);
return $this->processFqnNameImport($node, $docNode, $shortName, $fullyQualifiedName);
});
$this->updateNodeWithPhpDocInfo($node, $phpDocInfo);
@ -620,26 +621,21 @@ final class DocBlockManipulator
* @param AttributeAwareIdentifierTypeNode $attributeAwareNode
*/
private function processFqnNameImport(
Node $node,
AttributeAwareNodeInterface $attributeAwareNode,
string $shortName,
string $fullyQualifiedName
): AttributeAwareNodeInterface {
$alreadyImportedUses = array_merge($this->importedNames, $this->importsInClassCollection->get());
$alreadyImportedUses = array_unique($alreadyImportedUses);
foreach ($alreadyImportedUses as $alreadyImportedUse) {
$shortAlreadyImportedUsed = $this->getShortName($alreadyImportedUse);
if ($shortAlreadyImportedUsed === $shortName) {
if ($alreadyImportedUse === $fullyQualifiedName) {
$attributeAwareNode->name = $shortName;
}
return $attributeAwareNode;
if ($this->useAddingCommander->isShortImported($node, $fullyQualifiedName)) {
if ($this->useAddingCommander->isImportShortable($node, $fullyQualifiedName)) {
$attributeAwareNode->name = $shortName;
}
return $attributeAwareNode;
}
$attributeAwareNode->name = $shortName;
$this->importedNames[] = $fullyQualifiedName;
$this->useAddingCommander->addUseImport($node, $fullyQualifiedName);
return $attributeAwareNode;
}

View File

@ -86,7 +86,7 @@ CODE_SAMPLE
private function isIteratorToArrayFuncCall(Expr $expr): bool
{
return $expr instanceof Node\Expr\FuncCall && $this->isName($expr, 'iterator_to_array');
return $expr instanceof FuncCall && $this->isName($expr, 'iterator_to_array');
}
private function resolveValue(Expr $expr): Expr
@ -100,7 +100,7 @@ CODE_SAMPLE
return $expr;
}
if (! $expr->cond instanceof Node\Expr\FuncCall) {
if (! $expr->cond instanceof FuncCall) {
return $expr;
}

View File

@ -165,3 +165,7 @@ parameters:
- '#Class PhpParser\\Node\\Expr\\ArrayItem constructor invoked with 5 parameters, 1\-4 required#'
- '#Parameter \#2 \$name of method Rector\\Rector\\AbstractRector\:\:isName\(\) expects string, string\|null given#'
# file always exists here
- '#Cannot call method getRealPath\(\) on Symplify\\PackageBuilder\\FileSystem\\SmartFileInfo\|null#'
# cascade irelevant
- '#Parameter (.*?) expects array<PhpParser\\Node\\Stmt\>, array<PhpParser\\Node\> given#'

View File

@ -15,4 +15,4 @@ parameters:
php_version_features: '7.1'
services:
# Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~
Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~

View File

@ -134,6 +134,10 @@ abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorIn
$nodes = $this->nodeRemovingCommander->traverseNodes($nodes);
}
if ($this->useAddingCommander->isActive()) {
$nodes = $this->useAddingCommander->traverseNodes($nodes);
}
$this->tearDown();
return $nodes;

View File

@ -5,6 +5,7 @@ namespace Rector\Rector;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use Rector\Application\AppliedRectorCollector;
use Rector\CodingStyle\Application\UseAddingCommander;
use Rector\PhpParser\Node\Commander\NodeAddingCommander;
use Rector\PhpParser\Node\Commander\NodeRemovingCommander;
use Rector\PhpParser\Node\Commander\PropertyAddingCommander;
@ -33,17 +34,24 @@ trait NodeCommandersTrait
*/
private $propertyAddingCommander;
/**
* @var UseAddingCommander
*/
private $useAddingCommander;
/**
* @required
*/
public function setRequiredCommanders(
NodeRemovingCommander $nodeRemovingCommander,
NodeAddingCommander $nodeAddingCommander,
PropertyAddingCommander $propertyAddingCommander
PropertyAddingCommander $propertyAddingCommander,
UseAddingCommander $useAddingCommander
): void {
$this->nodeRemovingCommander = $nodeRemovingCommander;
$this->nodeAddingCommander = $nodeAddingCommander;
$this->propertyAddingCommander = $propertyAddingCommander;
$this->useAddingCommander = $useAddingCommander;
}
protected function addNodeAfterNode(Node $newNode, Node $positionNode): void
@ -73,6 +81,16 @@ trait NodeCommandersTrait
return $this->nodeRemovingCommander->isNodeRemoved($node);
}
protected function addUseImport(Node $node, string $useImport): void
{
$this->useAddingCommander->addUseImport($node, $useImport);
}
protected function addFunctionUseImport(Node $node, string $functionUseImport): void
{
$this->useAddingCommander->addFunctionUseImport($node, $functionUseImport);
}
/**
* @param Node[] $nodes
*/