diff --git a/src/Runtime/XSharp.Data/RDD/DbDataTable.prg b/src/Runtime/XSharp.Data/RDD/DbDataTable.prg index 35e28fd592..a4af8487c0 100644 --- a/src/Runtime/XSharp.Data/RDD/DbDataTable.prg +++ b/src/Runtime/XSharp.Data/RDD/DbDataTable.prg @@ -77,6 +77,13 @@ PROTECT _nArea AS LONG oData[nI] := oRDD:GetValue(nI) IF oData[nI] IS STRING VAR strValue oData[nI] := strValue:TrimEnd() + ELSEIF oData[nI] IS XSharp.IDate VAR dValue + // DBF data fields hand out a DbDate, which a DateTime column cannot store + IF dValue:Month == 0 + oData[nI] := DBNull.Value + ELSE + oData[nI] := dValue:Value + ENDIF ENDIF NEXT SELF:_AddRow(oData,oRDD:RecNo) diff --git a/src/Runtime/XSharp.VFP.Tests/CursorToXmlTests.prg b/src/Runtime/XSharp.VFP.Tests/CursorToXmlTests.prg new file mode 100644 index 0000000000..bf6cd6bd3b --- /dev/null +++ b/src/Runtime/XSharp.VFP.Tests/CursorToXmlTests.prg @@ -0,0 +1,278 @@ +// +// 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.IO +USING System.Text +USING XUnit + +// The expected values in these tests were captured from Visual FoxPro 9 +// running the same cursors through CURSORTOXML(). +BEGIN NAMESPACE XSharp.VFP.Tests + + CLASS CursorToXmlTests + + STATIC CONSTRUCTOR + XSharp.RuntimeState.Dialect := XSharpDialect.FoxPro + END CONSTRUCTOR + + #region helpers + + PRIVATE METHOD CreateTestCursor() AS VOID + CREATE CURSOR curxml (id I, nombre C(10), precio N(8,2), fecha D, activo L) + INSERT INTO curxml VALUES (1, "uno", 10.50, {^2024-01-15}, .T.) + INSERT INTO curxml VALUES (2, "dos", 20.75, {^2024-02-20}, .F.) + INSERT INTO curxml VALUES (3, "tres", 30.00, {^2024-03-25}, .T.) + GO TOP + END METHOD + + PRIVATE METHOD CreateTypesCursor() AS VOID + LOCAL tStamp, tEmpty, dEmpty AS USUAL + // A datetime literal containing a space cannot be written inside + // INSERT INTO, so the values are prepared first. + tStamp := CToT("2024-01-15 10:30:45") + tEmpty := CToT("") + dEmpty := SToD("") + CREATE CURSOR curtipos (cchar C(8), nnum N(10,3), ycur Y, ddate D, tstamp T, llog L) + INSERT INTO curtipos VALUES ("abc", 12.5, 99.95, {^2024-01-15}, tStamp, .T.) + INSERT INTO curtipos VALUES ("", 0, 0, dEmpty, tEmpty, .F.) + GO TOP + END METHOD + + PRIVATE METHOD TempFile() AS STRING + RETURN Path.Combine(Path.GetTempPath(), "CursorToXml_" + Guid.NewGuid():ToString("N") + ".xml") + END METHOD + + // Writes the given alias to a temporary file and returns the XML + PRIVATE METHOD ToXml(cAlias AS STRING) AS STRING + RETURN SELF:ToXml(cAlias, 512, 0, "") + END METHOD + + PRIVATE METHOD ToXml(cAlias AS STRING, nFlags AS LONG, nRecords AS LONG, cSchema AS STRING) AS STRING + LOCAL cXml AS STRING + VAR cFile := SELF:TempFile() + CursorToXml(cAlias, cFile, 1, nFlags, nRecords, cSchema) + cXml := File.ReadAllText(cFile) + File.Delete(cFile) + RETURN cXml + END METHOD + + PRIVATE METHOD CountOf(cText AS STRING, cNeedle AS STRING) AS LONG + LOCAL nCount := 0 AS LONG + LOCAL nPos := 0 AS LONG + DO WHILE TRUE + nPos := cText:IndexOf(cNeedle, nPos, StringComparison.Ordinal) + IF nPos < 0 + EXIT + ENDIF + nCount++ + nPos += cNeedle:Length + ENDDO + RETURN nCount + END METHOD + + #endregion + + #region structure + + [Fact, Trait("Category", "CursorToXml")]; + METHOD RootIsVfpDataAndRowElementIsLowercaseAlias AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml") + Assert.Contains("", cXml) + Assert.Contains("", cXml) + // VFP writes the alias in lower case, one element per record + Assert.Equal(3, SELF:CountOf(cXml, "")) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD FieldNamesAreLowercase AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml") + Assert.Contains("", cXml) + Assert.DoesNotContain("", cXml) + END METHOD + + #endregion + + #region value formatting + + [Fact, Trait("Category", "CursorToXml")]; + METHOD NumericKeepsTheFieldScale AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml") + // N(8,2): VFP keeps the trailing zeros of the declared scale + Assert.Contains("10.50", cXml) + Assert.Contains("30.00", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD CharacterIsTrimmed AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml") + Assert.Contains("uno", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD LogicalIsWrittenInLowercase AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml") + Assert.Contains("true", cXml) + Assert.Contains("false", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD DateUsesIsoFormat AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml") + Assert.Contains("2024-01-15", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD DateTimeUsesIsoFormat AS VOID + SELF:CreateTypesCursor() + VAR cXml := SELF:ToXml("curtipos") + Assert.Contains("2024-01-15T10:30:45", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD CurrencyAlwaysHasFourDecimals AS VOID + SELF:CreateTypesCursor() + VAR cXml := SELF:ToXml("curtipos") + Assert.Contains("99.9500", cXml) + Assert.Contains("0.0000", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD EmptyValuesProduceAnEmptyElement AS VOID + SELF:CreateTypesCursor() + VAR cXml := SELF:ToXml("curtipos") + // empty, but not NULL: the element is present and carries no text + Assert.True(cXml:Contains("") .OR. cXml:Contains("")) + Assert.True(cXml:Contains("") .OR. cXml:Contains("")) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD NullFieldsAreOmitted AS VOID + LOCAL cXml AS STRING + CREATE CURSOR curnul (id I, nombre C(10) NULL, fecha D NULL) + INSERT INTO curnul VALUES (1, "ok", {^2024-01-15}) + INSERT INTO curnul VALUES (2, NULL, NULL) + GO TOP + cXml := SELF:ToXml("curnul") + // VFP leaves the element out completely when the field is NULL + Assert.Equal(1, SELF:CountOf(cXml, "")) + Assert.Equal(1, SELF:CountOf(cXml, "")) + Assert.Equal(2, SELF:CountOf(cXml, "")) + END METHOD + + #endregion + + #region record selection and cursor position + + [Fact, Trait("Category", "CursorToXml")]; + METHOD NRecordsLimitsTheNumberOfRows AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml", 512, 2, "") + Assert.Equal(2, SELF:CountOf(cXml, "")) + Assert.DoesNotContain("tres", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD WritingEveryRecordLeavesThePointerAtEof AS VOID + SELF:CreateTestCursor() + SELF:ToXml("curxml") + // VFP does not restore the record pointer + Assert.True(EOF()) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD NRecordsLeavesThePointerOnTheLastRecordWritten AS VOID + SELF:CreateTestCursor() + SELF:ToXml("curxml", 512, 2, "") + Assert.False(EOF()) + Assert.Equal((DWORD) 2, RECNO()) + END METHOD + + #endregion + + #region output targets + + [Fact, Trait("Category", "CursorToXml")]; + METHOD FileOutputReturnsTheNumberOfBytesWritten AS VOID + LOCAL nBytes AS LONG + SELF:CreateTestCursor() + VAR cFile := SELF:TempFile() + nBytes := (LONG) CursorToXml("curxml", cFile, 1, 512, 0, "") + Assert.True(File.Exists(cFile)) + Assert.Equal(nBytes, (LONG) FileInfo{cFile}:Length) + File.Delete(cFile) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD MemVarOutputCreatesTheVariable AS VOID + PRIVATE cXmlOut + LOCAL nBytes AS LONG + SELF:CreateTestCursor() + // without the file flag cOutput names a memory variable + nBytes := (LONG) CursorToXml("curxml", "cXmlOut", 1, 0, 0, "") + Assert.True(nBytes > 0) + Assert.Contains("", cXmlOut) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD CurrentWorkAreaIsUsedWhenNoAreaIsGiven AS VOID + LOCAL cXml AS STRING + SELF:CreateTestCursor() + VAR cFile := SELF:TempFile() + CursorToXml(0, cFile, 1, 512, 0, "") + cXml := File.ReadAllText(cFile) + File.Delete(cFile) + Assert.Equal(3, SELF:CountOf(cXml, "")) + END METHOD + + #endregion + + #region flags and schema + + [Fact, Trait("Category", "CursorToXml")]; + METHOD ContinuousFlagRemovesTheLineBreaks AS VOID + SELF:CreateTestCursor() + // 512 = to file, 1 = one continuous string + VAR cXml := SELF:ToXml("curxml", 513, 0, "") + Assert.DoesNotContain(e"\n ", cXml) + Assert.Contains("", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD NoSchemaIsWrittenByDefault AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml") + Assert.DoesNotContain("xs:schema", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD InlineSchemaIsEmittedWhenSchemaNameIsOne AS VOID + SELF:CreateTestCursor() + VAR cXml := SELF:ToXml("curxml", 512, 0, "1") + Assert.Contains("schema", cXml) + END METHOD + + [Fact, Trait("Category", "CursorToXml")]; + METHOD ExternalSchemaFileIsCreated AS VOID + SELF:CreateTestCursor() + VAR cXsd := Path.Combine(Path.GetTempPath(), "CursorToXml_" + Guid.NewGuid():ToString("N") + ".xsd") + VAR cFile := SELF:TempFile() + CursorToXml("curxml", cFile, 1, 512, 0, cXsd) + Assert.True(File.Exists(cXsd)) + File.Delete(cXsd) + File.Delete(cFile) + END METHOD + + #endregion + + END CLASS + +END NAMESPACE diff --git a/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj b/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj index 816e140bb1..5ed2fddd0c 100644 --- a/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj +++ b/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj @@ -97,6 +97,7 @@ + diff --git a/src/Runtime/XSharp.VFP/Cursors/Xml.prg b/src/Runtime/XSharp.VFP/Cursors/Xml.prg new file mode 100644 index 0000000000..08f277695d --- /dev/null +++ b/src/Runtime/XSharp.VFP/Cursors/Xml.prg @@ -0,0 +1,222 @@ +// +// 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.IO +using System.Data +using System.Text +using System.Xml +using XSharp.RDD +using XSharp.RDD.Support +using System.Globalization + +// nFlags values for CursorToXml(). Only the ones implemented are listed here: +DEFINE C2X_FLAG_CONTINUOUS := 1 // Produce unformatted XML as one continuous string +DEFINE C2X_FLAG_TOFILE := 512 // Send the output to the file name in cOutput + + +/// +[FoxProFunction("CURSORTOXML", FoxFunctionCategory.General, FoxEngine.RuntimeCore, FoxFunctionStatus.Partial, FoxCriticality.Medium)]; +FUNCTION CursorToXML (uArea, cOutput, nOutputFormat, nFlags, nRecords, cSchemaName, cSchemaLocation, cNameSpace ) AS USUAL CLIPPER + local nArea as dword + local cTarget as string + local nFlagsInt as long + local nRecs as long + local cSchema as string + local cNs as string + local cXml as string + + if !IsString(cOutput) + throw Error.VoDbError(EG_ARG, EDB_PARAM, __FUNCTION__, nameof(cOutput), 2, {cOutput}) + endif + + cTarget := (string) cOutput + nFlagsInt := (long) iif(IsNumeric(nFlags), nFlags, 0) + nRecs := (long) iif(IsNumeric(nRecords), nRecords, 0) + cSchema := (string) iif(IsString(cSchema), cSchemaName, "") + cNs := (string) iif(IsString(cNameSpace), cNameSpace, "") + + // nOutputformat 1 = ELEMENTS (default), 2 = ATTRIBUTES, 3 = RAW. + // Only ELEMENTS is produces; the parameter is accepted but ignored + + // uArea: NIL or 0 means the current work area + if IsNil(uArea) .OR. (IsNumeric(uArea) .AND. (long) uArea == 0) + nArea := RuntimeState.CurrentWorkarea + else + nArea := _AreaFromParam(uArea) + endif + + if nArea == 0 + throw Error.VoDbError(EG_ARG, EDB_BADALIAS, __FUNCTION__, nameof(uArea), 1, {uArea}) + endif + + var nOldArea := RuntimeState.CurrentWorkarea + RuntimeState.CurrentWorkarea := nArea + try + cXml := __FoxCursorToXmlString(nRecs, cSchema, cNs, nFlagsInt) + finally + RuntimeState.CurrentWorkarea := nOldArea + end try + + if _AND(nFlagsInt, C2X_FLAG_TOFILE) != 0 + File.WriteAllText(cTarget, cXml, UTF8Encoding{FALSE}) + return (long) FileInfo{cTarget}:Length + endif + + // no file flag: cOutput is the name of a memory variable, create when missing + XSharp.MemVar.Put(cTarget, cXml) + RETURN Encoding.UTF8:GetByteCount(cXml) + +/// Builds the XML for the cursor in the current work area. +INTERNAL FUNCTION __FoxCursorToXmlString(nRecs as long, cSchema as string, cNs as string, nFlags as long) as string + local nMode as XmlWriteMode + + var oData := __FoxCursorDataTable(nRecs) + + // VFP always names the root element VFPData, whatever the output format is + var oSet := DataSet{"VFPData"} + if !String.IsNullOrEmpty(cNs) + oSet:Namespace := cNs + endif + oSet:Tables:Add(oData) + + // cSchemaName: "" = no schema, "1" = inline schema, anything else = external .xsd + nMode := XmlWriteMode.IgnoreSchema + if cSchema == "1" + nMode := XmlWriteMode.WriteSchema + elseif !String.IsNullOrEmpty(cSchema) + var cSchemaFile := cSchema + if String.IsNullOrEmpty(Path.GetExtension(cSchemaFile)) + cSchemaFile += ".xsd" + endif + oSet:WriteXmlSchema(cSchemaFile) + endif + + var oSettings := XmlWriterSettings{} + oSettings:Indent := _AND(nFlags, C2X_FLAG_CONTINUOUS) == 0 + oSettings:Encoding := UTF8Encoding{FALSE} + + var oStream := MemoryStream{} + var oWriter := XmlWriter.Create(oStream, oSettings) + oSet:WriteXml(oWriter, nMode) + oWriter:Flush() + oWriter:Close() + + return Encoding.UTF8:GetString(oStream:ToArray()) + + +/// Reads the cursor in the current work area into a table of preformatted values. +INTERNAL FUNCTION __FoxCursorDataTable(nRecs as long) as DataTable + local oResult := NULL as object + + if !CoreDb.Info(DBI_RDD_OBJECT, REF oResult) + throw Error.VoDbError(EG_NOTABLE, EDB_NOTABLE, __FUNCTION__, "uArea", 1, {}) + endif + var oRDD := (IRdd) oResult + var nFields := oRDD:FieldCount + var oTable := DataTable{oRDD:Alias:ToLower()} + + // Every column is written as text: that is the only way to reproduce the + // field width and decimal scale that VFP puts in the XML. + local aTypes as string[] + local aDecs as long[] + aTypes := string[]{nFields} + aDecs := long[]{nFields} + for var nI := 1 to nFields + var cName := ((string) oRDD:FieldInfo(nI, DBS_ALIAS, NULL)):ToLower() + aTypes[nI] := ((string) oRDD:FieldInfo(nI, DBS_TYPE, NULL)):ToUpper() + aDecs[nI] := (long) oRDD:FieldInfo(nI, DBS_DEC, NULL) + oTable:Columns:Add(cName, typeof(string)) + next + + // The record order and any active filter are honoured because we simply + // walk the work area from top to bottom. + var nWritten := 0 + oRDD:GoTop() + do while !oRDD:EoF + local oRow as object[] + oRow := object[]{nFields} + for var nI := 1 to nFields + oRow[nI] := __FoxXmlValue(oRDD:GetValue(nI), aTypes[nI], aDecs[nI]) + next + oTable:Rows:Add(oRow) + nWritten++ + // VFP does not restore the record pointer: it ends up at EOF when every + // record was written, or on the last record written when nRecords limits it + if nRecs > 0 .AND. nWritten >= nRecs + exit + endif + oRDD:Skip(1) + enddo + oTable:AcceptChanges() + return oTable + + +/// Formats one field value the way the VFP CursorToXml() output does. +INTERNAL FUNCTION __FoxXmlValue(oValue as object, cType as string, nDec as long) as object + local r8 as real8 + + // VFP leaves the element out altogether for a NULL field + if oValue == NULL .OR. oValue == DBNull.Value + return DBNull.Value + endif + + switch cType + case "L" + return iif((logic) oValue, "true", "false") + + case "D" + if oValue IS XSharp.IDate VAR dVal + // an empty date becomes an empty element, not a missing one + return iif(dVal:Month == 0, "", dVal:Value:ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)) + endif + return oValue:ToString() + + case "T" + local dtVal as DateTime + if oValue IS XSharp.IDate VAR dtSrc + dtVal := dtSrc:Value + else + dtVal := Convert.ToDateTime(oValue, CultureInfo.InvariantCulture) + endif + // "s" gives the ISO layout VFP uses: 2024-01-15T10:30:45 + return iif(dtVal:Year <= 1, "", dtVal:ToString("s", CultureInfo.InvariantCulture)) + + case "Y" + // VFP always writes currency with 4 decimals + r8 := __FoxXmlNumber(oValue) + return r8:ToString("F4", CultureInfo.InvariantCulture) + + case "I" + r8 := __FoxXmlNumber(oValue) + return r8:ToString("F0", CultureInfo.InvariantCulture) + + case "N" + case "F" + r8 := __FoxXmlNumber(oValue) + return r8:ToString("F" + nDec:ToString(), CultureInfo.InvariantCulture) + + case "B" + // The RDD does not report the scale of Double fields, so I cannot + // reproduce the fixed decimals VFP writes; emit the full value instead + // of truncating it. + r8 := __FoxXmlNumber(oValue) + if nDec > 0 + return r8:ToString("F" + nDec:ToString(), CultureInfo.InvariantCulture) + endif + return r8:ToString("R", CultureInfo.InvariantCulture) + end switch + + // C, M, V and anything else: VFP strips the padding blanks + return oValue:ToString():TrimEnd() + + +/// Unwraps the numeric types the RDD layer hands out. +INTERNAL FUNCTION __FoxXmlNumber(oValue as object) as real8 + if oValue IS XSharp.IFloat VAR fVal + return fVal:Value + endif + return Convert.ToDouble(oValue, CultureInfo.InvariantCulture) diff --git a/src/Runtime/XSharp.VFP/ToDo-C.prg b/src/Runtime/XSharp.VFP/ToDo-C.prg index 836fe5c500..45b2852d4d 100644 --- a/src/Runtime/XSharp.VFP/ToDo-C.prg +++ b/src/Runtime/XSharp.VFP/ToDo-C.prg @@ -44,13 +44,6 @@ FUNCTION CreateOffline (ViewName , cPath) THROW NotImplementedException{} // RETURN FALSE -/// -- todo -- -/// -[FoxProFunction("CURSORTOXML", FoxFunctionCategory.General, FoxEngine.RuntimeCore, FoxFunctionStatus.Stub, FoxCriticality.Medium)]; -FUNCTION CursorToXML (uArea, cOutput, nOutputFormat, nFlags, nRecords, cSchemaName, cSchemaLocation, cNameSpace ) - THROW NotImplementedException{} - // RETURN 0 - /// -- todo -- /// [FoxProFunction("CURVAL", FoxFunctionCategory.CursorAndTable, FoxEngine.WorkArea, FoxFunctionStatus.Stub, FoxCriticality.High)]; diff --git a/src/Runtime/XSharp.VFP/XSharp.VFP.xsproj b/src/Runtime/XSharp.VFP/XSharp.VFP.xsproj index 858549c2da..d48b4dfb3e 100644 --- a/src/Runtime/XSharp.VFP/XSharp.VFP.xsproj +++ b/src/Runtime/XSharp.VFP/XSharp.VFP.xsproj @@ -39,6 +39,11 @@ System.Data.dll False + + System.Xml + System.Xml.dll + False + @@ -57,6 +62,7 @@ + diff --git a/src/Runtime/XSharp.VFP/functionsToImplement.txt b/src/Runtime/XSharp.VFP/functionsToImplement.txt index c33b75383a..f2c86eb75e 100644 --- a/src/Runtime/XSharp.VFP/functionsToImplement.txt +++ b/src/Runtime/XSharp.VFP/functionsToImplement.txt @@ -57,7 +57,6 @@ CPCONVERT( ) CREATEOFFLINE( ) CURSORGETPROP( ) CURSORSETPROP( ) -CURSORTOXML( ) CURVAL( ) DROPOFFLINE( ) FLDLIST( ) diff --git a/src/Tools/VfpTools/VfpCompatMetrics/Config/vfp_universe.json b/src/Tools/VfpTools/VfpCompatMetrics/Config/vfp_universe.json index e06b088647..040d705c09 100644 --- a/src/Tools/VfpTools/VfpCompatMetrics/Config/vfp_universe.json +++ b/src/Tools/VfpTools/VfpCompatMetrics/Config/vfp_universe.json @@ -1433,6 +1433,13 @@ "Engine": "Macro", "Criticality": "High" }, + { + "Name": "FATTRIB", + "Category": "FileAndIO", + "Engine": "RuntimeCore", + "Criticality": "Medium", + "IsExtension": true + }, { "Name": "FCHSIZE", "Category": "FileAndIO", @@ -1535,6 +1542,13 @@ "Engine": "WorkArea", "Criticality": "High" }, + { + "Name": "FNAME", + "Category": "FileAndIO", + "Engine": "RuntimeCore", + "Criticality": "Medium", + "IsExtension": true + }, { "Name": "FONTMETRIC", "Category": "UIAndWindow", diff --git a/src/Tools/VfpTools/VfpCompatMetrics/Program.prg b/src/Tools/VfpTools/VfpCompatMetrics/Program.prg index 1e82df6bcb..17a7445188 100644 --- a/src/Tools/VfpTools/VfpCompatMetrics/Program.prg +++ b/src/Tools/VfpTools/VfpCompatMetrics/Program.prg @@ -161,7 +161,7 @@ BEGIN NAMESPACE VfpCompatMetrics // 3. Warnings and Debt - IF item:Status == "Full" && !String.IsNullOrWhiteSpace(item:Notes) + IF item:Status == "Full" && !String.IsNullOrWhiteSpace(item:Notes) && item:Notes != "X# Extension" issues:Add(ValidationIssue{}{ Level := "WARNING", FunctionName := item:Name, Message := "State is 'Full' but contains limitation notes." }) ENDIF