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

sanmai / phpstan-rules / 16047125402

03 Jul 2025 09:46AM UTC coverage: 94.048%. First build
16047125402

push

github

web-flow
Upgrade to PHPStan 2

26 of 28 new or added lines in 3 files covered. (92.86%)

79 of 84 relevant lines covered (94.05%)

25.19 hits per line

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

93.02
/src/Rules/RequireGuardClausesInLoopsRule.php
1
<?php
2

3
/**
4
 * Copyright 2025 Alexey Kopytko <alexey@kopytko.com>
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18

19
declare(strict_types=1);
20

21
namespace Sanmai\PHPStanRules\Rules;
22

23
use PhpParser\Node;
24
use PhpParser\Node\Expr;
25
use PhpParser\Node\Stmt;
26
use PhpParser\Node\Stmt\Continue_;
27
use PhpParser\Node\Stmt\Do_;
28
use PhpParser\Node\Stmt\Expression;
29
use PhpParser\Node\Stmt\For_;
30
use PhpParser\Node\Stmt\Foreach_;
31
use PhpParser\Node\Stmt\If_;
32
use PhpParser\Node\Stmt\Return_;
33
use PhpParser\Node\Stmt\While_;
34
use PHPStan\Analyser\Scope;
35
use PHPStan\Rules\Rule;
36
use PHPStan\Rules\RuleErrorBuilder;
37
use Override;
38

39
use function count;
40
use function in_array;
41

42
/**
43
 * @implements Rule<Node>
44
 */
45
final class RequireGuardClausesInLoopsRule implements Rule
46
{
47
    public const ERROR_MESSAGE = 'Use guard clauses instead of wrapping code in if statements. Consider using: if (!condition) { continue; }';
48

49
    #[Override]
50
    public function getNodeType(): string
51
    {
52
        return Node::class;
52✔
53
    }
54

55
    /**
56
     * @return list<\PHPStan\Rules\IdentifierRuleError>
57
     */
58
    #[Override]
59
    public function processNode(Node $node, Scope $scope): array
60
    {
61
        if (!$this->isLoopNode($node)) {
52✔
62
            return [];
52✔
63
        }
64

65
        $statements = $this->getLoopStatements($node);
48✔
66
        if (null === $statements || [] === $statements) {
48✔
67
            return [];
×
68
        }
69

70
        $errors = [];
48✔
71

72
        foreach ($statements as $index => $statement) {
48✔
73
            // Skip if it's not an if statement
74
            if (!$statement instanceof If_) {
48✔
75
                continue;
48✔
76
            }
77

78
            // Check if this if statement has else/elseif branches
79
            if (null !== $statement->else || count($statement->elseifs) > 0) {
48✔
80
                continue;
8✔
81
            }
82

83
            // Check if the if statement body contains only early returns
84
            if ($this->containsOnlyEarlyReturns($statement->stmts)) {
48✔
85
                continue;
24✔
86
            }
87

88
            // Check if there are statements after this if in the loop
89
            $hasStatementsAfter = $index < count($statements) - 1;
48✔
90

91
            // If the if body doesn't contain early returns and there are statements after it,
92
            // this should be a guard clause
93
            if (!$hasStatementsAfter && count($statement->stmts) <= 1) {
48✔
94
                // @infection-ignore-all
95
                continue;
16✔
96
            }
97

98
            $errors[] = RuleErrorBuilder::message(self::ERROR_MESSAGE)
48✔
99
                ->identifier('sanmai.requireGuardClauses')
48✔
100
                ->line($statement->getLine())
48✔
101
                ->build();
48✔
102
        }
103

104
        return $errors;
48✔
105
    }
106

107
    /**
108
     * @phpstan-assert-if-true For_|Foreach_|While_|Do_ $node
109
     * @psalm-assert-if-true For_|Foreach_|While_|Do_ $node
110
     */
111
    private function isLoopNode(Node $node): bool
112
    {
113
        return $node instanceof For_
52✔
114
            || $node instanceof Foreach_
52✔
115
            || $node instanceof While_
52✔
116
            || $node instanceof Do_;
52✔
117
    }
118

119
    /**
120
     * @return array<Stmt>|null
121
     */
122
    private function getLoopStatements(Node $node): ?array
123
    {
124
        if (!$this->isLoopNode($node)) {
48✔
NEW
125
            return null;
×
126
        }
127

128
        return $node->stmts;
48✔
129
    }
130

131
    /**
132
     * @param array<Stmt> $statements
133
     */
134
    private function containsOnlyEarlyReturns(array $statements): bool
135
    {
136
        if ([] === $statements) {
48✔
137
            return false;
×
138
        }
139

140
        foreach ($statements as $statement) {
48✔
141
            // Check for continue, return, break, throw
142
            if ($statement instanceof Continue_ || $statement instanceof Return_) {
48✔
143
                continue;
28✔
144
            }
145

146
            if ($statement instanceof Stmt\Break_) {
48✔
147
                continue;
16✔
148
            }
149

150
            /** @var Expression $statement */
151
            $expr = $statement->expr;
48✔
152

153
            // Check for throw expression (PHP 8+)
154
            if ($expr instanceof Expr\Throw_) {
48✔
155
                continue;
16✔
156
            }
157

158
            // Check for exit/die expressions
159
            if ($expr instanceof Expr\Exit_) {
48✔
160
                continue;
12✔
161
            }
162

163
            // Not an early return
164
            return false;
48✔
165
        }
166

167
        return true;
24✔
168
    }
169
}
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

© 2026 Coveralls, Inc