Skip to content

Commit 2d9af89

Browse files
authored
StringableInterfaceFixer - fix for import with alias of not Stringable (#1050)
1 parent 1623c49 commit 2d9af89

File tree

3 files changed

+69
-44
lines changed

3 files changed

+69
-44
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![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)
66
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
77
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
8-
![Tests](https://img.shields.io/badge/tests-3798-brightgreen.svg)
8+
![Tests](https://img.shields.io/badge/tests-3799-brightgreen.svg)
99
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
1010

1111
[![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)

src/Fixer/StringableInterfaceFixer.php

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use PhpCsFixer\FixerDefinition\CodeSample;
1515
use PhpCsFixer\FixerDefinition\FixerDefinition;
1616
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
17+
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
18+
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
1719
use PhpCsFixer\Tokenizer\Token;
1820
use PhpCsFixer\Tokenizer\Tokens;
1921

@@ -60,11 +62,21 @@ public function isRisky(): bool
6062

6163
public function fix(\SplFileInfo $file, Tokens $tokens): void
6264
{
63-
$namespaceStartIndex = 0;
65+
$useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
66+
67+
$stringableInterfaces = ['stringable'];
6468

6569
for ($index = 1; $index < $tokens->count(); $index++) {
6670
if ($tokens[$index]->isGivenKind(\T_NAMESPACE)) {
67-
$namespaceStartIndex = $index;
71+
$stringableInterfaces = [];
72+
continue;
73+
}
74+
75+
if ($tokens[$index]->isGivenKind(\T_USE)) {
76+
$name = self::getNameFromUse($index, $useDeclarations);
77+
if ($name !== null) {
78+
$stringableInterfaces[] = $name;
79+
}
6880
continue;
6981
}
7082

@@ -81,14 +93,36 @@ public function fix(\SplFileInfo $file, Tokens $tokens): void
8193
continue;
8294
}
8395

84-
if (self::doesImplementStringable($tokens, $namespaceStartIndex, $index, $classStartIndex)) {
96+
if (self::doesImplementStringable($tokens, $index, $classStartIndex, $stringableInterfaces)) {
8597
continue;
8698
}
8799

88100
self::addStringableInterface($tokens, $index);
89101
}
90102
}
91103

104+
/**
105+
* @param list<NamespaceUseAnalysis> $useDeclarations
106+
*/
107+
private static function getNameFromUse(int $index, array $useDeclarations): ?string
108+
{
109+
$uses = \array_filter(
110+
$useDeclarations,
111+
static fn (NamespaceUseAnalysis $namespaceUseAnalysis): bool => $namespaceUseAnalysis->getStartIndex() === $index,
112+
);
113+
114+
\assert(\count($uses) === 1);
115+
116+
$useDeclaration = \reset($uses);
117+
118+
$lowercasedFullName = \strtolower($useDeclaration->getFullName());
119+
if ($lowercasedFullName !== 'stringable' && $lowercasedFullName !== '\\stringable') {
120+
return null;
121+
}
122+
123+
return \strtolower($useDeclaration->getShortName());
124+
}
125+
92126
private static function doesHaveToStringMethod(Tokens $tokens, int $classStartIndex, int $classEndIndex): bool
93127
{
94128
$index = $classStartIndex;
@@ -115,23 +149,25 @@ private static function doesHaveToStringMethod(Tokens $tokens, int $classStartIn
115149
return false;
116150
}
117151

118-
private static function doesImplementStringable(Tokens $tokens, int $namespaceStartIndex, int $classKeywordIndex, int $classOpenBraceIndex): bool
119-
{
120-
$interfaces = self::getInterfaces($tokens, $classKeywordIndex, $classOpenBraceIndex);
121-
if ($interfaces === []) {
152+
/**
153+
* @param list<string> $stringableInterfaces
154+
*/
155+
private static function doesImplementStringable(
156+
Tokens $tokens,
157+
int $classKeywordIndex,
158+
int $classOpenBraceIndex,
159+
array $stringableInterfaces
160+
): bool {
161+
$implementedInterfaces = self::getInterfaces($tokens, $classKeywordIndex, $classOpenBraceIndex);
162+
if ($implementedInterfaces === []) {
122163
return false;
123164
}
124-
125-
if (\in_array('\\stringable', $interfaces, true)) {
165+
if (\in_array('\\stringable', $implementedInterfaces, true)) {
126166
return true;
127167
}
128168

129-
if ($namespaceStartIndex === 0 && \in_array('stringable', $interfaces, true)) {
130-
return true;
131-
}
132-
133-
foreach (self::getImports($tokens, $namespaceStartIndex, $classKeywordIndex) as $import) {
134-
if (\in_array($import, $interfaces, true)) {
169+
foreach ($stringableInterfaces as $stringableInterface) {
170+
if (\in_array($stringableInterface, $implementedInterfaces, true)) {
135171
return true;
136172
}
137173
}
@@ -170,34 +206,6 @@ private static function getInterfaces(Tokens $tokens, int $classKeywordIndex, in
170206
return $interfaces;
171207
}
172208

173-
/**
174-
* @return iterable<string>
175-
*/
176-
private static function getImports(Tokens $tokens, int $namespaceStartIndex, int $classKeywordIndex): iterable
177-
{
178-
for ($index = $namespaceStartIndex; $index < $classKeywordIndex; $index++) {
179-
if (!$tokens[$index]->isGivenKind(\T_USE)) {
180-
continue;
181-
}
182-
$nameIndex = $tokens->getNextMeaningfulToken($index);
183-
\assert(\is_int($nameIndex));
184-
185-
if ($tokens[$nameIndex]->isGivenKind(\T_NS_SEPARATOR)) {
186-
$nameIndex = $tokens->getNextMeaningfulToken($nameIndex);
187-
\assert(\is_int($nameIndex));
188-
}
189-
190-
$nextIndex = $tokens->getNextMeaningfulToken($nameIndex);
191-
\assert(\is_int($nextIndex));
192-
if ($tokens[$nextIndex]->isGivenKind(\T_AS)) {
193-
$nameIndex = $tokens->getNextMeaningfulToken($nextIndex);
194-
\assert(\is_int($nameIndex));
195-
}
196-
197-
yield \strtolower($tokens[$nameIndex]->getContent());
198-
}
199-
}
200-
201209
private static function addStringableInterface(Tokens $tokens, int $classIndex): void
202210
{
203211
$implementsIndex = $tokens->getNextTokenOfKind($classIndex, ['{', [\T_IMPLEMENTS]]);

tests/Fixer/StringableInterfaceFixerTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,23 @@ public function __toString() { return "Foo"; }
152152
}
153153
';
154154

155+
yield [
156+
<<<'PHP'
157+
<?php
158+
use NotStringable as Stringy;
159+
class Bar implements \Stringable, Stringy {
160+
public function __toString() { return ""; }
161+
}
162+
PHP,
163+
<<<'PHP'
164+
<?php
165+
use NotStringable as Stringy;
166+
class Bar implements Stringy {
167+
public function __toString() { return ""; }
168+
}
169+
PHP,
170+
];
171+
155172
$implementedInterfacesCases = [
156173
\Stringable::class,
157174
'Foo\\Stringable',

0 commit comments

Comments
 (0)