Passed
Push — 6.5.0.0 ( b63557...68c1a6 )
by Christian
14:12 queued 23s
created

InternalClassRule::processNode()   C

Complexity

Conditions 14
Paths 10

Size

Total Lines 41
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 20
nc 10
nop 2
dl 0
loc 41
rs 6.2666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\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...
9
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...
10
use PHPUnit\Framework\TestCase;
11
use Shopware\Core\Framework\Bundle;
12
use Shopware\Core\Framework\DataAbstractionLayer\Command\RefreshIndexCommand;
13
use Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexerRegistry;
14
use Shopware\Core\Framework\Demodata\Command\DemodataCommand;
15
use Shopware\Core\Framework\Demodata\DemodataContext;
16
use Shopware\Core\Framework\Demodata\DemodataGeneratorInterface;
17
use Shopware\Core\Framework\Demodata\DemodataRequest;
18
use Shopware\Core\Framework\Demodata\DemodataService;
19
use Shopware\Core\Framework\Demodata\Event\DemodataRequestCreatedEvent;
20
use Shopware\Core\Framework\Log\Package;
21
use Shopware\Core\Framework\Migration\MigrationStep;
22
use Shopware\Core\Framework\Plugin;
23
use Shopware\Core\Framework\Test\Api\ApiDefinition\ApiRoute\StoreApiTestOtherRoute;
24
use Shopware\Storefront\Controller\StorefrontController;
25
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
26
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
27
28
/**
29
 * @implements Rule<InClassNode>
30
 *
31
 * @internal
32
 */
33
#[Package('core')]
34
class InternalClassRule implements Rule
35
{
36
    private const TEST_CLASS_EXCEPTIONS = [
37
        StoreApiTestOtherRoute::class, // The test route is used to test the OpenApiGenerator, that class would ignore internal classes
38
    ];
39
40
    private const INTERNAL_NAMESPACES = [
41
        '\\DevOps\\StaticAnalyze',
42
    ];
43
    private const SUBSCRIBER_EXCEPTIONS = [
44
        RefreshIndexCommand::class,
45
    ];
46
    private const MESSAGE_HANDLER_EXCEPTIONS = [
47
        EntityIndexerRegistry::class,
48
    ];
49
    private const DEMO_DATA_EXCEPTIONS = [
50
        DemodataContext::class,
51
        DemodataGeneratorInterface::class,
52
        DemodataRequest::class,
53
        DemodataService::class,
54
        DemodataCommand::class,
55
        DemodataRequestCreatedEvent::class,
56
    ];
57
58
    public function getNodeType(): string
59
    {
60
        return InClassNode::class;
61
    }
62
63
    /**
64
     * @param InClassNode $node
65
     *
66
     * @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...
67
     */
68
    public function processNode(Node $node, Scope $scope): array
69
    {
70
        if ($this->isInternal($node)) {
71
            return [];
72
        }
73
74
        $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

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