Completed
Push — master ( e2e2f1...f855fb )
by Björn
03:26 queued 42s
created

ParamTagSniff::loadArgumentTokenOfTag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
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\DocTags;
6
7
use BestIt\CodeSniffer\CodeError;
8
use BestIt\CodeSniffer\CodeWarning;
9
use SlevomatCodingStandard\Helpers\TokenHelper;
10
use function array_filter;
11
use function array_values;
12
use function in_array;
13
use function preg_quote;
14
use function strtolower;
15
use const T_CLOSE_PARENTHESIS;
16
use const T_DOC_COMMENT_OPEN_TAG;
17
use const T_VARIABLE;
18
19
/**
20
 * Sniff for the param tag content.
21
 *
22
 * @author blange <[email protected]>
23
 * @package BestIt\Sniffs\DocTags
24
 */
25
class ParamTagSniff extends AbstractTagSniff
26
{
27
    use TagContentFormatTrait;
28
29
    /**
30
     * The error code for the missing description.
31
     *
32
     * @var string
33
     */
34
    public const CODE_TAG_MISSING_DESC = 'MissingDesc';
35
36
    /**
37
     * The error code if the type of the param tag is missing.
38
     *
39
     * @var string
40
     */
41
    public const CODE_TAG_MISSING_TYPE = 'MissingType';
42
43
    /**
44
     * The error code if the matching property is missing.
45
     *
46
     * @var string
47
     */
48
    public const CODE_TAG_MISSING_VARIABLE = 'MissingVariable';
49
50
    /**
51
     * The error code if every property is missing.
52
     *
53
     * @var string
54
     */
55
    public const CODE_TAG_MISSING_VARIABLES = 'MissingVariables';
56
57
    /**
58
     * Error code for the mixed type.
59
     *
60
     * @var string
61
     */
62
    public const CODE_TAG_MIXED_TYPE = 'MixedType';
63
64
    /**
65
     * Message for displaying the missing description.
66
     *
67
     * @var string
68
     */
69
    private const MESSAGE_TAG_MISSING_DESC = 'There is no description for your tag: %s.';
70
71
    /**
72
     * Message for displaying the missing type.
73
     *
74
     * @var string
75
     */
76
    private const MESSAGE_TAG_MISSING_TYPE = 'There is no type for your tag: %s.';
77
78
    /**
79
     * Message for displaying the missing property.
80
     *
81
     * @var string
82
     */
83
    private const MESSAGE_TAG_MISSING_VARIABLE = 'There is no property for your tag "%s".';
84
85
    /**
86
     * Message for displaying the missing properties.
87
     *
88
     * @var string
89
     */
90
    private const MESSAGE_TAG_MISSING_VARIABLES = 'There are no properties for your tags.';
91
92
    /**
93
     * The message for the mixed type warning.
94
     *
95
     * @var string
96
     */
97
    private const MESSAGE_TAG_MIXED_TYPE = 'We suggest that you avoid the "mixed" type and declare the ' .
98
        'required types in detail.';
99
100
    /**
101
     * The token of the real function argument if it exists or null.
102
     *
103
     * @var array|null Is filled by runtime.
104
     */
105
    private $argumentToken = null;
106
107
    /**
108
     * The used variable tokens for this method.
109
     *
110
     * @var array
111
     */
112
    protected $varTokens;
113
114
    /**
115
     * Simple check if the pattern is correct.
116
     *
117
     * @param string|null $tagContent
118
     * @throws CodeWarning
119
     *
120
     * @return bool True if it matches.
121
     */
122
    private function checkAgainstPattern(?string $tagContent = null): bool
123
    {
124
        if (!$return = $this->isValidContent($tagContent)) {
125
            throw (new CodeError(self::CODE_TAG_MISSING_VARIABLE, self::MESSAGE_TAG_MISSING_VARIABLE, $this->stackPos))
126
                ->setPayload([$tagContent])
127
                ->setToken($this->token);
0 ignored issues
show
Bug introduced by
It seems like $this->token can also be of type null; however, BestIt\CodeSniffer\CodeWarning::setToken() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
128
        }
129
130
        return $return;
131
    }
132
133
    /**
134
     * Checks if the argument of the function itself exists.
135
     *
136
     * @param null|string $tagContent
137
     * @throws CodeWarning
138
     *
139
     * @return void
140
     */
141
    private function checkArgumentItself(?string $tagContent = null): void
142
    {
143
        if (!$this->getArgumentTokenOfTag()) {
144
            throw (new CodeError(
145
                static::CODE_TAG_MISSING_VARIABLE,
146
                self::MESSAGE_TAG_MISSING_VARIABLE,
147
                $this->stackPos
148
            ))
149
                ->setPayload([$tagContent])
150
                ->setToken($this->token);
0 ignored issues
show
Bug introduced by
It seems like $this->token can also be of type null; however, BestIt\CodeSniffer\CodeWarning::setToken() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
151
        }
152
    }
153
154
    /**
155
     * Checks if the param contains a description.
156
     *
157
     * @throws CodeWarning
158
     *
159
     * @return bool Returns true if there is a desc.
160
     */
161
    private function checkDescription(): bool
162
    {
163
        if ($hasNoDesc = !@$this->matches['desc']) {
164
            throw (new CodeWarning(self::CODE_TAG_MISSING_DESC, self::MESSAGE_TAG_MISSING_DESC, $this->stackPos))
165
                ->setPayload([$this->matches['var']])
166
                ->setToken($this->token);
0 ignored issues
show
Bug introduced by
It seems like $this->token can also be of type null; however, BestIt\CodeSniffer\CodeWarning::setToken() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
167
        }
168
169
        return !$hasNoDesc;
170
    }
171
172
    /**
173
     * Checks if the param tag contains a valid type.
174
     *
175
     * @throws CodeWarning
176
     *
177
     * @return bool True if the type is valid.
178
     */
179
    private function checkType(): bool
180
    {
181
        if (!@$this->matches['type']) {
182
            throw (new CodeError(self::CODE_TAG_MISSING_TYPE, self::MESSAGE_TAG_MISSING_TYPE, $this->stackPos))
183
                ->setPayload([$this->matches['var']])
184
                ->setToken($this->token);
0 ignored issues
show
Bug introduced by
It seems like $this->token can also be of type null; however, BestIt\CodeSniffer\CodeWarning::setToken() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
185
        }
186
187
        if (strtolower($this->matches['type']) === 'mixed') {
188
            throw (new CodeWarning(self::CODE_TAG_MIXED_TYPE, self::MESSAGE_TAG_MIXED_TYPE, $this->stackPos))
189
                ->setToken($this->token);
0 ignored issues
show
Bug introduced by
It seems like $this->token can also be of type null; however, BestIt\CodeSniffer\CodeWarning::setToken() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
190
        }
191
192
193
        return true;
194
    }
195
196
    /**
197
     * Loads all variables of the following method.
198
     *
199
     * @return array
200
     */
201
    protected function findAllVariablePositions(): array
202
    {
203
        return TokenHelper::findNextAll(
204
            $this->file->getBaseFile(),
205
            [T_VARIABLE],
206
            $this->stackPos + 1,
207
            $this->file->findNext([T_CLOSE_PARENTHESIS], $this->stackPos + 1)
208
        );
209
    }
210
211
    /**
212
     * Returns a pattern to check if the content is valid.
213
     *
214
     * @return string The pattern which matches successful.
215
     */
216
    protected function getValidPattern(): string
217
    {
218
        $varOfThisTag = $this->getArgumentTokenOfTag();
219
220
        return '/(?P<type>[\w|\|\[\]]*) ?(?P<var>' . preg_quote($varOfThisTag['content'], '/') .
221
            ') ?(?P<desc>.*)/m';
222
    }
223
224
    /**
225
     * Returns the name of the real function argument for this parameter tag.
226
     *
227
     * @return array Null if there is no matching function argument.
228
     */
229
    private function getArgumentTokenOfTag(): array
230
    {
231
        if ($this->argumentToken === null) {
232
            $this->argumentToken = $this->loadArgumentTokenOfTag();
233
        }
234
235
        return $this->argumentToken;
236
    }
237
238
    /**
239
     * Loads and checks the variables of the following method.
240
     *
241
     * @throws CodeWarning We have param tags, so there should be variables in the method.
242
     *
243
     * @return array The positions of the methods variables if there are any.
244
     */
245
    private function loadAndCheckVarPositions(): array
246
    {
247
        $varPositions = $this->findAllVariablePositions();
248
249
        if (!$varPositions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $varPositions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
250
            throw (new CodeError(
251
                self::CODE_TAG_MISSING_VARIABLES,
252
                self::MESSAGE_TAG_MISSING_VARIABLES,
253
                $this->stackPos
254
            ))->setToken($this->token);
0 ignored issues
show
Bug introduced by
It seems like $this->token can also be of type null; however, BestIt\CodeSniffer\CodeWarning::setToken() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
255
        }
256
257
        $this->varTokens = array_filter($this->tokens, function (array $token) use ($varPositions): bool {
258
            return in_array($token['pointer'], $varPositions, true);
259
        });
260
261
        return $varPositions;
262
    }
263
264
    /**
265
     * Loads the function argument of this tag.
266
     *
267
     * @return array
268
     */
269
    private function loadArgumentTokenOfTag(): array
270
    {
271
        // Give me the other tags of this doc block before this one.
272
        $tagPosBeforeThis = TokenHelper::findNextAll(
273
            $this->file->getBaseFile(),
274
            $this->register(),
275
            $this->file->findPrevious([T_DOC_COMMENT_OPEN_TAG], $this->stackPos),
276
            $this->stackPos - 1
277
        );
278
279
        $tagPosBeforeThis = array_filter($tagPosBeforeThis, function (int $position) {
280
            return $this->tokens[$position]['content'] === '@param';
281
        });
282
283
        return (array_values($this->varTokens)[count($tagPosBeforeThis)]) ?? [];
284
    }
285
286
    /**
287
     * Processed the content of the required tag.
288
     *
289
     * @param null|string $tagContent The possible tag content or null.
290
     * @throws CodeWarning
291
     *
292
     * @return void
293
     */
294
    protected function processTagContent(?string $tagContent = null): void
295
    {
296
        $varPoss = $this->loadAndCheckVarPositions();
297
298
        if ($varPoss) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $varPoss of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
299
            $this->checkArgumentItself($tagContent);
300
            $this->checkAgainstPattern($tagContent);
301
            $this->checkType();
302
            $this->checkDescription();
303
        }
304
    }
305
306
    /**
307
     * For which tag should be sniffed?
308
     *
309
     * @return string The name of the tag without the "@"-prefix.
310
     */
311
    protected function registerTag(): string
312
    {
313
        return 'param';
314
    }
315
316
    /**
317
     * Sets up the test and loads the matching var if there is one.
318
     *
319
     * @return void
320
     */
321
    protected function setUp(): void
322
    {
323
        parent::setUp();
324
325
        $this->argumentToken = null;
326
    }
327
}
328