Passed
Push — trunk ( 74f3b3...1ec971 )
by Christian
12:53 queued 12s
created

getMethodContent()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 4
nop 3
dl 0
loc 22
rs 9.9
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\DevOps\StaticAnalyze\PHPStan\Rules\Deprecation;
4
5
use PhpParser\Node;
6
use PhpParser\Node\Stmt\ClassMethod;
7
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...
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\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...
10
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...
11
use PHPUnit\Framework\TestCase;
12
13
/**
14
 * @implements Rule<ClassMethod>
15
 *
16
 * @deprecated tag:v6.5.0 - reason:becomes-internal - will be internal in 6.5.0
17
 */
18
class DeprecatedMethodsThrowDeprecationRule implements Rule
19
{
20
    /**
21
     * There are some exceptions to this rule, where deprecated methods should not throw a deprecation notice.
22
     * This is mainly the reason if the deprecated code is still called from inside the core due to BC reasons.
23
     */
24
    private const RULE_EXCEPTIONS = [
25
        // Subscribers still need to be called for BC reasons, therefore they do not trigger deprecations.
26
        'reason:remove-subscriber',
27
        // Decorators still need to be called for BC reasons, therefore they do not trigger deprecations.
28
        'reason:remove-decorator',
29
        // Entities still need to be present in the DI container, therefore they do not trigger deprecations.
30
        'reason:remove-entity',
31
        // Classes that will be internal are still called from inside the core, therefore they do not trigger deprecations.
32
        'reason:becomes-internal',
33
        // Classes that will be final, can only be changed with the next major
34
        'reason:becomes-final',
35
        // If the return type change, the functionality itself is not deprecated, therefore they do not trigger deprecations.
36
        'reason:return-type-change',
37
        // If there will be in the class hierarchy of a class we mark the whole class as deprecated, but the functionality itself is not deprecated, therefore they do not trigger deprecations.
38
        'reason:class-hierarchy-change',
39
        // If we change the visibility of a method we can't know from where it was called and whether the call will be valid in the future, therefore they do not trigger deprecations.
40
        'reason:visibility-change',
41
    ];
42
43
    public function getNodeType(): string
44
    {
45
        return ClassMethod::class;
46
    }
47
48
    /**
49
     * @param ClassMethod $node
50
     *
51
     * @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...
52
     */
53
    public function processNode(Node $node, Scope $scope): array
54
    {
55
        if (!$scope->isInClass()) {
56
            // skip
57
            return [];
58
        }
59
60
        $class = $scope->getClassReflection();
61
62
        if ($class === null || $class->isInterface() || $this->isTestClass($class)) {
63
            return [];
64
        }
65
66
        if (!($node->isPublic() || $node->isProtected()) || $node->isAbstract() || $node->isMagic()) {
0 ignored issues
show
Bug introduced by
The method isAbstract() does not exist on PhpParser\Node. It seems like you code against a sub-type of PhpParser\Node such as PhpParser\Node\Stmt\ClassMethod or PhpParser\Node\Stmt\Class_ or PhpParser\Node\Stmt\ClassMethod. ( Ignorable by Annotation )

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

66
        if (!($node->isPublic() || $node->isProtected()) || $node->/** @scrutinizer ignore-call */ isAbstract() || $node->isMagic()) {
Loading history...
Bug introduced by
The method isPublic() does not exist on PhpParser\Node. It seems like you code against a sub-type of PhpParser\Node such as PhpParser\Node\Stmt\Property or PhpParser\Node\Stmt\ClassMethod or PhpParser\Node\Stmt\ClassConst or PhpParser\Node\Stmt\ClassMethod. ( Ignorable by Annotation )

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

66
        if (!($node->/** @scrutinizer ignore-call */ isPublic() || $node->isProtected()) || $node->isAbstract() || $node->isMagic()) {
Loading history...
Bug introduced by
The method isProtected() does not exist on PhpParser\Node. It seems like you code against a sub-type of PhpParser\Node such as PhpParser\Node\Stmt\Property or PhpParser\Node\Stmt\ClassMethod or PhpParser\Node\Stmt\ClassConst or PhpParser\Node\Stmt\ClassMethod. ( Ignorable by Annotation )

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

66
        if (!($node->isPublic() || $node->/** @scrutinizer ignore-call */ isProtected()) || $node->isAbstract() || $node->isMagic()) {
Loading history...
Bug introduced by
The method isMagic() does not exist on PhpParser\Node. It seems like you code against a sub-type of PhpParser\Node such as PhpParser\Node\Stmt\ClassMethod or PhpParser\Node\Stmt\ClassMethod. ( Ignorable by Annotation )

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

66
        if (!($node->isPublic() || $node->isProtected()) || $node->isAbstract() || $node->/** @scrutinizer ignore-call */ isMagic()) {
Loading history...
67
            return [];
68
        }
69
70
        $methodContent = $this->getMethodContent($node, $scope, $class);
71
        $method = $class->getMethod($node->name->name, $scope);
0 ignored issues
show
Bug introduced by
Accessing name on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
72
73
        $classDeprecation = $class->getDeprecatedDescription();
74
        if ($classDeprecation && !$this->handlesDeprecationCorrectly($classDeprecation, $methodContent)) {
75
            return [
76
                \sprintf(
77
                    'Class "%s" is marked as deprecated, but method "%s" does not call "Feature::triggerDeprecationOrThrow". All public methods of deprecated classes need to trigger a deprecation warning.',
78
                    $class->getName(),
79
                    $method->getName()
80
                ),
81
            ];
82
        }
83
84
        $methodDeprecation = $method->getDeprecatedDescription() ?? '';
85
86
        // by default deprecations from parent methods are also available on all implementing methods
87
        // we will copy the deprecation to the implementing method, if they also have an affect there
88
        $deprecationOfParentMethod = !str_contains($method->getDocComment() ?? '', $methodDeprecation) && !str_contains($method->getDocComment() ?? '', 'inheritdoc');
89
90
        if (!$deprecationOfParentMethod && $methodDeprecation && !$this->handlesDeprecationCorrectly($methodDeprecation, $methodContent)) {
91
            return [
92
                \sprintf(
93
                    'Method "%s" of class "%s" is marked as deprecated, but does not call "Feature::triggerDeprecationOrThrow". All deprecated methods need to trigger a deprecation warning.',
94
                    $method->getName(),
95
                    $class->getName()
96
                ),
97
            ];
98
        }
99
100
        return [];
101
    }
102
103
    private function getMethodContent(Node $node, Scope $scope, ClassReflection $class): string
104
    {
105
        /** @var string $filename */
106
        $filename = $class->getFileName();
107
108
        $trait = $scope->getTraitReflection();
109
110
        if ($trait) {
111
            /** @var string $filename */
112
            $filename = $trait->getFileName();
113
        }
114
115
        $file = new \SplFileObject($filename);
116
        $file->seek($node->getStartLine() - 1);
117
118
        $content = '';
119
        for ($i = 0; $i <= ($node->getEndLine() - $node->getStartLine()); ++$i) {
120
            $content .= $file->current();
121
            $file->next();
122
        }
123
124
        return $content;
125
    }
126
127
    private function handlesDeprecationCorrectly(string $deprecation, string $method): bool
128
    {
129
        foreach (self::RULE_EXCEPTIONS as $exception) {
130
            if (\str_contains($deprecation, $exception)) {
131
                return true;
132
            }
133
        }
134
135
        return \str_contains($method, 'Feature::triggerDeprecationOrThrow(');
136
    }
137
138
    private function isTestClass(ClassReflection $class): bool
139
    {
140
        $namespace = $class->getName();
141
142
        if (\str_contains($namespace, '\\Test\\')) {
143
            return true;
144
        }
145
146
        if (\str_contains($namespace, '\\Tests\\')) {
147
            return true;
148
        }
149
150
        if ($class->getParentClass() === null) {
151
            return false;
152
        }
153
154
        return $class->getParentClass()->getName() === TestCase::class;
155
    }
156
}
157