From 0be46aa182aff21ba79ce8132f2ee281d4d2e0b6 Mon Sep 17 00:00:00 2001 From: Irwin Rodriguez Date: Sat, 2 May 2026 23:38:05 +0200 Subject: [PATCH 1/2] Implement COMPOBJ() function --- src/Runtime/XSharp.VFP.Tests/CompObjTests.prg | 103 ++++++++++++++++++ .../XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj | 1 + src/Runtime/XSharp.VFP/ClassFunctions.prg | 74 +++++++++++++ src/Runtime/XSharp.VFP/ToDo-C.prg | 7 -- 4 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/Runtime/XSharp.VFP.Tests/CompObjTests.prg diff --git a/src/Runtime/XSharp.VFP.Tests/CompObjTests.prg b/src/Runtime/XSharp.VFP.Tests/CompObjTests.prg new file mode 100644 index 0000000000..fff33274cf --- /dev/null +++ b/src/Runtime/XSharp.VFP.Tests/CompObjTests.prg @@ -0,0 +1,103 @@ +// +// Copyright (c) XSharp B.V. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +// + + +USING System +USING System.Collections.Generic +USING System.Text +USING XUnit + +BEGIN NAMESPACE XSharp.VFP.Tests + CLASS SimpleObj + PROPERTY Name AS STRING AUTO + PROPERTY Age AS INT AUTO + END CLASS + + CLASS NestedObj + PROPERTY Title AS STRING AUTO + PROPERTY Child AS SimpleObj AUTO + END CLASS + + CLASS ExtraPropObj + PROPERTY Name AS STRING AUTO + PROPERTY Age AS INT AUTO + PROPERTY Extra AS STRING AUTO + END CLASS + + CLASS CompObjTests + STATIC CONSTRUCTOR + XSharp.RuntimeState.Dialect := XSharpDialect.FoxPro + END CONSTRUCTOR + + [Fact, Trait("Category", "ClassAndObject")]; + METHOD SameObjReferenceTest AS VOID + VAR o := SimpleObj{} + o:Name := "Test" + o:Age := 42 + Assert.True(COMPOBJ(o, o)) + END METHOD + + [Fact, Trait("Category", "ClassAndObject")]; + METHOD IdenticalObjectsTest AS VOID + VAR o1 := SimpleObj{} + o1:Name := "Test" + o1:Age := 42 + + VAR o2 := SimpleObj{} + o2:Name := "Test" + o2:Age := 42 + + Assert.True(COMPOBJ(o1, o2)) + END METHOD + + [Fact, Trait("Category", "ClassAndObject")]; + METHOD DifferentValuesTest AS VOID + LOCAL o1, o2 AS SimpleObj + o1 := SimpleObj{} + o1:Name := "Test" + o1:Age := 42 + + o2 := SimpleObj{} + o2:Name := "Test" + o2:Age := 99 + + Assert.False(COMPOBJ(o1, o2)) + END METHOD + + [Fact, Trait("Category", "ClassAndObject")]; + METHOD NestedObjectsEqualTest AS VOID + LOCAL o1, o2 AS NestedObj + + o1 := NestedObj{} + o1:Title := "A" + o1:Child := SimpleObj{} + o1:Child:Name := "X" + o1:Child:Age := 1 + + o2 := NestedObj{} + o2:Title := "A" + o2:Child := SimpleObj{} + o2:Child:Name := "X" + o2:Child:Age := 1 + + Assert.True(COMPOBJ(o1, o2)) + END METHOD + + [Fact, Trait("Category", "ClassAndObject")]; + METHOD BothNullTest AS VOID + Assert.True(COMPOBJ(NULL_OBJECT, NULL_OBJECT)) + END METHOD + + [Fact, Trait("Category", "ClassAndObject")]; + METHOD OneNullTest AS VOID + VAR o := SimpleObj{} + + Assert.False(COMPOBJ(o, NULL_OBJECT)) + Assert.False(COMPOBJ(NULL_OBJECT, o)) + END METHOD + + END CLASS +END NAMESPACE // XSharp.VFP.Tests diff --git a/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj b/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj index 846b21cb8c..1f6ed65acf 100644 --- a/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj +++ b/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj @@ -94,6 +94,7 @@ + diff --git a/src/Runtime/XSharp.VFP/ClassFunctions.prg b/src/Runtime/XSharp.VFP/ClassFunctions.prg index 69c4e0bf6c..5d204af673 100644 --- a/src/Runtime/XSharp.VFP/ClassFunctions.prg +++ b/src/Runtime/XSharp.VFP/ClassFunctions.prg @@ -4,6 +4,8 @@ // See License.txt in the project root for license information. // +USING System.Reflection + /// [FoxProFunction("ADDPROPERTY", FoxFunctionCategory.ClassAndObject, FoxEngine.LanguageCore, FoxFunctionStatus.Full, FoxCriticality.High)]; FUNCTION AddProperty (oObjectName AS OBJECT, cPropertyName AS STRING, eNewValue := NIL AS USUAL) AS LOGIC @@ -34,3 +36,75 @@ FUNCTION GETPEM( uObject as USUAL, cProperty as STRING) as USUAL endif return IVarGet(uObject, cProperty) +/// +[FoxProFunction("COMPOBJ", FoxFunctionCategory.ClassAndObject, FoxEngine.LanguageCore, FoxFunctionStatus.Full, FoxCriticality.Medium)]; +FUNCTION CompObj (oExpression1 AS OBJECT, oExpression2 AS OBJECT) AS LOGIC + IF oExpression1 == NULL_OBJECT .AND. oExpression2 == NULL_OBJECT + RETURN TRUE + ENDIF + + IF oExpression1 == NULL_OBJECT .OR. oExpression2 == NULL_OBJECT + RETURN FALSE + ENDIF + + VAR aProps1 := oExpression1:GetType():GetProperties(BindingFlags.Public | BindingFlags.Instance) + VAR aProps2 := oExpression2:GetType():GetProperties(BindingFlags.Public | BindingFlags.Instance) + + IF aProps1:Length != aProps2:Length + RETURN FALSE + ENDIF + + FOREACH VAR oProp1 IN aProps1 + IF !oProp1:CanRead + LOOP + ENDIF + + IF oProp1:GetIndexParameters():Length > 0 + LOOP + ENDIF + + LOCAL oProp2 := NULL AS PropertyInfo + FOREACH VAR oP IN aProps2 + IF oP:Name == oProp1:Name + oProp2 := oP + EXIT + ENDIF + NEXT + + IF oProp2 == NULL .OR. !oProp2:CanRead + RETURN FALSE + ENDIF + + IF oProp2:GetIndexParameters():Length > 0 + LOOP + ENDIF + + VAR uVal1 := oProp1:GetValue(oExpression1) + VAR uVal2 := oProp2:GetValue(oExpression2) + + IF uVal1 == NULL .AND. uVal2 == NULL + LOOP + ENDIF + + IF uVal1 == NULL .OR. uVal2 == NULL + RETURN FALSE + ENDIF + + IF uVal1 IS OBJECT VAR o1 .AND. uVal2 IS OBJECT VAR o2 + VAR oType := o1:GetType() + IF oType:IsValueType .OR. oType == typeof(STRING) + IF !uVal1:Equals(uVal2) + RETURN FALSE + ENDIF + ELSE + IF !CompObj(o1, o2) + RETURN FALSE + ENDIF + ENDIF + ELSEIF !uVal1:Equals(uVal2) + RETURN FALSE + ENDIF + NEXT + + RETURN TRUE + diff --git a/src/Runtime/XSharp.VFP/ToDo-C.prg b/src/Runtime/XSharp.VFP/ToDo-C.prg index 5d020d9538..c9a6b66a42 100644 --- a/src/Runtime/XSharp.VFP/ToDo-C.prg +++ b/src/Runtime/XSharp.VFP/ToDo-C.prg @@ -13,13 +13,6 @@ FUNCTION Candidate (nIndexNumber , uArea) THROW NotImplementedException{} // RETURN FALSE -/// -- todo -- -/// -[FoxProFunction("COMPOBJ", FoxFunctionCategory.ClassAndObject, FoxEngine.LanguageCore, FoxFunctionStatus.Stub, FoxCriticality.Medium)]; -FUNCTION CompObj (oExpression1, oExpression2) - THROW NotImplementedException{} - // RETURN FALSE - /// -- todo -- /// [FoxProFunction("CPCONVERT", FoxFunctionCategory.EnvironmentAndSystem, FoxEngine.RuntimeCore, FoxFunctionStatus.Stub, FoxCriticality.Medium)]; From 4bf81c4814f54dcddfb7bead4d330c87eec61e9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 13:32:22 +0000 Subject: [PATCH 2/2] fix: address COMPOBJ review feedback - O(1) dict lookup, fixed length check, depth guard, tests Agent-Logs-Url: https://github.com/X-Sharp/XSharpPublic/sessions/f467ceb6-90a9-4fe4-8b57-737527ffeb28 Co-authored-by: RobertvanderHulst <14240939+RobertvanderHulst@users.noreply.github.com> --- src/Runtime/XSharp.VFP.Tests/CompObjTests.prg | 39 ++++++++++++++- src/Runtime/XSharp.VFP/ClassFunctions.prg | 50 +++++++++++-------- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/Runtime/XSharp.VFP.Tests/CompObjTests.prg b/src/Runtime/XSharp.VFP.Tests/CompObjTests.prg index fff33274cf..07d7b8ffc8 100644 --- a/src/Runtime/XSharp.VFP.Tests/CompObjTests.prg +++ b/src/Runtime/XSharp.VFP.Tests/CompObjTests.prg @@ -6,8 +6,6 @@ USING System -USING System.Collections.Generic -USING System.Text USING XUnit BEGIN NAMESPACE XSharp.VFP.Tests @@ -27,6 +25,15 @@ BEGIN NAMESPACE XSharp.VFP.Tests PROPERTY Extra AS STRING AUTO END CLASS + CLASS WithIndexerObj + PROPERTY Name AS STRING AUTO + PROPERTY SELF[nIndex AS INT] AS INT + GET + RETURN nIndex + END GET + END PROPERTY + END CLASS + CLASS CompObjTests STATIC CONSTRUCTOR XSharp.RuntimeState.Dialect := XSharpDialect.FoxPro @@ -99,5 +106,33 @@ BEGIN NAMESPACE XSharp.VFP.Tests Assert.False(COMPOBJ(NULL_OBJECT, o)) END METHOD + [Fact, Trait("Category", "ClassAndObject")]; + METHOD ExtraPropertyReturnsFalseTest AS VOID + // oExpression2 has an extra "Extra" property not present on oExpression1 + VAR o1 := SimpleObj{} + o1:Name := "Test" + o1:Age := 42 + + VAR o2 := ExtraPropObj{} + o2:Name := "Test" + o2:Age := 42 + o2:Extra := "Extra" + + Assert.False(COMPOBJ(o1, o2)) + Assert.False(COMPOBJ(o2, o1)) + END METHOD + + [Fact, Trait("Category", "ClassAndObject")]; + METHOD IndexedPropertyIgnoredTest AS VOID + // Indexed properties must be ignored; only the regular Name property is compared + VAR o1 := WithIndexerObj{} + o1:Name := "Test" + + VAR o2 := WithIndexerObj{} + o2:Name := "Test" + + Assert.True(COMPOBJ(o1, o2)) + END METHOD + END CLASS END NAMESPACE // XSharp.VFP.Tests diff --git a/src/Runtime/XSharp.VFP/ClassFunctions.prg b/src/Runtime/XSharp.VFP/ClassFunctions.prg index 5d204af673..8edf5b6661 100644 --- a/src/Runtime/XSharp.VFP/ClassFunctions.prg +++ b/src/Runtime/XSharp.VFP/ClassFunctions.prg @@ -5,6 +5,7 @@ // USING System.Reflection +USING System.Collections.Generic /// [FoxProFunction("ADDPROPERTY", FoxFunctionCategory.ClassAndObject, FoxEngine.LanguageCore, FoxFunctionStatus.Full, FoxCriticality.High)]; @@ -39,6 +40,9 @@ FUNCTION GETPEM( uObject as USUAL, cProperty as STRING) as USUAL /// [FoxProFunction("COMPOBJ", FoxFunctionCategory.ClassAndObject, FoxEngine.LanguageCore, FoxFunctionStatus.Full, FoxCriticality.Medium)]; FUNCTION CompObj (oExpression1 AS OBJECT, oExpression2 AS OBJECT) AS LOGIC + RETURN CompObjHelper(oExpression1, oExpression2, 0) + +INTERNAL FUNCTION CompObjHelper(oExpression1 AS OBJECT, oExpression2 AS OBJECT, nDepth AS INT) AS LOGIC IF oExpression1 == NULL_OBJECT .AND. oExpression2 == NULL_OBJECT RETURN TRUE ENDIF @@ -47,37 +51,36 @@ FUNCTION CompObj (oExpression1 AS OBJECT, oExpression2 AS OBJECT) AS LOGIC RETURN FALSE ENDIF - VAR aProps1 := oExpression1:GetType():GetProperties(BindingFlags.Public | BindingFlags.Instance) - VAR aProps2 := oExpression2:GetType():GetProperties(BindingFlags.Public | BindingFlags.Instance) - - IF aProps1:Length != aProps2:Length - RETURN FALSE + // Guard against cyclic object graphs causing infinite recursion. + // Returning TRUE (assume equal) at the depth limit mirrors the VFP behaviour of + // not recursing indefinitely; real cycles are extremely rare in practice. + LOCAL CONST MAX_COMPOBJ_DEPTH := 50 AS INT + IF nDepth > MAX_COMPOBJ_DEPTH + RETURN TRUE ENDIF - FOREACH VAR oProp1 IN aProps1 - IF !oProp1:CanRead - LOOP + // Build a name→PropertyInfo map for oExpression2's comparable (readable, non-indexed) properties + VAR dict2 := Dictionary{} + FOREACH VAR oP IN oExpression2:GetType():GetProperties(BindingFlags.Public | BindingFlags.Instance) + IF oP:CanRead .AND. oP:GetIndexParameters():Length == 0 + dict2[oP:Name] := oP ENDIF + NEXT - IF oProp1:GetIndexParameters():Length > 0 + // Iterate oExpression1's comparable properties and compare with O(1) lookup into dict2 + LOCAL nComparableCount := 0 AS INT + FOREACH VAR oProp1 IN oExpression1:GetType():GetProperties(BindingFlags.Public | BindingFlags.Instance) + IF !oProp1:CanRead .OR. oProp1:GetIndexParameters():Length > 0 LOOP ENDIF - LOCAL oProp2 := NULL AS PropertyInfo - FOREACH VAR oP IN aProps2 - IF oP:Name == oProp1:Name - oProp2 := oP - EXIT - ENDIF - NEXT + nComparableCount++ - IF oProp2 == NULL .OR. !oProp2:CanRead + IF !dict2:ContainsKey(oProp1:Name) RETURN FALSE ENDIF - IF oProp2:GetIndexParameters():Length > 0 - LOOP - ENDIF + VAR oProp2 := dict2[oProp1:Name] VAR uVal1 := oProp1:GetValue(oExpression1) VAR uVal2 := oProp2:GetValue(oExpression2) @@ -97,7 +100,7 @@ FUNCTION CompObj (oExpression1 AS OBJECT, oExpression2 AS OBJECT) AS LOGIC RETURN FALSE ENDIF ELSE - IF !CompObj(o1, o2) + IF !CompObjHelper(o1, o2, nDepth + 1) RETURN FALSE ENDIF ENDIF @@ -106,5 +109,10 @@ FUNCTION CompObj (oExpression1 AS OBJECT, oExpression2 AS OBJECT) AS LOGIC ENDIF NEXT + // If oExpression2 has more comparable properties than oExpression1, they differ + IF nComparableCount != dict2:Count + RETURN FALSE + ENDIF + RETURN TRUE