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

sanmai / phpstan-rules / 16045486657

03 Jul 2025 08:29AM UTC coverage: 94.382%. First build
16045486657

Pull #2

github

web-flow
Merge 9850cd73f into ab8883211
Pull Request #2: Fix the build

20 of 22 new or added lines in 3 files covered. (90.91%)

84 of 89 relevant lines covered (94.38%)

25.03 hits per line

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

93.18
/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
    #[Override]
48
    public function getNodeType(): string
49
    {
50
        return Node::class;
52✔
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)) {
52✔
60
            return [];
52✔
61
        }
62

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

68
        $errors = [];
48✔
69

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

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

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

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

89
            // If the if body doesn't contain early returns and there are statements after it,
90
            // this should be a guard clause
91
            if ($hasStatementsAfter || count($statement->stmts) > 1) {
48✔
92
                $errors[] = RuleErrorBuilder::message(
48✔
93
                    'Use guard clauses instead of wrapping code in if statements. Consider using: if (!condition) { continue; }'
48✔
94
                )
48✔
95
                    ->identifier('sanmai.requireGuardClauses')
48✔
96
                    ->line($statement->getLine())
48✔
97
                    ->build();
48✔
98
            }
99
        }
100

101
        return $errors;
48✔
102
    }
103

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

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

125
        return $node->stmts;
48✔
126
    }
127

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

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

143
            if ($statement instanceof Stmt\Break_) {
48✔
144
                continue;
16✔
145
            }
146

147
            /** @var Expression $statement */
148
            $expr = $statement->expr;
48✔
149

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

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

160
            // Not an early return
161
            return false;
48✔
162
        }
163

164
        return true;
24✔
165
    }
166
}
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