Completed
Push — master ( 368282...c4d7dc )
by Gytis
03:56
created

FqcnMethodSniff   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 261
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 141
dl 0
loc 261
ccs 0
cts 189
cp 0
rs 3.2
c 0
b 0
f 0
wmc 65

7 Methods

Rating   Name   Duplication   Size   Complexity  
A containsType() 0 5 5
D processSigType() 0 80 25
D hasUselessDocBlock() 0 62 18
A process() 0 10 3
B processMethod() 0 45 11
A configure() 0 11 2
A register() 0 6 1

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