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

FqcnMethodSniff::processSigType()   F

Complexity

Conditions 26
Paths 513

Size

Total Lines 106
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 26.0054

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 26
eloc 54
c 3
b 0
f 0
nc 513
nop 7
dl 0
loc 106
ccs 49
cts 50
cp 0.98
crap 26.0054
rs 0.6763

How to fix   Long Method    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
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