Repository: integrated-application-development/sonar-delphi
Pull request #380: Disambiguate index property read/write methods correctly
Author: login: fourls
Created: 2025-05-07T06:03:09Z | Merged: 2025-05-07T06:31:09Z

When a property has an `index` specifier, its `read` specifier must be a function with an `Integer` final argument, and its `write` specifier must be a procedure with an `Integer` second-last argument. We were not previously modelling this behaviour, causing `read` and `write` specifier methods to fail to resolve when an `index` property is present.


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c76214a07..4046da3dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -59,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Name resolution failures when accessing ancestors of enclosing types from nested type methods.
 - Name resolution failures on invocations of methods with generic open array parameters.
 - Name resolution failures around `Create` calls on types with `constructor` constraints.
+- Name resolution failures on `read` and `write` specifiers of indexed properties.
 - Incorrect file position calculation for multiline string tokens.
 - Analysis errors around `type of` type declarations.
 
diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/NameResolutionHelper.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/NameResolutionHelper.java
index 56af410f9..be5f4186c 100644
--- a/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/NameResolutionHelper.java
+++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/symbol/resolve/NameResolutionHelper.java
@@ -81,6 +81,7 @@
 import org.sonar.plugins.communitydelphi.api.symbol.scope.DelphiScope;
 import org.sonar.plugins.communitydelphi.api.symbol.scope.FileScope;
 import org.sonar.plugins.communitydelphi.api.symbol.scope.RoutineScope;
+import org.sonar.plugins.communitydelphi.api.type.IntrinsicType;
 import org.sonar.plugins.communitydelphi.api.type.Type;
 import org.sonar.plugins.communitydelphi.api.type.Type.AliasType;
 import org.sonar.plugins.communitydelphi.api.type.Type.ProceduralType;
@@ -252,17 +253,15 @@ public void resolve(PropertyNode property) {
     if (read != null) {
       NameResolver readResolver = createNameResolver();
       readResolver.readPrimaryExpression(read.getExpression());
-      readResolver.disambiguateParameters(property.getParameterTypes());
+      readResolver.disambiguateParameters(getGetterParameterTypes(property));
       readResolver.addToSymbolTable();
     }
 
     PropertyWriteSpecifierNode write = property.getWriteSpecifier();
     if (write != null) {
-      List<Type> parameterTypes = new ArrayList<>(property.getParameterTypes());
-      parameterTypes.add(property.getType());
       NameResolver writeResolver = createNameResolver();
       writeResolver.readPrimaryExpression(write.getExpression());
-      writeResolver.disambiguateParameters(parameterTypes);
+      writeResolver.disambiguateParameters(getSetterParameterTypes(property));
       writeResolver.addToSymbolTable();
     }
 
@@ -272,6 +271,25 @@ public void resolve(PropertyNode property) {
     }
   }
 
+  private List<Type> getGetterParameterTypes(PropertyNode property) {
+    if (property.getIndexSpecifier() == null) {
+      return property.getParameterTypes();
+    }
+
+    var parameterTypes = new ArrayList<>(property.getParameterTypes());
+    parameterTypes.add(typeFactory.getIntrinsic(IntrinsicType.INTEGER));
+    return parameterTypes;
+  }
+
+  private List<Type> getSetterParameterTypes(PropertyNode property) {
+    var parameterTypes = new ArrayList<>(property.getParameterTypes());
+    if (property.getIndexSpecifier() != null) {
+      parameterTypes.add(typeFactory.getIntrinsic(IntrinsicType.INTEGER));
+    }
+    parameterTypes.add(property.getType());
+    return parameterTypes;
+  }
+
   public void resolve(RoutineDeclarationNode routine) {
     resolveRoutine(routine);
   }
diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/executor/DelphiSymbolTableExecutorTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/executor/DelphiSymbolTableExecutorTest.java
index 193706998..342cd64f3 100644
--- a/delphi-frontend/src/test/java/au/com/integradev/delphi/executor/DelphiSymbolTableExecutorTest.java
+++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/executor/DelphiSymbolTableExecutorTest.java
@@ -103,6 +103,27 @@ void testSimilarParameterDeclarations() {
     verifyUsages(9, 19);
   }
 
+  @Test
+  void testProperties() {
+    execute("Properties.pas");
+    // Field
+    verifyUsages(10, 4, reference(19, 33), reference(19, 46));
+    // Normal getter
+    verifyUsages(16, 13, reference(20, 33), reference(21, 33));
+    // Normal setter
+    verifyUsages(14, 14, reference(20, 50));
+    // Const setter
+    verifyUsages(12, 14, reference(21, 50));
+    // Alias setter
+    verifyUsages(13, 14, reference(22, 55));
+    // Alias getter
+    verifyUsages(17, 13, reference(22, 33));
+    // Index getter
+    verifyUsages(29, 13, reference(32, 41));
+    // Index setter
+    verifyUsages(30, 14, reference(32, 58));
+  }
+
   @Test
   void testRecords() {
     execute("Records.pas");
diff --git a/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/Properties.pas b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/Properties.pas
new file mode 100644
index 000000000..72a188141
--- /dev/null
+++ b/delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/Properties.pas
@@ -0,0 +1,37 @@
+unit Properties;
+
+interface
+
+type
+  TIntAlias = Integer;
+
+  TMyObj = class(TObject)
+  private
+    FField: Integer;
+
+    procedure SetMyFieldConst(const Val: Integer);
+    procedure SetMyFieldAlias(Val: TIntAlias);
+    procedure SetMyField(Val: Integer);
+
+    function GetMyField: Integer;
+    function GetMyFieldAlias: TIntAlias;
+  public
+    property Prop1: Integer read FField write FField;
+    property Prop2: Integer read GetMyField write SetMyField;
+    property Prop3: Integer read GetMyField write SetMyFieldConst;
+    property Prop4: Integer read GetMyFieldAlias write SetMyFieldAlias;
+  end;
+
+  TIndexObj = class(TObject)
+  private
+    FField: TObject;
+
+    function GetMyField(Index: Integer): TObject;
+    procedure SetMyField(Index: Integer; Value: TObject);
+  public
+    property Prop1: TObject index 0 read GetMyField write SetMyField;
+  end;
+
+implementation
+
+end.
\ No newline at end of file


Reviews:
user: login: cirras
body: Nice - all looks good.

As is tradition, I have a changelog nitpick.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T06:19:57Z
user: login: fourls
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T06:20:59Z
user: login: cirras
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T06:21:46Z
user: login: fourls
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T06:23:02Z
user: login: cirras
body: Looks good to me. 👍
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-07T06:31:04Z

Review comments:
diff_hunk: @@ -59,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Name resolution failures when accessing ancestors of enclosing types from nested type methods.
 - Name resolution failures on invocations of methods with generic open array parameters.
 - Name resolution failures around `Create` calls on types with `constructor` constraints.
+- Name resolution failures on `read` and `write` specifiers for properties with an `index` specifier.
path: CHANGELOG.md
user: login: cirras
body: nit;
```suggestion
- Name resolution failures on `read` and `write` specifiers of indexed properties.
```
created_at: 2025-05-07T06:08:07Z
author_association: COLLABORATOR
diff_hunk: @@ -59,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Name resolution failures when accessing ancestors of enclosing types from nested type methods.
 - Name resolution failures on invocations of methods with generic open array parameters.
 - Name resolution failures around `Create` calls on types with `constructor` constraints.
+- Name resolution failures on `read` and `write` specifiers for properties with an `index` specifier.
path: CHANGELOG.md
user: login: fourls
body: Fixed
created_at: 2025-05-07T06:20:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+unit Properties;
+
+interface
+
+type
+  TIntAlias = Integer;
+
+  TMyObj = class(TObject)
+  private
+    FField: Integer;
+
+    procedure SetMyFieldConst(const Val: Integer);
+    procedure SetMyFieldAlias(Val: TIntAlias);
+    procedure SetMyField(Val: Integer);
+
+    function GetMyField: Integer;
+    function GetMyFieldAlias: IntAlias;
path: delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/Properties.pas
user: login: cirras
body: Almost missed this.
```suggestion
    function GetMyFieldAlias: TIntAlias;
```
created_at: 2025-05-07T06:21:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+unit Properties;
+
+interface
+
+type
+  TIntAlias = Integer;
+
+  TMyObj = class(TObject)
+  private
+    FField: Integer;
+
+    procedure SetMyFieldConst(const Val: Integer);
+    procedure SetMyFieldAlias(Val: TIntAlias);
+    procedure SetMyField(Val: Integer);
+
+    function GetMyField: Integer;
+    function GetMyFieldAlias: IntAlias;
path: delphi-frontend/src/test/resources/au/com/integradev/delphi/symbol/Properties.pas
user: login: fourls
body: lol, fixed again
created_at: 2025-05-07T06:23:02Z
author_association: COLLABORATOR

PR comments:
user: login: fourls
created_at: 2025-05-07T06:06:27Z
body: Renamed a variable for consistency.
author_association: COLLABORATOR
user: login: fourls
created_at: 2025-05-07T06:19:30Z
body: Disregard my fail-on-upgrade test, I found out why it didn't work 🤦 
![image](https://github.com/user-attachments/assets/c43edbc7-713c-439a-83c8-7702844e23bb)

author_association: COLLABORATOR