Passed
Push — master ( d83336...d7c119 )
by Gytis
01:52
created

FqcnMethodSniff   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 125
dl 0
loc 230
rs 3.44
c 0
b 0
f 0
wmc 62

6 Methods

Rating   Name   Duplication   Size   Complexity  
A process() 0 10 3
A register() 0 6 1
A containsType() 0 5 5
D processSigType() 0 80 25
C hasUselessDocBlock() 0 60 17
B processMethod() 0 45 11

How to fix   Complexity   

Complex Class

Complex classes like FqcnMethodSniff 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 FqcnMethodSniff, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Gskema\TypeSniff\Sniffs\CodeElement;
4
5
use PHP_CodeSniffer\Files\File;
6
use Gskema\TypeSniff\Core\CodeElement\Element\AbstractFqcnMethodElement;
7
use Gskema\TypeSniff\Core\CodeElement\Element\ClassMethodElement;
8
use Gskema\TypeSniff\Core\CodeElement\Element\CodeElementInterface;
9
use Gskema\TypeSniff\Core\CodeElement\Element\InterfaceMethodElement;
10
use Gskema\TypeSniff\Core\DocBlock\DocBlock;
11
use Gskema\TypeSniff\Core\DocBlock\UndefinedDocBlock;
12
use Gskema\TypeSniff\Core\Type\Common\ArrayType;
13
use Gskema\TypeSniff\Core\Type\Common\UndefinedType;
14
use Gskema\TypeSniff\Core\Type\Common\VoidType;
15
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
16
use Gskema\TypeSniff\Core\Type\DocBlock\CompoundType;
17
use Gskema\TypeSniff\Core\Type\DocBlock\NullType;
18
use Gskema\TypeSniff\Core\Type\DocBlock\TypedArrayType;
19
use Gskema\TypeSniff\Core\Type\TypeConverter;
20
use Gskema\TypeSniff\Core\Type\TypeInterface;
21
22
class FqcnMethodSniff implements CodeElementSniffInterface
23
{
24
    /**
25
     * @inheritDoc
26
     */
27
    public function register(): array
28
    {
29
        return [
30
            ClassMethodElement::class,
31
            // TraitMethodElement::class, // can be used to implement interface, not possible to know if it is extended
32
            InterfaceMethodElement::class,
33
        ];
34
    }
35
36
    /**
37
     * @inheritDoc
38
     * @param AbstractFqcnMethodElement $method
39
     */
40
    public function process(File $file, CodeElementInterface $method): void
41
    {
42
        $warningCountBefore = $file->getWarningCount();
43
44
        // @TODO Assert description
45
        $this->processMethod($file, $method);
46
47
        $hasNewWarnings = $file->getWarningCount() > $warningCountBefore;
48
        if (!$hasNewWarnings && $this->hasUselessDocBlock($method)) {
49
            $file->addWarningOnLine('Useless PHPDoc', $method->getLine(), 'FqcnMethodSniff');
50
        }
51
    }
52
53
    protected function processMethod(File $file, AbstractFqcnMethodElement $method): void
54
    {
55
        $fnSig = $method->getSignature();
56
        $docBlock = $method->getDocBlock();
57
        $isMagicMethod = '__' === substr($fnSig->getName(), 0, 2);
58
        $isConstructMethod = '__construct' === $fnSig->getName();
59
        $hasInheritDocTag = $docBlock->hasTag('inheritdoc');
60
61
        // @inheritDoc
62
        // __construct can be detected as extended and magic, but we want to inspect it anyway
63
        if (!$isConstructMethod) {
64
            if ($hasInheritDocTag || $isMagicMethod) {
65
                return;
66
            } elseif ($method->isExtended()) {
67
                $file->addWarningOnLine('Missing @inheritDoc tag', $method->getLine(), 'FqcnMethodSniff');
68
                return;
69
            }
70
        }
71
72
        // @param
73
        foreach ($fnSig->getParams() as $fnParam) {
74
            $paramName = $fnParam->getName();
75
            $tag = $docBlock->getParamTag($paramName);
76
77
            $subject = sprintf('parameter $%s', $paramName);
78
79
            $fnType = $fnParam->getType();
80
            $fnTypeLine = $fnParam->getLine();
81
            $docType = $tag ? $tag->getType() : null;
82
            $docTypeLine = $tag ? $tag->getLine() : $fnTypeLine;
83
84
            $this->processSigType($file, $docBlock, $subject, $fnType, $fnTypeLine, $docType, $docTypeLine);
85
        }
86
87
        // @return
88
        if (!$isConstructMethod) {
89
            $docType = $docBlock->getReturnTag();
90
            $this->processSigType(
91
                $file,
92
                $docBlock,
93
                'return value',
94
                $fnSig->getReturnType(),
95
                $fnSig->getReturnLine(),
96
                $docType ? $docType->getType() : null,
97
                $docType ? $docType->getLine() : $fnSig->getLine()
98
            );
99
        }
100
    }
101
102
    protected function processSigType(
103
        File $file,
104
        DocBlock $docBlock,
105
        string $subject,
106
        TypeInterface $fnType,
107
        int $fnTypeLine,
108
        ?TypeInterface $docType,
109
        int $docTypeLine
110
    ): void {
111
        // @TODO Required mixed[][] instead of array[]
112
113
        $warnings = [];
114
        if ($docBlock instanceof UndefinedDocBlock) {
115
            // doc_block:undefined, fn_type:?
116
            if ($fnType instanceof UndefinedType) {
117
                $warnings[$fnTypeLine] = 'Add type declaration for :subject: or create PHPDoc with type hint';
118
            } elseif ($this->containsType($fnType, ArrayType::class)) {
119
                $warnings[$fnTypeLine] = 'Create PHPDoc with typed array type hint for :subject:, .e.g.: "string[]" or "SomeClass[]"';
120
            }
121
        } elseif (null === $docType) {
122
            // doc_block:defined, doc_tag:missing
123
            if ('return value' === $subject) { // @TODO ??
124
                if (!($fnType instanceof VoidType)) {
125
                    $warnings[$fnTypeLine] = 'Missing PHPDoc tag or void type declaration for :subject:';
126
                }
127
            } else {
128
                $warnings[$fnTypeLine] = 'Missing PHPDoc tag for :subject:';
129
            }
130
        } elseif ($docType instanceof UndefinedType) {
131
            // doc_block:defined, doc_type:undefined
132
            $suggestedFnType = TypeConverter::toExpectedDocType($fnType);
133
            if (null !== $suggestedFnType) {
134
                $warnings[$docTypeLine] = sprintf(
135
                    'Add type hint in PHPDoc tag for :subject:, e.g. "%s"',
136
                    $suggestedFnType->toString()
137
                );
138
            } else {
139
                $warnings[$docTypeLine] = 'Add type hint in PHPDoc tag for :subject:';
140
            }
141
        } elseif ($fnType instanceof UndefinedType) {
142
            // doc_block:defined, doc_type:defined, fn_type:undefined
143
            if ($docType instanceof NullType) {
144
                $warnings[$fnTypeLine] = sprintf('Add type declaration for :subject:');
145
            } elseif ($suggestedFnType = TypeConverter::toFunctionType($docType)) {
146
                $warnings[$fnTypeLine] = sprintf('Add type declaration for :subject:, e.g.: "%s"', $suggestedFnType->toString());
147
            }
148
        } elseif ($this->containsType($fnType, ArrayType::class)) {
149
            // doc_block:defined, doc_type:defined, fn_type:array
150
            $docHasTypedArray = $this->containsType($docType, TypedArrayType::class);
151
            $docHasArray = $this->containsType($docType, ArrayType::class);
152
153
            if ($docHasTypedArray && $docHasArray) {
154
                $warnings[$docTypeLine] = 'Remove array type, typed array type is present in PHPDoc for :subject:.';
155
            } elseif (!$docHasTypedArray && $docHasArray) {
156
                $warnings[$docTypeLine] = 'Replace array type with typed array type in PHPDoc for :subject:. Use mixed[] for generic arrays.';
157
            } elseif (!$docHasTypedArray && !$docHasArray) {
158
                $warnings[$docTypeLine] = 'Add typed array type in PHPDoc for :subject:. Use mixed[] for generic arrays.';
159
            }
160
        } elseif ($fnType instanceof NullableType) {
161
            // doc_block:defined, doc_type:defined, fn_type:nullable
162
            if (!$this->containsType($docType, NullType::class)) {
163
                $warnings[$docTypeLine] = 'Add "null" type hint in PHPDoc for :subject:';
164
            }
165
        } else {
166
            // doc_block:defined, doc_type:defined, fn_type:defined
167
            $expectedDocType = TypeConverter::toExpectedDocType($fnType);
168
            $expectedDocTypes = $expectedDocType instanceof CompoundType
169
                ? $expectedDocType->getTypes()
170
                : array_filter([$expectedDocType]);
171
172
            foreach ($expectedDocTypes as $expectedDocType) {
173
                if (!$this->containsType($docType, get_class($expectedDocType))) {
174
                    $warnings[$docTypeLine] = sprintf('Add "%s" type hint in PHPDoc for :subject:', $fnType->toString());
175
                }
176
            }
177
        }
178
179
        foreach ($warnings as $line => $warningTpl) {
180
            $warning = str_replace(':subject:', $subject, $warningTpl);
181
            $file->addWarningOnLine($warning, $line, 'FqcnMethodSniff');
182
        }
183
    }
184
185
    protected function containsType(TypeInterface $type, string $typeClassName): bool
186
    {
187
        return is_a($type, $typeClassName)
188
            || ($type instanceof CompoundType && $type->containsType($typeClassName))
189
            || ($type instanceof NullableType && $type->containsType($typeClassName));
190
    }
191
192
    protected function hasUselessDocBlock(AbstractFqcnMethodElement $method): bool
193
    {
194
        $fnSig = $method->getSignature();
195
        $docBlock = $method->getDocBlock();
196
197
        $usefulTagNames = ['deprecated', 'throws', 'dataprovider', 'see', 'todo', 'inheritdoc']; // @TODO ??
198
        $usefulTagNames = array_diff($usefulTagNames, ['param', 'return']);
199
200
        $docReturnTag = $docBlock->getReturnTag();
201
202
        if ($docBlock instanceof UndefinedDocBlock
203
            || $docBlock->hasDescription()
204
            // || $docBlock->hasContentLines()
205
            || $docBlock->hasOneOfTags($usefulTagNames)
206
            || ($docReturnTag && $docReturnTag->hasDescription())
207
        ) {
208
            return false;
209
        }
210
211
        $paramTags = $docBlock->getParamTags();
212
        foreach ($paramTags as $paramTag) {
213
            if ($paramTag->hasDescription()) {
214
                return false;
215
            }
216
        }
217
218
        foreach ($fnSig->getParams() as $fnParam) {
219
            $paramTag = $docBlock->getParamTag($fnParam->getName());
220
            if (null === $paramTag) {
221
                continue;
222
            }
223
224
            if ($paramTag->hasDescription()) {
225
                return false;
226
            }
227
228
            // @TODO Cleaner way?
229
            $fnType = $fnParam->getType();
230
            $rawFnType = $fnType instanceof NullableType
231
                ? $fnType->toDocString()
232
                : $fnType->toString();
233
            if ($paramTag->getType()->toString() !== $rawFnType) {
234
                return false;
235
            }
236
        }
237
238
        $returnTag  = $docBlock->getReturnTag();
239
        $returnType = $fnSig->getReturnType();
240
241
        // @TODO ??
242
        if ($returnTag && $returnType) {
243
            $rawReturnType = $returnType instanceof NullableType
244
                ? $returnType->toDocString()
245
                : $returnType->toString();
246
            if ($returnTag->getType()->toString() !== $rawReturnType) {
247
                return false;
248
            }
249
        }
250
251
        return true;
252
    }
253
}
254