Skip to content

Commit 8635158

Browse files
committed
Add check for array iterations with out of range high bounds
1 parent 2fa0766 commit 8635158

File tree

6 files changed

+468
-0
lines changed

6 files changed

+468
-0
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
- `IterationPastHighBound` analysis rule, which flags `for` loops that iterate past the end of the collection.
1213
- **API:** `EnumeratorOccurrence` type.
1314
- **API:** `EnumeratorOccurrence::getGetEnumerator` method.
1415
- **API:** `EnumeratorOccurrence::getMoveNext` 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
@@ -103,6 +103,7 @@ public final class CheckList {
103103
InstanceInvokedConstructorCheck.class,
104104
InterfaceGuidCheck.class,
105105
InterfaceNameCheck.class,
106+
IterationPastHighBoundCheck.class,
106107
LegacyInitializationSectionCheck.class,
107108
LoopExecutingAtMostOnceCheck.class,
108109
LowercaseKeywordCheck.class,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 org.sonar.check.Rule;
22+
import org.sonar.plugins.communitydelphi.api.ast.ExpressionNode;
23+
import org.sonar.plugins.communitydelphi.api.ast.ForToStatementNode;
24+
import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode;
25+
import org.sonar.plugins.communitydelphi.api.ast.PrimaryExpressionNode;
26+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
27+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext;
28+
import org.sonar.plugins.communitydelphi.api.reporting.QuickFix;
29+
import org.sonar.plugins.communitydelphi.api.reporting.QuickFixEdit;
30+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration;
31+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration;
32+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.TypedDeclaration;
33+
34+
@Rule(key = "IterationPastHighBound")
35+
public class IterationPastHighBoundCheck extends DelphiCheck {
36+
@Override
37+
public DelphiCheckContext visit(ForToStatementNode forStatement, DelphiCheckContext context) {
38+
var target = forStatement.getTargetExpression().skipParentheses();
39+
40+
if (isSuspiciousTargetExpression(target)) {
41+
context
42+
.newIssue()
43+
.onNode(target)
44+
.withMessage("Ensure this for loop is not iterating beyond the end of the collection.")
45+
.withQuickFixes(
46+
QuickFix.newFix("Subtract one from expression")
47+
.withEdit(QuickFixEdit.insertAfter(" - 1", target)))
48+
.report();
49+
}
50+
51+
return super.visit(forStatement, context);
52+
}
53+
54+
private boolean isSuspiciousTargetExpression(ExpressionNode expression) {
55+
if (!(expression instanceof PrimaryExpressionNode)) {
56+
return false;
57+
}
58+
59+
var reference = expression.getChild(0);
60+
if (!(reference instanceof NameReferenceNode)) {
61+
return false;
62+
}
63+
64+
var declaration = ((NameReferenceNode) reference).getLastName().getNameDeclaration();
65+
return isArrayLength(declaration) || isCount(declaration);
66+
}
67+
68+
private boolean isCount(NameDeclaration declaration) {
69+
if (!(declaration instanceof TypedDeclaration)) {
70+
return false;
71+
}
72+
73+
return declaration.getName().equalsIgnoreCase("Count")
74+
&& ((TypedDeclaration) declaration).getType().isInteger();
75+
}
76+
77+
private boolean isArrayLength(NameDeclaration declaration) {
78+
if (!(declaration instanceof RoutineNameDeclaration)) {
79+
return false;
80+
}
81+
82+
var routine = (RoutineNameDeclaration) declaration;
83+
return routine.fullyQualifiedName().equals("System.Length")
84+
&& routine.getParameters().get(0).getType().is("<array>");
85+
}
86+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>
3+
The upper range of <code>for</code> loops is inclusive in Delphi, meaning that a typical
4+
zero-based collection iteration should start at 0 and end at one before the array length:
5+
</p>
6+
<pre>
7+
for I := 0 to Length(Array) - 1 do
8+
WriteLn(Array[I]);
9+
</pre>
10+
<p>
11+
Forgetting to subtract one from the length of the collection is a common mistake in Delphi code,
12+
especially since in most other languages the convention is for the upper bound to be exclusive.
13+
If forgotten, this may cause access violations, buffer overruns, and other memory issues.
14+
</p>
15+
<p>
16+
When using non zero-based collections, the <code>Low</code> and <code>High</code> intrinsics
17+
should be used instead of iterating based on length.
18+
</p>
19+
<p>
20+
This rule catches suspicious cases of <code>System.Length</code> or <code>Count</code> properties
21+
being used as an upper bound. Iterations over strings are excluded as they are typically
22+
one-based.
23+
</p>
24+
<h2>How to fix it</h2>
25+
<p>
26+
If the collection is zero-based, subtract one from the upper bound:
27+
</p>
28+
<pre data-diff-id="1" data-diff-type="noncompliant">
29+
for I := 0 to Length(Array) do
30+
WriteLn(Array[I]);
31+
</pre>
32+
<pre data-diff-id="1" data-diff-type="compliant">
33+
for I := 0 to Length(Array) - 1 do
34+
WriteLn(Array[I]);
35+
</pre>
36+
<p>
37+
If the collection is not zero-based, use the <code>Low</code> and <code>High</code> intrinsics
38+
to iterate instead:
39+
</p>
40+
<pre data-diff-id="2" data-diff-type="noncompliant">
41+
for I := 1 to Length(Array) do
42+
WriteLn(Array[I]);
43+
</pre>
44+
<pre data-diff-id="2" data-diff-type="compliant">
45+
for I := Low(Array) to High(Array) do
46+
WriteLn(Array[I]);
47+
</pre>
48+
<h2>Resources</h2>
49+
<ul>
50+
<li>
51+
<a href="https://docwiki.embarcadero.com/RADStudio/en/Type_Declarations">
52+
Delphi's Object Pascal Style Guide: Type Declarations
53+
</a>
54+
</li>
55+
</ul>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"title": "Loops should not iterate beyond the end of the collection",
3+
"type": "BUG",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant/Issue",
7+
"constantCost": "15min"
8+
},
9+
"code": {
10+
"attribute": "LOGICAL",
11+
"impacts": {
12+
"RELIABILITY": "MEDIUM",
13+
"MAINTAINABILITY": "LOW"
14+
}
15+
},
16+
"tags": ["suspicious"],
17+
"defaultSeverity": "Critical",
18+
"scope": "ALL",
19+
"quickfix": "unknown"
20+
}

0 commit comments

Comments
 (0)