Completed
Push — master ( 0dc62e...17dafe )
by Gytis
02:45 queued 11s
created

FqcnMethodSniff   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 294
Duplicated Lines 0 %

Test Coverage

Coverage 97.89%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 146
c 7
b 0
f 0
dl 0
loc 294
ccs 139
cts 142
cp 0.9789
rs 3.04
wmc 67

8 Methods

Rating   Name   Duplication   Size   Complexity  
A containsType() 0 5 5
F processSigType() 0 106 26
A process() 0 10 3
A implodeTypes() 0 8 3
B processMethod() 0 45 11
A configure() 0 11 2
A register() 0 6 1
C hasUselessDocBlock() 0 54 16

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 Gskema\TypeSniff\Core\Type\TypeComparator;
6
use PHP_CodeSniffer\Files\File;
7
use Gskema\TypeSniff\Core\CodeElement\Element\AbstractFqcnMethodElement;
8
use Gskema\TypeSniff\Core\CodeElement\Element\ClassMethodElement;
9
use Gskema\TypeSniff\Core\CodeElement\Element\CodeElementInterface;
10
use Gskema\TypeSniff\Core\CodeElement\Element\InterfaceMethodElement;
11
use Gskema\TypeSniff\Core\DocBlock\DocBlock;
12
use Gskema\TypeSniff\Core\DocBlock\UndefinedDocBlock;
13
use Gskema\TypeSniff\Core\Type\Common\ArrayType;
14
use Gskema\TypeSniff\Core\Type\Common\UndefinedType;
15
use Gskema\TypeSniff\Core\Type\Common\VoidType;
16
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
17
use Gskema\TypeSniff\Core\Type\DocBlock\CompoundType;
18
use Gskema\TypeSniff\Core\Type\DocBlock\NullType;
19
use Gskema\TypeSniff\Core\Type\DocBlock\TypedArrayType;
20
use Gskema\TypeSniff\Core\Type\TypeConverter;
21
use Gskema\TypeSniff\Core\Type\TypeInterface;
22
23
class FqcnMethodSniff implements CodeElementSniffInterface
24
{
25
    /** @var string[] */
26
    protected $baseUsefulTags = [
27
        '@deprecated',
28
        '@throws',
29
        '@dataProvider',
30
        '@see',
31
        '@todo',
32
        '@inheritDoc'
33
    ];
34
35
    /** @var string[] */
36
    protected $usefulTags = [];
37
38
    /**
39
     * @inheritDoc
40
     */
41 4
    public function configure(array $config): void
42
    {
43 4
        $rawTags = array_merge($this->baseUsefulTags, $config['usefulTags'] ?? []);
44
45 4
        $usefulTags = [];
46 4
        foreach ($rawTags as $rawTag) {
47 4
            $usefulTags[] = strtolower(ltrim($rawTag, '@'));
48
        }
49 4
        $usefulTags = array_unique($usefulTags);
50
51 4
        $this->usefulTags = $usefulTags;
52 4
    }
53
54
    /**
55
     * @inheritDoc
56
     */
57 4
    public function register(): array
58
    {
59
        return [
60 4
            ClassMethodElement::class,
61
            // TraitMethodElement::class, // can be used to implement interface, not possible to know if it is extended
62
            InterfaceMethodElement::class,
63
        ];
64
    }
65
66
    /**
67
     * @inheritDoc
68
     * @param AbstractFqcnMethodElement $method
69
     */
70 3
    public function process(File $file, CodeElementInterface $method): void
71
    {
72 3
        $warningCountBefore = $file->getWarningCount();
73
74
        // @TODO Assert description
75 3
        $this->processMethod($file, $method);
76
77 3
        $hasNewWarnings = $file->getWarningCount() > $warningCountBefore;
78 3
        if (!$hasNewWarnings && $this->hasUselessDocBlock($method)) {
79 1
            $file->addWarningOnLine('Useless PHPDoc', $method->getLine(), 'FqcnMethodSniff');
80
        }
81 3
    }
82
83 3
    protected function processMethod(File $file, AbstractFqcnMethodElement $method): void
84
    {
85 3
        $fnSig = $method->getSignature();
86 3
        $docBlock = $method->getDocBlock();
87 3
        $isMagicMethod = '__' === substr($fnSig->getName(), 0, 2);
88 3
        $isConstructMethod = '__construct' === $fnSig->getName();
89 3
        $hasInheritDocTag = $docBlock->hasTag('inheritdoc');
90
91
        // @inheritDoc
92
        // __construct can be detected as extended and magic, but we want to inspect it anyway
93 3
        if (!$isConstructMethod) {
94 3
            if ($hasInheritDocTag || $isMagicMethod) {
95 1
                return;
96 3
            } elseif ($method->isExtended()) {
97 1
                $file->addWarningOnLine('Missing @inheritDoc tag', $method->getLine(), 'FqcnMethodSniff');
98 1
                return;
99
            }
100
        }
101
102
        // @param
103 3
        foreach ($fnSig->getParams() as $fnParam) {
104 3
            $paramName = $fnParam->getName();
105 3
            $tag = $docBlock->getParamTag($paramName);
106
107 3
            $subject = sprintf('parameter $%s', $paramName);
108
109 3
            $fnType = $fnParam->getType();
110 3
            $fnTypeLine = $fnParam->getLine();
111 3
            $docType = $tag ? $tag->getType() : null;
112 3
            $docTypeLine = $tag ? $tag->getLine() : $fnTypeLine;
113
114 3
            $this->processSigType($file, $docBlock, $subject, $fnType, $fnTypeLine, $docType, $docTypeLine);
115
        }
116
117
        // @return
118 3
        if (!$isConstructMethod) {
119 3
            $docType = $docBlock->getReturnTag();
120 3
            $this->processSigType(
121 3
                $file,
122 3
                $docBlock,
123 3
                'return value',
124 3
                $fnSig->getReturnType(),
125 3
                $fnSig->getReturnLine(),
126 3
                $docType ? $docType->getType() : null,
127 3
                $docType ? $docType->getLine() : $fnSig->getLine()
128
            );
129
        }
130 3
    }
131
132 3
    protected function processSigType(
133
        File $file,
134
        DocBlock $docBlock,
135
        string $subject,
136
        TypeInterface $fnType,
137
        int $fnTypeLine,
138
        ?TypeInterface $docType,
139
        int $docTypeLine
140
    ): void {
141
        // @TODO Required mixed[][] instead of array[]
142
143 3
        $isReturnType = 'return value' === $subject;
144
        // $isParamType = !$isReturnType;
145
146
        /** @var string[][] $warnings */
147 3
        $warnings = [];
148 3
        if ($docBlock instanceof UndefinedDocBlock) {
149
            // Require docType for undefined type or array type
150 2
            if ($fnType instanceof UndefinedType) {
151 2
                $warnings[$fnTypeLine][] = 'Add type declaration for :subject: or create PHPDoc with type hint';
152 1
            } elseif ($this->containsType($fnType, ArrayType::class)) {
153 2
                $warnings[$fnTypeLine][] = 'Create PHPDoc with typed array type hint for :subject:, .e.g.: "string[]" or "SomeClass[]"';
154
            }
155 3
        } elseif (null === $docType) {
156
            // Require docTag unless void return type
157 2
            if ($isReturnType) {
158 2
                if (!($fnType instanceof VoidType)) {
159 2
                    $warnings[$fnTypeLine][] = 'Missing PHPDoc tag or void type declaration for :subject:';
160
                }
161
            } else {
162 2
                $warnings[$fnTypeLine][] = 'Missing PHPDoc tag for :subject:';
163
            }
164
        } else {
165 3
            $docTypeDefined = !($docType instanceof UndefinedType);
166 3
            $fnTypeDefined = !($fnType instanceof UndefinedType);
167
168 3
            if ($docTypeDefined) {
169
                // Require typed array type
170
                // Require composite with null instead of null
171
                // @TODO true/void/false/$this/ cannot be param tags
172
173 3
                $docHasTypedArray = $this->containsType($docType, TypedArrayType::class);
174 3
                $docHasArray = $this->containsType($docType, ArrayType::class);
175
176 3
                if ($docHasTypedArray && $docHasArray) {
177 1
                    $warnings[$docTypeLine][] = 'Remove array type, typed array type is present in PHPDoc for :subject:.';
178 3
                } elseif (!$docHasTypedArray && $docHasArray) {
179 2
                    $warnings[$docTypeLine][] = 'Replace array type with typed array type in PHPDoc for :subject:. Use mixed[] for generic arrays.';
180
                }
181
182 3
                if ($docType instanceof NullType) {
183 1
                    if ($isReturnType) {
184
                        $warnings[$docTypeLine][] = 'Use void :subject :type declaration or change type to compound, e.g. SomeClass|null';
185
                    } else {
186 3
                        $warnings[$docTypeLine][] = 'Change type hint for :subject: to compound, e.g. SomeClass|null';
187
                    }
188
                }
189
            } else {
190
                // Require docType (example from fnType)
191 1
                $exampleDocType = TypeConverter::toExampleDocType($fnType);
192 1
                if (null !== $exampleDocType) {
193 1
                    $warnings[$docTypeLine][] = sprintf('Add type hint in PHPDoc tag for :subject:, e.g. "%s"', $exampleDocType->toString());
194
                } else {
195 1
                    $warnings[$docTypeLine][] = 'Add type hint in PHPDoc tag for :subject:';
196
                }
197
            }
198
199 3
            if (!$fnTypeDefined) {
200
                // Require fnType if possible (check, suggest from docType)
201 1
                if ($suggestedFnType = TypeConverter::toFunctionType($docType)) {
202 1
                    $warnings[$fnTypeLine][] = sprintf('Add type declaration for :subject:, e.g.: "%s"', $suggestedFnType->toString());
203
                }
204
                // @TODO Warning for null?
205
            }
206
207 3
            if ($docTypeDefined && $fnTypeDefined) {
208
                // Require to add missing types to docType,
209
                // @TODO Remove void return docType,
210
211
                /** @var TypeInterface[] $wrongDocTypes */
212
                /** @var TypeInterface[] $missingDocTypes */
213 3
                [$wrongDocTypes, $missingDocTypes] = TypeComparator::compare($docType, $fnType);
214
215 3
                if ($wrongDocTypes) {
216 3
                    $warnings[$docTypeLine][] = sprintf(
217 3
                        'Type %s "%s" %s not compatible with :subject: type declaration',
218 3
                        isset($wrongDocTypes[1]) ? 'hints' : 'hint',
219 3
                        $this->implodeTypes($wrongDocTypes),
220 3
                        isset($wrongDocTypes[1]) ? 'are' : 'is'
221
                    );
222
                }
223
224 3
                if ($missingDocTypes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $missingDocTypes of type Gskema\TypeSniff\Core\Type\TypeInterface[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
225 3
                    $warnings[$docTypeLine][] = sprintf(
226 3
                        'Missing "%s" %s in :subject: type hint',
227 3
                        $this->implodeTypes($missingDocTypes),
228 3
                        isset($missingDocTypes[1]) ? 'types' : 'type'
229
                    );
230
                }
231
            }
232
        }
233
234 3
        foreach ($warnings as $line => $lineWarnings) {
235 3
            foreach ($lineWarnings as $warningTpl) {
236 3
                $warning = str_replace(':subject:', $subject, $warningTpl);
237 3
                $file->addWarningOnLine($warning, $line, 'FqcnMethodSniff');
238
            }
239
        }
240 3
    }
241
242 3
    protected function containsType(TypeInterface $type, string $typeClassName): bool
243
    {
244 3
        return is_a($type, $typeClassName)
245 3
            || ($type instanceof CompoundType && $type->containsType($typeClassName))
246 3
            || ($type instanceof NullableType && $type->containsType($typeClassName));
247
    }
248
249 3
    protected function hasUselessDocBlock(AbstractFqcnMethodElement $method): bool
250
    {
251 3
        $fnSig = $method->getSignature();
252 3
        $docBlock = $method->getDocBlock();
253
254 3
        $usefulTagNames = array_diff($this->usefulTags, ['param', 'return']);
255
256 3
        $docReturnTag = $docBlock->getReturnTag();
257
258 3
        $hasUsefulDescription = $docBlock->hasDescription()
259 3
            && !preg_match('#^\w+\s+constructor\.?$#', $docBlock->getDescription());
260
261 3
        if ($docBlock instanceof UndefinedDocBlock
262 3
            || $hasUsefulDescription
263 3
            || $docBlock->hasOneOfTags($usefulTagNames)
264 3
            || ($docReturnTag && $docReturnTag->hasDescription())
265
        ) {
266 2
            return false;
267
        }
268
269 3
        foreach ($fnSig->getParams() as $fnParam) {
270 2
            $paramTag = $docBlock->getParamTag($fnParam->getName());
271 2
            if (null === $paramTag) {
272
                continue;
273
            }
274
275 2
            if ($paramTag->hasDescription()) {
276 1
                return false;
277
            }
278
279
            // @TODO Cleaner way?
280 1
            $fnType = $fnParam->getType();
281 1
            $rawFnType = $fnType instanceof NullableType
282 1
                ? $fnType->toDocString()
283 1
                : $fnType->toString();
284 1
            if ($paramTag->getType()->toString() !== $rawFnType) {
285 1
                return false;
286
            }
287
        }
288
289 2
        $returnTag  = $docBlock->getReturnTag();
290 2
        $returnType = $fnSig->getReturnType();
291
292
        // @TODO ??
293 2
        if ($returnTag && $returnType) {
294 1
            $rawReturnType = $returnType instanceof NullableType
295
                ? $returnType->toDocString()
296 1
                : $returnType->toString();
297 1
            if ($returnTag->getType()->toString() !== $rawReturnType) {
298 1
                return false;
299
            }
300
        }
301
302 1
        return true;
303
    }
304
305
    /**
306
     * @param TypeInterface[] $types
307
     * @return string|null
308
     */
309 3
    protected function implodeTypes(array $types): ?string
310
    {
311 3
        $rawTypes = [];
312 3
        foreach ($types as $type) {
313 3
            $rawTypes[] = $type->toString();
314
        }
315
316 3
        return $rawTypes ? implode(', ', $rawTypes) : null;
317
    }
318
}
319