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

sanmai / phpstan-rules / 16045390351

03 Jul 2025 08:23AM UTC coverage: 94.382%. First build
16045390351

Pull #2

github

web-flow
Merge 64b3ab231 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
    private function isLoopNode(Node $node): bool
105
    {
106
        return $node instanceof For_
52✔
107
            || $node instanceof Foreach_
52✔
108
            || $node instanceof While_
52✔
109
            || $node instanceof Do_;
52✔
110
    }
111

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

121
        // We know it's a loop node, so we can safely access stmts property
122
        /** @psalm-suppress NoInterfaceProperties, MixedReturnStatement */
123
        /** @phpstan-ignore-next-line */
124
        return $node->stmts;
48✔
125
    }
126

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

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

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

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

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

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

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

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