Passed
Push — master ( f2cb43...4e9f60 )
by Björn
56s queued 10s
created

ReturnTypeDeclarationSniff::tearDown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BestIt\Sniffs\TypeHints;
6
7
use BestIt\CodeSniffer\File;
8
use BestIt\Sniffs\AbstractSniff;
9
use BestIt\Sniffs\DocPosProviderTrait;
10
use BestIt\Sniffs\FunctionRegistrationTrait;
11
use BestIt\Sniffs\SuppressingTrait;
12
use SlevomatCodingStandard\Helpers\Annotation;
13
use SlevomatCodingStandard\Helpers\FunctionHelper;
14
use SlevomatCodingStandard\Helpers\TypeHintHelper;
15
use function strpos;
16
use function substr;
17
18
/**
19
 * Class ReturnTypeDeclarationSniff
20
 *
21
 * @author Stephan Weber <[email protected]>
22
 * @package BestIt\Sniffs\TypeHints
23
 */
24
class ReturnTypeDeclarationSniff extends AbstractSniff
25
{
26
    use DocPosProviderTrait;
27
    use FunctionRegistrationTrait;
28
    use SuppressingTrait;
29
30
    /**
31
     * The error code for this sniff.
32
     *
33
     * @var string
34
     */
35
    public const CODE_MISSING_RETURN_TYPE_HINT = 'MissingReturnTypeHint';
36
37
    /**
38
     * Returns true if this sniff may run.
39
     *
40
     * @return bool
41
     */
42
    protected function areRequirementsMet(): bool
43
    {
44
        return !$this->isSniffSuppressed(static::CODE_MISSING_RETURN_TYPE_HINT) && !$this->hasInheritdocAnnotation();
45
    }
46
47
    /**
48
     * Check method type hints based on return annotation.
49
     *
50
     * @return void
51
     */
52
    protected function processToken(): void
53
    {
54
        if ($this->hasReturnType()) {
55
            return;
56
        }
57
58
        $returnAnnotation = FunctionHelper::findReturnAnnotation($this->file->getBaseFile(), $this->stackPos);
59
        $hasReturnAnnotation = $this->hasReturnAnnotation($returnAnnotation);
60
        $returnTypeHintDef = '';
61
62
        if ($hasReturnAnnotation) {
63
            $returnTypeHintDef = preg_split('~\\s+~', $returnAnnotation->getContent())[0];
64
        }
65
66
        $returnsValue = $this->returnsValue($hasReturnAnnotation, $returnTypeHintDef);
67
68
        if (!$hasReturnAnnotation && $returnsValue) {
69
            $this->file->addError(
70
                sprintf(
71
                    '%s %s() does not have return type hint nor @return annotation for its return value.',
72
                    $this->getFunctionTypeLabel($this->file, $this->stackPos),
0 ignored issues
show
Bug introduced by
It seems like $this->file can be null; however, getFunctionTypeLabel() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
73
                    FunctionHelper::getFullyQualifiedName($this->file->getBaseFile(), $this->stackPos)
74
                ),
75
                $this->stackPos,
76
                self::CODE_MISSING_RETURN_TYPE_HINT
77
            );
78
        }
79
80
        if (!$hasReturnAnnotation || !$returnsValue) {
81
            return;
82
        }
83
84
        $oneTypeHintDef = $this->definitionContainsOneTypeHint($returnTypeHintDef);
85
        $isValidTypeHint = $this->isValidTypeHint($returnTypeHintDef);
86
87
        if ($oneTypeHintDef && $isValidTypeHint) {
88
            $possibleReturnType = $returnTypeHintDef;
89
            $nullableReturnType = false;
90
91
            $fixable = $this->file->addFixableError(
92
                sprintf(
93
                    '%s %s() does not have return type hint for its return value'
94
                    . ' but it should be possible to add it based on @return annotation "%s".',
95
                    $this->getFunctionTypeLabel($this->file, $this->stackPos),
0 ignored issues
show
Bug introduced by
It seems like $this->file can be null; however, getFunctionTypeLabel() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
96
                    FunctionHelper::getFullyQualifiedName($this->file->getBaseFile(), $this->stackPos),
97
                    $returnTypeHintDef
98
                ),
99
                $this->stackPos,
100
                self::CODE_MISSING_RETURN_TYPE_HINT
101
            );
102
103
            $this->fixTypeHint(
104
                $fixable,
105
                $possibleReturnType,
106
                $nullableReturnType
107
            );
108
        }
109
    }
110
111
    /**
112
     * Checks if the function returns a value
113
     *
114
     * @param bool $hasReturnAnnotation Function has a return annotation
115
     * @param string $returnTypeHintDef Return annotation type
116
     *
117
     * @return bool Function returns a value other than void
118
     */
119
    private function returnsValue(bool $hasReturnAnnotation, string $returnTypeHintDef): bool
120
    {
121
        $returnsValue = ($hasReturnAnnotation && $returnTypeHintDef !== 'void');
122
123
        if (!FunctionHelper::isAbstract($this->file->getBaseFile(), $this->stackPos)) {
124
            $returnsValue = FunctionHelper::returnsValue($this->file->getBaseFile(), $this->stackPos);
125
        }
126
127
        return $returnsValue;
128
    }
129
130
    /**
131
     * Check if type hint sniff should be suppressed
132
     *
133
     * @return bool Suppressed or not
134
     */
135
    private function hasReturnType(): bool
136
    {
137
        return FunctionHelper::findReturnTypeHint($this->file->getBaseFile(), $this->stackPos) !== null;
138
    }
139
140
    /**
141
     * Check if function has a return annotation
142
     *
143
     * @param Annotation|null $returnAnnotation Annotation of the function
144
     *
145
     * @return bool Function has a annotation
146
     */
147
    private function hasReturnAnnotation($returnAnnotation): bool
148
    {
149
        return $returnAnnotation !== null && $returnAnnotation->getContent() !== null;
150
    }
151
152
    /**
153
     * Fixes the type hint error
154
     *
155
     * @todo Check prior php 7.1 with the void return type.
156
     *
157
     * @param bool $fix Error is fixable
158
     * @param string $possibleReturnType Return annotation value
159
     * @param bool $nullableReturnType Is the return type nullable
160
     *
161
     * @return void
162
     */
163
    private function fixTypeHint(bool $fix, string $possibleReturnType, bool $nullableReturnType)
164
    {
165
        if ($fix) {
166
            $this->file->fixer->beginChangeset();
167
            $returnTypeHint = $possibleReturnType;
168
169
            if (TypeHintHelper::isSimpleTypeHint($possibleReturnType)) {
170
                $returnTypeHint = TypeHintHelper::convertLongSimpleTypeHintToShort($possibleReturnType);
171
            }
172
173
            if (substr($returnTypeHint, -2) === '[]') {
174
                $returnTypeHint = 'array';
175
            }
176
177
            $this->file->fixer->addContent(
178
                $this->token['parenthesis_closer'],
179
                sprintf(': %s%s', ($nullableReturnType ? '?' : ''), $returnTypeHint)
180
            );
181
            $this->file->fixer->endChangeset();
182
        }
183
    }
184
185
    /**
186
     * Check if method has an inheritdoc annotation
187
     *
188
     * @return bool Has inheritdoc
189
     */
190
    private function hasInheritdocAnnotation(): bool
191
    {
192
        $return = false;
193
194
        if ($this->getDocCommentPos()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getDocCommentPos() of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
195
            $docBlockContent = trim($this->getDocHelper()->getBlockStartToken()['content']);
196
197
            $return = strpos($docBlockContent, '@inheritdoc') !== false;
198
        }
199
200
        return $return;
201
    }
202
203
    /**
204
     * Check if method or function
205
     *
206
     * @param File $file The php cs file
207
     * @param int $functionPointer The current token
208
     *
209
     * @return string Returns either Method or Function
210
     */
211
    private function getFunctionTypeLabel(File $file, int $functionPointer): string
212
    {
213
        return FunctionHelper::isMethod($file->getBaseFile(), $functionPointer) ? 'Method' : 'Function';
214
    }
215
216
    /**
217
     * Check if the return annotation type is valid.
218
     *
219
     * @param string $typeHint The type value of the return annotation
220
     *
221
     * @return bool Type is valid
222
     */
223
    private function isValidTypeHint(string $typeHint): bool
224
    {
225
        return TypeHintHelper::isSimpleTypeHint($typeHint)
226
            || !TypeHintHelper::isSimpleUnofficialTypeHints($typeHint);
227
    }
228
229
    /**
230
     * Check if return annotation has only one type.
231
     *
232
     * @param string $typeHintDefinition The type values of the return annotation
233
     *
234
     * @return bool Return annotation has only one type
235
     */
236
    private function definitionContainsOneTypeHint(string $typeHintDefinition): bool
237
    {
238
        return strpos($typeHintDefinition, '|') === false;
239
    }
240
241
    /**
242
     * Resets the data of this sniff.
243
     *
244
     * @return void
245
     */
246
    protected function tearDown(): void
247
    {
248
        $this->resetDocCommentPos();
249
    }
250
}
251