Skip to content

Commit

Permalink
NoImportFromGlobalNamespaceFixer - do not remove import when it is …
Browse files Browse the repository at this point in the history
…a partial namespace (#959)
  • Loading branch information
kubawerlos authored Feb 3, 2024
1 parent 7b8f147 commit 559b405
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 48 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Latest stable version](https://img.shields.io/packagist/v/kubawerlos/php-cs-fixer-custom-fixers.svg?label=current%20version)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
![Tests](https://img.shields.io/badge/tests-3528-brightgreen.svg)
![Tests](https://img.shields.io/badge/tests-3534-brightgreen.svg)
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)

[![CI status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml/badge.svg)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml)
Expand Down
125 changes: 80 additions & 45 deletions src/Fixer/NoImportFromGlobalNamespaceFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,92 +64,127 @@ public function fix(\SplFileInfo $file, Tokens $tokens): void

private function fixImports(Tokens $tokens, int $startIndex, int $endIndex, bool $isInGlobalNamespace): void
{
$imports = [];
$importedClassesIndices = self::getImportCandidateIndices($tokens, $startIndex, $endIndex);

for ($index = $startIndex; $index < $endIndex; $index++) {
if ($tokens[$index]->isGivenKind(\T_USE)) {
$imports = $this->removeImportFromGlobalNamespace($tokens, $imports, $index);
continue;
}
if (!$isInGlobalNamespace) {
for ($index = $endIndex; $index > $startIndex; $index--) {
if ($tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
$importedClassesIndices = $this->updateComment($tokens, $importedClassesIndices, $index);
continue;
}

if ($isInGlobalNamespace) {
continue;
}
if (!$tokens[$index]->isGivenKind(\T_STRING)) {
continue;
}

if ($tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
$this->updateComment($tokens, $imports, $index);
continue;
$importedClassesIndices = $this->updateUsage($tokens, $importedClassesIndices, $index);
}

if (!$tokens[$index]->isGivenKind(\T_STRING)) {
continue;
}

$endIndex += $this->updateUsageAndReturnNumberOfInsertedTokens($tokens, $imports, $index);
}

self::clearImports($tokens, $importedClassesIndices);
}

/**
* @param list<string> $imports
*
* @return list<string>
* @return array<string, null|int>
*/
private function removeImportFromGlobalNamespace(Tokens $tokens, array $imports, int $index): array
private static function getImportCandidateIndices(Tokens $tokens, int $startIndex, int $endIndex): array
{
$classNameIndex = $tokens->getNextMeaningfulToken($index);
\assert(\is_int($classNameIndex));
$importedClassesIndices = [];

if ($tokens[$classNameIndex]->isGivenKind(\T_NS_SEPARATOR)) {
$classNameIndex = $tokens->getNextMeaningfulToken($classNameIndex);
foreach (\array_keys($tokens->findGivenKind(\T_USE, $startIndex, $endIndex)) as $index) {
$classNameIndex = $tokens->getNextMeaningfulToken($index);
\assert(\is_int($classNameIndex));
}

$semicolonIndex = $tokens->getNextMeaningfulToken($classNameIndex);
\assert(\is_int($semicolonIndex));
if ($tokens[$classNameIndex]->isGivenKind(\T_NS_SEPARATOR)) {
$classNameIndex = $tokens->getNextMeaningfulToken($classNameIndex);
\assert(\is_int($classNameIndex));
}

$semicolonIndex = $tokens->getNextMeaningfulToken($classNameIndex);
\assert(\is_int($semicolonIndex));

if ($tokens[$semicolonIndex]->equals(';')) {
$imports[] = $tokens[$classNameIndex]->getContent();
$tokens->clearRange($index, $semicolonIndex);
TokenRemover::removeWithLinesIfPossible($tokens, $semicolonIndex);
if (!$tokens[$semicolonIndex]->equals(';')) {
continue;
}

$importedClassesIndices[$tokens[$classNameIndex]->getContent()] = $classNameIndex;
}

return $imports;
return $importedClassesIndices;
}

/**
* @param list<string> $imports
* @param array<string, null|int> $importedClassesIndices
*
* @return array<string, null|int>
*/
private function updateComment(Tokens $tokens, array $imports, int $index): void
private function updateComment(Tokens $tokens, array $importedClassesIndices, int $index): array
{
$content = $tokens[$index]->getContent();

foreach ($imports as $import) {
$content = Preg::replace(\sprintf('/\b(?<!\\\\)%s\b/', $import), '\\' . $import, $content);
foreach ($importedClassesIndices as $importedClassName => $importedClassIndex) {
$content = Preg::replace(\sprintf('/\b(?<!\\\\)%s(?!\\\\)\b/', $importedClassName), '\\' . $importedClassName, $content);
if ($importedClassIndex !== null && Preg::match(\sprintf('/\b(?<!\\\\)%s(?=\\\\)\b/', $importedClassName), $content)) {
$importedClassesIndices[$importedClassName] = null;
}
}

if ($content !== $tokens[$index]->getContent()) {
$tokens[$index] = new Token([\T_DOC_COMMENT, $content]);
}

return $importedClassesIndices;
}

/**
* @param list<string> $imports
* @param array<string, null|int> $importedClassesIndices
*
* @return array<string, null|int>
*/
private function updateUsageAndReturnNumberOfInsertedTokens(Tokens $tokens, array $imports, int $index): int
private function updateUsage(Tokens $tokens, array $importedClassesIndices, int $index): array
{
if (!\in_array($tokens[$index]->getContent(), $imports, true)) {
return 0;
if (!\in_array($tokens[$index]->getContent(), \array_keys($importedClassesIndices), true)) {
return $importedClassesIndices;
}

$prevIndex = $tokens->getPrevMeaningfulToken($index);
\assert(\is_int($prevIndex));

if ($tokens[$prevIndex]->isGivenKind([\T_CONST, \T_DOUBLE_COLON, \T_NS_SEPARATOR, \T_OBJECT_OPERATOR, \T_FUNCTION])) {
return 0;
if ($tokens[$prevIndex]->isGivenKind([\T_CONST, \T_DOUBLE_COLON, \T_FUNCTION, \T_NS_SEPARATOR, \T_OBJECT_OPERATOR, \T_USE])) {
return $importedClassesIndices;
}

$nextIndex = $tokens->getNextMeaningfulToken($index);
\assert(\is_int($nextIndex));

if ($tokens[$nextIndex]->isGivenKind(\T_NS_SEPARATOR)) {
$importedClassesIndices[$tokens[$index]->getContent()] = null;

return $importedClassesIndices;
}

$tokens->insertAt($index, new Token([\T_NS_SEPARATOR, '\\']));

return 1;
return $importedClassesIndices;
}

/**
* @param array<string, null|int> $importedClassesIndices
*/
private static function clearImports(Tokens $tokens, array $importedClassesIndices): void
{
foreach ($importedClassesIndices as $importedClassIndex) {
if ($importedClassIndex === null) {
continue;
}
$useIndex = $tokens->getPrevTokenOfKind($importedClassIndex, [[\T_USE]]);
\assert(\is_int($useIndex));

$semicolonIndex = $tokens->getNextTokenOfKind($importedClassIndex, [';']);
\assert(\is_int($semicolonIndex));

$tokens->clearRange($useIndex, $semicolonIndex);
TokenRemover::removeWithLinesIfPossible($tokens, $useIndex);
}
}
}
152 changes: 150 additions & 2 deletions tests/Fixer/NoImportFromGlobalNamespaceFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,13 @@ public function __construct() {}
yield [
'<?php
namespace Foo;
use DateTime;
class Bar {
public function __construct() {
new \DateTime();
new Baz\DateTime();
\DateTime::createFromFormat("Y-m-d");
\DateTime\Baz::createFromFormat("Y-m-d");
DateTime\Baz::createFromFormat("Y-m-d");
Baz::DateTime();
$baz->DateTime();
}
Expand All @@ -161,6 +162,7 @@ public function __construct() {
yield [
'<?php
namespace Foo;
use DateTime;
class Bar {
/**
* @param \DateTime $a
Expand All @@ -170,7 +172,7 @@ class Bar {
* @param int|\DateTime $e
* @param \DateTime|string $f
* @param bool|\DateTime|string $g
* @param \DateTime\Baz $h
* @param DateTime\Baz $h
* @param DateTimeBaz $i
*/
public function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i) {}
Expand Down Expand Up @@ -313,6 +315,152 @@ function Bar() {}
$input .= \sprintf("echo Bar::BAZ_%d;\n", $i);
}
yield [$expected, $input];

yield [
<<<'PHP'
<?php
namespace Foo;
use NotClassButPartOfNamespace;
class Bar
{
public function baz()
{
return new NotClassButPartOfNamespace\ThisIsClass();
}
}
PHP,
];

yield [
<<<'PHP'
<?php
namespace N;
use Foo;
class C
{
public function f1() { return new Foo\Bar(); }
public function f2() { return new Bar\Foo(); }
public function f3() { return new Bar\Foo\Baz(); }
}
PHP,
];

yield [
<<<'PHP'
<?php
namespace Foo;
use Namespace1;
use Namespace1\Namespace2;
class Bar
{
public function f1() { return new \Namespace1(); }
public function f2() { return new Namespace1\Namespace2(); }
}
PHP,
<<<'PHP'
<?php
namespace Foo;
use Namespace1;
use Namespace1\Namespace2;
class Bar
{
public function f1() { return new Namespace1(); }
public function f2() { return new Namespace1\Namespace2(); }
}
PHP,
];

yield [
<<<'PHP'
<?php
namespace Foo;
use Vendor;
class Bar
{
/**
* @param \Vendor $x
* @param Vendor\SomeClass $y
*/
public function baz($x, $y) {}
}
PHP,
<<<'PHP'
<?php
namespace Foo;
use Vendor;
class Bar
{
/**
* @param Vendor $x
* @param Vendor\SomeClass $y
*/
public function baz($x, $y) {}
}
PHP,
];

yield [
<<<'PHP'
<?php
namespace Root;
use Vendor1\Class1a;
use Vendor1\Class1b;
use Vendor3;
use Vendor4\Class4;
class Test
{
/** @return \Vendor2 */
public function f1() {}
/** @return Vendor3\Class3 */
public function f2() {}
/** @return \Vendor4 */
public function f3() {}
}
PHP,
<<<'PHP'
<?php
namespace Root;
use Vendor1\Class1a;
use Vendor1\Class1b;
use Vendor2;
use Vendor3;
use Vendor4;
use Vendor4\Class4;
class Test
{
/** @return Vendor2 */
public function f1() {}
/** @return Vendor3\Class3 */
public function f2() {}
/** @return Vendor4 */
public function f3() {}
}
PHP,
];
yield [
<<<'PHP'
<?php
namespace Root;
use Vendor2;
class Test
{
public function f1(): \Vendor2 {}
public function f2(): Vendor2\Class2 {}
}
PHP,
<<<'PHP'
<?php
namespace Root;
use Vendor1;
use Vendor2;
use Vendor3;
class Test
{
public function f1(): Vendor2 {}
public function f2(): Vendor2\Class2 {}
}
PHP,
];
}

/**
Expand Down

0 comments on commit 559b405

Please sign in to comment.