FqcnMethodSniff::reportNullableBasicGetter()   B
last analyzed

Complexity

Conditions 11
Paths 10

Size

Total Lines 46
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 11

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 11
eloc 27
c 2
b 0
f 1
nc 10
nop 3
dl 0
loc 46
ccs 26
cts 26
cp 1
crap 11
rs 7.3166

How to fix   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 Gskema\TypeSniff\Core\CodeElement\Element\ClassElement;
6
use Gskema\TypeSniff\Core\DocBlock\Tag\VarTag;
7
use Gskema\TypeSniff\Core\SniffHelper;
8
use Gskema\TypeSniff\Core\Type\Common\UndefinedType;
9
use Gskema\TypeSniff\Core\Type\DocBlock\NullType;
10
use Gskema\TypeSniff\Core\Type\TypeHelper;
11
use Gskema\TypeSniff\Inspection\FnTypeInspector;
12
use Gskema\TypeSniff\Inspection\DocTypeInspector;
13
use Gskema\TypeSniff\Inspection\Subject\AbstractTypeSubject;
14
use Gskema\TypeSniff\Inspection\Subject\ParamTypeSubject;
15
use Gskema\TypeSniff\Inspection\Subject\ReturnTypeSubject;
16
use PHP_CodeSniffer\Files\File;
17
use Gskema\TypeSniff\Core\CodeElement\Element\AbstractFqcnMethodElement;
18
use Gskema\TypeSniff\Core\CodeElement\Element\ClassMethodElement;
19
use Gskema\TypeSniff\Core\CodeElement\Element\CodeElementInterface;
20
use Gskema\TypeSniff\Core\CodeElement\Element\InterfaceMethodElement;
21
use Gskema\TypeSniff\Core\DocBlock\UndefinedDocBlock;
22
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
23
24
/**
25
 * @see FqcnMethodSniffTest
26
 */
27
class FqcnMethodSniff implements CodeElementSniffInterface
28
{
29
    protected const CODE = 'FqcnMethodSniff';
30
31
    /** @var string[] */
32
    protected array $invalidTags = [];
33
34
    protected bool $reportMissingTags = true;
35
36
    protected bool $reportNullableBasicGetter = true;
37
38
    protected string $reportType = 'warning';
39
40
    protected bool $addViolationId = false;
41
42
    /**
43
     * @inheritDoc
44
     */
45 15
    public function configure(array $config): void
46
    {
47
        // TagInterface uses lowercase tags names, no @ symbol in front
48 15
        $invalidTags = [];
49 15
        foreach ($config['invalidTags'] ?? [] as $rawTag) {
50 1
            $invalidTags[] = strtolower(ltrim($rawTag, '@'));
51
        }
52 15
        $invalidTags = array_unique($invalidTags);
53
54 15
        $this->invalidTags = $invalidTags;
55 15
        $this->reportMissingTags = (bool)($config['reportMissingTags'] ?? true);
56 15
        $this->reportNullableBasicGetter = (bool)($config['reportNullableBasicGetter'] ?? true);
57 15
        $this->reportType = (string)($config['reportType'] ?? 'warning');
58 15
        $this->addViolationId = (bool)($config['addViolationId'] ?? false);
59 15
    }
60
61
    /**
62
     * @inheritDoc
63
     */
64 15
    public function register(): array
65
    {
66
        return [
67 15
            ClassMethodElement::class,
68
            // TraitMethodElement::class, // can be used to implement interface, not possible to know if it is extended
69
            InterfaceMethodElement::class,
70
        ];
71
    }
72
73
    /**
74
     * @inheritDoc
75
     * @param AbstractFqcnMethodElement $method
76
     */
77 12
    public function process(File $file, CodeElementInterface $method, CodeElementInterface $parentElement): void
78
    {
79 12
        $warningCountBefore = $file->getWarningCount();
80
81 12
        $this->reportInvalidTags($file, $method, $this->invalidTags);
82 12
        $this->processMethod($file, $method, $parentElement);
83
84 12
        $hasNewWarnings = $file->getWarningCount() > $warningCountBefore;
85 12
        if (!$hasNewWarnings && $this->hasUselessDocBlock($method)) {
86 4
            $originId = $this->addViolationId ? $method->getId() : null;
0 ignored issues
show
Bug introduced by
The method getId() does not exist on Gskema\TypeSniff\Core\Co...nt\CodeElementInterface. It seems like you code against a sub-type of Gskema\TypeSniff\Core\Co...nt\CodeElementInterface such as Gskema\TypeSniff\Core\Co...stractFqcnMethodElement. ( Ignorable by Annotation )

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

86
            $originId = $this->addViolationId ? $method->/** @scrutinizer ignore-call */ getId() : null;
Loading history...
87 4
            SniffHelper::addViolation($file, 'Useless PHPDoc', $method->getLine(), static::CODE, $this->reportType, $originId);
88
        }
89 12
    }
90
91 12
    protected function processMethod(File $file, AbstractFqcnMethodElement $method, CodeElementInterface $parent): void
92
    {
93 12
        $fnSig = $method->getSignature();
94 12
        $docBlock = $method->getDocBlock();
95 12
        $isMagicMethod = '__' === substr($fnSig->getName(), 0, 2);
96 12
        $isConstructMethod = '__construct' === $fnSig->getName();
97 12
        $hasInheritDocTag = $docBlock->hasTag('inheritdoc');
98
99
        // @inheritDoc
100
        // __construct can be detected as extended and magic, but we want to inspect it anyway
101 12
        if (!$isConstructMethod) {
102 12
            if ($hasInheritDocTag || $isMagicMethod) {
103 1
                return;
104 12
            } elseif ($method->getMetadata()->isExtended()) {
105 1
                $originId = $this->addViolationId ? $method->getId() : null;
106 1
                SniffHelper::addViolation($file, 'Missing @inheritDoc tag. Remove duplicated parent PHPDoc content.', $method->getLine(), static::CODE, $this->reportType, $originId);
107 1
                return;
108
            }
109
        }
110
111
        // @param
112 12
        foreach ($fnSig->getParams() as $fnParam) {
113 10
            $paramTag = $docBlock->getParamTag($fnParam->getName());
114 10
            $id = $method->getId() . $fnParam->getName();
115 10
            $subject = ParamTypeSubject::fromParam($fnParam, $paramTag, $docBlock, $id);
116 10
            $this->processSigType($subject);
117 10
            $subject->writeViolationsTo($file, static::CODE, $this->reportType, $this->addViolationId);
118
        }
119
120
        // @return
121 12
        if (!$isConstructMethod) {
122 12
            $returnTag = $docBlock->getReturnTag();
123 12
            $subject = ReturnTypeSubject::fromSignature($fnSig, $returnTag, $docBlock, $method->getAttributeNames(), $method->getId());
124 12
            $this->processSigType($subject);
125 12
            if ($method instanceof ClassMethodElement && $parent instanceof ClassElement) {
126 10
                $this->reportNullableBasicGetter && $this->reportNullableBasicGetter($subject, $method, $parent);
127
            }
128 12
            $subject->writeViolationsTo($file, static::CODE, $this->reportType, $this->addViolationId);
129
        } else {
130 7
            foreach ($docBlock->getDescriptionLines() as $lineNum => $descLine) {
131 2
                if (preg_match('#^\w+\s+constructor\.?$#', $descLine)) {
132 2
                    $originId = $this->addViolationId ? $method->getId() : null;
133 2
                    SniffHelper::addViolation($file, 'Useless description.', $lineNum, static::CODE, $this->reportType, $originId);
134
                }
135
            }
136
        }
137 12
    }
138
139 12
    protected function processSigType(AbstractTypeSubject $subject): void
140
    {
141 12
        FnTypeInspector::reportSuggestedTypes($subject);
142 12
        FnTypeInspector::reportReplaceableTypes($subject);
143
144 12
        if ($this->reportMissingTags || $subject->hasDocTypeTag()) {
145 12
            DocTypeInspector::reportMandatoryTypes($subject);
146 12
            DocTypeInspector::reportSuggestedTypes($subject);
147 12
            DocTypeInspector::reportReplaceableTypes($subject);
148
149 12
            DocTypeInspector::reportRemovableTypes($subject);
150 12
            DocTypeInspector::reportInvalidTypes($subject);
151 12
            DocTypeInspector::reportMissingOrWrongTypes($subject);
152
        } else {
153 2
            DocTypeInspector::reportMandatoryTypes($subject, true);
154
        }
155 12
    }
156
157 16
    protected function hasUselessDocBlock(AbstractFqcnMethodElement $method): bool
158
    {
159 16
        $fnSig = $method->getSignature();
160 16
        $docBlock = $method->getDocBlock();
161
162 16
        $docReturnTag = $docBlock->getReturnTag();
163
164
        if (
165 16
            $docBlock instanceof UndefinedDocBlock
166 15
            || $docBlock->hasDescription()
167 14
            || ($docReturnTag && $docReturnTag->hasDescription())
168
            // check if other "useful" tags are present
169 16
            || array_diff($docBlock->getTagNames(), ['param', 'return'])
170
        ) {
171 9
            return false;
172
        }
173
174 14
        foreach ($fnSig->getParams() as $fnParam) {
175 11
            $paramTag = $docBlock->getParamTag($fnParam->getName());
176 11
            if (null === $paramTag) {
177 1
                return false; // missing, needs to be fixed
178
            }
179
180 10
            if ($paramTag->hasDescription()) {
181 3
                return false;
182
            }
183
184 7
            $fnType = $fnParam->getType();
185 7
            $rawFnType = $fnType instanceof NullableType
186 2
                ? $fnType->toDocString()
187 7
                : $fnType->toString();
188 7
            if ($paramTag->getType()->toString() !== $rawFnType) {
189 5
                return false;
190
            }
191
        }
192
193 8
        $returnTag  = $docBlock->getReturnTag();
194 8
        $returnType = $fnSig->getReturnType();
195
196 8
        if ($returnTag && $returnType) {
197 7
            $rawReturnType = $returnType instanceof NullableType
198 4
                ? $returnType->toDocString()
199 7
                : $returnType->toString();
200 7
            if ($returnTag->getType()->toString() !== $rawReturnType) {
201 4
                return false;
202
            }
203
        }
204
205 5
        return true;
206
    }
207
208
    /**
209
     * @param File                      $file
210
     * @param AbstractFqcnMethodElement $method
211
     * @param string[]                  $invalidTags
212
     */
213 12
    protected function reportInvalidTags(File $file, AbstractFqcnMethodElement $method, array $invalidTags): void
214
    {
215 12
        foreach ($method->getDocBlock()->getTags() as $tag) {
216 10
            foreach ($invalidTags as $invalidTagName) {
217 1
                if ($tag->getName() === $invalidTagName) {
218 1
                    $originId = $this->addViolationId ? $method->getId() . $invalidTagName : null;
219 1
                    SniffHelper::addViolation($file, 'Useless tag', $tag->getLine(), static::CODE, $this->reportType, $originId);
220
                }
221
            }
222
        }
223 12
    }
224
225 10
    protected function reportNullableBasicGetter(
226
        ReturnTypeSubject $subject,
227
        ClassMethodElement $method,
228
        ClassElement $class
229
    ): void {
230 10
        $propName = $method->getMetadata()->getBasicGetterPropName();
231 10
        if (null === $propName) {
232 7
            return;
233
        }
234
235 5
        $prop = $class->getProperty($propName);
236 5
        if (null === $prop) {
237 5
            return;
238
        }
239
240
        /** @var VarTag|null $varTag */
241 3
        $varTag = $prop->getDocBlock()->getTagsByName('var')[0] ?? null;
242 3
        if (null === $varTag) {
243 1
            return;
244
        }
245
246 3
        $propDocType = $varTag->getType();
247 3
        $isPropNullable = TypeHelper::containsType($varTag->getType(), NullType::class);
248 3
        if (!$isPropNullable) {
249 1
            return;
250
        }
251
252 3
        $returnDocType = $subject->getDocType();
253 3
        $isGetterDocTypeNullable = TypeHelper::containsType($returnDocType, NullType::class);
254 3
        if ($returnDocType || $this->reportMissingTags) {
255 2
            if (!$isGetterDocTypeNullable && $subject->hasDefinedDocBlock()) {
256 2
                $subject->addDocTypeWarning(sprintf(
257 2
                    'Returned property $%s is nullable, add null return doc type, e.g. %s',
258
                    $propName,
259 2
                    $propDocType->toString()
260
                ));
261
            }
262
        }
263
264
        // Only report in fn type is defined. Doc type and fn type is synced by other sniffs.
265 3
        $returnFnType = $subject->getFnType();
266 3
        if (!($returnFnType instanceof UndefinedType) && !($returnFnType instanceof NullableType)) {
267 1
            $subject->addFnTypeWarning(sprintf(
268 1
                'Returned property $%s is nullable, use nullable return type declaration, e.g. ?%s',
269
                $propName,
270 1
                $returnFnType->toString()
271
            ));
272
        }
273 3
    }
274
}
275