Passed
Push — master ( 17dafe...3e29f4 )
by Gytis
02:42 queued 11s
created

FqcnMethodSniff::hasUselessDocBlock()   C

Complexity

Conditions 15
Paths 20

Size

Total Lines 49
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 15.0603

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 15
eloc 30
c 3
b 0
f 0
nc 20
nop 1
dl 0
loc 49
ccs 29
cts 31
cp 0.9355
crap 15.0603
rs 5.9166

How to fix   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 Gskema\TypeSniff\Core\Type\TypeHelper;
7
use PHP_CodeSniffer\Files\File;
8
use Gskema\TypeSniff\Core\CodeElement\Element\AbstractFqcnMethodElement;
9
use Gskema\TypeSniff\Core\CodeElement\Element\ClassMethodElement;
10
use Gskema\TypeSniff\Core\CodeElement\Element\CodeElementInterface;
11
use Gskema\TypeSniff\Core\CodeElement\Element\InterfaceMethodElement;
12
use Gskema\TypeSniff\Core\DocBlock\DocBlock;
13
use Gskema\TypeSniff\Core\DocBlock\UndefinedDocBlock;
14
use Gskema\TypeSniff\Core\Type\Common\ArrayType;
15
use Gskema\TypeSniff\Core\Type\Common\UndefinedType;
16
use Gskema\TypeSniff\Core\Type\Common\VoidType;
17
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
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 5
    public function configure(array $config): void
42
    {
43 5
        $rawTags = array_merge($this->baseUsefulTags, $config['usefulTags'] ?? []);
44
45 5
        $usefulTags = [];
46 5
        foreach ($rawTags as $rawTag) {
47 5
            $usefulTags[] = strtolower(ltrim($rawTag, '@'));
48
        }
49 5
        $usefulTags = array_unique($usefulTags);
50
51 5
        $this->usefulTags = $usefulTags;
52 5
    }
53
54
    /**
55
     * @inheritDoc
56
     */
57 5
    public function register(): array
58
    {
59
        return [
60 5
            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 4
    public function process(File $file, CodeElementInterface $method): void
71
    {
72 4
        $warningCountBefore = $file->getWarningCount();
73
74
        // @TODO Assert description
75 4
        $this->processMethod($file, $method);
76
77 4
        $hasNewWarnings = $file->getWarningCount() > $warningCountBefore;
78 4
        if (!$hasNewWarnings && $this->hasUselessDocBlock($method)) {
79 2
            $file->addWarningOnLine('Useless PHPDoc', $method->getLine(), 'FqcnMethodSniff');
80
        }
81 4
    }
82
83 4
    protected function processMethod(File $file, AbstractFqcnMethodElement $method): void
84
    {
85 4
        $fnSig = $method->getSignature();
86 4
        $docBlock = $method->getDocBlock();
87 4
        $isMagicMethod = '__' === substr($fnSig->getName(), 0, 2);
88 4
        $isConstructMethod = '__construct' === $fnSig->getName();
89 4
        $hasInheritDocTag = $docBlock->hasTag('inheritdoc');
90
91
        // @inheritDoc
92
        // __construct can be detected as extended and magic, but we want to inspect it anyway
93 4
        if (!$isConstructMethod) {
94 4
            if ($hasInheritDocTag || $isMagicMethod) {
95 1
                return;
96 4
            } elseif ($method->isExtended()) {
97 1
                $file->addWarningOnLine('Missing @inheritDoc tag. Remove duplicated parent PHPDoc content.', $method->getLine(), 'FqcnMethodSniff');
98 1
                return;
99
            }
100
        }
101
102
        // @param
103 4
        foreach ($fnSig->getParams() as $fnParam) {
104 4
            $paramName = $fnParam->getName();
105 4
            $tag = $docBlock->getParamTag($paramName);
106
107 4
            $subject = sprintf('parameter $%s', $paramName);
108
109 4
            $fnType = $fnParam->getType();
110 4
            $fnTypeLine = $fnParam->getLine();
111 4
            $docType = $tag ? $tag->getType() : null;
112 4
            $docTypeLine = $tag ? $tag->getLine() : $fnTypeLine;
113 4
            $valueType = $fnParam->getValueType();
114
115 4
            $this->processSigType($file, $docBlock, $subject, $fnType, $fnTypeLine, $docType, $docTypeLine, $valueType);
116
        }
117
118
        // @return
119 4
        if (!$isConstructMethod) {
120 4
            $docType = $docBlock->getReturnTag();
121 4
            $this->processSigType(
122 4
                $file,
123
                $docBlock,
124 4
                'return value',
125 4
                $fnSig->getReturnType(),
126 4
                $fnSig->getReturnLine(),
127 4
                $docType ? $docType->getType() : null,
128 4
                $docType ? $docType->getLine() : $fnSig->getLine(),
129 4
                new UndefinedType()
130
            );
131
        } else {
132 2
            foreach ($docBlock->getDescriptionLines() as $lineNum => $descLine) {
133 1
                if (preg_match('#^\w+\s+constructor\.?$#', $descLine)) {
134 1
                    $file->addWarningOnLine('Useless description.', $lineNum, 'FqcnMethodSniff');
135
                }
136
            }
137
        }
138 4
    }
139
140 4
    protected function processSigType(
141
        File $file,
142
        DocBlock $docBlock,
143
        string $subject,
144
        TypeInterface $fnType,
145
        int $fnTypeLine,
146
        ?TypeInterface $docType,
147
        int $docTypeLine,
148
        ?TypeInterface $valueType
149
    ): void {
150 4
        $isReturnType = 'return value' === $subject;
151 4
        $docTypeDefined = !($docType instanceof UndefinedType);
152 4
        $fnTypeDefined = !($fnType instanceof UndefinedType);
153
154
        /** @var string[][] $warnings */
155 4
        $warnings = [];
156
157
        // func1(string $arg1 = null) -> ?string
158 4
        if ($valueType instanceof NullType && $fnTypeDefined && !($fnType instanceof NullableType)) {
159 1
            $warnings[$fnTypeLine][] = sprintf(
160 1
                'Change :subject: type declaration to nullable, e.g. %s. Remove default null value if this argument is required.',
161 1
                (new NullableType($fnType))->toString()
162
            );
163
        }
164
165 4
        if ($docBlock instanceof UndefinedDocBlock) {
166
            // Require docType for undefined type or array type
167 3
            if ($fnType instanceof UndefinedType) {
168 2
                $warnings[$fnTypeLine][] = 'Add type declaration for :subject: or create PHPDoc with type hint';
169 2
            } elseif (TypeHelper::containsType($fnType, ArrayType::class)) {
170 3
                $warnings[$fnTypeLine][] = 'Create PHPDoc with typed array type hint for :subject:, .e.g.: "string[]" or "SomeClass[]"';
171
            }
172 4
        } elseif (null === $docType) {
173
            // Require docTag unless void return type
174 4
            if ($isReturnType) {
175 4
                if (!($fnType instanceof VoidType)) {
176 4
                    $warnings[$fnTypeLine][] = 'Missing PHPDoc tag or void type declaration for :subject:';
177
                }
178
            } else {
179 4
                $warnings[$fnTypeLine][] = 'Missing PHPDoc tag for :subject:';
180
            }
181
        } else {
182 4
            if ($docTypeDefined) {
183
                // Require typed array type
184
                // Require composite with null instead of null
185
                // @TODO true/void/false/$this/ cannot be param tags
186
187 4
                $docHasTypedArray = TypeHelper::containsType($docType, TypedArrayType::class);
188 4
                $docHasArray = TypeHelper::containsType($docType, ArrayType::class);
189
190 4
                if (!$docHasTypedArray && $docHasArray) {
191 2
                    $warnings[$docTypeLine][] = 'Replace array type with typed array type in PHPDoc for :subject:. Use mixed[] for generic arrays. Correct array depth must be specified.';
192
                }
193
194 4
                if ($redundantTypes = TypeComparator::getRedundantDocTypes($docType)) {
195 2
                    $warnings[$docTypeLine][] = sprintf('Remove redundant :subject: type hints "%s"', TypeHelper::listRawTypes($redundantTypes));
196
                }
197
198 4
                if ($docHasTypedArray && $fakeType = TypeHelper::getFakeTypedArrayType($docType)) {
199 1
                    $msg = sprintf(
200 1
                        'Use a more specific type in typed array hint "%s" for :subject:. Correct array depth must be specified.',
201 1
                        $fakeType->toString()
202
                    );
203 1
                    $warnings[$docTypeLine][] = $msg;
204
                }
205
206 4
                if ($docType instanceof NullType) {
207 1
                    if ($isReturnType) {
208
                        $warnings[$docTypeLine][] = 'Use void :subject :type declaration or change type to compound, e.g. SomeClass|null';
209
                    } else {
210 4
                        $warnings[$docTypeLine][] = 'Change type hint for :subject: to compound, e.g. SomeClass|null';
211
                    }
212
                }
213
            } else {
214
                // Require docType (example from fnType)
215 1
                $exampleDocType = TypeConverter::toExampleDocType($fnType);
216 1
                if (null !== $exampleDocType) {
217 1
                    $warnings[$docTypeLine][] = sprintf('Add type hint in PHPDoc tag for :subject:, e.g. "%s"', $exampleDocType->toString());
218
                } else {
219 1
                    $warnings[$docTypeLine][] = 'Add type hint in PHPDoc tag for :subject:';
220
                }
221
            }
222
223 4
            if (!$fnTypeDefined) {
224
                // Require fnType if possible (check, suggest from docType)
225 3
                if ($suggestedFnType = TypeConverter::toExampleFnType($docType)) {
226 2
                    $warnings[$fnTypeLine][] = sprintf('Add type declaration for :subject:, e.g.: "%s"', $suggestedFnType->toString());
227
                }
228
            }
229
230 4
            if ($docTypeDefined && $fnTypeDefined) {
231
                // Require to add missing types to docType,
232 4
                if ($fnType instanceof VoidType && $docType instanceof VoidType) {
233 1
                    $warnings[$docTypeLine][] = 'Remove @return void tag, not necessary';
234
                }
235
236
                /** @var TypeInterface[] $wrongDocTypes */
237
                /** @var TypeInterface[] $missingDocTypes */
238 4
                [$wrongDocTypes, $missingDocTypes] = TypeComparator::compare($docType, $fnType, $valueType);
239
240 4
                if ($wrongDocTypes) {
241 3
                    $warnings[$docTypeLine][] = sprintf(
242 3
                        'Type %s "%s" %s not compatible with :subject: type declaration',
243 3
                        isset($wrongDocTypes[1]) ? 'hints' : 'hint',
244 3
                        TypeHelper::listRawTypes($wrongDocTypes),
245 3
                        isset($wrongDocTypes[1]) ? 'are' : 'is'
246
                    );
247
                }
248
249 4
                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...
250 3
                    $warnings[$docTypeLine][] = sprintf(
251 3
                        'Missing "%s" %s in :subject: type hint',
252 3
                        TypeHelper::listRawTypes($missingDocTypes),
253 3
                        isset($missingDocTypes[1]) ? 'types' : 'type'
254
                    );
255
                }
256
            }
257
        }
258
259 4
        foreach ($warnings as $line => $lineWarnings) {
260 4
            foreach ($lineWarnings as $warningTpl) {
261 4
                $warning = str_replace(':subject:', $subject, $warningTpl);
262 4
                $file->addWarningOnLine($warning, $line, 'FqcnMethodSniff');
263
            }
264
        }
265 4
    }
266
267 4
    protected function hasUselessDocBlock(AbstractFqcnMethodElement $method): bool
268
    {
269 4
        $fnSig = $method->getSignature();
270 4
        $docBlock = $method->getDocBlock();
271
272 4
        $usefulTagNames = array_diff($this->usefulTags, ['param', 'return']);
273
274 4
        $docReturnTag = $docBlock->getReturnTag();
275
276 4
        if ($docBlock instanceof UndefinedDocBlock
277 4
            || $docBlock->hasDescription()
278 4
            || $docBlock->hasOneOfTags($usefulTagNames)
279 4
            || ($docReturnTag && $docReturnTag->hasDescription())
280
        ) {
281 2
            return false;
282
        }
283
284 4
        foreach ($fnSig->getParams() as $fnParam) {
285 4
            $paramTag = $docBlock->getParamTag($fnParam->getName());
286 4
            if (null === $paramTag) {
287
                continue;
288
            }
289
290 4
            if ($paramTag->hasDescription()) {
291 1
                return false;
292
            }
293
294 3
            $fnType = $fnParam->getType();
295 3
            $rawFnType = $fnType instanceof NullableType
296 2
                ? $fnType->toDocString()
297 3
                : $fnType->toString();
298 3
            if ($paramTag->getType()->toString() !== $rawFnType) {
299 2
                return false;
300
            }
301
        }
302
303 3
        $returnTag  = $docBlock->getReturnTag();
304 3
        $returnType = $fnSig->getReturnType();
305
306 3
        if ($returnTag && $returnType) {
307 2
            $rawReturnType = $returnType instanceof NullableType
308
                ? $returnType->toDocString()
309 2
                : $returnType->toString();
310 2
            if ($returnTag->getType()->toString() !== $rawReturnType) {
311 2
                return false;
312
            }
313
        }
314
315 2
        return true;
316
    }
317
}
318