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

FqcnMethodSniff::process()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 3
rs 10
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