Skip to content

Commit 52ac5e4

Browse files
broaden protected-visibility rule to all Shopsys code (#4618)
2 parents 1fdb518 + d43761b commit 52ac5e4

40 files changed

Lines changed: 682 additions & 165 deletions
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopsys\CodingStandards\CsFixer;
6+
7+
use Override;
8+
use PhpCsFixer\Fixer\ClassNotation\FinalClassFixer;
9+
use PhpCsFixer\Fixer\FixerInterface;
10+
use PhpCsFixer\FixerDefinition\FixerDefinition;
11+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
12+
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
13+
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
14+
use PhpCsFixer\Tokenizer\Analyzer\FullyQualifiedNameAnalyzer;
15+
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
16+
use PhpCsFixer\Tokenizer\Token;
17+
use PhpCsFixer\Tokenizer\Tokens;
18+
use SplFileInfo;
19+
20+
abstract class AbstractFinalClassByParentFixer implements FixerInterface
21+
{
22+
abstract protected function getDescription(): string;
23+
24+
/**
25+
* @return array<class-string>
26+
*/
27+
abstract protected function getMatchingParentClasses(): array;
28+
29+
#[Override]
30+
public function getDefinition(): FixerDefinitionInterface
31+
{
32+
return new FixerDefinition($this->getDescription(), []);
33+
}
34+
35+
#[Override]
36+
public function isCandidate(Tokens $tokens): bool
37+
{
38+
return $tokens->isTokenKindFound(T_EXTENDS) || $tokens->isTokenKindFound(T_IMPLEMENTS);
39+
}
40+
41+
#[Override]
42+
public function isRisky(): bool
43+
{
44+
return false;
45+
}
46+
47+
#[Override]
48+
public function fix(SplFileInfo $file, Tokens $tokens): void
49+
{
50+
$namespacesAnalyzer = new NamespacesAnalyzer();
51+
$namespaces = $namespacesAnalyzer->getDeclarations($tokens);
52+
53+
foreach ($namespaces as $namespace) {
54+
$this->fixNamespace($tokens, $namespace);
55+
}
56+
}
57+
58+
#[Override]
59+
public function getPriority(): int
60+
{
61+
return (new FinalClassFixer())->getPriority() + 1;
62+
}
63+
64+
#[Override]
65+
public function supports(SplFileInfo $file): bool
66+
{
67+
return true;
68+
}
69+
70+
protected function fixNamespace(Tokens $tokens, NamespaceAnalysis $namespace): void
71+
{
72+
$fullyQualifiedNameAnalyzer = new FullyQualifiedNameAnalyzer($tokens);
73+
74+
for ($index = $namespace->getScopeEndIndex(); $index >= $namespace->getScopeStartIndex(); --$index) {
75+
$token = $tokens[$index];
76+
77+
if (!$token->isGivenKind(T_CLASS)) {
78+
continue;
79+
}
80+
81+
if ($this->hasClassModifier($tokens, $index, [T_ABSTRACT, T_FINAL])) {
82+
continue;
83+
}
84+
85+
$classOpenIndex = $tokens->getNextTokenOfKind($index, ['{']);
86+
87+
if ($classOpenIndex === null) {
88+
continue;
89+
}
90+
91+
if (!$this->hasMatchingParent($tokens, $fullyQualifiedNameAnalyzer, $index, $classOpenIndex)) {
92+
continue;
93+
}
94+
95+
$tokens->insertAt($index, new Token([T_FINAL, 'final']));
96+
$tokens->insertAt($index + 1, new Token([T_WHITESPACE, ' ']));
97+
}
98+
}
99+
100+
/**
101+
* @param int[] $modifiers
102+
*/
103+
protected function hasClassModifier(Tokens $tokens, int $classIndex, array $modifiers): bool
104+
{
105+
$prevIndex = $tokens->getPrevMeaningfulToken($classIndex);
106+
107+
while ($prevIndex !== null && $tokens[$prevIndex]->isGivenKind(T_READONLY)) {
108+
$prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
109+
}
110+
111+
return $prevIndex !== null && $tokens[$prevIndex]->isGivenKind($modifiers);
112+
}
113+
114+
protected function hasMatchingParent(
115+
Tokens $tokens,
116+
FullyQualifiedNameAnalyzer $fullyQualifiedNameAnalyzer,
117+
int $classIndex,
118+
int $classOpenIndex,
119+
): bool {
120+
foreach ([T_EXTENDS, T_IMPLEMENTS] as $parentTokenKind) {
121+
$parentTokenIndex = $tokens->getNextTokenOfKind($classIndex, [[$parentTokenKind]]);
122+
123+
if ($parentTokenIndex === null || $parentTokenIndex > $classOpenIndex) {
124+
continue;
125+
}
126+
127+
$parentIndex = $tokens->getNextMeaningfulToken($parentTokenIndex);
128+
129+
while ($parentIndex !== null && $parentIndex < $classOpenIndex) {
130+
if (!$this->isClassNameToken($tokens[$parentIndex])) {
131+
break;
132+
}
133+
134+
$parentName = $this->getClassNameFromTokens($tokens, $parentIndex);
135+
$fullyQualifiedParentName = $fullyQualifiedNameAnalyzer->getFullyQualifiedName(
136+
$parentName,
137+
$parentIndex,
138+
NamespaceUseAnalysis::TYPE_CLASS,
139+
);
140+
141+
if (in_array($fullyQualifiedParentName, $this->getMatchingParentClasses(), true)) {
142+
return true;
143+
}
144+
145+
while ($parentIndex !== null && $parentIndex < $classOpenIndex && !$tokens[$parentIndex]->equals(',')) {
146+
$parentIndex = $tokens->getNextMeaningfulToken($parentIndex);
147+
}
148+
149+
if ($parentIndex === null || $parentIndex >= $classOpenIndex) {
150+
break;
151+
}
152+
153+
$parentIndex = $tokens->getNextMeaningfulToken($parentIndex);
154+
}
155+
}
156+
157+
return false;
158+
}
159+
160+
protected function getClassNameFromTokens(Tokens $tokens, int $startIndex): string
161+
{
162+
$className = '';
163+
$index = $startIndex;
164+
165+
while ($index !== null && $this->isClassNameToken($tokens[$index])) {
166+
$className .= $tokens[$index]->getContent();
167+
$index = $tokens->getNextMeaningfulToken($index);
168+
}
169+
170+
return $className;
171+
}
172+
173+
protected function isClassNameToken(Token $token): bool
174+
{
175+
return $token->isGivenKind([
176+
T_STRING,
177+
T_NS_SEPARATOR,
178+
T_NAME_FULLY_QUALIFIED,
179+
T_NAME_QUALIFIED,
180+
T_NAME_RELATIVE,
181+
]);
182+
}
183+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopsys\CodingStandards\CsFixer;
6+
7+
use Override;
8+
9+
class FinalCompilerPassFixer extends AbstractFinalClassByParentFixer
10+
{
11+
#[Override]
12+
protected function getDescription(): string
13+
{
14+
return 'Compiler passes implementing CompilerPassInterface must be final.';
15+
}
16+
17+
#[Override]
18+
protected function getMatchingParentClasses(): array
19+
{
20+
return [
21+
'Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface',
22+
];
23+
}
24+
25+
#[Override]
26+
public function getName(): string
27+
{
28+
return 'Shopsys/final_compiler_pass';
29+
}
30+
}

src/CsFixer/FinalFormTypeFixer.php

Lines changed: 8 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -5,168 +5,27 @@
55
namespace Shopsys\CodingStandards\CsFixer;
66

77
use Override;
8-
use PhpCsFixer\Fixer\ClassNotation\FinalClassFixer;
9-
use PhpCsFixer\Fixer\FixerInterface;
10-
use PhpCsFixer\FixerDefinition\FixerDefinition;
11-
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
12-
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
13-
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
14-
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
15-
use PhpCsFixer\Tokenizer\Token;
16-
use PhpCsFixer\Tokenizer\Tokens;
17-
use SplFileInfo;
188

19-
class FinalFormTypeFixer implements FixerInterface
9+
class FinalFormTypeFixer extends AbstractFinalClassByParentFixer
2010
{
2111
#[Override]
22-
public function getDefinition(): FixerDefinitionInterface
12+
protected function getDescription(): string
2313
{
24-
return new FixerDefinition(
25-
'Form types extending AbstractType or AbstractTypeExtension must be final.',
26-
[],
27-
);
14+
return 'Form types extending AbstractType or AbstractTypeExtension must be final.';
2815
}
2916

3017
#[Override]
31-
public function isCandidate(Tokens $tokens): bool
18+
protected function getMatchingParentClasses(): array
3219
{
33-
return $tokens->isTokenKindFound(T_EXTENDS);
34-
}
35-
36-
#[Override]
37-
public function isRisky(): bool
38-
{
39-
return false;
40-
}
41-
42-
#[Override]
43-
public function fix(SplFileInfo $file, Tokens $tokens): void
44-
{
45-
$namespacesAnalyzer = new NamespacesAnalyzer();
46-
$namespaces = $namespacesAnalyzer->getDeclarations($tokens);
47-
48-
// Process each namespace (including global namespace)
49-
foreach ($namespaces as $namespace) {
50-
$this->fixNamespace($tokens, $namespace);
51-
}
52-
}
53-
54-
private function fixNamespace(Tokens $tokens, NamespaceAnalysis $namespace): void
55-
{
56-
$usesAnalyzer = new NamespaceUsesAnalyzer();
57-
$uses = $usesAnalyzer->getDeclarationsInNamespace($tokens, $namespace);
58-
59-
// Process tokens in reverse order to avoid index shifting
60-
for ($index = $namespace->getScopeEndIndex(); $index >= $namespace->getScopeStartIndex(); --$index) {
61-
$token = $tokens[$index];
62-
63-
if (!$token->isGivenKind(T_CLASS)) {
64-
continue;
65-
}
66-
67-
// Check if this is an abstract class or already final
68-
$prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($index);
69-
70-
if ($prevMeaningfulIndex !== null) {
71-
$prevToken = $tokens[$prevMeaningfulIndex];
72-
73-
if ($prevToken->isGivenKind([T_ABSTRACT, T_FINAL])) {
74-
continue;
75-
}
76-
}
77-
78-
// Find extends keyword after class declaration
79-
$extendsIndex = $tokens->getNextTokenOfKind($index, [[T_EXTENDS]]);
80-
81-
if ($extendsIndex === null) {
82-
continue;
83-
}
84-
85-
// Get the parent class name
86-
$parentClassIndex = $tokens->getNextMeaningfulToken($extendsIndex);
87-
88-
if ($parentClassIndex === null) {
89-
continue;
90-
}
91-
92-
$parentName = $this->getClassNameFromTokens($tokens, $parentClassIndex);
93-
$resolvedParentName = $this->resolveClassNameWithUses($parentName, $uses);
94-
95-
if (!$this->isFormTypeClass($resolvedParentName)) {
96-
continue;
97-
}
98-
99-
// Insert "final" before class keyword
100-
$tokens->insertAt($index, new Token([T_FINAL, 'final']));
101-
$tokens->insertAt($index + 1, new Token([T_WHITESPACE, ' ']));
102-
}
20+
return [
21+
'Symfony\Component\Form\AbstractType',
22+
'Symfony\Component\Form\AbstractTypeExtension',
23+
];
10324
}
10425

10526
#[Override]
10627
public function getName(): string
10728
{
10829
return 'Shopsys/final_form_type';
10930
}
110-
111-
#[Override]
112-
public function getPriority(): int
113-
{
114-
// Before native FinalClassFixer
115-
return (new FinalClassFixer())->getPriority() + 1;
116-
}
117-
118-
#[Override]
119-
public function supports(SplFileInfo $file): bool
120-
{
121-
return true;
122-
}
123-
124-
private function getClassNameFromTokens(Tokens $tokens, int $startIndex): string
125-
{
126-
$className = '';
127-
$index = $startIndex;
128-
129-
// Collect all parts of the class name (including namespace separators)
130-
while ($index !== null && $tokens[$index]->isGivenKind([T_STRING, T_NS_SEPARATOR])) {
131-
$className .= $tokens[$index]->getContent();
132-
$index = $tokens->getNextMeaningfulToken($index);
133-
}
134-
135-
return $className;
136-
}
137-
138-
/**
139-
* Resolve class name using use statements
140-
*
141-
* @param \PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis[] $uses
142-
*/
143-
private function resolveClassNameWithUses(string $className, array $uses): string
144-
{
145-
// Already fully qualified - return as is
146-
if (str_starts_with($className, '\\')) {
147-
return $className;
148-
}
149-
150-
// Check for exact match in use statements
151-
foreach ($uses as $use) {
152-
if ($className === $use->getShortName()) {
153-
return $use->getFullName();
154-
}
155-
}
156-
157-
// Not found in imports - return as is (could be relative to current namespace)
158-
return $className;
159-
}
160-
161-
private function isFormTypeClass(string $className): bool
162-
{
163-
return in_array($className, [
164-
'AbstractType',
165-
'AbstractTypeExtension',
166-
'Symfony\Component\Form\AbstractType',
167-
'Symfony\Component\Form\AbstractTypeExtension',
168-
'\Symfony\Component\Form\AbstractType',
169-
'\Symfony\Component\Form\AbstractTypeExtension',
170-
], true);
171-
}
17231
}

0 commit comments

Comments
 (0)