Passed
Push — trunk ( ad06ea...8f56e6 )
by Christian
23:45 queued 17s
created

InternalClassRule   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 92
c 0
b 0
f 0
dl 0
loc 221
rs 8.5599
wmc 48

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A isMigrationStep() 0 9 2
A isStorefrontController() 0 9 2
C processNode() 0 47 15
A isFinal() 0 3 3
A isInternal() 0 3 2
A getNodeType() 0 3 1
A isBundle() 0 13 4
A isInNamespace() 0 3 1
A isMessageHandler() 0 10 2
A isParentInternalAndAbstract() 0 17 4
A isEventSubscriber() 0 11 3
A isInInternalNamespace() 0 11 3
A isTestClass() 0 21 5

How to fix   Complexity   

Complex Class

Complex classes like InternalClassRule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use InternalClassRule, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\DevOps\StaticAnalyze\PHPStan\Rules\Internal;
4
5
use PhpParser\Node;
6
use PHPStan\Analyser\Scope;
0 ignored issues
show
Bug introduced by
The type PHPStan\Analyser\Scope was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use PHPStan\Node\InClassNode;
0 ignored issues
show
Bug introduced by
The type PHPStan\Node\InClassNode was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use PHPStan\Reflection\ClassReflection;
0 ignored issues
show
Bug introduced by
The type PHPStan\Reflection\ClassReflection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use PHPStan\Reflection\ReflectionProvider;
0 ignored issues
show
Bug introduced by
The type PHPStan\Reflection\ReflectionProvider was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use PHPStan\Rules\Rule;
0 ignored issues
show
Bug introduced by
The type PHPStan\Rules\Rule was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use PHPStan\Rules\RuleError;
0 ignored issues
show
Bug introduced by
The type PHPStan\Rules\RuleError was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use PHPUnit\Framework\TestCase;
13
use Shopware\Core\Framework\Bundle;
14
use Shopware\Core\Framework\DataAbstractionLayer\Command\RefreshIndexCommand;
15
use Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexerRegistry;
16
use Shopware\Core\Framework\Demodata\Command\DemodataCommand;
17
use Shopware\Core\Framework\Demodata\DemodataContext;
18
use Shopware\Core\Framework\Demodata\DemodataGeneratorInterface;
19
use Shopware\Core\Framework\Demodata\DemodataRequest;
20
use Shopware\Core\Framework\Demodata\DemodataService;
21
use Shopware\Core\Framework\Demodata\Event\DemodataRequestCreatedEvent;
22
use Shopware\Core\Framework\Log\Package;
23
use Shopware\Core\Framework\Migration\MigrationStep;
24
use Shopware\Core\Framework\Plugin;
25
use Shopware\Core\Framework\Test\Api\ApiDefinition\ApiRoute\StoreApiTestOtherRoute;
26
use Shopware\Storefront\Controller\StorefrontController;
27
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
28
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
29
30
/**
31
 * @implements Rule<InClassNode>
32
 *
33
 * @internal
34
 */
35
#[Package('core')]
36
class InternalClassRule implements Rule
37
{
38
    private const TEST_CLASS_EXCEPTIONS = [
39
        StoreApiTestOtherRoute::class, // The test route is used to test the OpenApiGenerator, that class would ignore internal classes
40
    ];
41
42
    private const INTERNAL_NAMESPACES = [
43
        '\\DevOps\\StaticAnalyze',
44
    ];
45
    private const SUBSCRIBER_EXCEPTIONS = [
46
        RefreshIndexCommand::class,
47
    ];
48
    private const MESSAGE_HANDLER_EXCEPTIONS = [
49
        EntityIndexerRegistry::class,
50
    ];
51
    private const DEMO_DATA_EXCEPTIONS = [
52
        DemodataContext::class,
53
        DemodataGeneratorInterface::class,
54
        DemodataRequest::class,
55
        DemodataService::class,
56
        DemodataCommand::class,
57
        DemodataRequestCreatedEvent::class,
58
    ];
59
60
    private ReflectionProvider $reflectionProvider;
61
62
    public function __construct(ReflectionProvider $reflectionProvider)
63
    {
64
        $this->reflectionProvider = $reflectionProvider;
65
    }
66
67
    public function getNodeType(): string
68
    {
69
        return InClassNode::class;
70
    }
71
72
    /**
73
     * @param InClassNode $node
74
     *
75
     * @return array<array-key, RuleError|string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, RuleError|string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, RuleError|string>.
Loading history...
76
     */
77
    public function processNode(Node $node, Scope $scope): array
78
    {
79
        $doc = $node->getDocComment()?->getText() ?? '';
80
81
        if ($this->isInternal($doc)) {
82
            return [];
83
        }
84
85
        $class = $node->getClassReflection()->getName();
0 ignored issues
show
Bug introduced by
The method getClassReflection() does not exist on PhpParser\Node. ( Ignorable by Annotation )

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

85
        $class = $node->/** @scrutinizer ignore-call */ getClassReflection()->getName();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
86
87
        if ($this->isTestClass($node)) {
88
            return [\sprintf('Test classes (%s) must be flagged @internal to not be captured by the BC checker', $node->getClassReflection()->getName())];
89
        }
90
91
        if ($this->isStorefrontController($node)) {
92
            return ['Storefront controllers must be flagged @internal to not be captured by the BC checker. The BC promise is checked over the route annotation.'];
93
        }
94
95
        if ($this->isBundle($node)) {
96
            return ['Bundles must be flagged @internal to not be captured by the BC checker.'];
97
        }
98
99
        if ($this->isEventSubscriber($node) && !$this->isFinal($node->getClassReflection(), $doc) && !\in_array($class, self::SUBSCRIBER_EXCEPTIONS, true)) {
100
            return ['Event subscribers must be flagged @internal to not be captured by the BC checker.'];
101
        }
102
103
        if ($namespace = $this->isInInternalNamespace($node)) {
104
            return ['Classes in `' . $namespace . '` namespace must be flagged @internal to not be captured by the BC checker.'];
105
        }
106
107
        if ($this->isInNamespace($node, '\\Framework\\Demodata') && !\in_array($class, self::DEMO_DATA_EXCEPTIONS, true)) {
108
            return ['Classes in `Framework\\Demodata` namespace must be flagged @internal to not be captured by the BC checker.'];
109
        }
110
111
        if ($this->isMigrationStep($node)) {
112
            return ['Migrations must be flagged @internal to not be captured by the BC checker.'];
113
        }
114
115
        if ($this->isMessageHandler($node) && !\in_array($class, self::MESSAGE_HANDLER_EXCEPTIONS, true)) {
116
            return ['MessageHandlers must be flagged @internal to not be captured by the BC checker.'];
117
        }
118
119
        if ($this->isParentInternalAndAbstract($scope)) {
120
            return ['Classes that extend an @internal abstract class must be flagged @internal to not be captured by the BC checker.'];
121
        }
122
123
        return [];
124
    }
125
126
    private function isTestClass(InClassNode $node): bool
127
    {
128
        $namespace = $node->getClassReflection()->getName();
129
130
        if (\in_array($namespace, self::TEST_CLASS_EXCEPTIONS, true)) {
131
            return false;
132
        }
133
134
        if (\str_contains($namespace, '\\Test\\')) {
135
            return true;
136
        }
137
138
        if (\str_contains($namespace, '\\Tests\\')) {
139
            return true;
140
        }
141
142
        if ($node->getClassReflection()->getParentClass() === null) {
143
            return false;
144
        }
145
146
        return $node->getClassReflection()->getParentClass()->getName() === TestCase::class;
147
    }
148
149
    private function isInternal(string $doc): bool
150
    {
151
        return \str_contains($doc, '@internal') || \str_contains($doc, 'reason:becomes-internal');
152
    }
153
154
    private function isStorefrontController(InClassNode $node): bool
155
    {
156
        $class = $node->getClassReflection();
157
158
        if ($class->getParentClass() === null) {
159
            return false;
160
        }
161
162
        return $class->getParentClass()->getName() === StorefrontController::class;
163
    }
164
165
    private function isBundle(InClassNode $node): bool
166
    {
167
        $class = $node->getClassReflection();
168
169
        if ($class->getParentClass() === null) {
170
            return false;
171
        }
172
173
        if ($class->isAnonymous()) {
174
            return false;
175
        }
176
177
        return $class->getParentClass()->getName() === Bundle::class && $class->getName() !== Plugin::class;
178
    }
179
180
    private function isEventSubscriber(InClassNode $node): bool
181
    {
182
        $class = $node->getClassReflection();
183
184
        foreach ($class->getInterfaces() as $interface) {
185
            if ($interface->getName() === EventSubscriberInterface::class) {
186
                return true;
187
            }
188
        }
189
190
        return false;
191
    }
192
193
    private function isInInternalNamespace(InClassNode $node): ?string
194
    {
195
        $namespace = $node->getClassReflection()->getName();
196
197
        foreach (self::INTERNAL_NAMESPACES as $internalNamespace) {
198
            if (\str_contains($namespace, $internalNamespace)) {
199
                return $internalNamespace;
200
            }
201
        }
202
203
        return null;
204
    }
205
206
    private function isInNamespace(InClassNode $node, string $namespace): bool
207
    {
208
        return \str_contains($node->getClassReflection()->getName(), $namespace);
209
    }
210
211
    private function isMigrationStep(InClassNode $node): bool
212
    {
213
        $class = $node->getClassReflection();
214
215
        if ($class->getParentClass() === null) {
216
            return false;
217
        }
218
219
        return $class->getParentClass()->getName() === MigrationStep::class;
220
    }
221
222
    private function isMessageHandler(InClassNode $node): bool
223
    {
224
        $class = $node->getClassReflection()->getNativeReflection();
225
226
        if ($class->isAbstract()) {
227
            // abstract base classes should not be final
228
            return false;
229
        }
230
231
        return !empty($class->getAttributes(AsMessageHandler::class));
232
    }
233
234
    private function isFinal(ClassReflection $class, string $doc): bool
235
    {
236
        return str_contains($doc, '@final') || str_contains($doc, 'reason:becomes-final') || $class->isFinal();
237
    }
238
239
    private function isParentInternalAndAbstract(Scope $scope): bool
240
    {
241
        $parent = $scope->getClassReflection()->getParentClass();
242
243
        if ($parent === null) {
244
            return false;
245
        }
246
247
        if (!$parent->isAbstract()) {
248
            return false;
249
        }
250
251
        $native = $parent->getNativeReflection();
252
253
        $doc = $native->getDocComment() ?: '';
254
255
        return $this->isInternal($doc);
256
    }
257
}
258