Skip to content

Commit

Permalink
Add check for loops executing at most once
Browse files Browse the repository at this point in the history
  • Loading branch information
jgardn3r committed Aug 7, 2024
1 parent f5544d6 commit 434a17d
Show file tree
Hide file tree
Showing 6 changed files with 616 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- "Correct to (correct case)" quick fix for `LowercaseKeyword`.
- Ability to create a control flow graph.
- `RedundantJump` analysis rule, which flags redundant jump statements, e.g., `Continue`, `Exit`.
- `LoopExecutingAtMostOnce` analysis rule, which flags loop statements that can execute at most once.
- **API:** `PropertyNameDeclaration::getImplementedTypes` method.
- **API:** `PropertyNode::getDefaultSpecifier` method.
- **API:** `PropertyNode::getImplementsSpecifier` method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public final class CheckList {
InterfaceGuidCheck.class,
InterfaceNameCheck.class,
LegacyInitializationSectionCheck.class,
LoopExecutingAtMostOnceCheck.class,
LowercaseKeywordCheck.class,
MathFunctionSingleOverloadCheck.class,
MemberDeclarationOrderCheck.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/*
* Sonar Delphi Plugin
* Copyright (C) 2019 Integrated Application Development
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package au.com.integradev.delphi.checks;

import au.com.integradev.delphi.antlr.ast.node.AnonymousMethodNodeImpl;
import au.com.integradev.delphi.antlr.ast.node.RoutineImplementationNodeImpl;
import au.com.integradev.delphi.cfg.ControlFlowGraphImpl;
import au.com.integradev.delphi.cfg.ControlFlowGraphImpl.Block;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.function.Supplier;
import org.sonar.check.Rule;
import org.sonar.plugins.communitydelphi.api.ast.CompoundStatementNode;
import org.sonar.plugins.communitydelphi.api.ast.DelphiNode;
import org.sonar.plugins.communitydelphi.api.ast.ForInStatementNode;
import org.sonar.plugins.communitydelphi.api.ast.ForStatementNode;
import org.sonar.plugins.communitydelphi.api.ast.ForToStatementNode;
import org.sonar.plugins.communitydelphi.api.ast.GotoStatementNode;
import org.sonar.plugins.communitydelphi.api.ast.IfStatementNode;
import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode;
import org.sonar.plugins.communitydelphi.api.ast.RaiseStatementNode;
import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode;
import org.sonar.plugins.communitydelphi.api.ast.StatementNode;
import org.sonar.plugins.communitydelphi.api.ast.WhileStatementNode;
import org.sonar.plugins.communitydelphi.api.cfg.ControlFlowGraph;
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext;
import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration;
import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration;

@Rule(key = "LoopExecutingAtMostOnce")
public class LoopExecutingAtMostOnceCheck extends DelphiCheck {
private static final Set<String> EXIT_METHODS =
Set.of("System.Exit", "System.Break", "System.Halt");

@Override
public DelphiCheckContext visit(RaiseStatementNode node, DelphiCheckContext context) {
return visitExitingNode(node, context, "raise");
}

@Override
public DelphiCheckContext visit(GotoStatementNode node, DelphiCheckContext context) {
return visitExitingNode(node, context, "goto");
}

@Override
public DelphiCheckContext visit(NameReferenceNode node, DelphiCheckContext context) {
NameDeclaration declaration = node.getLastName().getNameDeclaration();
if (!(declaration instanceof RoutineNameDeclaration)) {
return context;
}
String fullyQualifiedName = ((RoutineNameDeclaration) declaration).fullyQualifiedName();
if (!EXIT_METHODS.contains(fullyQualifiedName)) {
return context;
}

return visitExitingNode(node, context, fullyQualifiedName);
}

private DelphiCheckContext visitExitingNode(
DelphiNode node, DelphiCheckContext context, String description) {
DelphiNode enclosingStatement = findEnclosingStatement(node);
DelphiNode enclosingLoop = findEnclosingLoop(node);
if (enclosingStatement == null || enclosingLoop == null) {
return context;
}

if (isViolation(enclosingLoop, node) && violationInRelevantStatement(enclosingStatement)) {
reportIssue(
context,
node,
String.format("Remove this \"%s\" statement or make it conditional.", description));
}
return context;
}

private static boolean isLoopNode(DelphiNode node) {
return node instanceof RepeatStatementNode
|| node instanceof ForStatementNode
|| node instanceof WhileStatementNode;
}

private static DelphiNode findEnclosingStatement(DelphiNode node) {
DelphiNode parent = node;
if (!(parent instanceof StatementNode)) {
// Finding the enclosing statement of the NameReferenceNode
parent = parent.getFirstParentOfType(StatementNode.class);
}
if (parent == null) {
return null;
}
do {
parent = parent.getFirstParentOfType(StatementNode.class);
} while (parent instanceof CompoundStatementNode);
return parent;
}

private static DelphiNode findEnclosingLoop(DelphiNode node) {
DelphiNode parent = node;
while (parent != null && !isLoopNode(parent)) {
parent = parent.getFirstParentOfType(StatementNode.class);
}
return parent;
}

private static boolean isViolation(DelphiNode loop, DelphiNode jump) {
ControlFlowGraphImpl cfg = getCFG(loop);
Block loopBlock =
getTerminatorBlock(cfg, loop)
.orElseThrow(
() -> new IllegalStateException("CFG necessarily contains the loop block"));

return !hasPredecessorInBlock(loopBlock, loop) && !jumpsBeforeLoop(cfg, loopBlock, jump);
}

private static boolean violationInRelevantStatement(DelphiNode enclosingStatement) {
if (!(enclosingStatement instanceof IfStatementNode)) {
return true;
}

IfStatementNode ifStatement = (IfStatementNode) enclosingStatement;
if (!ifStatement.hasElseBranch()) {
return false;
}
return !(ifStatement.getElseStatement() instanceof IfStatementNode);
}

private static Optional<Block> getTerminatorBlock(ControlFlowGraphImpl cfg, DelphiNode loop) {
return cfg.blocks().stream().filter(block -> loop.equals(block.terminator())).findFirst();
}

private static boolean hasPredecessorInBlock(Block block, DelphiNode loop) {
for (Block predecessor : block.predecessors()) {
List<DelphiNode> predecessorElements = predecessor.elements();
if (predecessorElements.isEmpty()) {
return hasPredecessorInBlock(predecessor, loop);
}
DelphiNode predecessorFirstElement = predecessorElements.get(0);

if (isForStatementInitializer(predecessorFirstElement, loop)) {
continue;
}

if (isDescendant(predecessorFirstElement, loop)) {
return true;
}
}

return false;
}

private static boolean jumpsBeforeLoop(
ControlFlowGraphImpl cfg, Block loopBlock, DelphiNode jump) {
if (!(jump instanceof GotoStatementNode)) {
return false;
}
Optional<Block> jumpBlock = getTerminatorBlock(cfg, jump);
if (jumpBlock.isEmpty()) {
return false;
}
Block jumpTarget = jumpBlock.get().successors().iterator().next();
if (jumpTarget == null) {
return false;
}
if (loopBlock.terminator() instanceof RepeatStatementNode
&& loopBlock.falseBlock().equals(jumpTarget)) {
return true;
}

Set<Block> visited = new HashSet<>();
PriorityQueue<Block> queue =
new PriorityQueue<>((a, b) -> Math.abs(a.id() - b.id() - loopBlock.id()));
queue.add(jumpTarget);
while (!queue.isEmpty()) {
Block search = queue.poll();
if (search.equals(loopBlock)) {
return true;
}
if (search.successors().size() == 1 && search.successors().contains(jumpTarget)) {
return false;
}

if (search.equals(cfg.exitBlock())) {
return false;
}
visited.add(search);
search.successors().stream().filter(b -> !visited.contains(b)).forEach(queue::add);
}

return false;
}

private static boolean isForStatementInitializer(DelphiNode lastElement, DelphiNode loop) {
if (loop instanceof ForToStatementNode) {
return isDescendant(lastElement, ((ForToStatementNode) loop).getInitializerExpression())
|| isDescendant(lastElement, ((ForToStatementNode) loop).getTargetExpression());
}
return loop instanceof ForInStatementNode
&& isDescendant(lastElement, ((ForInStatementNode) loop).getEnumerable());
}

private static boolean isDescendant(DelphiNode descendant, DelphiNode target) {
DelphiNode parent = descendant;
while (parent != null) {
if (parent.equals(target)) {
return true;
}
parent = parent.getParent();
}
return false;
}

private static Supplier<ControlFlowGraph> getCFGSupplier(DelphiNode node) {
if (node instanceof RoutineImplementationNodeImpl) {
return ((RoutineImplementationNodeImpl) node)::cfg;
}
if (node instanceof AnonymousMethodNodeImpl) {
return ((AnonymousMethodNodeImpl) node)::cfg;
}
return null;
}

private static ControlFlowGraphImpl getCFG(DelphiNode loop) {
DelphiNode parent = loop.getParent();
Supplier<ControlFlowGraph> cfgSupplier = getCFGSupplier(parent);
while (parent != null && cfgSupplier == null) {
parent = parent.getParent();
cfgSupplier = getCFGSupplier(parent);
}
if (cfgSupplier != null) {
return (ControlFlowGraphImpl) cfgSupplier.get();
}
return new ControlFlowGraphImpl(loop.findChildrenOfType(StatementNode.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<h2>Why is this an issue?</h2>
<p>
Loops with at most one iteration are equivalent to an <code>if</code> statement. Using loops in
this place makes the code less readable.
</p>
<p>
If the intention was to execute the loop once, an <code>if</code> statement should be used.
Otherwise, the jumping statement should be made conditional such that loop can execute multiple
times.
</p>
<p>
Loops with at most one iteration can happen with a statement that unconditionally transfers
control is misplaced inside the body of the loop.
</p>
<p>
These statements are:
</p>
<ul>
<li><code>Exit</code></li>
<li><code>Break</code></li>
<li><code>Continue</code></li>
<li><code>Halt</code></li>
<li><code>raise</code></li>
<li><code>goto</code></li>
</ul>

<h2>How to fix it</h2>
<p>
Make the statement that affects execution of the loop conditional, or remove it all together.
</p>
<pre data-diff-id="1" data-diff-type="noncompliant">
var I := 0;
while I &lt; 10 do begin
Inc(I);
break; // Noncompliant
end;
</pre>
<pre data-diff-id="2" data-diff-type="noncompliant">
for var I := 0 to 10 do begin
if I = 2 then
break // Noncompliant
else begin
Writeln(I);
return; // Noncompliant
end;
end;
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
var I := 0;
while I &lt; 10 do begin
Inc(I);
end;
</pre>
<pre data-diff-id="2" data-diff-type="compliant">
for var I := 0 to 10 do begin
if I = 2 then
break
else
Writeln(I);
end;
</pre>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"title": "Loops with at most one iteration should be refactored",
"type": "BUG",
"status": "ready",
"remediation": {
"func": "Constant/Issue",
"constantCost": "5min"
},
"code": {
"attribute": "CLEAR",
"impacts": {
"RELIABILITY": "MEDIUM"
}
},
"tags": ["clumsy"],
"defaultSeverity": "Major",
"scope": "ALL",
"quickfix": "unknown"
}
Loading

0 comments on commit 434a17d

Please sign in to comment.