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

sanmai / phpstan-rules / 16053277167

03 Jul 2025 02:32PM UTC coverage: 94.118% (-1.0%) from 95.082%
16053277167

Pull #5

github

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

22 of 23 new or added lines in 1 file covered. (95.65%)

2 existing lines in 1 file now uncovered.

112 of 119 relevant lines covered (94.12%)

8.67 hits per line

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

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

40
use function count;
41

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

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

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

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

70
        // Simple rule: if the loop body is ONLY an if statement, flag it
71
        if (1 === count($statements) && $statements[0] instanceof If_) {
12✔
72
            $ifStatement = $statements[0];
8✔
73

74
            // Exception: Allow if the if body contains only return, yield, or throw
75
            if ($this->containsOnlyReturnYieldOrThrow($ifStatement->stmts)) {
8✔
76
                return [];
4✔
77
            }
78

79
            return [
8✔
80
                RuleErrorBuilder::message(self::ERROR_MESSAGE)
8✔
81
                    ->identifier('sanmai.requireGuardClauses')
8✔
82
                    ->line($ifStatement->getLine())
8✔
83
                    ->build(),
8✔
84
            ];
8✔
85
        }
86

87
        return [];
8✔
88
    }
89

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

102
    private function isAllowedStatement(Node $statement): bool
103
    {
104
        // Skip empty statements
105
        if ($statement instanceof Stmt\Nop) {
8✔
106
            return true;
4✔
107
        }
108

109
        // Direct return statement
110
        if ($statement instanceof Return_) {
8✔
111
            return true;
4✔
112
        }
113

114
        // Expression statement that might be yield, yield from, or throw
115
        if ($statement instanceof Expression) {
8✔
116
            $expr = $statement->expr;
8✔
117
            return $expr instanceof Yield_
8✔
118
                || $expr instanceof YieldFrom
8✔
119
                || $expr instanceof Throw_;
8✔
120
        }
121

NEW
122
        return false;
×
123
    }
124

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

134
        return $node->stmts;
12✔
135
    }
136

137
    /**
138
     * @param array<Stmt> $statements
139
     */
140
    private function containsOnlyReturnYieldOrThrow(array $statements): bool
141
    {
142
        if ([] === $statements) {
8✔
UNCOV
143
            return false;
×
144
        }
145

146
        foreach ($statements as $statement) {
8✔
147
            if (!$this->isAllowedStatement($statement)) {
8✔
148
                return false;
8✔
149
            }
150
        }
151

152
        return true;
4✔
153
    }
154
}
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