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

sanmai / phpstan-rules / 16054155120

03 Jul 2025 03:10PM UTC coverage: 94.215% (+0.6%) from 93.6%
16054155120

Pull #6

github

web-flow
Merge 25d5d4154 into 106b8e27e
Pull Request #6: Simplify Guard Clauses Rule

10 of 11 new or added lines in 1 file covered. (90.91%)

3 existing lines in 1 file now uncovered.

114 of 121 relevant lines covered (94.21%)

8.69 hits per line

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

90.48
/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\Exit_;
25
use PhpParser\Node\Expr\Throw_;
26
use PhpParser\Node\Expr\Yield_;
27
use PhpParser\Node\Expr\YieldFrom;
28
use PhpParser\Node\Stmt;
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

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

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

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

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

71
        if (1 !== count($statements)) {
12✔
72
            return [];
8✔
73
        }
74

75

76
        if (!$statements[0] instanceof If_) {
8✔
UNCOV
77
            return [];
×
78
        }
79

80
        // Simple rule: if the loop body is ONLY an if statement, flag it
81
        $ifStatement = $statements[0];
8✔
82

83
        // Exception: Allow if the if body contains only return, yield, or throw
84
        if ($this->containsOnlyOneStatement($ifStatement->stmts)) {
8✔
85
            return [];
8✔
86
        }
87

88
        return [
8✔
89
            RuleErrorBuilder::message(self::ERROR_MESSAGE)
8✔
90
                ->identifier('sanmai.requireGuardClauses')
8✔
91
                ->line($ifStatement->getLine())
8✔
92
                ->build(),
8✔
93
        ];
8✔
94
    }
95

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

108
    private function isYieldOrYieldFrom(Node $statement): bool
109
    {
110
        if (!$statement instanceof Expression) {
8✔
111
            return false;
4✔
112
        }
113

114
        return $statement->expr instanceof Yield_
8✔
115
                || $statement->expr instanceof YieldFrom;
8✔
116
    }
117

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

127
        return $node->stmts;
12✔
128
    }
129

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

139
        $count = 0;
8✔
140
        foreach ($statements as $statement) {
8✔
141
            if ($statement instanceof Stmt\Nop) {
8✔
142
                // Skip empty statements
143
                continue;
4✔
144
            }
145

146
            if ($this->isYieldOrYieldFrom($statement)) {
8✔
147
                // Allow as many yields as needed
148
                continue;
4✔
149
            }
150

151
            $count++;
8✔
152

153
            if ($count > 1) {
8✔
154
                return false; // More than one statement found
8✔
155
            }
156
        }
157

158
        return true;
8✔
159
    }
160
}
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