Skip to content

Commit 19f823c

Browse files
Slamdunkkeradus
authored andcommitted
Add FinalPublicMethodForAbstractClassFixer
1 parent 9f68642 commit 19f823c

File tree

3 files changed

+345
-0
lines changed

3 files changed

+345
-0
lines changed

README.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,12 @@ 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_public_method_for_abstract_class**
681+
682+
All public methods of abstract classes should be final.
683+
684+
*Risky rule: risky when overriding public methods of abstract classes.*
685+
680686
* **final_static_access**
681687

682688
Converts ``static`` access to ``self`` access in final classes.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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 Filippo Tessarotto <zoeslam@gmail.com>
23+
*/
24+
final class FinalPublicMethodForAbstractClassFixer extends AbstractFixer
25+
{
26+
/**
27+
* @var array
28+
*/
29+
private $magicMethods = [
30+
'__construct' => true,
31+
'__destruct' => true,
32+
'__call' => true,
33+
'__callstatic' => true,
34+
'__get' => true,
35+
'__set' => true,
36+
'__isset' => true,
37+
'__unset' => true,
38+
'__sleep' => true,
39+
'__wakeup' => true,
40+
'__tostring' => true,
41+
'__invoke' => true,
42+
'__set_state' => true,
43+
'__clone' => true,
44+
'__debuginfo' => true,
45+
];
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function getDefinition()
51+
{
52+
return new FixerDefinition(
53+
'All public methods of abstract classes should be final.',
54+
[
55+
new CodeSample(
56+
'<?php
57+
58+
abstract class AbstractMachine
59+
{
60+
public function start()
61+
{}
62+
}
63+
'
64+
),
65+
],
66+
'Enforce API encapsulation in an inheritance architecture. '
67+
.'If you want to override a method, use the Template method pattern.',
68+
'Risky when overriding public methods of abstract classes'
69+
);
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function isCandidate(Tokens $tokens)
76+
{
77+
return $tokens->isAllTokenKindsFound([T_CLASS, T_ABSTRACT, T_PUBLIC, T_FUNCTION]);
78+
}
79+
80+
/**
81+
* {@inheritdoc}
82+
*/
83+
public function isRisky()
84+
{
85+
return true;
86+
}
87+
88+
/**
89+
* {@inheritdoc}
90+
*/
91+
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
92+
{
93+
$classes = array_keys($tokens->findGivenKind(T_CLASS));
94+
95+
while ($classIndex = array_pop($classes)) {
96+
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($classIndex)];
97+
if (!$prevToken->isGivenKind([T_ABSTRACT])) {
98+
continue;
99+
}
100+
101+
$classOpen = $tokens->getNextTokenOfKind($classIndex, ['{']);
102+
$classClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
103+
104+
$this->fixClass($tokens, $classOpen, $classClose);
105+
}
106+
}
107+
108+
/**
109+
* @param int $classOpenIndex
110+
* @param int $classCloseIndex
111+
*/
112+
private function fixClass(Tokens $tokens, $classOpenIndex, $classCloseIndex)
113+
{
114+
for ($index = $classCloseIndex - 1; $index > $classOpenIndex; --$index) {
115+
// skip method contents
116+
if ($tokens[$index]->equals('}')) {
117+
$index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
118+
119+
continue;
120+
}
121+
122+
// skip non public methods
123+
if (!$tokens[$index]->isGivenKind(T_PUBLIC)) {
124+
continue;
125+
}
126+
$nextIndex = $tokens->getNextMeaningfulToken($index);
127+
$nextToken = $tokens[$nextIndex];
128+
if ($nextToken->isGivenKind(T_STATIC)) {
129+
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
130+
$nextToken = $tokens[$nextIndex];
131+
}
132+
133+
// skip uses, attributes, constants etc
134+
if (!$nextToken->isGivenKind(T_FUNCTION)) {
135+
continue;
136+
}
137+
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
138+
$nextToken = $tokens[$nextIndex];
139+
140+
// skip magic methods
141+
if (isset($this->magicMethods[strtolower($nextToken->getContent())])) {
142+
continue;
143+
}
144+
145+
$prevIndex = $tokens->getPrevMeaningfulToken($index);
146+
$prevToken = $tokens[$prevIndex];
147+
if ($prevToken->isGivenKind(T_STATIC)) {
148+
$index = $prevIndex;
149+
$prevIndex = $tokens->getPrevMeaningfulToken($index);
150+
$prevToken = $tokens[$prevIndex];
151+
}
152+
// skip already final methods
153+
if ($prevToken->isGivenKind([T_FINAL])) {
154+
$index = $prevIndex;
155+
156+
continue;
157+
}
158+
159+
$tokens->insertAt(
160+
$index,
161+
[
162+
new Token([T_FINAL, 'final']),
163+
new Token([T_WHITESPACE, ' ']),
164+
]
165+
);
166+
}
167+
}
168+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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 Filippo Tessarotto <zoeslam@gmail.com>
19+
*
20+
* @internal
21+
*
22+
* @covers \PhpCsFixer\Fixer\ClassNotation\FinalPublicMethodForAbstractClassFixer
23+
*/
24+
final class FinalPublicMethodForAbstractClassFixerTest extends AbstractFixerTestCase
25+
{
26+
/**
27+
* @param string $expected PHP source code
28+
* @param null|string $input PHP source code
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+
$original = $fixed = $this->getClassElementStubs();
40+
$fixed = str_replace('public function f1', 'final public function f1', $fixed);
41+
$fixed = str_replace('public static function f4', 'final public static function f4', $fixed);
42+
$fixed = str_replace('static public function f7', 'final static public function f7', $fixed);
43+
44+
return [
45+
'regular-class' => ["<?php class MyClass { {$original} }"],
46+
'final-class' => ["<?php final class MyClass { {$original} }"],
47+
'trait' => ["<?php trait MyClass { {$original} }"],
48+
'interface' => [
49+
'<?php interface MyClass {
50+
public function f1();
51+
public static function f4();
52+
static public function f7();
53+
}',
54+
],
55+
'magic-methods' => [
56+
'<?php abstract class MyClass {
57+
public function __construct() {}
58+
public function __destruct() {}
59+
public function __call($a, $b) {}
60+
public static function __callStatic($a, $b) {}
61+
public function __get($a) {}
62+
public function __set($a, $b) {}
63+
public function __isset($a) {}
64+
public function __unset($a) {}
65+
public function __sleep() {}
66+
public function __wakeup() {}
67+
public function __toString() {}
68+
public function __invoke() {}
69+
public function __set_state() {}
70+
public function __clone() {}
71+
public function __debugInfo() {}
72+
}',
73+
],
74+
'magic-methods-casing' => [
75+
'<?php abstract class MyClass {
76+
public function __Construct() {}
77+
public function __SET($a, $b) {}
78+
public function __ToString() {}
79+
public function __DeBuGiNfO() {}
80+
}',
81+
],
82+
'non magic-methods' => [
83+
'<?php abstract class MyClass {
84+
final public function __foo() {}
85+
final public static function __bar($a, $b) {}
86+
}',
87+
'<?php abstract class MyClass {
88+
public function __foo() {}
89+
public static function __bar($a, $b) {}
90+
}',
91+
],
92+
'abstract-class' => [
93+
"<?php abstract class MyClass { {$fixed} }",
94+
"<?php abstract class MyClass { {$original} }",
95+
],
96+
];
97+
}
98+
99+
/**
100+
* @param string $expected PHP source code
101+
* @param null|string $input PHP source code
102+
*
103+
* @dataProvider provideFix70Cases
104+
* @requires PHP 7.0
105+
*/
106+
public function testFix70($expected, $input = null)
107+
{
108+
$this->doTest($expected, $input);
109+
}
110+
111+
public function provideFix70Cases()
112+
{
113+
return [
114+
'anonymous-class' => [
115+
sprintf(
116+
'<?php abstract class MyClass { private function test() { $a = new class { %s }; } }',
117+
$this->getClassElementStubs()
118+
),
119+
],
120+
];
121+
}
122+
123+
/**
124+
* @param string $expected PHP source code
125+
* @param null|string $input PHP source code
126+
*
127+
* @dataProvider provideFix72Cases
128+
* @requires PHP 7.2
129+
*/
130+
public function testFix72($expected, $input = null)
131+
{
132+
$this->doTest($expected, $input);
133+
}
134+
135+
public function provideFix72Cases()
136+
{
137+
return [
138+
'constant visibility' => [
139+
'<?php abstract class MyClass {
140+
public const A = 1;
141+
protected const B = 2;
142+
private const C = 3;
143+
}',
144+
],
145+
];
146+
}
147+
148+
/**
149+
* @return string
150+
*/
151+
private function getClassElementStubs()
152+
{
153+
return '
154+
public $a1;
155+
protected $a2;
156+
private $a3;
157+
public static $a4;
158+
protected static $a5;
159+
private static $a6;
160+
public function f1(){}
161+
protected function f2(){}
162+
private function f3(){}
163+
public static function f4(){}
164+
protected static function f5(){}
165+
private static function f6(){}
166+
static public function f7(){}
167+
static protected function f8(){}
168+
static private function f9(){}
169+
';
170+
}
171+
}

0 commit comments

Comments
 (0)