Passed
Push — trunk ( ef2a1d...661dd3 )
by Christian
12:51 queued 12s
created

InternalClassRule::processNode()   B

Complexity

Conditions 10
Paths 9

Size

Total Lines 43
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

62
            $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...
63
            /**
64
             * @deprecated tag:v6.5.0 - remove deprecation check, as all listener should become internal in v6.5.0
65
             */
66
            if (\str_contains($classDeprecation, 'reason:becomes-internal') || \str_contains($classDeprecation, 'reason:remove-subscriber')) {
67
                return [];
68
            }
69
70
            return ['Event subscribers must be flagged @internal to not be captured by the BC checker.'];
71
        }
72
73
        if ($namespace = $this->isInInternalNamespace($node)) {
74
            $classDeprecation = $node->getClassReflection()->getDeprecatedDescription() ?? '';
75
            /**
76
             * @deprecated tag:v6.5.0 - remove deprecation check, as all classes in internal namespaces should become internal in v6.5.0
77
             */
78
            if (\str_contains($classDeprecation, 'reason:becomes-internal')) {
79
                return [];
80
            }
81
82
            return ['Classes in `' . $namespace . '` namespace must be flagged @internal to not be captured by the BC checker.'];
83
        }
84
85
        return [];
86
    }
87
88
    private function isTestClass(InClassNode $node): bool
89
    {
90
        $namespace = $node->getClassReflection()->getName();
91
92
        if (\in_array($namespace, self::TEST_CLASS_EXCEPTIONS, true)) {
93
            return false;
94
        }
95
96
        if (\str_contains($namespace, '\\Test\\')) {
97
            return true;
98
        }
99
100
        if (\str_contains($namespace, '\\Tests\\')) {
101
            return true;
102
        }
103
104
        if ($node->getClassReflection()->getParentClass() === null) {
105
            return false;
106
        }
107
108
        return $node->getClassReflection()->getParentClass()->getName() === TestCase::class;
109
    }
110
111
    private function isInternal(InClassNode $class): bool
112
    {
113
        $doc = $class->getDocComment();
114
115
        if ($doc === null) {
116
            return false;
117
        }
118
119
        return \str_contains($doc->getText(), '@internal') || \str_contains($doc->getText(), 'reason:becomes-internal');
120
    }
121
122
    private function isStorefrontController(InClassNode $node): bool
123
    {
124
        $class = $node->getClassReflection();
125
126
        if ($class->getParentClass() === null) {
127
            return false;
128
        }
129
130
        return $class->getParentClass()->getName() === StorefrontController::class;
131
    }
132
133
    private function isBundle(InClassNode $node): bool
134
    {
135
        $class = $node->getClassReflection();
136
137
        if ($class->getParentClass() === null) {
138
            return false;
139
        }
140
141
        if ($class->isAnonymous()) {
142
            return false;
143
        }
144
145
        return $class->getParentClass()->getName() === Bundle::class && $class->getName() !== Plugin::class;
146
    }
147
148
    private function isEventSubscriber(InClassNode $node): bool
149
    {
150
        $class = $node->getClassReflection();
151
152
        foreach ($class->getInterfaces() as $interface) {
153
            if ($interface->getName() === EventSubscriberInterface::class) {
154
                return true;
155
            }
156
        }
157
158
        return false;
159
    }
160
161
    private function isInInternalNamespace(InClassNode $node): ?string
162
    {
163
        $namespace = $node->getClassReflection()->getName();
164
165
        foreach (self::INTERNAL_NAMESPACES as $internalNamespace) {
166
            if (\str_contains($namespace, $internalNamespace)) {
167
                return $internalNamespace;
168
            }
169
        }
170
171
        return null;
172
    }
173
}
174