Skip to content

Commit 5b1e11f

Browse files
Slamdunkkeradus
authored andcommitted
Add FinalClassFixer
1 parent 110bc11 commit 5b1e11f

File tree

4 files changed

+178
-8
lines changed

4 files changed

+178
-8
lines changed

README.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,12 @@ Choose from the list of available rules:
639639
Converts implicit variables into explicit ones in double-quoted strings
640640
or heredoc syntax.
641641

642+
* **final_class**
643+
644+
All classes must be final, except abstract ones and Doctrine entities.
645+
646+
*Risky rule: risky when subclassing non-abstract classes.*
647+
642648
* **final_internal_class** [@PhpCsFixer:risky]
643649

644650
Internal classes should be ``final``.
@@ -649,10 +655,13 @@ Choose from the list of available rules:
649655

650656
- ``annotation-black-list`` (``array``): class level annotations tags that must be
651657
omitted to fix the class, even if all of the white list ones are used
652-
as well. (case insensitive); defaults to ``['@final', '@Entity', '@ORM']``
658+
as well. (case insensitive); defaults to ``['@final', '@Entity',
659+
'@ORM\\Entity']``
653660
- ``annotation-white-list`` (``array``): class level annotations tags that must be
654661
set in order to fix the class. (case insensitive); defaults to
655662
``['@internal']``
663+
- ``consider-absent-docblock-as-internal-class`` (``bool``): should classes
664+
without any DocBlock be fixed to final?; defaults to ``false``
656665

657666
* **fopen_flag_order** [@Symfony:risky, @PhpCsFixer:risky]
658667

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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\AbstractProxyFixer;
16+
use PhpCsFixer\FixerDefinition\CodeSample;
17+
use PhpCsFixer\FixerDefinition\FixerDefinition;
18+
19+
/**
20+
* @author Filippo Tessarotto <zoeslam@gmail.com>
21+
*/
22+
final class FinalClassFixer extends AbstractProxyFixer
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function getDefinition()
28+
{
29+
return new FixerDefinition(
30+
'All classes must be final, except abstract ones and Doctrine entities.',
31+
[
32+
new CodeSample(
33+
'<?php
34+
class MyApp {}
35+
'
36+
),
37+
],
38+
'No exception and no configuration are intentional. Beside Doctrine entities and of course abstract classes, there is no single reason not to declare all classes final. '
39+
.'If you want to subclass a class, mark the parent class as abstract and create two child classes, one empty if necessary: you\'ll gain much more fine grained type-hinting. '
40+
.'If you need to mock a standalone class, create an interface, or maybe it\'s a value-object that shouldn\'t be mocked at all. '
41+
.'If you need to extend a standalone class, create an interface and use the Composite pattern. '
42+
.'If you aren\'t ready yet for serious OOP, go with FinalInternalClassFixer, it\'s fine.',
43+
'Risky when subclassing non-abstract classes.'
44+
);
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
protected function createProxyFixers()
51+
{
52+
$fixer = new FinalInternalClassFixer();
53+
$fixer->configure([
54+
'annotation-white-list' => [],
55+
'consider-absent-docblock-as-internal-class' => true,
56+
]);
57+
58+
return [$fixer];
59+
}
60+
}

src/Fixer/ClassNotation/FinalInternalClassFixer.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
2121
use PhpCsFixer\FixerDefinition\CodeSample;
2222
use PhpCsFixer\FixerDefinition\FixerDefinition;
23+
use PhpCsFixer\Preg;
2324
use PhpCsFixer\Tokenizer\Token;
2425
use PhpCsFixer\Tokenizer\Tokens;
2526
use Symfony\Component\OptionsResolver\Options;
@@ -73,7 +74,7 @@ public function getDefinition()
7374
*/
7475
public function isCandidate(Tokens $tokens)
7576
{
76-
return $tokens->isAllTokenKindsFound([T_CLASS, T_DOC_COMMENT]);
77+
return $tokens->isTokenKindFound(T_CLASS);
7778
}
7879

7980
/**
@@ -143,9 +144,13 @@ protected function createConfigurationDefinition()
143144
(new FixerOptionBuilder('annotation-black-list', 'Class level annotations tags that must be omitted to fix the class, even if all of the white list ones are used as well. (case insensitive)'))
144145
->setAllowedTypes(['array'])
145146
->setAllowedValues($annotationsAsserts)
146-
->setDefault(['@final', '@Entity', '@ORM'])
147+
->setDefault(['@final', '@Entity', '@ORM\Entity'])
147148
->setNormalizer($annotationsNormalizer)
148149
->getOption(),
150+
(new FixerOptionBuilder('consider-absent-docblock-as-internal-class', 'Should classes without any DocBlock be fixed to final?'))
151+
->setAllowedTypes(['bool'])
152+
->setDefault(false)
153+
->getOption(),
149154
]);
150155
}
151156

@@ -157,23 +162,26 @@ protected function createConfigurationDefinition()
157162
*/
158163
private function isClassCandidate(Tokens $tokens, $index)
159164
{
160-
if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind([T_ABSTRACT, T_FINAL])) {
165+
if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind([T_ABSTRACT, T_FINAL, T_NEW])) {
161166
return false; // ignore class; it is abstract or already final
162167
}
163168

164169
$docToken = $tokens[$tokens->getPrevNonWhitespace($index)];
165170

166171
if (!$docToken->isGivenKind(T_DOC_COMMENT)) {
167-
return false; // ignore class; it has no class-level PHPDoc
172+
return $this->configuration['consider-absent-docblock-as-internal-class'];
168173
}
169174

170175
$doc = new DocBlock($docToken->getContent());
171176
$tags = [];
172177

173178
foreach ($doc->getAnnotations() as $annotation) {
174-
$tag = strtolower($annotation->getTag()->getName());
175-
if (isset($this->configuration['annotation-black-list'][$tag])) {
176-
return false; // ignore class: class-level PHPDoc contains tag that has been black listed through configuration
179+
Preg::match('/@\S+(?=\s|$)/', $annotation->getContent(), $matches);
180+
$tag = strtolower(substr(array_shift($matches), 1));
181+
foreach ($this->configuration['annotation-black-list'] as $tagStart => $true) {
182+
if (0 === strpos($tag, $tagStart)) {
183+
return false; // ignore class: class-level PHPDoc contains tag that has been black listed through configuration
184+
}
177185
}
178186

179187
$tags[$tag] = true;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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\FinalClassFixer
23+
*/
24+
final class FinalClassFixerTest 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+
['<?php /** @Entity */ class MyEntity {}'],
41+
['<?php use Doctrine\ORM\Mapping as ORM; /** @ORM\Entity */ class MyEntity {}'],
42+
['<?php /** @entity */ class MyEntity {}'],
43+
['<?php use Doctrine\ORM\Mapping as ORM; /** @orm\entity */ class MyEntity {}'],
44+
['<?php abstract class MyAbstract {}'],
45+
['<?php trait MyTrait {}'],
46+
['<?php interface MyInterface {}'],
47+
['<?php echo Exception::class;'],
48+
[
49+
'<?php final class MyClass {}',
50+
'<?php class MyClass {}',
51+
],
52+
[
53+
'<?php final class MyClass extends MyAbstract {}',
54+
'<?php class MyClass extends MyAbstract {}',
55+
],
56+
[
57+
'<?php final class MyClass implements MyInterface {}',
58+
'<?php class MyClass implements MyInterface {}',
59+
],
60+
[
61+
'<?php /** @codeCoverageIgnore */ final class MyEntity {}',
62+
'<?php /** @codeCoverageIgnore */ class MyEntity {}',
63+
],
64+
[
65+
'<?php final class A {} abstract class B {} final class C {}',
66+
'<?php class A {} abstract class B {} class C {}',
67+
],
68+
[
69+
'<?php /** @internal Map my app to an @Entity */ final class MyMapper {}',
70+
'<?php /** @internal Map my app to an @Entity */ class MyMapper {}',
71+
],
72+
];
73+
}
74+
75+
/**
76+
* @param string $expected PHP source code
77+
* @param null|string $input PHP source code
78+
*
79+
* @dataProvider provideFix70Cases
80+
* @requires PHP 7.0
81+
*/
82+
public function testFix70($expected, $input = null)
83+
{
84+
$this->doTest($expected, $input);
85+
}
86+
87+
public function provideFix70Cases()
88+
{
89+
return [
90+
['<?php $anonymClass = new class {};'],
91+
];
92+
}
93+
}

0 commit comments

Comments
 (0)