Skip to content

Commit 4799e37

Browse files
fourlscirras
authored andcommitted
Add check for array iterations with out of range high bounds
1 parent 91314d3 commit 4799e37

File tree

7 files changed

+520
-0
lines changed

7 files changed

+520
-0
lines changed

CHANGELOG.md

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

1212
- Support for MSBuild item and item metadata expressions in project files.
1313
- `ExhaustiveEnumCase` analysis rule, which flags `case` statements that do not handle all values in an enumeration.
14+
- `IterationPastHighBound` analysis rule, which flags `for` loops that iterate past the end of the collection.
1415
- **API:** `EnumeratorOccurrence` type.
1516
- **API:** `ForInStatementNode::getEnumeratorOccurrence` method.
1617
- **API:** `TypeOfTypeNode::getTypeReferenceNode` 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
InstanceInvokedConstructorCheck.class,
105105
InterfaceGuidCheck.class,
106106
InterfaceNameCheck.class,
107+
IterationPastHighBoundCheck.class,
107108
LegacyInitializationSectionCheck.class,
108109
LoopExecutingAtMostOnceCheck.class,
109110
LowercaseKeywordCheck.class,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.IntegerLiteralNode;
25+
import org.sonar.plugins.communitydelphi.api.ast.NameReferenceNode;
26+
import org.sonar.plugins.communitydelphi.api.ast.PrimaryExpressionNode;
27+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
28+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext;
29+
import org.sonar.plugins.communitydelphi.api.reporting.QuickFix;
30+
import org.sonar.plugins.communitydelphi.api.reporting.QuickFixEdit;
31+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.NameDeclaration;
32+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.RoutineNameDeclaration;
33+
import org.sonar.plugins.communitydelphi.api.symbol.declaration.TypedDeclaration;
34+
35+
@Rule(key = "IterationPastHighBound")
36+
public class IterationPastHighBoundCheck extends DelphiCheck {
37+
@Override
38+
public DelphiCheckContext visit(ForToStatementNode forStatement, DelphiCheckContext context) {
39+
var target = forStatement.getTargetExpression().skipParentheses();
40+
var initializer = forStatement.getInitializerExpression().skipParentheses();
41+
42+
if (isZero(initializer) && isSuspiciousTargetExpression(target)) {
43+
context
44+
.newIssue()
45+
.onNode(target)
46+
.withMessage("Ensure this for loop is not iterating beyond the end of the collection.")
47+
.withQuickFixes(
48+
QuickFix.newFix("Subtract one from expression")
49+
.withEdit(QuickFixEdit.insertAfter(" - 1", target)))
50+
.report();
51+
}
52+
53+
return super.visit(forStatement, context);
54+
}
55+
56+
private boolean isZero(ExpressionNode expression) {
57+
if (!(expression instanceof PrimaryExpressionNode)) {
58+
return false;
59+
}
60+
61+
var child = expression.getChild(0);
62+
return child instanceof IntegerLiteralNode
63+
&& ((IntegerLiteralNode) child).getValue().intValue() == 0;
64+
}
65+
66+
private boolean isSuspiciousTargetExpression(ExpressionNode expression) {
67+
if (!(expression instanceof PrimaryExpressionNode)) {
68+
return false;
69+
}
70+
71+
var reference = expression.getChild(0);
72+
if (!(reference instanceof NameReferenceNode)) {
73+
return false;
74+
}
75+
76+
var declaration = ((NameReferenceNode) reference).getLastName().getNameDeclaration();
77+
return isArrayLength(declaration) || isCount(declaration);
78+
}
79+
80+
private boolean isCount(NameDeclaration declaration) {
81+
if (!(declaration instanceof TypedDeclaration)) {
82+
return false;
83+
}
84+
85+
return declaration.getName().equalsIgnoreCase("Count")
86+
&& ((TypedDeclaration) declaration).getType().isInteger();
87+
}
88+
89+
private boolean isArrayLength(NameDeclaration declaration) {
90+
if (!(declaration instanceof RoutineNameDeclaration)) {
91+
return false;
92+
}
93+
94+
var routine = (RoutineNameDeclaration) declaration;
95+
return routine.fullyQualifiedName().equals("System.Length")
96+
&& routine.getParameters().get(0).getType().is("<array>");
97+
}
98+
}
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+
}

delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"InlineDeclarationCapturedByAnonymousMethod",
4949
"InstanceInvokedConstructor",
5050
"InterfaceName",
51+
"IterationPastHighBound",
5152
"LegacyInitializationSection",
5253
"LoopExecutingAtMostOnce",
5354
"LowercaseKeyword",

0 commit comments

Comments
 (0)