Skip to content

Commit a7ef56f

Browse files
authored
Add PhpdocSelfAccessorFixer (#42)
1 parent 3f668c7 commit a7ef56f

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
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+
## [Unreleased]
4+
- Add PhpdocSelfAccessorFixer
5+
36
## v1.7.0 - *2018-08-06*
47
- Add NoReferenceInFunctionDefinitionFixer
58
- Add NoImportFromGlobalNamespaceFixer

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ In your PHP CS Fixer configuration register fixers and use them:
165165
function a($foo, $bar) {}
166166
```
167167

168+
- **PhpdocSelfAccessorFixer** - in PHPDoc inside class or interface element `self` should be preferred over the class name itself.
169+
```diff
170+
<?php
171+
class Foo {
172+
/**
173+
- * @var Foo
174+
+ * @var self
175+
*/
176+
private $instance;
177+
}
178+
```
179+
168180
- **PhpdocSingleLineVarFixer** - `@var` annotation must be in single line when is the only content.
169181
```diff
170182
<?php
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PhpCsFixerCustomFixers\Fixer;
6+
7+
use PhpCsFixer\DocBlock\DocBlock;
8+
use PhpCsFixer\FixerDefinition\CodeSample;
9+
use PhpCsFixer\FixerDefinition\FixerDefinition;
10+
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
11+
use PhpCsFixer\Tokenizer\Token;
12+
use PhpCsFixer\Tokenizer\Tokens;
13+
use PhpCsFixer\Tokenizer\TokensAnalyzer;
14+
15+
final class PhpdocSelfAccessorFixer extends AbstractFixer
16+
{
17+
public function getDefinition(): FixerDefinition
18+
{
19+
return new FixerDefinition(
20+
'In PHPDoc inside class or interface element `self` should be preferred over the class name itself.',
21+
[new CodeSample('<?php
22+
class Foo {
23+
/**
24+
* @var Foo
25+
*/
26+
private $instance;
27+
}
28+
')]
29+
);
30+
}
31+
32+
public function isCandidate(Tokens $tokens): bool
33+
{
34+
return $tokens->isAnyTokenKindsFound([T_CLASS, T_INTERFACE]) && $tokens->isTokenKindFound(T_DOC_COMMENT);
35+
}
36+
37+
public function isRisky(): bool
38+
{
39+
return false;
40+
}
41+
42+
public function fix(\SplFileInfo $file, Tokens $tokens): void
43+
{
44+
$tokensAnalyzer = new TokensAnalyzer($tokens);
45+
46+
foreach ((new NamespacesAnalyzer())->getDeclarations($tokens) as $namespace) {
47+
for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex(); $index++) {
48+
if (!$tokens[$index]->isGivenKind([T_CLASS, T_INTERFACE]) || $tokensAnalyzer->isAnonymousClass($index)) {
49+
continue;
50+
}
51+
52+
$nameIndex = $tokens->getNextTokenOfKind($index, [[T_STRING]]);
53+
$startIndex = $tokens->getNextTokenOfKind($nameIndex, ['{']);
54+
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startIndex);
55+
56+
$name = $tokens[$nameIndex]->getContent();
57+
58+
$this->replaceNameOccurrences($tokens, $namespace->getFullName(), $name, $startIndex, $endIndex);
59+
60+
$index = $endIndex;
61+
}
62+
}
63+
}
64+
65+
public function getPriority(): int
66+
{
67+
return 0;
68+
}
69+
70+
private function replaceNameOccurrences(Tokens $tokens, string $namespace, string $name, int $startIndex, int $endIndex): void
71+
{
72+
for ($index = $startIndex; $index < $endIndex; $index++) {
73+
if (!$tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
74+
continue;
75+
}
76+
77+
$docBlock = new DocBlock($tokens[$index]->getContent());
78+
79+
$fqcn = ($namespace !== '' ? '\\' . $namespace : '') . '\\' . $name;
80+
81+
foreach ($docBlock->getAnnotations() as $annotation) {
82+
if (!$annotation->supportTypes()) {
83+
continue;
84+
}
85+
86+
$types = [];
87+
foreach ($annotation->getTypes() as $type) {
88+
$types[] = \preg_replace(
89+
\sprintf('/(?<![a-zA-Z0-9_\x7f-\xff\\\\])(%s|%s)\b(?!\\\\)/', $name, \preg_quote($fqcn, '/')),
90+
'self',
91+
$type
92+
);
93+
}
94+
95+
if ($types === $annotation->getTypes()) {
96+
continue;
97+
}
98+
$annotation->setTypes($types);
99+
$tokens[$index] = new Token([T_DOC_COMMENT, $docBlock->getContent()]);
100+
}
101+
}
102+
}
103+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Tests\Fixer;
6+
7+
/**
8+
* @internal
9+
*
10+
* @covers \PhpCsFixerCustomFixers\Fixer\PhpdocSelfAccessorFixer
11+
*/
12+
final class PhpdocSelfAccessorFixerTest extends AbstractFixerTestCase
13+
{
14+
public function testPriority(): void
15+
{
16+
static::assertSame(0, $this->fixer->getPriority());
17+
}
18+
19+
public function testIsRisky(): void
20+
{
21+
static::assertFalse($this->fixer->isRisky());
22+
}
23+
24+
/**
25+
* @param string $expected
26+
* @param string|null $input
27+
*
28+
* @dataProvider provideFixCases
29+
*/
30+
public function testFix(string $expected, string $input = null): void
31+
{
32+
$this->doTest($expected, $input);
33+
}
34+
35+
public function provideFixCases(): \Iterator
36+
{
37+
yield [ // no namespace - do not change
38+
'<?php
39+
class Foo {
40+
/**
41+
* @var FooBar
42+
*/
43+
private $instance;
44+
45+
/**
46+
* @param Foo\Bar $x
47+
* @param Bar\Foo $x
48+
* @param int $x count of Foo
49+
* @see Foo documentation
50+
*/
51+
public function bar(...$params) {}
52+
}',
53+
];
54+
55+
yield [ // no namespace - change
56+
'<?php
57+
class Foo {
58+
/**
59+
* @var self
60+
*/
61+
private $instance;
62+
63+
/**
64+
* @param self $x
65+
* @param self $x
66+
* @param bool|self $x
67+
* @param self|int $x
68+
* @param bool|self|int $x
69+
* @param bool|self|int $x
70+
* @param self[] $foos
71+
* @param self[] $foos
72+
* @param Array<int, self> $foos
73+
* @param Array<self, int> $foos
74+
*
75+
* @return self
76+
*/
77+
public function bar(...$params) {}
78+
}',
79+
'<?php
80+
class Foo {
81+
/**
82+
* @var Foo
83+
*/
84+
private $instance;
85+
86+
/**
87+
* @param Foo $x
88+
* @param \Foo $x
89+
* @param bool|Foo $x
90+
* @param Foo|int $x
91+
* @param bool|Foo|int $x
92+
* @param bool|\Foo|int $x
93+
* @param Foo[] $foos
94+
* @param \Foo[] $foos
95+
* @param Array<int, Foo> $foos
96+
* @param Array<\Foo, int> $foos
97+
*
98+
* @return Foo
99+
*/
100+
public function bar(...$params) {}
101+
}',
102+
];
103+
104+
yield [ // with namespace - do not change
105+
'<?php
106+
namespace Some\Thing;
107+
class Foo {
108+
/**
109+
* @param \Foo $x
110+
* @param Some\Foo $x
111+
* @param Thing\Foo $x
112+
* @param Some\Thing\Foo $x
113+
* @param Foo\Some $x
114+
* @param Foo\Some\Thing $x
115+
* @param Foo\Some\Thing\Foo $x
116+
* @param \Foo[] $foos
117+
* @param Array<int, \Foo> $foos
118+
*/
119+
public function bar(...$params) {}
120+
}',
121+
];
122+
123+
yield [ // with namespace - change
124+
'<?php
125+
namespace Some\Thing;
126+
class Foo {
127+
/**
128+
* @param self $x
129+
* @param self $x
130+
* @param bool|self $x
131+
* @param self|int $x
132+
* @param bool|self|int $x
133+
* @param self[] $foos
134+
* @param self[] $foos
135+
* @param Array<int, self> $foos
136+
* @param Array<self, int> $foos
137+
*/
138+
public function bar(...$params) {}
139+
}',
140+
'<?php
141+
namespace Some\Thing;
142+
class Foo {
143+
/**
144+
* @param Foo $x
145+
* @param \Some\Thing\Foo $x
146+
* @param bool|\Some\Thing\Foo $x
147+
* @param \Some\Thing\Foo|int $x
148+
* @param bool|\Some\Thing\Foo|int $x
149+
* @param Foo[] $foos
150+
* @param \Some\Thing\Foo[] $foos
151+
* @param Array<int, Foo> $foos
152+
* @param Array<\Some\Thing\Foo, int> $foos
153+
*/
154+
public function bar(...$params) {}
155+
}',
156+
];
157+
}
158+
}

0 commit comments

Comments
 (0)