• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

webimpress / coding-standard / 4086684577

pending completion
4086684577

Pull #178

github

GitHub
Merge 75aa3b533 into 18aa29088
Pull Request #178: Bump phpunit/phpunit from 9.5.20 to 9.6.0

6985 of 6999 relevant lines covered (99.8%)

1.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.46
/src/WebimpressCodingStandard/Sniffs/Functions/ThrowsSniff.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace WebimpressCodingStandard\Sniffs\Functions;
6

7
use PHP_CodeSniffer\Files\File;
8
use PHP_CodeSniffer\Sniffs\Sniff;
9
use PHP_CodeSniffer\Util\Tokens;
10
use WebimpressCodingStandard\Helper\MethodsTrait;
11

12
use function array_reverse;
13
use function array_unique;
14
use function count;
15
use function in_array;
16
use function preg_split;
17
use function strtolower;
18
use function trim;
19

20
use const T_BITWISE_OR;
21
use const T_CATCH;
22
use const T_CLOSE_CURLY_BRACKET;
23
use const T_CLOSE_PARENTHESIS;
24
use const T_CLOSE_SHORT_ARRAY;
25
use const T_CLOSE_SQUARE_BRACKET;
26
use const T_CLOSURE;
27
use const T_DOC_COMMENT_STRING;
28
use const T_FUNCTION;
29
use const T_NEW;
30
use const T_NS_SEPARATOR;
31
use const T_OPEN_CURLY_BRACKET;
32
use const T_OPEN_PARENTHESIS;
33
use const T_OPEN_SHORT_ARRAY;
34
use const T_OPEN_SQUARE_BRACKET;
35
use const T_SEMICOLON;
36
use const T_STRING;
37
use const T_THROW;
38
use const T_TRY;
39
use const T_VARIABLE;
40

41
class ThrowsSniff implements Sniff
42
{
43
    use MethodsTrait;
44

45
    /**
46
     * @var string[]
47
     */
48
    private $throwTags = [];
49

50
    /**
51
     * @var int[]
52
     */
53
    private $nameTokens = [T_NS_SEPARATOR, T_STRING];
54

55
    /**
56
     * @return int[]
57
     */
58
    public function register() : array
59
    {
60
        return [T_FUNCTION];
1✔
61
    }
62

63
    /**
64
     * @param int $stackPtr
65
     */
66
    public function process(File $phpcsFile, $stackPtr)
67
    {
68
        $this->initScope($phpcsFile, $stackPtr);
1✔
69

70
        $this->throwTags = [];
1✔
71

72
        if ($commentStart = $this->getCommentStart($phpcsFile, $stackPtr)) {
1✔
73
            $this->processThrowsDoc($phpcsFile, $commentStart);
1✔
74
        }
75
        $this->processThrowStatements($phpcsFile, $stackPtr);
1✔
76
    }
77

78
    private function processThrowsDoc(File $phpcsFile, int $commentStart) : void
79
    {
80
        $tokens = $phpcsFile->getTokens();
1✔
81

82
        foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
1✔
83
            if (strtolower($tokens[$tag]['content']) !== '@throws') {
1✔
84
                continue;
1✔
85
            }
86

87
            $exception = null;
1✔
88
            if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) {
1✔
89
                $split = preg_split('/\s/', $tokens[$tag + 2]['content'], 2);
1✔
90
                $exception = $split[0];
1✔
91
                $description = isset($split[1]) ? trim($split[1]) : null;
1✔
92
                $suggested = $this->getSuggestedType($exception);
1✔
93

94
                if ($exception !== $suggested) {
1✔
95
                    $error = 'Invalid exception type; expected %s, but found %s';
1✔
96
                    $data = [
1✔
97
                        $suggested,
1✔
98
                        $exception,
1✔
99
                    ];
1✔
100
                    $fix = $phpcsFile->addFixableError($error, $tag + 2, 'InvalidType', $data);
1✔
101

102
                    if ($fix) {
1✔
103
                        $content = trim($suggested . ' ' . $description);
1✔
104
                        $phpcsFile->fixer->replaceToken($tag + 2, $content);
1✔
105
                    }
106
                }
107

108
                $this->throwTags[$tag] = $suggested;
1✔
109
            }
110

111
            if (! $exception) {
1✔
112
                $error = 'Exception type missing for @throws tag in function comment';
1✔
113
                $phpcsFile->addError($error, $tag, 'MissingType');
1✔
114
            }
115
        }
116
    }
117

118
    protected function processThrowStatements(File $phpcsFile, int $stackPtr) : void
119
    {
120
        $tokens = $phpcsFile->getTokens();
1✔
121

122
        // Skip function without body
123
        if (! isset($tokens[$stackPtr]['scope_opener'])) {
1✔
124
            return;
1✔
125
        }
126

127
        $scopeBegin = $tokens[$stackPtr]['scope_opener'];
1✔
128
        $scopeEnd = $tokens[$stackPtr]['scope_closer'];
1✔
129

130
        $thrownExceptions = [];
1✔
131
        $thrownVariables = 0;
1✔
132
        $foundThrows = false;
1✔
133

134
        $throw = $scopeBegin;
1✔
135
        while (true) {
1✔
136
            $throw = $phpcsFile->findNext(T_THROW, $throw + 1, $scopeEnd);
1✔
137

138
            // Throw statement not found.
139
            if (! $throw) {
1✔
140
                break;
1✔
141
            }
142

143
            // The throw statement is in another scope.
144
            if (! $this->isLastScope($phpcsFile, $tokens[$throw]['conditions'], $stackPtr)) {
1✔
145
                continue;
1✔
146
            }
147

148
            $foundThrows = true;
1✔
149

150
            $next = $phpcsFile->findNext(Tokens::$emptyTokens, $throw + 1, null, true);
1✔
151
            if ($tokens[$next]['code'] === T_NEW) {
1✔
152
                $currException = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true);
1✔
153

154
                if (in_array($tokens[$currException]['code'], $this->nameTokens, true)) {
1✔
155
                    $end = $phpcsFile->findNext($this->nameTokens, $currException + 1, null, true);
1✔
156

157
                    $class = $phpcsFile->getTokensAsString($currException, $end - $currException);
1✔
158
                    $suggested = $this->getSuggestedType($class);
1✔
159

160
                    if ($class !== $suggested) {
1✔
161
                        $error = 'Invalid exception class name; expected %s, but found %s';
1✔
162
                        $data = [
1✔
163
                            $suggested,
1✔
164
                            $class,
1✔
165
                        ];
1✔
166
                        $fix = $phpcsFile->addFixableError($error, $currException, 'InvalidExceptionClassName', $data);
1✔
167

168
                        if ($fix) {
1✔
169
                            $phpcsFile->fixer->beginChangeset();
1✔
170
                            $phpcsFile->fixer->replaceToken($currException, $suggested);
1✔
171
                            for ($i = $currException + 1; $i < $end; ++$i) {
1✔
172
                                $phpcsFile->fixer->replaceToken($i, '');
1✔
173
                            }
174
                            $phpcsFile->fixer->endChangeset();
1✔
175
                        }
176
                    }
177

178
                    $thrownExceptions[] = $suggested;
1✔
179
                    continue;
1✔
180
                }
181
            } elseif ($tokens[$next]['code'] === T_VARIABLE) {
1✔
182
                $catch = $phpcsFile->findPrevious(T_CATCH, $throw, $scopeBegin);
1✔
183

184
                if ($catch) {
1✔
185
                    $thrownVar = $phpcsFile->findPrevious(
1✔
186
                        T_VARIABLE,
1✔
187
                        $tokens[$catch]['parenthesis_closer'] - 1,
1✔
188
                        $tokens[$catch]['parenthesis_opener']
1✔
189
                    );
1✔
190

191
                    if ($tokens[$thrownVar]['content'] === $tokens[$next]['content']) {
1✔
192
                        $exceptions = $this->getExceptions(
1✔
193
                            $phpcsFile,
1✔
194
                            $tokens[$catch]['parenthesis_opener'] + 1,
1✔
195
                            $thrownVar - 1
1✔
196
                        );
1✔
197

198
                        foreach ($exceptions as $exception) {
1✔
199
                            $thrownExceptions[] = $exception;
1✔
200
                        }
201
                    }
202

203
                    continue;
1✔
204
                }
205
            }
206

207
            ++$thrownVariables;
1✔
208
        }
209

210
        if (! $foundThrows) {
1✔
211
            // It should be disabled if we want to declare implicit throws
212
            foreach ($this->throwTags as $ptr => $class) {
1✔
213
                $error = 'Function does not throw any exception but has @throws tag';
1✔
214
                $phpcsFile->addError($error, $ptr, 'AdditionalThrowTag');
1✔
215
            }
216

217
            return;
1✔
218
        }
219

220
        // Only need one @throws tag for each type of exception thrown.
221
        $thrownExceptions = array_unique($thrownExceptions);
1✔
222

223
        // Make sure @throws tag count matches thrown count.
224
        $thrownCount = count($thrownExceptions) ?: 1;
1✔
225
        $tagCount = count(array_unique($this->throwTags));
1✔
226

227
        if ($thrownVariables > 0) {
1✔
228
            if ($thrownCount > $tagCount) {
1✔
229
                $error = 'Expected at least %d @throws tag(s) in function comment; %d found';
1✔
230
                $data = [
1✔
231
                    $thrownCount,
1✔
232
                    $tagCount,
1✔
233
                ];
1✔
234
                $phpcsFile->addError($error, $stackPtr, 'WrongNumberAtLeast', $data);
1✔
235
                return;
1✔
236
            }
237
        } else {
238
            if ($thrownCount !== $tagCount) {
1✔
239
                $error = 'Expected %d @throws tag(s) in function comment; %d found';
1✔
240
                $data = [
1✔
241
                    $thrownCount,
1✔
242
                    $tagCount,
1✔
243
                ];
1✔
244
                $phpcsFile->addError($error, $stackPtr, 'WrongNumberExact', $data);
1✔
245
                return;
1✔
246
            }
247
        }
248

249
        foreach ($thrownExceptions as $throw) {
1✔
250
            if (! in_array($throw, $this->throwTags, true)) {
1✔
251
                $error = 'Missing @throws tag for "%s" exception';
1✔
252
                $data = [$throw];
1✔
253
                $phpcsFile->addError($error, $stackPtr, 'Missing', $data);
1✔
254
            }
255
        }
256
    }
257

258
    /**
259
     * @return string[]
260
     */
261
    private function getExceptions(File $phpcsFile, int $from, int $to) : array
262
    {
263
        $tokens = $phpcsFile->getTokens();
1✔
264

265
        $exceptions = [];
1✔
266
        $currName = '';
1✔
267
        $start = null;
1✔
268
        $end = null;
1✔
269

270
        for ($i = $from; $i <= $to; ++$i) {
1✔
271
            if (in_array($tokens[$i]['code'], $this->nameTokens, true)) {
1✔
272
                if ($currName === '') {
1✔
273
                    $start = $i;
1✔
274
                }
275

276
                $end = $i;
1✔
277
                $currName .= $tokens[$i]['content'];
1✔
278
            }
279

280
            if ($tokens[$i]['code'] === T_BITWISE_OR || $i === $to) {
1✔
281
                $suggested = $this->getSuggestedType($currName);
1✔
282

283
                if ($suggested !== $currName) {
1✔
284
                    $error = 'Invalid exception class name in catch; expected %s, but found %s';
1✔
285
                    $data = [
1✔
286
                        $suggested,
1✔
287
                        $currName,
1✔
288
                    ];
1✔
289
                    $fix = $phpcsFile->addFixableError($error, $start, 'InvalidCatchClassName', $data);
1✔
290

291
                    if ($fix) {
1✔
292
                        $phpcsFile->fixer->beginChangeset();
1✔
293
                        $phpcsFile->fixer->replaceToken($start, $suggested);
1✔
294
                        for ($j = $start + 1; $j <= $end; ++$j) {
1✔
295
                            $phpcsFile->fixer->replaceToken($j, '');
1✔
296
                        }
297
                        $phpcsFile->fixer->endChangeset();
1✔
298
                    }
299
                }
300

301
                $exceptions[] = $suggested;
1✔
302
                $currName = '';
1✔
303
                $start = null;
1✔
304
                $end = null;
1✔
305
            }
306
        }
307

308
        return $exceptions;
1✔
309
    }
310

311
    /**
312
     * Check if $scope is the last closure/function/try condition.
313
     *
314
     * @param string[] $conditions
315
     * @param int $scope Scope to check in conditions.
316
     */
317
    private function isLastScope(File $phpcsFile, array $conditions, int $scope) : bool
318
    {
319
        $tokens = $phpcsFile->getTokens();
1✔
320

321
        foreach (array_reverse($conditions, true) as $ptr => $code) {
1✔
322
            if ($code !== T_FUNCTION && $code !== T_CLOSURE && $code !== T_TRY) {
1✔
323
                continue;
1✔
324
            }
325

326
            if ($code === T_CLOSURE && $ptr !== $scope) {
1✔
327
                // Check if closure is called.
328
                $afterClosure = $phpcsFile->findNext(
1✔
329
                    Tokens::$emptyTokens,
1✔
330
                    $tokens[$ptr]['scope_closer'] + 1,
1✔
331
                    null,
1✔
332
                    true
1✔
333
                );
1✔
334
                if ($afterClosure && $tokens[$afterClosure]['code'] === T_CLOSE_PARENTHESIS) {
1✔
335
                    $next = $phpcsFile->findNext(Tokens::$emptyTokens, $afterClosure + 1, null, true);
1✔
336
                    if ($next && $tokens[$next]['code'] === T_OPEN_PARENTHESIS) {
1✔
337
                        return true;
1✔
338
                    }
339
                }
340

341
                // Check if closure is passed to function/class.
342
                if (($token = $this->findPrevious($phpcsFile, $ptr))
1✔
343
                    && in_array($tokens[$token]['code'], [T_STRING, T_VARIABLE], true)
1✔
344
                ) {
345
                    return true;
1✔
346
                }
347
            }
348

349
            return $ptr === $scope;
1✔
350
        }
351

352
        return false;
×
353
    }
354

355
    private function findPrevious(File $phpcsFile, int $ptr) : ?int
356
    {
357
        $tokens = $phpcsFile->getTokens();
1✔
358

359
        while (--$ptr) {
1✔
360
            if ($tokens[$ptr]['code'] === T_CLOSE_PARENTHESIS) {
1✔
361
                $ptr = $tokens[$ptr]['parenthesis_opener'];
1✔
362
            } elseif ($tokens[$ptr]['code'] === T_CLOSE_CURLY_BRACKET
1✔
363
                || $tokens[$ptr]['code'] === T_CLOSE_SHORT_ARRAY
1✔
364
                || $tokens[$ptr]['code'] === T_CLOSE_SQUARE_BRACKET
1✔
365
            ) {
366
                $ptr = $tokens[$ptr]['bracket_opener'];
1✔
367
            } elseif ($tokens[$ptr]['code'] === T_OPEN_PARENTHESIS) {
1✔
368
                return $phpcsFile->findPrevious(Tokens::$emptyTokens, $ptr - 1, null, true);
1✔
369
            } elseif (in_array(
1✔
370
                $tokens[$ptr]['code'],
1✔
371
                [T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET],
1✔
372
                true
1✔
373
            )) {
1✔
374
                break;
1✔
375
            }
376
        }
377

378
        return null;
1✔
379
    }
380
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc