Passed
Push — master ( 60afa0...967e42 )
by Gytis
02:06
created

FqcnMethodSniff::processSigType()   D

Complexity

Conditions 27
Paths 60

Size

Total Lines 85
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 27.0061

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 27
eloc 51
c 3
b 0
f 0
nc 60
nop 7
dl 0
loc 85
ccs 48
cts 49
cp 0.9796
crap 27.0061
rs 4.1666

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