Completed
Branch 0.14.0 (b23fa7)
by Gytis
12:27 queued 07:51
created

FqcnMethodSniff::process()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
nc 2
nop 3
dl 0
loc 10
ccs 7
cts 7
cp 1
crap 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
namespace Gskema\TypeSniff\Sniffs\CodeElement;
4
5
use Gskema\TypeSniff\Core\CodeElement\Element\ClassElement;
6
use Gskema\TypeSniff\Core\DocBlock\Tag\VarTag;
7
use Gskema\TypeSniff\Core\Type\Common\UndefinedType;
8
use Gskema\TypeSniff\Core\Type\DocBlock\NullType;
9
use Gskema\TypeSniff\Core\Type\TypeHelper;
10
use Gskema\TypeSniff\Inspection\FnTypeInspector;
11
use Gskema\TypeSniff\Inspection\DocTypeInspector;
12
use Gskema\TypeSniff\Inspection\Subject\AbstractTypeSubject;
13
use Gskema\TypeSniff\Inspection\Subject\ParamTypeSubject;
14
use Gskema\TypeSniff\Inspection\Subject\ReturnTypeSubject;
15
use PHP_CodeSniffer\Files\File;
16
use Gskema\TypeSniff\Core\CodeElement\Element\AbstractFqcnMethodElement;
17
use Gskema\TypeSniff\Core\CodeElement\Element\ClassMethodElement;
18
use Gskema\TypeSniff\Core\CodeElement\Element\CodeElementInterface;
19
use Gskema\TypeSniff\Core\CodeElement\Element\InterfaceMethodElement;
20
use Gskema\TypeSniff\Core\DocBlock\UndefinedDocBlock;
21
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
22
23
/**
24
 * @see FqcnMethodSniffTest
25
 */
26
class FqcnMethodSniff implements CodeElementSniffInterface
27
{
28
    protected const CODE = 'FqcnMethodSniff';
29
30
    /** @var string[] */
31
    protected $invalidTags = [];
32
33
    /** @var bool */
34
    protected $reportMissingTags = true;
35
36
    /** @var bool */
37
    protected $reportNullableBasicGetterDocType = true;
38
39
    /** @var bool */
40
    protected $reportNullableBasicGetterFnType = true;
41
42
    /**
43
     * @inheritDoc
44
     */
45 6
    public function configure(array $config): void
46
    {
47
        // TagInterface uses lowercase tags names, no @ symbol in front
48 6
        $invalidTags = [];
49 6
        foreach ($config['invalidTags'] ?? [] as $rawTag) {
50 1
            $invalidTags[] = strtolower(ltrim($rawTag, '@'));
51
        }
52 6
        $invalidTags = array_unique($invalidTags);
53
54 6
        $this->invalidTags = $invalidTags;
55 6
        $this->reportMissingTags = $config['reportMissingTags'] ?? true;
56 6
        $this->reportNullableBasicGetterDocType = $config['reportNullableBasicGetterDocType'] ?? true;
57 6
        $this->reportNullableBasicGetterFnType = $config['reportNullableBasicGetterFnType'] ?? true;
58 6
    }
59
60
    /**
61
     * @inheritDoc
62
     */
63 6
    public function register(): array
64
    {
65
        return [
66 6
            ClassMethodElement::class,
67
            // TraitMethodElement::class, // can be used to implement interface, not possible to know if it is extended
68
            InterfaceMethodElement::class,
69
        ];
70
    }
71
72
    /**
73
     * @inheritDoc
74
     * @param AbstractFqcnMethodElement $method
75
     */
76 5
    public function process(File $file, CodeElementInterface $method, CodeElementInterface $parentElement): void
77
    {
78 5
        $warningCountBefore = $file->getWarningCount();
79
80 5
        static::reportInvalidTags($file, $method, $this->invalidTags);
81 5
        $this->processMethod($file, $method, $parentElement);
82
83 5
        $hasNewWarnings = $file->getWarningCount() > $warningCountBefore;
84 5
        if (!$hasNewWarnings && $this->hasUselessDocBlock($method)) {
85 3
            $file->addWarningOnLine('Useless PHPDoc', $method->getLine(), static::CODE);
86
        }
87 5
    }
88
89 5
    protected function processMethod(File $file, AbstractFqcnMethodElement $method, CodeElementInterface $parent): void
90
    {
91 5
        $fnSig = $method->getSignature();
92 5
        $docBlock = $method->getDocBlock();
93 5
        $isMagicMethod = '__' === substr($fnSig->getName(), 0, 2);
94 5
        $isConstructMethod = '__construct' === $fnSig->getName();
95 5
        $hasInheritDocTag = $docBlock->hasTag('inheritdoc');
96
97
        // @inheritDoc
98
        // __construct can be detected as extended and magic, but we want to inspect it anyway
99 5
        if (!$isConstructMethod) {
100 5
            if ($hasInheritDocTag || $isMagicMethod) {
101 1
                return;
102 5
            } elseif ($method->getMetadata()->isExtended()) {
103 1
                $file->addWarningOnLine('Missing @inheritDoc tag. Remove duplicated parent PHPDoc content.', $method->getLine(), static::CODE);
104 1
                return;
105
            }
106
        }
107
108
        // @param
109 5
        foreach ($fnSig->getParams() as $fnParam) {
110 5
            $paramTag = $docBlock->getParamTag($fnParam->getName());
111 5
            $subject = ParamTypeSubject::fromParam($fnParam, $paramTag, $docBlock);
112 5
            $this->processSigType($file, $subject);
113
        }
114
115
        // @return
116 5
        if (!$isConstructMethod) {
117 5
            $returnTag = $docBlock->getReturnTag();
118 5
            $subject = ReturnTypeSubject::fromSignature($fnSig, $returnTag, $docBlock);
119 5
            $this->processSigType($file, $subject);
120
121 5
            if ($method instanceof ClassMethodElement && $parent instanceof ClassElement) {
122 5
                ($this->reportNullableBasicGetterDocType || $this->reportNullableBasicGetterFnType) // skip if both disabled
123 5
                && static::reportNullableBasicGetter($file, $subject, $method, $parent);
0 ignored issues
show
Bug Best Practice introduced by
The method Gskema\TypeSniff\Sniffs\...rtNullableBasicGetter() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

123
                && static::/** @scrutinizer ignore-call */ reportNullableBasicGetter($file, $subject, $method, $parent);
Loading history...
124
            }
125
        } else {
126 2
            foreach ($docBlock->getDescriptionLines() as $lineNum => $descLine) {
127 1
                if (preg_match('#^\w+\s+constructor\.?$#', $descLine)) {
128 1
                    $file->addWarningOnLine('Useless description.', $lineNum, static::CODE);
129
                }
130
            }
131
        }
132 5
    }
133
134 5
    protected function processSigType(File $file, AbstractTypeSubject $subject): void
135
    {
136 5
        FnTypeInspector::reportMandatoryTypes($subject);
137 5
        FnTypeInspector::reportSuggestedTypes($subject);
138 5
        FnTypeInspector::reportReplaceableTypes($subject);
139
140 5
        if ($this->reportMissingTags || $subject->hasDocTypeTag()) {
141 5
            DocTypeInspector::reportMandatoryTypes($subject);
142 5
            DocTypeInspector::reportSuggestedTypes($subject);
143 5
            DocTypeInspector::reportReplaceableTypes($subject);
144
145 5
            DocTypeInspector::reportRemovableTypes($subject);
146 5
            DocTypeInspector::reportInvalidTypes($subject);
147 5
            DocTypeInspector::reportMissingOrWrongTypes($subject);
148
        } else {
149 1
            DocTypeInspector::reportMandatoryTypes($subject, true);
150
        }
151
152 5
        $subject->writeWarningsTo($file, static::CODE);
153 5
    }
154
155 10
    protected function hasUselessDocBlock(AbstractFqcnMethodElement $method): bool
156
    {
157 10
        $fnSig = $method->getSignature();
158 10
        $docBlock = $method->getDocBlock();
159
160 10
        $docReturnTag = $docBlock->getReturnTag();
161
162 10
        if ($docBlock instanceof UndefinedDocBlock
163 10
            || $docBlock->hasDescription()
164 9
            || ($docReturnTag && $docReturnTag->hasDescription())
165
            // check if other "useful" tags are present
166 10
            || array_diff($docBlock->getTagNames(), ['param', 'return'])
167
        ) {
168 4
            return false;
169
        }
170
171 9
        foreach ($fnSig->getParams() as $fnParam) {
172 9
            $paramTag = $docBlock->getParamTag($fnParam->getName());
173 9
            if (null === $paramTag) {
174 1
                return false; // missing, needs to be fixed
175
            }
176
177 8
            if ($paramTag->hasDescription()) {
178 3
                return false;
179
            }
180
181 5
            $fnType = $fnParam->getType();
182 5
            $rawFnType = $fnType instanceof NullableType
183 1
                ? $fnType->toDocString()
184 5
                : $fnType->toString();
185 5
            if ($paramTag->getType()->toString() !== $rawFnType) {
186 3
                return false;
187
            }
188
        }
189
190 4
        $returnTag  = $docBlock->getReturnTag();
191 4
        $returnType = $fnSig->getReturnType();
192
193 4
        if ($returnTag && $returnType) {
194 3
            $rawReturnType = $returnType instanceof NullableType
195 1
                ? $returnType->toDocString()
196 3
                : $returnType->toString();
197 3
            if ($returnTag->getType()->toString() !== $rawReturnType) {
198 1
                return false;
199
            }
200
        }
201
202 4
        return true;
203
    }
204
205
    /**
206
     * @param File                      $file
207
     * @param AbstractFqcnMethodElement $method
208
     * @param static[]                  $invalidTags
209
     */
210 5
    protected static function reportInvalidTags(File $file, AbstractFqcnMethodElement $method, array $invalidTags): void
211
    {
212 5
        foreach ($method->getDocBlock()->getTags() as $tag) {
213 5
            foreach ($invalidTags as $invalidTagName) {
214 1
                if ($tag->getName() === $invalidTagName) {
215 1
                    $file->addWarningOnLine('Useless tag', $tag->getLine(), static::CODE);
216
                }
217
            }
218
        }
219 5
    }
220
221 5
    protected function reportNullableBasicGetter(
222
        File $file,
223
        ReturnTypeSubject $subject,
224
        ClassMethodElement $method,
225
        ClassElement $class
226
    ): void {
227 5
        $propName = $method->getMetadata()->getBasicGetterPropName();
228 5
        if (null === $propName) {
229 5
            return;
230
        }
231
232 1
        $prop = $class->getProperty($propName);
233 1
        if (null === $prop) {
234
            return;
235
        }
236
237
        /** @var VarTag|null $varTag */
238 1
        $varTag = $prop->getDocBlock()->getTagsByName('var')[0] ?? null;
239 1
        if (null === $varTag) {
240
            return;
241
        }
242
243 1
        $propDocType = $varTag->getType();
244 1
        $isPropNullable = TypeHelper::containsType($varTag->getType(), NullType::class);
245 1
        if (!$isPropNullable) {
246
            return;
247
        }
248
249 1
        $returnDocType = $subject->getDocType();
250 1
        $isGetterDocTypeNullable = TypeHelper::containsType($returnDocType, NullType::class);
251 1
        if (!$isGetterDocTypeNullable && $subject->hasDefinedDocBlock()) {
252 1
            $this->reportNullableBasicGetterDocType && $subject->addDocTypeWarning(sprintf(
253 1
                'Returned property $%s is nullable, add null return doc type, e.g. %s',
254
                $propName,
255 1
                ($returnDocType->toString() ?? $propDocType).'|null'
0 ignored issues
show
Bug introduced by
Are you sure $returnDocType->toString() ?? $propDocType of type Gskema\TypeSniff\Core\Type\TypeInterface|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

255
                (/** @scrutinizer ignore-type */ $returnDocType->toString() ?? $propDocType).'|null'
Loading history...
256
            ));
257
        }
258
259
        // Only report in fn type is defined. Doc type and fn type is synced by other sniffs.
260 1
        $returnFnType = $subject->getFnType();
261 1
        if (!($returnFnType instanceof UndefinedType) && !($returnFnType instanceof NullableType)) {
262 1
            $this->reportNullableBasicGetterFnType && $subject->addFnTypeWarning(sprintf(
263 1
                'Returned property $%s is nullable, use nullable return type declaration, e.g. ?%s',
264
                $propName,
265 1
                $returnFnType->toString()
266
            ));
267
        }
268
269 1
        $subject->writeWarningsTo($file, static::CODE);
270 1
    }
271
}
272