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

FqcnMethodSniff::containsType()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 5

Importance

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