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

sanmai / phpstan-rules / 16051589973

03 Jul 2025 01:20PM UTC coverage: 95.313% (+0.2%) from 95.082%
16051589973

Pull #5

github

web-flow
Merge 17d794694 into ef5046d0d
Pull Request #5: Only flags loops where the ONLY content is an if statement

18 of 20 new or added lines in 1 file covered. (90.0%)

1 existing line in 1 file now uncovered.

122 of 128 relevant lines covered (95.31%)

12.59 hits per line

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

93.88
/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\Expr\Yield_;
26
use PhpParser\Node\Expr\YieldFrom;
27
use PhpParser\Node\Stmt;
28
use PhpParser\Node\Stmt\Continue_;
29
use PhpParser\Node\Stmt\Do_;
30
use PhpParser\Node\Stmt\Expression;
31
use PhpParser\Node\Stmt\For_;
32
use PhpParser\Node\Stmt\Foreach_;
33
use PhpParser\Node\Stmt\If_;
34
use PhpParser\Node\Stmt\Return_;
35
use PhpParser\Node\Stmt\While_;
36
use PHPStan\Analyser\Scope;
37
use PHPStan\Rules\Rule;
38
use PHPStan\Rules\RuleErrorBuilder;
39
use Override;
40

41
use function count;
42
use function in_array;
43

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

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

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

67
        $statements = $this->getLoopStatements($node);
28✔
68
        if (null === $statements || [] === $statements) {
28✔
UNCOV
69
            return [];
×
70
        }
71

72
        $errors = [];
28✔
73

74
        // Simple rule: if the loop body is ONLY an if statement, it should use guard clauses
75
        if (1 === count($statements) && $statements[0] instanceof If_) {
28✔
76
            $ifStatement = $statements[0];
24✔
77

78
            // Skip if it has elseif branches or else clause
79
            if ([] !== $ifStatement->elseifs || null !== $ifStatement->else) {
24✔
80
                return $errors;
4✔
81
            }
82

83
            // Skip if the if body contains only early returns (already using guard pattern)
84
            if ($this->containsOnlyEarlyReturns($ifStatement->stmts)) {
24✔
85
                return $errors;
16✔
86
            }
87

88
            $errors[] = RuleErrorBuilder::message(self::ERROR_MESSAGE)
20✔
89
                ->identifier('sanmai.requireGuardClauses')
20✔
90
                ->line($ifStatement->getLine())
20✔
91
                ->build();
20✔
92
        }
93

94
        return $errors;
28✔
95
    }
96

97
    /**
98
     * @phpstan-assert-if-true For_|Foreach_|While_|Do_ $node
99
     * @psalm-assert-if-true For_|Foreach_|While_|Do_ $node
100
     */
101
    private function isLoopNode(Node $node): bool
102
    {
103
        return $node instanceof For_
28✔
104
            || $node instanceof Foreach_
28✔
105
            || $node instanceof While_
28✔
106
            || $node instanceof Do_;
28✔
107
    }
108

109
    /**
110
     * @return array<Stmt>|null
111
     */
112
    private function getLoopStatements(Node $node): ?array
113
    {
114
        if (!$this->isLoopNode($node)) {
28✔
NEW
115
            return null;
×
116
        }
117

118
        return $node->stmts;
28✔
119
    }
120

121
    /**
122
     * @param array<Stmt> $statements
123
     */
124
    private function containsOnlyEarlyReturns(array $statements): bool
125
    {
126
        if ([] === $statements) {
24✔
NEW
127
            return false;
×
128
        }
129

130
        $hasYieldFrom = false;
24✔
131
        $lastStatementIndex = count($statements) - 1;
24✔
132

133
        foreach ($statements as $index => $statement) {
24✔
134
            // Check for continue, return, break, throw
135
            if ($statement instanceof Continue_ || $statement instanceof Return_) {
24✔
136
                continue;
12✔
137
            }
138

139
            if ($statement instanceof Stmt\Break_) {
20✔
140
                continue;
4✔
141
            }
142

143
            if ($statement instanceof Expression) {
20✔
144
                $expr = $statement->expr;
20✔
145

146
                // Check for throw expression (PHP 8+)
147
                if ($expr instanceof Expr\Throw_) {
20✔
148
                    continue;
8✔
149
                }
150

151
                // Check for exit/die expressions
152
                if ($expr instanceof Expr\Exit_) {
20✔
153
                    continue;
8✔
154
                }
155

156
                // Check for yield from expressions
157
                if ($expr instanceof YieldFrom) {
20✔
158
                    // yield from is ok if it's followed by an early return
159
                    if ($index < $lastStatementIndex) {
8✔
160
                        $hasYieldFrom = true;
8✔
161
                        continue;
8✔
162
                    }
163
                    // yield from as the last statement is not an early return
164
                    return false;
8✔
165
                }
166

167
                // Regular yield is not an early return
168
                if ($expr instanceof Yield_) {
20✔
169
                    return false;
4✔
170
                }
171
            }
172

173
            // Not an early return or allowed expression
174
            return false;
20✔
175
        }
176

177
        return true;
16✔
178
    }
179

180
}
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