VisibilityRequiredFixer::isCandidate()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <[email protected]>
9
 *     Dariusz Rumiński <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace PhpCsFixer\Fixer\ClassNotation;
16
17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
22
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23
use PhpCsFixer\FixerDefinition\CodeSample;
24
use PhpCsFixer\FixerDefinition\FixerDefinition;
25
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
26
use PhpCsFixer\FixerDefinition\VersionSpecification;
27
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
28
use PhpCsFixer\Tokenizer\CT;
29
use PhpCsFixer\Tokenizer\Token;
30
use PhpCsFixer\Tokenizer\Tokens;
31
use PhpCsFixer\Tokenizer\TokensAnalyzer;
32
33
/**
34
 * Fixer for rules defined in PSR2 ¶4.3, ¶4.5.
35
 *
36
 * @author Dariusz Rumiński <[email protected]>
37
 * @author SpacePossum
38
 */
39
final class VisibilityRequiredFixer extends AbstractFixer implements ConfigurableFixerInterface
40
{
41
    /**
42
     * {@inheritdoc}
43
     */
44
    public function getDefinition(): FixerDefinitionInterface
45
    {
46
        return new FixerDefinition(
47
            'Visibility MUST be declared on all properties and methods; `abstract` and `final` MUST be declared before the visibility; `static` MUST be declared after the visibility.',
48
            [
49
                new CodeSample(
50
                    '<?php
51
class Sample
52
{
53
    var $a;
54
    static protected $var_foo2;
55
56
    function A()
57
    {
58
    }
59
}
60
'
61
                ),
62
                new VersionSpecificCodeSample(
63
                    '<?php
64
class Sample
65
{
66
    const SAMPLE = 1;
67
}
68
',
69
                    new VersionSpecification(70100),
70
                    ['elements' => ['const']]
71
                ),
72
            ]
73
        );
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function isCandidate(Tokens $tokens): bool
80
    {
81
        return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
88
    {
89
        return new FixerConfigurationResolver([
90
            (new FixerOptionBuilder('elements', 'The structural elements to fix (PHP >= 7.1 required for `const`).'))
91
                ->setAllowedTypes(['array'])
92
                ->setAllowedValues([new AllowedValueSubset(['property', 'method', 'const'])])
93
                ->setDefault(['property', 'method', 'const'])
94
                ->getOption(),
95
        ]);
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
102
    {
103
        $tokensAnalyzer = new TokensAnalyzer($tokens);
104
        $propertyTypeDeclarationKinds = [T_STRING, T_NS_SEPARATOR, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, CT::T_TYPE_ALTERNATION];
105
106
        foreach (array_reverse($tokensAnalyzer->getClassyElements(), true) as $index => $element) {
107
            if (!\in_array($element['type'], $this->configuration['elements'], true)) {
108
                continue;
109
            }
110
111
            if (\PHP_VERSION_ID < 70100 && 'const' === $element['type']) {
112
                continue;
113
            }
114
115
            $abstractFinalIndex = null;
116
            $visibilityIndex = null;
117
            $staticIndex = null;
118
            $typeIndex = null;
119
            $prevIndex = $tokens->getPrevMeaningfulToken($index);
120
            $expectedKinds = [T_ABSTRACT, T_FINAL, T_PRIVATE, T_PROTECTED, T_PUBLIC, T_STATIC, T_VAR];
121
122
            if ('property' === $element['type']) {
123
                $expectedKinds = array_merge($expectedKinds, $propertyTypeDeclarationKinds);
124
            }
125
126
            while ($tokens[$prevIndex]->isGivenKind($expectedKinds)) {
127
                if ($tokens[$prevIndex]->isGivenKind([T_ABSTRACT, T_FINAL])) {
128
                    $abstractFinalIndex = $prevIndex;
129
                } elseif ($tokens[$prevIndex]->isGivenKind(T_STATIC)) {
130
                    $staticIndex = $prevIndex;
131
                } elseif ($tokens[$prevIndex]->isGivenKind($propertyTypeDeclarationKinds)) {
132
                    $typeIndex = $prevIndex;
133
                } else {
134
                    $visibilityIndex = $prevIndex;
135
                }
136
137
                $prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
0 ignored issues
show
Bug introduced by
It seems like $prevIndex can also be of type null; however, parameter $index of PhpCsFixer\Tokenizer\Tok...etPrevMeaningfulToken() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

137
                $prevIndex = $tokens->getPrevMeaningfulToken(/** @scrutinizer ignore-type */ $prevIndex);
Loading history...
138
            }
139
140
            if (null !== $typeIndex) {
141
                $index = $typeIndex;
142
            }
143
144
            if ($tokens[$prevIndex]->equals(',')) {
145
                continue;
146
            }
147
148
            if (null !== $staticIndex) {
149
                if ($this->isKeywordPlacedProperly($tokens, $staticIndex, $index)) {
150
                    $index = $staticIndex;
151
                } else {
152
                    $this->moveTokenAndEnsureSingleSpaceFollows($tokens, $staticIndex, $index);
153
                }
154
            }
155
156
            if (null === $visibilityIndex) {
157
                $tokens->insertAt($index, [new Token([T_PUBLIC, 'public']), new Token([T_WHITESPACE, ' '])]);
158
            } else {
159
                if ($tokens[$visibilityIndex]->isGivenKind(T_VAR)) {
160
                    $tokens[$visibilityIndex] = new Token([T_PUBLIC, 'public']);
161
                }
162
                if ($this->isKeywordPlacedProperly($tokens, $visibilityIndex, $index)) {
163
                    $index = $visibilityIndex;
164
                } else {
165
                    $this->moveTokenAndEnsureSingleSpaceFollows($tokens, $visibilityIndex, $index);
166
                }
167
            }
168
169
            if (null === $abstractFinalIndex) {
170
                continue;
171
            }
172
173
            if ($this->isKeywordPlacedProperly($tokens, $abstractFinalIndex, $index)) {
174
                continue;
175
            }
176
177
            $this->moveTokenAndEnsureSingleSpaceFollows($tokens, $abstractFinalIndex, $index);
178
        }
179
    }
180
181
    private function isKeywordPlacedProperly(Tokens $tokens, int $keywordIndex, int $comparedIndex): bool
182
    {
183
        return $keywordIndex + 2 === $comparedIndex && ' ' === $tokens[$keywordIndex + 1]->getContent();
184
    }
185
186
    private function moveTokenAndEnsureSingleSpaceFollows(Tokens $tokens, int $fromIndex, int $toIndex): void
187
    {
188
        $tokens->insertAt($toIndex, [$tokens[$fromIndex], new Token([T_WHITESPACE, ' '])]);
189
190
        $tokens->clearAt($fromIndex);
191
        if ($tokens[$fromIndex + 1]->isWhitespace()) {
192
            $tokens->clearAt($fromIndex + 1);
193
        }
194
    }
195
}
196