Passed
Push — master ( e5e542...80e7c7 )
by Gytis
04:34 queued 13s
created

FqcnMethodSniff::processSigType()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
c 4
b 0
f 0
nc 2
nop 2
dl 0
loc 19
ccs 13
cts 13
cp 1
crap 3
rs 9.8333
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 $reportNullableBasicGetter = true;
38
39
    /**
40
     * @inheritDoc
41
     */
42 10
    public function configure(array $config): void
43
    {
44
        // TagInterface uses lowercase tags names, no @ symbol in front
45 10
        $invalidTags = [];
46 10
        foreach ($config['invalidTags'] ?? [] as $rawTag) {
47 1
            $invalidTags[] = strtolower(ltrim($rawTag, '@'));
48
        }
49 10
        $invalidTags = array_unique($invalidTags);
50
51 10
        $this->invalidTags = $invalidTags;
52 10
        $this->reportMissingTags = $config['reportMissingTags'] ?? true;
53 10
        $this->reportNullableBasicGetter = $config['reportNullableBasicGetter'] ?? true;
54 10
    }
55
56
    /**
57
     * @inheritDoc
58
     */
59 10
    public function register(): array
60
    {
61
        return [
62 10
            ClassMethodElement::class,
63
            // TraitMethodElement::class, // can be used to implement interface, not possible to know if it is extended
64
            InterfaceMethodElement::class,
65
        ];
66
    }
67
68
    /**
69
     * @inheritDoc
70
     * @param AbstractFqcnMethodElement $method
71
     */
72 8
    public function process(File $file, CodeElementInterface $method, CodeElementInterface $parentElement): void
73
    {
74 8
        $warningCountBefore = $file->getWarningCount();
75
76 8
        static::reportInvalidTags($file, $method, $this->invalidTags);
77 8
        $this->processMethod($file, $method, $parentElement);
78
79 8
        $hasNewWarnings = $file->getWarningCount() > $warningCountBefore;
80 8
        if (!$hasNewWarnings && $this->hasUselessDocBlock($method)) {
81 3
            $file->addWarningOnLine('Useless PHPDoc', $method->getLine(), static::CODE);
82
        }
83 8
    }
84
85 8
    protected function processMethod(File $file, AbstractFqcnMethodElement $method, CodeElementInterface $parent): void
86
    {
87 8
        $fnSig = $method->getSignature();
88 8
        $docBlock = $method->getDocBlock();
89 8
        $isMagicMethod = '__' === substr($fnSig->getName(), 0, 2);
90 8
        $isConstructMethod = '__construct' === $fnSig->getName();
91 8
        $hasInheritDocTag = $docBlock->hasTag('inheritdoc');
92
93
        // @inheritDoc
94
        // __construct can be detected as extended and magic, but we want to inspect it anyway
95 8
        if (!$isConstructMethod) {
96 8
            if ($hasInheritDocTag || $isMagicMethod) {
97 1
                return;
98 8
            } elseif ($method->getMetadata()->isExtended()) {
99 1
                $file->addWarningOnLine('Missing @inheritDoc tag. Remove duplicated parent PHPDoc content.', $method->getLine(), static::CODE);
100 1
                return;
101
            }
102
        }
103
104
        // @param
105 8
        foreach ($fnSig->getParams() as $fnParam) {
106 7
            $paramTag = $docBlock->getParamTag($fnParam->getName());
107 7
            $subject = ParamTypeSubject::fromParam($fnParam, $paramTag, $docBlock);
108 7
            $this->processSigType($file, $subject);
109
        }
110
111
        // @return
112 8
        if (!$isConstructMethod) {
113 8
            $returnTag = $docBlock->getReturnTag();
114 8
            $subject = ReturnTypeSubject::fromSignature($fnSig, $returnTag, $docBlock);
115 8
            $this->processSigType($file, $subject);
116
117 8
            if ($method instanceof ClassMethodElement && $parent instanceof ClassElement) {
118 8
                $this->reportNullableBasicGetter && static::reportNullableBasicGetter($file, $subject, $method, $parent);
119
            }
120
        } else {
121 5
            foreach ($docBlock->getDescriptionLines() as $lineNum => $descLine) {
122 1
                if (preg_match('#^\w+\s+constructor\.?$#', $descLine)) {
123 1
                    $file->addWarningOnLine('Useless description.', $lineNum, static::CODE);
124
                }
125
            }
126
        }
127 8
    }
128
129 8
    protected function processSigType(File $file, AbstractTypeSubject $subject): void
130
    {
131 8
        FnTypeInspector::reportMandatoryTypes($subject);
132 8
        FnTypeInspector::reportSuggestedTypes($subject);
133 8
        FnTypeInspector::reportReplaceableTypes($subject);
134
135 8
        if ($this->reportMissingTags || $subject->hasDocTypeTag()) {
136 8
            DocTypeInspector::reportMandatoryTypes($subject);
137 8
            DocTypeInspector::reportSuggestedTypes($subject);
138 8
            DocTypeInspector::reportReplaceableTypes($subject);
139
140 8
            DocTypeInspector::reportRemovableTypes($subject);
141 8
            DocTypeInspector::reportInvalidTypes($subject);
142 8
            DocTypeInspector::reportMissingOrWrongTypes($subject);
143
        } else {
144 1
            DocTypeInspector::reportMandatoryTypes($subject, true);
145
        }
146
147 8
        $subject->writeWarningsTo($file, static::CODE);
148 8
    }
149
150 12
    protected function hasUselessDocBlock(AbstractFqcnMethodElement $method): bool
151
    {
152 12
        $fnSig = $method->getSignature();
153 12
        $docBlock = $method->getDocBlock();
154
155 12
        $docReturnTag = $docBlock->getReturnTag();
156
157 12
        if ($docBlock instanceof UndefinedDocBlock
158 10
            || $docBlock->hasDescription()
159 9
            || ($docReturnTag && $docReturnTag->hasDescription())
160
            // check if other "useful" tags are present
161 12
            || array_diff($docBlock->getTagNames(), ['param', 'return'])
162
        ) {
163 6
            return false;
164
        }
165
166 9
        foreach ($fnSig->getParams() as $fnParam) {
167 9
            $paramTag = $docBlock->getParamTag($fnParam->getName());
168 9
            if (null === $paramTag) {
169 1
                return false; // missing, needs to be fixed
170
            }
171
172 8
            if ($paramTag->hasDescription()) {
173 3
                return false;
174
            }
175
176 5
            $fnType = $fnParam->getType();
177 5
            $rawFnType = $fnType instanceof NullableType
178 1
                ? $fnType->toDocString()
179 5
                : $fnType->toString();
180 5
            if ($paramTag->getType()->toString() !== $rawFnType) {
181 3
                return false;
182
            }
183
        }
184
185 4
        $returnTag  = $docBlock->getReturnTag();
186 4
        $returnType = $fnSig->getReturnType();
187
188 4
        if ($returnTag && $returnType) {
189 3
            $rawReturnType = $returnType instanceof NullableType
190 1
                ? $returnType->toDocString()
191 3
                : $returnType->toString();
192 3
            if ($returnTag->getType()->toString() !== $rawReturnType) {
193 1
                return false;
194
            }
195
        }
196
197 4
        return true;
198
    }
199
200
    /**
201
     * @param File                      $file
202
     * @param AbstractFqcnMethodElement $method
203
     * @param static[]                  $invalidTags
204
     */
205 8
    protected static function reportInvalidTags(File $file, AbstractFqcnMethodElement $method, array $invalidTags): void
206
    {
207 8
        foreach ($method->getDocBlock()->getTags() as $tag) {
208 5
            foreach ($invalidTags as $invalidTagName) {
209 1
                if ($tag->getName() === $invalidTagName) {
210 1
                    $file->addWarningOnLine('Useless tag', $tag->getLine(), static::CODE);
211
                }
212
            }
213
        }
214 8
    }
215
216 7
    protected static function reportNullableBasicGetter(
217
        File $file,
218
        ReturnTypeSubject $subject,
219
        ClassMethodElement $method,
220
        ClassElement $class
221
    ): void {
222 7
        $propName = $method->getMetadata()->getBasicGetterPropName();
223 7
        if (null === $propName) {
224 5
            return;
225
        }
226
227 3
        $prop = $class->getProperty($propName);
228 3
        if (null === $prop) {
229 3
            return;
230
        }
231
232
        /** @var VarTag|null $varTag */
233 1
        $varTag = $prop->getDocBlock()->getTagsByName('var')[0] ?? null;
234 1
        if (null === $varTag) {
235 1
            return;
236
        }
237
238 1
        $propDocType = $varTag->getType();
239 1
        $isPropNullable = TypeHelper::containsType($varTag->getType(), NullType::class);
240 1
        if (!$isPropNullable) {
241 1
            return;
242
        }
243
244 1
        $returnDocType = $subject->getDocType();
245 1
        $isGetterDocTypeNullable = TypeHelper::containsType($returnDocType, NullType::class);
246 1
        if (!$isGetterDocTypeNullable && $subject->hasDefinedDocBlock()) {
247 1
            $subject->addDocTypeWarning(sprintf(
248 1
                'Returned property $%s is nullable, add null return doc type, e.g. %s',
249 1
                $propName,
250 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

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