Skip to content

Commit 670c2b3

Browse files
authored
Add StringableInterfaceFixer (#537)
1 parent 9eb28f6 commit 670c2b3

File tree

5 files changed

+407
-1
lines changed

5 files changed

+407
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG for PHP CS Fixer: custom fixers
22

3+
## v2.6.0
4+
- Add StringableInterfaceFixer
5+
36
## v2.5.0 - *2021-05-04*
47
- Add PHP CS Fixer v3 support
58

README.md

Lines changed: 15 additions & 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-2790-brightgreen.svg)
6+
![Tests](https://img.shields.io/badge/tests-2827-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&event=push)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions)
@@ -468,6 +468,20 @@ Single space must precede - not preceded by linebreak - statement.
468468
+$foo = new Foo();
469469
```
470470

471+
#### StringableInterfaceFixer
472+
Class that implements the `__toString()` method must explicitly implement the `Stringable` interface.
473+
```diff
474+
<?php
475+
-class Foo
476+
+class Foo implements \Stringable
477+
{
478+
public function __toString()
479+
{
480+
return "Foo";
481+
}
482+
}
483+
```
484+
471485

472486
## Contributing
473487
Request feature or report bug by creating [issue](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues).
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace PhpCsFixerCustomFixers\Fixer;
15+
16+
use PhpCsFixer\FixerDefinition\CodeSample;
17+
use PhpCsFixer\FixerDefinition\FixerDefinition;
18+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
19+
use PhpCsFixer\Tokenizer\Token;
20+
use PhpCsFixer\Tokenizer\Tokens;
21+
22+
final class StringableInterfaceFixer extends AbstractFixer
23+
{
24+
public function getDefinition(): FixerDefinitionInterface
25+
{
26+
return new FixerDefinition(
27+
'Class that implements the `__toString()` method must explicitly implement the `Stringable` interface.',
28+
[new CodeSample('<?php
29+
class Foo
30+
{
31+
public function __toString()
32+
{
33+
return "Foo";
34+
}
35+
}
36+
')]
37+
);
38+
}
39+
40+
/**
41+
* Must run before ClassDefinitionFixer.
42+
*/
43+
public function getPriority(): int
44+
{
45+
return 37;
46+
}
47+
48+
public function isCandidate(Tokens $tokens): bool
49+
{
50+
return \PHP_VERSION_ID >= 80000 && $tokens->isAllTokenKindsFound([\T_CLASS, \T_STRING]);
51+
}
52+
53+
public function isRisky(): bool
54+
{
55+
return false;
56+
}
57+
58+
public function fix(\SplFileInfo $file, Tokens $tokens): void
59+
{
60+
$isNamespaced = false;
61+
62+
for ($index = 1; $index < $tokens->count(); $index++) {
63+
/** @var Token $token */
64+
$token = $tokens[$index];
65+
66+
if ($token->isGivenKind(\T_NAMESPACE)) {
67+
$isNamespaced = true;
68+
continue;
69+
}
70+
71+
if (!$token->isGivenKind(\T_CLASS)) {
72+
continue;
73+
}
74+
75+
/** @var int $classStartIndex */
76+
$classStartIndex = $tokens->getNextTokenOfKind($index, ['{']);
77+
78+
$classEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classStartIndex);
79+
80+
if (!$this->doesHaveToStringMethod($tokens, $classStartIndex, $classEndIndex)) {
81+
continue;
82+
}
83+
84+
$this->addStringableInterface($tokens, $index, $isNamespaced);
85+
}
86+
}
87+
88+
private function doesHaveToStringMethod(Tokens $tokens, int $classStartIndex, int $classEndIndex): bool
89+
{
90+
for ($index = $classStartIndex + 1; $index < $classEndIndex; $index++) {
91+
/** @var Token $token */
92+
$token = $tokens[$index];
93+
94+
if (!$token->isGivenKind(\T_FUNCTION)) {
95+
continue;
96+
}
97+
98+
$functionNameIndex = $tokens->getNextTokenOfKind($index, [[\T_STRING]]);
99+
100+
if ($functionNameIndex === null || $functionNameIndex > $classEndIndex) {
101+
return false;
102+
}
103+
104+
/** @var Token $functionNameToken */
105+
$functionNameToken = $tokens[$functionNameIndex];
106+
107+
if ($functionNameToken->equals([\T_STRING, '__toString'], false)) {
108+
return true;
109+
}
110+
}
111+
112+
return false;
113+
}
114+
115+
private function addStringableInterface(Tokens $tokens, int $classIndex, bool $isNamespaced): void
116+
{
117+
/** @var int $classNameIndex */
118+
$classNameIndex = $tokens->getNextTokenOfKind($classIndex, [[\T_STRING]]);
119+
120+
/** @var int $implementsIndex */
121+
$implementsIndex = $tokens->getNextTokenOfKind($classNameIndex, ['{', [\T_IMPLEMENTS]]);
122+
123+
/** @var Token $implementsToken */
124+
$implementsToken = $tokens[$implementsIndex];
125+
126+
if ($implementsToken->equals('{')) {
127+
/** @var int $prevIndex */
128+
$prevIndex = $tokens->getPrevMeaningfulToken($implementsIndex);
129+
130+
$tokens->insertAt(
131+
$prevIndex + 1,
132+
[
133+
new Token([\T_WHITESPACE, ' ']),
134+
new Token([\T_IMPLEMENTS, 'implements']),
135+
new Token([\T_WHITESPACE, ' ']),
136+
new Token([\T_NS_SEPARATOR, '\\']),
137+
new Token([\T_STRING, 'Stringable']),
138+
]
139+
);
140+
141+
return;
142+
}
143+
144+
/** @var int $implementsEndIndex */
145+
$implementsEndIndex = $tokens->getNextTokenOfKind($classNameIndex, ['{']);
146+
if ($this->isStringableAlreadyUsed($tokens, $implementsIndex + 1, $implementsEndIndex - 1, $isNamespaced)) {
147+
return;
148+
}
149+
150+
/** @var int $prevIndex */
151+
$prevIndex = $tokens->getPrevMeaningfulToken($implementsEndIndex);
152+
153+
$tokens->insertAt(
154+
$prevIndex + 1,
155+
[
156+
new Token(','),
157+
new Token([\T_WHITESPACE, ' ']),
158+
new Token([\T_NS_SEPARATOR, '\\']),
159+
new Token([\T_STRING, 'Stringable']),
160+
]
161+
);
162+
}
163+
164+
private function isStringableAlreadyUsed(Tokens $tokens, int $implementsStartIndex, int $implementsEndIndex, bool $isNamespaced): bool
165+
{
166+
for ($index = $implementsStartIndex; $index < $implementsEndIndex; $index++) {
167+
/** @var Token $token */
168+
$token = $tokens[$index];
169+
170+
if (!$token->equals([\T_STRING, 'Stringable'], false)) {
171+
continue;
172+
}
173+
174+
/** @var int $namespaceSeparatorIndex */
175+
$namespaceSeparatorIndex = $tokens->getPrevMeaningfulToken($index);
176+
177+
/** @var Token $namespaceSeparatorToken */
178+
$namespaceSeparatorToken = $tokens[$namespaceSeparatorIndex];
179+
180+
if ($namespaceSeparatorToken->isGivenKind(\T_NS_SEPARATOR)) {
181+
/** @var int $beforeNamespaceSeparatorIndex */
182+
$beforeNamespaceSeparatorIndex = $tokens->getPrevMeaningfulToken($namespaceSeparatorIndex);
183+
184+
/** @var Token $beforeNamespaceSeparatorToken */
185+
$beforeNamespaceSeparatorToken = $tokens[$beforeNamespaceSeparatorIndex];
186+
187+
if (!$beforeNamespaceSeparatorToken->isGivenKind(\T_STRING)) {
188+
return true;
189+
}
190+
} else {
191+
if (!$isNamespaced) {
192+
return true;
193+
}
194+
}
195+
}
196+
197+
return false;
198+
}
199+
}

0 commit comments

Comments
 (0)