Skip to content

Commit 5da90c8

Browse files
authored
StringableInterfaceFixer - fix for aliases (#785)
1 parent 77dbeee commit 5da90c8

File tree

3 files changed

+153
-60
lines changed

3 files changed

+153
-60
lines changed

README.md

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

99
[![CI Status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/workflows/CI/badge.svg?branch=main)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions)

src/Fixer/StringableInterfaceFixer.php

Lines changed: 92 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function isRisky(): bool
5555

5656
public function fix(\SplFileInfo $file, Tokens $tokens): void
5757
{
58-
$namespaceStartIndex = null;
58+
$namespaceStartIndex = 0;
5959

6060
for ($index = 1; $index < $tokens->count(); $index++) {
6161
if ($tokens[$index]->isGivenKind(\T_NAMESPACE)) {
@@ -76,7 +76,11 @@ public function fix(\SplFileInfo $file, Tokens $tokens): void
7676
continue;
7777
}
7878

79-
$this->addStringableInterface($tokens, $index, $namespaceStartIndex);
79+
if ($this->doesImplementStringable($tokens, $namespaceStartIndex, $index + 1, $classStartIndex - 1)) {
80+
continue;
81+
}
82+
83+
$this->addStringableInterface($tokens, $index);
8084
}
8185
}
8286

@@ -101,7 +105,92 @@ private function doesHaveToStringMethod(Tokens $tokens, int $classStartIndex, in
101105
return false;
102106
}
103107

104-
private function addStringableInterface(Tokens $tokens, int $classIndex, ?int $namespaceStartIndex): void
108+
private function doesImplementStringable(Tokens $tokens, int $namespaceStartIndex, int $classKeywordIndex, int $classOpenBraceIndex): bool
109+
{
110+
$interfaces = $this->getInterfaces($tokens, $classKeywordIndex, $classOpenBraceIndex);
111+
if ($interfaces === []) {
112+
return false;
113+
}
114+
115+
if (\in_array('\stringable', $interfaces, true)) {
116+
return true;
117+
}
118+
119+
if ($namespaceStartIndex === 0 && \in_array('stringable', $interfaces, true)) {
120+
return true;
121+
}
122+
123+
foreach ($this->getImports($tokens, $namespaceStartIndex, $classKeywordIndex) as $import) {
124+
if (\in_array($import, $interfaces, true)) {
125+
return true;
126+
}
127+
}
128+
129+
return false;
130+
}
131+
132+
/**
133+
* @return array<string>
134+
*/
135+
private function getInterfaces(Tokens $tokens, int $classKeywordIndex, int $classOpenBraceIndex): array
136+
{
137+
$startIndex = $tokens->getNextTokenOfKind($classKeywordIndex, ['{', [\T_IMPLEMENTS]]);
138+
\assert(\is_int($startIndex));
139+
140+
$interfaces = [];
141+
for ($index = $startIndex; $index < $classOpenBraceIndex; $index++) {
142+
if (!$tokens[$index]->isGivenKind(\T_STRING)) {
143+
continue;
144+
}
145+
146+
$interface = \strtolower($tokens[$index]->getContent());
147+
148+
$prevIndex = $tokens->getPrevMeaningfulToken($index);
149+
\assert(\is_int($prevIndex));
150+
if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
151+
$interface = '\\' . $interface;
152+
$prevPrevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
153+
\assert(\is_int($prevPrevIndex));
154+
if ($tokens[$prevPrevIndex]->isGivenKind(\T_STRING)) {
155+
continue;
156+
}
157+
}
158+
159+
$interfaces[] = $interface;
160+
}
161+
162+
return $interfaces;
163+
}
164+
165+
/**
166+
* @return iterable<string>
167+
*/
168+
private function getImports(Tokens $tokens, int $namespaceStartIndex, int $classKeywordIndex): iterable
169+
{
170+
for ($index = $namespaceStartIndex; $index < $classKeywordIndex; $index++) {
171+
if (!$tokens[$index]->isGivenKind(\T_USE)) {
172+
continue;
173+
}
174+
$nameIndex = $tokens->getNextMeaningfulToken($index);
175+
\assert(\is_int($nameIndex));
176+
177+
if ($tokens[$nameIndex]->isGivenKind(\T_NS_SEPARATOR)) {
178+
$nameIndex = $tokens->getNextMeaningfulToken($nameIndex);
179+
\assert(\is_int($nameIndex));
180+
}
181+
182+
$nextIndex = $tokens->getNextMeaningfulToken($nameIndex);
183+
\assert(\is_int($nextIndex));
184+
if ($tokens[$nextIndex]->isGivenKind(\T_AS)) {
185+
$nameIndex = $tokens->getNextMeaningfulToken($nextIndex);
186+
\assert(\is_int($nameIndex));
187+
}
188+
189+
yield \strtolower($tokens[$nameIndex]->getContent());
190+
}
191+
}
192+
193+
private function addStringableInterface(Tokens $tokens, int $classIndex): void
105194
{
106195
$implementsIndex = $tokens->getNextTokenOfKind($classIndex, ['{', [\T_IMPLEMENTS]]);
107196
\assert(\is_int($implementsIndex));
@@ -126,9 +215,6 @@ private function addStringableInterface(Tokens $tokens, int $classIndex, ?int $n
126215

127216
$implementsEndIndex = $tokens->getNextTokenOfKind($implementsIndex, ['{']);
128217
\assert(\is_int($implementsEndIndex));
129-
if ($this->isStringableAlreadyUsed($tokens, $implementsIndex + 1, $implementsEndIndex - 1, $namespaceStartIndex)) {
130-
return;
131-
}
132218

133219
$prevIndex = $tokens->getPrevMeaningfulToken($implementsEndIndex);
134220
\assert(\is_int($prevIndex));
@@ -143,57 +229,4 @@ private function addStringableInterface(Tokens $tokens, int $classIndex, ?int $n
143229
]
144230
);
145231
}
146-
147-
private function isStringableAlreadyUsed(Tokens $tokens, int $implementsStartIndex, int $implementsEndIndex, ?int $namespaceStartIndex): bool
148-
{
149-
for ($index = $implementsStartIndex; $index < $implementsEndIndex; $index++) {
150-
if (!$tokens[$index]->equals([\T_STRING, 'Stringable'], false)) {
151-
continue;
152-
}
153-
154-
$namespaceSeparatorIndex = $tokens->getPrevMeaningfulToken($index);
155-
\assert(\is_int($namespaceSeparatorIndex));
156-
157-
if ($tokens[$namespaceSeparatorIndex]->isGivenKind(\T_NS_SEPARATOR)) {
158-
$beforeNamespaceSeparatorIndex = $tokens->getPrevMeaningfulToken($namespaceSeparatorIndex);
159-
\assert(\is_int($beforeNamespaceSeparatorIndex));
160-
161-
if (!$tokens[$beforeNamespaceSeparatorIndex]->isGivenKind(\T_STRING)) {
162-
return true;
163-
}
164-
} else {
165-
if ($namespaceStartIndex === null) {
166-
return true;
167-
}
168-
if ($this->isStringableImported($tokens, $namespaceStartIndex, $index)) {
169-
return true;
170-
}
171-
}
172-
}
173-
174-
return false;
175-
}
176-
177-
private function isStringableImported(Tokens $tokens, int $startIndex, int $endIndex): bool
178-
{
179-
for ($index = $startIndex; $index < $endIndex; $index++) {
180-
if (!$tokens[$index]->equals([\T_STRING, 'Stringable'], false)) {
181-
continue;
182-
}
183-
184-
$useIndex = $tokens->getPrevMeaningfulToken($index);
185-
\assert(\is_int($useIndex));
186-
187-
if ($tokens[$useIndex]->isGivenKind(\T_NS_SEPARATOR)) {
188-
$useIndex = $tokens->getPrevMeaningfulToken($useIndex);
189-
\assert(\is_int($useIndex));
190-
}
191-
192-
if ($tokens[$useIndex]->isGivenKind(\T_USE)) {
193-
return true;
194-
}
195-
}
196-
197-
return false;
198-
}
199232
}

tests/Fixer/StringableInterfaceFixerTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ public function __toString() { return ""; }
5252
}
5353
'];
5454

55+
yield ['<?php
56+
use Stringable as Stringy;
57+
class Bar implements Stringy {
58+
public function __toString() { return ""; }
59+
}
60+
'];
61+
62+
yield ['<?php
63+
namespace Foo;
64+
use Stringable as Stringy;
65+
class Bar implements Stringy {
66+
public function __toString() { return ""; }
67+
}
68+
'];
69+
5570
yield ['<?php
5671
namespace Foo;
5772
use \Stringable;
@@ -112,6 +127,23 @@ public function __toString() { return "Foo"; }
112127
',
113128
];
114129

130+
yield [
131+
'<?php namespace FooNamespace;
132+
use Bar\Stringable;
133+
class Foo implements Stringable, \Stringable
134+
{
135+
public function __toString() { return "Foo"; }
136+
}
137+
',
138+
'<?php namespace FooNamespace;
139+
use Bar\Stringable;
140+
class Foo implements Stringable
141+
{
142+
public function __toString() { return "Foo"; }
143+
}
144+
',
145+
];
146+
115147
yield [
116148
'<?php namespace FooNamespace;
117149
class Foo implements Stringable, \Stringable
@@ -240,5 +272,33 @@ class Foo4 { public function __noString() { return "4"; } }
240272
class Foo5 { public function __noString() { return "5"; } }
241273
',
242274
];
275+
276+
yield [
277+
'<?php
278+
namespace Foo { class C implements I, \Stringable { public function __toString() { return ""; } }}
279+
namespace Bar { class C implements \Stringable, I { public function __toString() { return ""; } }}
280+
namespace Baz { class C implements I, \Stringable { public function __toString() { return ""; } }}
281+
;
282+
',
283+
'<?php
284+
namespace Foo { class C implements I { public function __toString() { return ""; } }}
285+
namespace Bar { class C implements \Stringable, I { public function __toString() { return ""; } }}
286+
namespace Baz { class C implements I { public function __toString() { return ""; } }}
287+
;
288+
',
289+
];
290+
291+
yield [
292+
'<?php
293+
namespace Foo { use Stringable as Stringy; class C {} }
294+
namespace Bar { class C implements Stringy, \Stringable { public function __toString() { return ""; } }}
295+
;
296+
',
297+
'<?php
298+
namespace Foo { use Stringable as Stringy; class C {} }
299+
namespace Bar { class C implements Stringy { public function __toString() { return ""; } }}
300+
;
301+
',
302+
];
243303
}
244304
}

0 commit comments

Comments
 (0)