-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add check for loops executing at most once
- Loading branch information
Showing
6 changed files
with
616 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
254 changes: 254 additions & 0 deletions
254
...hi-checks/src/main/java/au/com/integradev/delphi/checks/LoopExecutingAtMostOnceCheck.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
.../main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 < 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 < 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> |
19 changes: 19 additions & 0 deletions
19
.../main/resources/org/sonar/l10n/delphi/rules/community-delphi/LoopExecutingAtMostOnce.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
Oops, something went wrong.