Skip to content

Commit 077ef6c

Browse files
committed
Add check for loops executing at most once
1 parent 1a1444c commit 077ef6c

File tree

7 files changed

+1026
-0
lines changed

7 files changed

+1026
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- `RedundantJump` analysis rule, which flags redundant jump statements, e.g., `Continue`, `Exit`.
13+
- `LoopExecutingAtMostOnce` analysis rule, which flags loop statements that can execute at most once.
1314
- **API:** `RepeatStatementNode::getGuardExpression` method.
1415
- **API:** `RepeatStatementNode::getStatementList` method.
1516
- **API:** `CaseStatementNode::getSelectorExpression` method.

delphi-checks/src/main/java/au/com/integradev/delphi/checks/CheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public final class CheckList {
104104
InterfaceGuidCheck.class,
105105
InterfaceNameCheck.class,
106106
LegacyInitializationSectionCheck.class,
107+
LoopExecutingAtMostOnceCheck.class,
107108
LowercaseKeywordCheck.class,
108109
MathFunctionSingleOverloadCheck.class,
109110
MemberDeclarationOrderCheck.class,
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/*
2+
* Sonar Delphi Plugin
3+
* Copyright (C) 2025 Integrated Application Development
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 3 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public
16+
* License along with this program; if not, write to the Free Software
17+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
18+
*/
19+
package au.com.integradev.delphi.checks;
20+
21+
import au.com.integradev.delphi.antlr.ast.node.AnonymousMethodNodeImpl;
22+
import au.com.integradev.delphi.antlr.ast.node.RoutineImplementationNodeImpl;
23+
import au.com.integradev.delphi.cfg.ControlFlowGraphFactory;
24+
import au.com.integradev.delphi.cfg.api.Block;
25+
import au.com.integradev.delphi.cfg.api.Branch;
26+
import au.com.integradev.delphi.cfg.api.ControlFlowGraph;
27+
import au.com.integradev.delphi.cfg.api.Terminated;
28+
import java.util.ArrayDeque;
29+
import java.util.ArrayList;
30+
import java.util.Deque;
31+
import java.util.HashSet;
32+
import java.util.List;
33+
import java.util.Optional;
34+
import java.util.Queue;
35+
import java.util.Set;
36+
import java.util.function.Supplier;
37+
import org.sonar.check.Rule;
38+
import org.sonar.plugins.communitydelphi.api.ast.CompoundStatementNode;
39+
import org.sonar.plugins.communitydelphi.api.ast.DelphiAst;
40+
import org.sonar.plugins.communitydelphi.api.ast.DelphiNode;
41+
import org.sonar.plugins.communitydelphi.api.ast.FinalizationSectionNode;
42+
import org.sonar.plugins.communitydelphi.api.ast.ForInStatementNode;
43+
import org.sonar.plugins.communitydelphi.api.ast.ForStatementNode;
44+
import org.sonar.plugins.communitydelphi.api.ast.ForToStatementNode;
45+
import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode;
46+
import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode;
47+
import org.sonar.plugins.communitydelphi.api.ast.InitializationSectionNode;
48+
import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode;
49+
import org.sonar.plugins.communitydelphi.api.ast.RaiseStatementNode;
50+
import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode;
51+
import org.sonar.plugins.communitydelphi.api.ast.StatementListNode;
52+
import org.sonar.plugins.communitydelphi.api.ast.StatementNode;
53+
import org.sonar.plugins.communitydelphi.api.ast.WhileStatementNode;
54+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
55+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext;
56+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext.Location;
57+
import org.sonar.plugins.communitydelphi.api.check.FilePosition;
58+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration;
59+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration;
60+
61+
@Rule(key = "LoopExecutingAtMostOnce")
62+
public class LoopExecutingAtMostOnceCheck extends DelphiCheck {
63+
private static final Set<String> EXIT_METHODS =
64+
Set.of("System.Exit", "System.Break", "System.Halt");
65+
66+
private final Deque<DelphiNode> loopStack = new ArrayDeque<>();
67+
private final Deque<List<Location>> violations = new ArrayDeque<>();
68+
69+
// Loops
70+
71+
private void pushLoop(DelphiNode node) {
72+
loopStack.push(node);
73+
violations.push(new ArrayList<>());
74+
}
75+
76+
private void popLoop(DelphiCheckContext context) {
77+
DelphiNode loop = loopStack.pop();
78+
List<Location> loopViolations = violations.pop();
79+
if (loop == null || loopViolations == null || loopViolations.isEmpty()) {
80+
return;
81+
}
82+
83+
context
84+
.newIssue()
85+
.onFilePosition(FilePosition.from(loop.getFirstToken()))
86+
.withMessage("Remove this loop that executes only once.")
87+
.withSecondaries(loopViolations)
88+
.report();
89+
}
90+
91+
@Override
92+
public DelphiCheckContext visit(ForStatementNode node, DelphiCheckContext data) {
93+
pushLoop(node);
94+
DelphiCheckContext result = super.visit(node, data);
95+
popLoop(data);
96+
return result;
97+
}
98+
99+
@Override
100+
public DelphiCheckContext visit(RepeatStatementNode node, DelphiCheckContext data) {
101+
pushLoop(node);
102+
DelphiCheckContext result = super.visit(node, data);
103+
popLoop(data);
104+
return result;
105+
}
106+
107+
@Override
108+
public DelphiCheckContext visit(WhileStatementNode node, DelphiCheckContext data) {
109+
pushLoop(node);
110+
DelphiCheckContext result = super.visit(node, data);
111+
popLoop(data);
112+
return result;
113+
}
114+
115+
// Statements
116+
117+
@Override
118+
public DelphiCheckContext visit(RaiseStatementNode node, DelphiCheckContext context) {
119+
return visitExitingNode(node, context, "raise");
120+
}
121+
122+
@Override
123+
public DelphiCheckContext visit(GotoStatementNode node, DelphiCheckContext context) {
124+
return visitExitingNode(node, context, "goto");
125+
}
126+
127+
@Override
128+
public DelphiCheckContext visit(NameReferenceNode node, DelphiCheckContext context) {
129+
NameDeclaration declaration = node.getLastName().getNameDeclaration();
130+
if (!(declaration instanceof RoutineNameDeclaration)) {
131+
return context;
132+
}
133+
String fullyQualifiedName = ((RoutineNameDeclaration) declaration).fullyQualifiedName();
134+
if (!EXIT_METHODS.contains(fullyQualifiedName)) {
135+
return context;
136+
}
137+
138+
return visitExitingNode(node, context, declaration.getImage());
139+
}
140+
141+
private DelphiCheckContext visitExitingNode(
142+
DelphiNode exitingNode, DelphiCheckContext context, String description) {
143+
144+
if (isInViolatingLoop(exitingNode) && isUnconditionalJump(exitingNode)) {
145+
List<Location> violationLocations = violations.peek();
146+
if (violationLocations == null) {
147+
return context;
148+
}
149+
violationLocations.add(
150+
new Location(
151+
String.format("Remove this \"%s\" statement or make it conditional.", description),
152+
exitingNode));
153+
}
154+
return context;
155+
}
156+
157+
private boolean isInViolatingLoop(DelphiNode jump) {
158+
DelphiNode loop = this.loopStack.peek();
159+
if (loop == null) {
160+
return false;
161+
}
162+
ControlFlowGraph cfg = getCFG(loop);
163+
Block loopBlock =
164+
getTerminatorBlock(cfg, loop)
165+
.orElseThrow(
166+
() -> new IllegalStateException("CFG necessarily contains the loop block"));
167+
168+
return !hasPredecessorInBlock(loopBlock, loop) && !jumpsBeforeLoop(cfg, loopBlock, jump);
169+
}
170+
171+
private static boolean isUnconditionalJump(DelphiNode node) {
172+
DelphiNode lastStatement = node;
173+
for (StatementNode statement : node.getParentsOfType(StatementNode.class)) {
174+
if (statement instanceof ForStatementNode
175+
|| statement instanceof RepeatStatementNode
176+
|| statement instanceof WhileStatementNode) {
177+
// Reached the loop, it is a non-conditional statement or in a chain of `else` blocks
178+
return true;
179+
}
180+
181+
if (statement instanceof IfStatementNode
182+
&& ((IfStatementNode) statement).getElseStatement() != lastStatement) {
183+
// If we are in the `if then` branch, then it is not relevant
184+
return false;
185+
}
186+
187+
lastStatement = statement;
188+
}
189+
return false;
190+
}
191+
192+
private static Optional<Block> getTerminatorBlock(ControlFlowGraph cfg, DelphiNode terminator) {
193+
return cfg.getBlocks().stream()
194+
.filter(Terminated.class::isInstance)
195+
.filter(terminated -> terminator.equals(((Terminated) terminated).getTerminator()))
196+
.findFirst();
197+
}
198+
199+
private static boolean hasPredecessorInBlock(Block block, DelphiNode loop) {
200+
for (Block predecessor : block.getPredecessors()) {
201+
List<DelphiNode> predecessorElements = predecessor.getElements();
202+
if (predecessorElements.isEmpty()) {
203+
return hasPredecessorInBlock(predecessor, loop);
204+
}
205+
DelphiNode predecessorFirstElement = predecessorElements.get(0);
206+
207+
if (isForStatementInitializer(predecessorFirstElement, loop)) {
208+
continue;
209+
}
210+
211+
if (isDescendant(predecessorFirstElement, loop)) {
212+
return true;
213+
}
214+
}
215+
216+
return false;
217+
}
218+
219+
private static boolean jumpsBeforeLoop(ControlFlowGraph cfg, Block loopBlock, DelphiNode node) {
220+
if (!(node instanceof GotoStatementNode)) {
221+
// If the node isn't a `goto`, it cannot jump before the loop
222+
return false;
223+
}
224+
Optional<Block> jumpBlock = getTerminatorBlock(cfg, node);
225+
if (jumpBlock.isEmpty()) {
226+
// Unable to find a block whose terminator is the `goto`
227+
return false;
228+
}
229+
Block jumpTarget = jumpBlock.get().getSuccessors().iterator().next();
230+
if (jumpTarget == null) {
231+
// There are no successors to the jump block
232+
return false;
233+
}
234+
if (loopBlock instanceof Branch) {
235+
Branch loopBranch = (Branch) loopBlock;
236+
if (loopBranch.getTerminator() instanceof RepeatStatementNode
237+
&& loopBranch.getFalseBlock().equals(jumpTarget)) {
238+
// If the jump target is the start of a `repeat` loop, it is before the loop
239+
return true;
240+
}
241+
}
242+
243+
// From the jump target, recursively search the successors to find the loop block. Whether the
244+
// loop block is found relates to if the jump is to before the loop.
245+
Set<Block> visited = new HashSet<>();
246+
Queue<Block> queue = new ArrayDeque<>();
247+
queue.add(jumpTarget);
248+
while (!queue.isEmpty()) {
249+
Block search = queue.poll();
250+
if (search.equals(loopBlock)) {
251+
return true;
252+
}
253+
if ((search.getSuccessors().size() == 1 && search.getSuccessors().contains(jumpTarget))
254+
|| search.equals(cfg.getExitBlock())) {
255+
return false;
256+
}
257+
258+
visited.add(search);
259+
search.getSuccessors().stream().filter(b -> !visited.contains(b)).forEach(queue::add);
260+
}
261+
262+
return false;
263+
}
264+
265+
private static boolean isForStatementInitializer(DelphiNode lastElement, DelphiNode loop) {
266+
if (loop instanceof ForToStatementNode) {
267+
return isDescendant(lastElement, ((ForToStatementNode) loop).getInitializerExpression())
268+
|| isDescendant(lastElement, ((ForToStatementNode) loop).getTargetExpression());
269+
}
270+
return loop instanceof ForInStatementNode
271+
&& isDescendant(lastElement, ((ForInStatementNode) loop).getEnumerable());
272+
}
273+
274+
private static boolean isDescendant(DelphiNode descendant, DelphiNode target) {
275+
DelphiNode parent = descendant;
276+
while (parent != null) {
277+
if (parent.equals(target)) {
278+
return true;
279+
}
280+
parent = parent.getParent();
281+
}
282+
return false;
283+
}
284+
285+
private static Supplier<ControlFlowGraph> getCFGSupplier(DelphiNode node) {
286+
if (node instanceof RoutineImplementationNodeImpl) {
287+
return ((RoutineImplementationNodeImpl) node)::getControlFlowGraph;
288+
}
289+
if (node instanceof AnonymousMethodNodeImpl) {
290+
return ((AnonymousMethodNodeImpl) node)::getControlFlowGraph;
291+
}
292+
if (node instanceof CompoundStatementNode && node.getParent() instanceof DelphiAst) {
293+
return () -> ControlFlowGraphFactory.create((CompoundStatementNode) node);
294+
}
295+
if (node instanceof StatementListNode
296+
&& (node.getParent() instanceof InitializationSectionNode
297+
|| node.getParent() instanceof FinalizationSectionNode)) {
298+
return () -> ControlFlowGraphFactory.create((StatementListNode) node);
299+
}
300+
return null;
301+
}
302+
303+
private static ControlFlowGraph getCFG(DelphiNode loop) {
304+
DelphiNode parent = loop.getParent();
305+
Supplier<ControlFlowGraph> cfgSupplier = getCFGSupplier(parent);
306+
while (parent != null && cfgSupplier == null) {
307+
parent = parent.getParent();
308+
cfgSupplier = getCFGSupplier(parent);
309+
}
310+
if (cfgSupplier != null) {
311+
return cfgSupplier.get();
312+
}
313+
return ControlFlowGraphFactory.create(loop.findChildrenOfType(StatementNode.class));
314+
}
315+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>
3+
Loops with at most one iteration are equivalent to an <code>if</code> statement. Using loops in
4+
this case makes the code less readable.
5+
</p>
6+
<p>
7+
If the intention was to execute the loop once, an <code>if</code> statement may be used or the
8+
loop removed.
9+
Otherwise, the jumping statement should be made conditional so the loop can execute more than
10+
once.
11+
</p>
12+
<p>
13+
Loops with at most one iteration can happen with a statement that unconditionally transfers
14+
control is misplaced inside the body of the loop.
15+
</p>
16+
<p>
17+
These statements are:
18+
</p>
19+
<ul>
20+
<li><code>Exit</code></li>
21+
<li><code>Break</code></li>
22+
<li><code>Halt</code></li>
23+
<li><code>raise</code></li>
24+
<li><code>goto</code></li>
25+
</ul>
26+
27+
<h2>How to fix it</h2>
28+
<p>
29+
Make the statement that affects execution of the loop conditional, or remove it all together.
30+
</p>
31+
<pre data-diff-id="1" data-diff-type="noncompliant">
32+
var I := 0;
33+
while I &lt; 10 do begin
34+
Inc(I);
35+
Break; // Noncompliant
36+
end;
37+
</pre>
38+
<pre data-diff-id="2" data-diff-type="noncompliant">
39+
for var I := 0 to 10 do begin
40+
if I = 2 then
41+
Break // Noncompliant
42+
else begin
43+
Writeln(I);
44+
Exit; // Noncompliant
45+
end;
46+
end;
47+
</pre>
48+
<h4>Compliant solution</h4>
49+
<pre data-diff-id="1" data-diff-type="compliant">
50+
var I := 0;
51+
while I &lt; 10 do begin
52+
Inc(I);
53+
end;
54+
</pre>
55+
<pre data-diff-id="2" data-diff-type="compliant">
56+
for var I := 0 to 10 do begin
57+
if I = 2 then
58+
Break
59+
else begin
60+
Writeln(I);
61+
end;
62+
end;
63+
</pre>

0 commit comments

Comments
 (0)