Passed
Push — trunk ( 37af04...c71cc1 )
by Christian
13:15 queued 12s
created

InternalClassRule::isInternal()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
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\Migration\MigrationStep;
13
use Shopware\Core\Framework\Plugin;
14
use Shopware\Core\Framework\Test\Api\ApiDefinition\ApiRoute\StoreApiTestOtherRoute;
15
use Shopware\Storefront\Controller\StorefrontController;
16
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
18
19
/**
20
 * @package core
21
 * @implements Rule<InClassNode>
22
 *
23
 * @deprecated tag:v6.5.0 - reason:becomes-internal - will be internal in 6.5.0
24
 */
25
class InternalClassRule implements Rule
26
{
27
    private const TEST_CLASS_EXCEPTIONS = [
28
        StoreApiTestOtherRoute::class, // The test route is used to test the OpenApiGenerator, that class would ignore internal classes
29
    ];
30
31
    private const INTERNAL_NAMESPACES = [
32
        '\\DevOps\\StaticAnalyze',
33
        '\\Framework\\Demodata',
34
    ];
35
36
    public function getNodeType(): string
37
    {
38
        return InClassNode::class;
39
    }
40
41
    /**
42
     * @param InClassNode $node
43
     *
44
     * @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...
45
     */
46
    public function processNode(Node $node, Scope $scope): array
47
    {
48
        if ($this->isInternal($node)) {
49
            return [];
50
        }
51
52
        if ($this->isTestClass($node)) {
53
            return ['Test classes must be flagged @internal to not be captured by the BC checker'];
54
        }
55
56
        if ($this->isStorefrontController($node)) {
57
            return ['Storefront controllers must be flagged @internal to not be captured by the BC checker. The BC promise is checked over the route annotation.'];
58
        }
59
60
        if ($this->isBundle($node)) {
61
            return ['Bundles must be flagged @internal to not be captured by the BC checker.'];
62
        }
63
64
        if ($this->isEventSubscriber($node)) {
65
            $classDeprecation = $node->getClassReflection()->getDeprecatedDescription() ?? '';
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

65
            $classDeprecation = $node->/** @scrutinizer ignore-call */ getClassReflection()->getDeprecatedDescription() ?? '';

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