Skip to content

Commit 58e7a8d

Browse files
ntzmSpacePossum
authored andcommitted
FinalStaticAccessFixer - Introduction
1 parent 22d8f59 commit 58e7a8d

File tree

6 files changed

+374
-0
lines changed

6 files changed

+374
-0
lines changed

README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,10 @@ Choose from the list of available rules:
677677
- ``consider-absent-docblock-as-internal-class`` (``bool``): should classes
678678
without any DocBlock be fixed to final?; defaults to ``false``
679679

680+
* **final_static_access**
681+
682+
Converts ``static`` access to ``self`` access in final classes.
683+
680684
* **fopen_flag_order** [@Symfony:risky, @PhpCsFixer:risky]
681685

682686
Order the flags in ``fopen`` calls, ``b`` and ``t`` must be last.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PHP CS Fixer.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
8+
*
9+
* This source file is subject to the MIT license that is bundled
10+
* with this source code in the file LICENSE.
11+
*/
12+
13+
namespace PhpCsFixer\Fixer\ClassNotation;
14+
15+
use PhpCsFixer\AbstractFixer;
16+
use PhpCsFixer\FixerDefinition\CodeSample;
17+
use PhpCsFixer\FixerDefinition\FixerDefinition;
18+
use PhpCsFixer\Tokenizer\Token;
19+
use PhpCsFixer\Tokenizer\Tokens;
20+
21+
/**
22+
* @author ntzm
23+
*/
24+
final class FinalStaticAccessFixer extends AbstractFixer
25+
{
26+
/**
27+
* {@inheritdoc}
28+
*/
29+
public function getDefinition()
30+
{
31+
return new FixerDefinition(
32+
'Converts `static` access to `self` access in final classes.',
33+
[
34+
new CodeSample(
35+
'<?php
36+
final class Sample
37+
{
38+
public function getFoo()
39+
{
40+
return static::class;
41+
}
42+
}
43+
'
44+
),
45+
]
46+
);
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
public function getPriority()
53+
{
54+
// Should be run after FinalInternalClass and PhpUnitTestCaseStaticMethodCalls
55+
return -1;
56+
}
57+
58+
/**
59+
* {@inheritdoc}
60+
*/
61+
public function isCandidate(Tokens $tokens)
62+
{
63+
return $tokens->isAllTokenKindsFound([T_FINAL, T_CLASS, T_STATIC]);
64+
}
65+
66+
/**
67+
* {@inheritdoc}
68+
*/
69+
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
70+
{
71+
for ($index = $tokens->count() - 1; 0 <= $index; --$index) {
72+
if (!$tokens[$index]->isGivenKind(T_FINAL)) {
73+
continue;
74+
}
75+
76+
$classTokenIndex = $tokens->getNextMeaningfulToken($index);
77+
78+
if (!$tokens[$classTokenIndex]->isGivenKind(T_CLASS)) {
79+
continue;
80+
}
81+
82+
$startClassIndex = $tokens->getNextTokenOfKind(
83+
$classTokenIndex,
84+
['{']
85+
);
86+
87+
$endClassIndex = $tokens->findBlockEnd(
88+
Tokens::BLOCK_TYPE_CURLY_BRACE,
89+
$startClassIndex
90+
);
91+
92+
$this->replaceStaticAccessWithSelfAccessBetween(
93+
$tokens,
94+
$startClassIndex,
95+
$endClassIndex
96+
);
97+
}
98+
}
99+
100+
/**
101+
* @param Tokens $tokens
102+
* @param int $startIndex
103+
* @param int $endIndex
104+
*/
105+
private function replaceStaticAccessWithSelfAccessBetween(
106+
Tokens $tokens,
107+
$startIndex,
108+
$endIndex
109+
) {
110+
for ($index = $startIndex; $index <= $endIndex; ++$index) {
111+
if ($tokens[$index]->isGivenKind(T_CLASS)) {
112+
$index = $this->getEndOfAnonymousClass($tokens, $index);
113+
114+
continue;
115+
}
116+
117+
if (!$tokens[$index]->isGivenKind(T_STATIC)) {
118+
continue;
119+
}
120+
121+
$doubleColonIndex = $tokens->getNextMeaningfulToken($index);
122+
123+
if (!$tokens[$doubleColonIndex]->isGivenKind(T_DOUBLE_COLON)) {
124+
continue;
125+
}
126+
127+
$tokens[$index] = new Token([T_STRING, 'self']);
128+
}
129+
}
130+
131+
/**
132+
* @param Tokens $tokens
133+
* @param int $index
134+
*
135+
* @return int
136+
*/
137+
private function getEndOfAnonymousClass(Tokens $tokens, $index)
138+
{
139+
$instantiationBraceStart = $tokens->getNextMeaningfulToken($index);
140+
141+
if ($tokens[$instantiationBraceStart]->equals('(')) {
142+
$index = $tokens->findBlockEnd(
143+
Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
144+
$instantiationBraceStart
145+
);
146+
}
147+
148+
$bodyBraceStart = $tokens->getNextTokenOfKind($index, ['{']);
149+
150+
return $tokens->findBlockEnd(
151+
Tokens::BLOCK_TYPE_CURLY_BRACE,
152+
$bodyBraceStart
153+
);
154+
}
155+
}

tests/AutoReview/FixerFactoryTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public function provideFixersPriorityCases()
8989
[$fixers['escape_implicit_backslashes'], $fixers['heredoc_to_nowdoc']],
9090
[$fixers['escape_implicit_backslashes'], $fixers['single_quote']],
9191
[$fixers['explicit_string_variable'], $fixers['simple_to_complex_string_variable']],
92+
[$fixers['final_internal_class'], $fixers['final_static_access']],
9293
[$fixers['final_internal_class'], $fixers['self_static_accessor']],
9394
[$fixers['fully_qualified_strict_types'], $fixers['no_superfluous_phpdoc_tags']],
9495
[$fixers['function_to_constant'], $fixers['native_function_casing']],
@@ -171,6 +172,7 @@ public function provideFixersPriorityCases()
171172
[$fixers['php_unit_no_expectation_annotation'], $fixers['no_empty_phpdoc']],
172173
[$fixers['php_unit_no_expectation_annotation'], $fixers['php_unit_expectation']],
173174
[$fixers['php_unit_dedicate_assert'], $fixers['php_unit_dedicate_assert_internal_type']],
175+
[$fixers['php_unit_test_case_static_method_calls'], $fixers['final_static_access']],
174176
[$fixers['phpdoc_add_missing_param_annotation'], $fixers['no_empty_phpdoc']],
175177
[$fixers['phpdoc_add_missing_param_annotation'], $fixers['phpdoc_align']],
176178
[$fixers['phpdoc_add_missing_param_annotation'], $fixers['phpdoc_order']],
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PHP CS Fixer.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
8+
*
9+
* This source file is subject to the MIT license that is bundled
10+
* with this source code in the file LICENSE.
11+
*/
12+
13+
namespace PhpCsFixer\Tests\Fixer\ClassNotation;
14+
15+
use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
16+
17+
/**
18+
* @author ntzm
19+
*
20+
* @internal
21+
*
22+
* @covers \PhpCsFixer\Fixer\ClassNotation\FinalStaticAccessFixer
23+
*/
24+
final class FinalStaticAccessFixerTest extends AbstractFixerTestCase
25+
{
26+
/**
27+
* @param string $expected
28+
* @param null|string $input
29+
*
30+
* @dataProvider provideFixCases
31+
*/
32+
public function testFix($expected, $input = null)
33+
{
34+
$this->doTest($expected, $input);
35+
}
36+
37+
public function provideFixCases()
38+
{
39+
return [
40+
'in method as class' => [
41+
'<?php final class A { public function b() { echo self::class; } }',
42+
'<?php final class A { public function b() { echo static::class; } }',
43+
],
44+
'handles alternate case' => [
45+
'<?php final class A { public function b() { echo self::class; } }',
46+
'<?php final class A { public function b() { echo sTaTiC::class; } }',
47+
],
48+
'in method as property' => [
49+
'<?php final class A { public function b() { echo self::$c; } }',
50+
'<?php final class A { public function b() { echo static::$c; } }',
51+
],
52+
'in method as call' => [
53+
'<?php final class A { public function b() { echo self::c(); } }',
54+
'<?php final class A { public function b() { echo static::c(); } }',
55+
],
56+
'in method as const' => [
57+
'<?php final class A { public function b() { echo self::C; } }',
58+
'<?php final class A { public function b() { echo static::C; } }',
59+
],
60+
'does not change non-final classes' => [
61+
'<?php class A { public function b() { echo static::c(); } }',
62+
],
63+
'does not change static property' => [
64+
'<?php final class A { public static $b = null; }',
65+
],
66+
'does not change static method' => [
67+
'<?php final class A { public static function b() {} }',
68+
],
69+
'does not change static variables' => [
70+
'<?php final class A { public function b() { static $c = null; } }',
71+
],
72+
'does not change static lambda' => [
73+
'<?php final class A { public function b() { $c = static function () {}; } }',
74+
],
75+
'in multiple methods and classes' => [
76+
'<?php
77+
final class X {
78+
public function b() { echo self::class; }
79+
public function c() { echo self::class; }
80+
}
81+
final class Y {
82+
public static $var = 2;
83+
public function b() { echo self::class; }
84+
public function c() { static $a = 0; echo self::class.$a; }
85+
}
86+
class Foo {
87+
public function b() { echo static::class; }
88+
public function c() { echo static::class; }
89+
}
90+
final class Z {
91+
public function b() { echo self::class; }
92+
public function c() { return static function(){}; }
93+
}
94+
',
95+
'<?php
96+
final class X {
97+
public function b() { echo static::class; }
98+
public function c() { echo static::class; }
99+
}
100+
final class Y {
101+
public static $var = 2;
102+
public function b() { echo static::class; }
103+
public function c() { static $a = 0; echo static::class.$a; }
104+
}
105+
class Foo {
106+
public function b() { echo static::class; }
107+
public function c() { echo static::class; }
108+
}
109+
final class Z {
110+
public function b() { echo static::class; }
111+
public function c() { return static function(){}; }
112+
}
113+
',
114+
],
115+
];
116+
}
117+
118+
/**
119+
* @param string $expected
120+
* @param null|string $input
121+
*
122+
* @dataProvider provideFix70Cases
123+
* @requires PHP 7.0
124+
*/
125+
public function testFix70($expected, $input = null)
126+
{
127+
$this->doTest($expected, $input);
128+
}
129+
130+
public function provideFix70Cases()
131+
{
132+
return [
133+
'property' => [
134+
'<?php final class A { public $b = self::class; }',
135+
'<?php final class A { public $b = static::class; }',
136+
],
137+
'does not change non-final classes' => [
138+
'<?php class A { public $b = static::class; }',
139+
],
140+
'does not change anonymous classes' => [
141+
'<?php $a = new class { public $b = static::class; };',
142+
],
143+
'handles comments' => [
144+
'<?php /*a*/final/*b*/class/*c*/A { public $b = /*1*/self/*2*/::/*3*/class; }',
145+
'<?php /*a*/final/*b*/class/*c*/A { public $b = /*1*/static/*2*/::/*3*/class; }',
146+
],
147+
'property and nested anonymous class' => [
148+
'<?php final class A { public $b = self::class; public function foo(){ return new class { public $b = static::class; }; }}',
149+
'<?php final class A { public $b = static::class; public function foo(){ return new class { public $b = static::class; }; }}',
150+
],
151+
'property and nested anonymous class with set function' => [
152+
'<?php final class A { public $b = self::class; public function foo(){ return new class ($a = function () {}) { public $b = static::class; }; }}',
153+
'<?php final class A { public $b = static::class; public function foo(){ return new class ($a = function () {}) { public $b = static::class; }; }}',
154+
],
155+
'property and nested anonymous class with set anonymous class' => [
156+
'<?php final class A { public $b = self::class; public function foo(){ return new class ($a = new class {}) { public $b = static::class; }; }}',
157+
'<?php final class A { public $b = static::class; public function foo(){ return new class ($a = new class {}) { public $b = static::class; }; }}',
158+
],
159+
'property and nested anonymous class with change after' => [
160+
'<?php final class A { public function foo(){ return new class { public $b = static::class; }; } public $b = self::class; }',
161+
'<?php final class A { public function foo(){ return new class { public $b = static::class; }; } public $b = static::class; }',
162+
],
163+
'property and nested anonymous class with extends' => [
164+
'<?php final class A { public $b = self::class; public function foo(){ return new class extends X implements Y { public $b = static::class; }; }}',
165+
'<?php final class A { public $b = static::class; public function foo(){ return new class extends X implements Y { public $b = static::class; }; }}',
166+
],
167+
];
168+
}
169+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
Integration of fixers: final_internal_class,final_static_access.
3+
--RULESET--
4+
{"final_internal_class": true, "final_static_access": true}
5+
--EXPECT--
6+
<?php
7+
/**
8+
* @internal
9+
*/
10+
final class A {
11+
public function b() {
12+
return self::class;
13+
}
14+
}
15+
16+
--INPUT--
17+
<?php
18+
/**
19+
* @internal
20+
*/
21+
class A {
22+
public function b() {
23+
return static::class;
24+
}
25+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Integration of fixers: php_unit_test_case_static_method_calls,final_static_access.
3+
--RULESET--
4+
{"php_unit_test_case_static_method_calls": true, "final_static_access": true}
5+
--EXPECT--
6+
<?php
7+
final class A extends TestCase {
8+
public function testA() {
9+
return self::assertSame(1, 1);
10+
}
11+
}
12+
13+
--INPUT--
14+
<?php
15+
final class A extends TestCase {
16+
public function testA() {
17+
return $this->assertSame(1, 1);
18+
}
19+
}

0 commit comments

Comments
 (0)