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

sanmai / phpstan-rules / 16255432326

14 Jul 2025 12:54AM UTC coverage: 94.488%. Remained the same
16255432326

Pull #16

github

web-flow
Merge b94d040ef into 0b3535e3f
Pull Request #16: Improve tests, fix bugs

10 of 10 new or added lines in 3 files covered. (100.0%)

7 existing lines in 3 files now uncovered.

120 of 127 relevant lines covered (94.49%)

26.17 hits per line

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

91.67
/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\Yield_;
25
use PhpParser\Node\Expr\YieldFrom;
26
use PhpParser\Node\Stmt;
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\While_;
33
use PHPStan\Analyser\Scope;
34
use PHPStan\Rules\Rule;
35
use PHPStan\Rules\RuleErrorBuilder;
36
use Override;
37

38
use function count;
39

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

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

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

63
        $statements = $this->getLoopStatements($node);
36✔
64
        if (null === $statements || [] === $statements) {
36✔
UNCOV
65
            return [];
×
66
        }
67

68
        if (1 !== count($statements)) {
36✔
69
            return [];
24✔
70
        }
71

72

73
        if (!$statements[0] instanceof If_) {
24✔
UNCOV
74
            return [];
×
75
        }
76

77
        // Simple rule: if the loop body is ONLY an if statement, flag it
78
        $ifStatement = $statements[0];
24✔
79

80
        // Exception: Allow if the if body contains only return, yield, or throw
81
        if ($this->containsOnlyOneStatement($ifStatement->stmts)) {
24✔
82
            return [];
24✔
83
        }
84

85
        return [
24✔
86
            RuleErrorBuilder::message(self::ERROR_MESSAGE)
24✔
87
                ->identifier('sanmai.requireGuardClauses')
24✔
88
                ->line($ifStatement->getLine())
24✔
89
                ->build(),
24✔
90
        ];
24✔
91
    }
92

93
    /**
94
     * @phpstan-assert-if-true For_|Foreach_|While_|Do_ $node
95
     * @psalm-assert-if-true For_|Foreach_|While_|Do_ $node
96
     */
97
    private function isLoopNode(Node $node): bool
98
    {
99
        if ($node instanceof For_) {
48✔
100
            return true;
24✔
101
        }
102

103
        if ($node instanceof Foreach_) {
48✔
104
            return true;
36✔
105
        }
106

107
        if ($node instanceof While_) {
48✔
108
            return true;
24✔
109
        }
110

111
        if ($node instanceof Do_) {
48✔
112
            return true;
24✔
113
        }
114

115
        return false;
48✔
116
    }
117

118
    private function isYieldOrYieldFrom(Node $statement): bool
119
    {
120
        if (!$statement instanceof Expression) {
24✔
121
            return false;
12✔
122
        }
123

124
        return $statement->expr instanceof Yield_
24✔
125
                || $statement->expr instanceof YieldFrom;
24✔
126
    }
127

128
    /**
129
     * @return array<Stmt>|null
130
     */
131
    private function getLoopStatements(Node $node): ?array
132
    {
133
        if (!$this->isLoopNode($node)) {
36✔
UNCOV
134
            return null;
×
135
        }
136

137
        return $node->stmts;
36✔
138
    }
139

140
    /**
141
     * @param array<Stmt> $statements
142
     */
143
    private function containsOnlyOneStatement(array $statements): bool
144
    {
145
        if ([] === $statements) {
24✔
UNCOV
146
            return false;
×
147
        }
148

149
        $count = 0;
24✔
150
        foreach ($statements as $statement) {
24✔
151
            if ($statement instanceof Stmt\Nop) {
24✔
152
                // Skip empty statements, i.e. only with comments
153
                continue;
12✔
154
            }
155

156
            if ($this->isYieldOrYieldFrom($statement)) {
24✔
157
                // Allow as many yields as needed, but with only one following statement
158
                $count = 0;
12✔
159
                continue;
12✔
160
            }
161

162
            $count++;
24✔
163

164
            if ($count > 1) {
24✔
165
                return false; // More than one statement found
24✔
166
            }
167
        }
168

169
        return true;
24✔
170
    }
171
}
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