diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/HasDiagnostic/InvocationWithInitPrimaryKeyFieldsIsTrue.al b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/HasDiagnostic/InvocationWithInitPrimaryKeyFieldsIsTrue.al new file mode 100644 index 0000000..9f7d71f --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/HasDiagnostic/InvocationWithInitPrimaryKeyFieldsIsTrue.al @@ -0,0 +1,28 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + FromRec: Record MyTableA; + ToRec: Record MyTableB; + begin + [|ToRec.TransferFields(FromRec, true)|]; + end; +} + +table 50100 MyTableA +{ + fields + { + [|field(1; "Primary Key"; Code[20]) { }|] + field(2; MyField; Integer) { } + } +} + +table 50101 MyTableB +{ + fields + { + [|field(1; "Other Primary Key"; Code[20]) { }|] // Same ID (1) as in MyTableA, different name + field(2; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/NoDiagnostic/InvocationWithInitPrimaryKeyFieldsIsFalse.al b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/NoDiagnostic/InvocationWithInitPrimaryKeyFieldsIsFalse.al new file mode 100644 index 0000000..57804af --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/NoDiagnostic/InvocationWithInitPrimaryKeyFieldsIsFalse.al @@ -0,0 +1,28 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + FromRec: Record MyTableA; + ToRec: Record MyTableB; + begin + [|ToRec.TransferFields(FromRec, false)|]; + end; +} + +table 50100 MyTableA +{ + fields + { + [|field(1; "Primary Key"; Code[20]) { }|] + field(2; MyField; Integer) { } + } +} + +table 50101 MyTableB +{ + fields + { + [|field(1; "Other Primary Key"; Code[20]) { }|] // Same ID (1) as in MyTableA, different name + field(2; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/TransferFieldsNameMismatch.cs b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/TransferFieldsNameMismatch.cs index 7a5b785..5bdf3da 100644 --- a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/TransferFieldsNameMismatch.cs +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/TransferFieldsNameMismatch.cs @@ -24,6 +24,7 @@ public void Setup() [TestCase("InvocationRecWithTable")] [TestCase("InvocationRecWithTablexRec")] [TestCase("InvocationSkipFieldsNotMatchingType")] + [TestCase("InvocationWithInitPrimaryKeyFieldsIsTrue")] [TestCase("InvocationWithReturnValue")] [TestCase("InvocationWithVarGlobals")] [TestCase("InvocationWithVarLocalAndGlobal")] @@ -49,6 +50,7 @@ public async Task HasDiagnostic(string testCase) [TestCase("BuiltInInvocation")] [TestCase("Invocation_Pragma")] [TestCase("InvocationSkipFieldsNotMatchingType")] + [TestCase("InvocationWithInitPrimaryKeyFieldsIsFalse")] [TestCase("TableExt_Paired_Extension_Pragma")] [TestCase("TableExt_Paired_SingleTableExt")] [TestCase("TableExt_Unpaired")] diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/HasDiagnostic/InvocationWithInitPrimaryKeyFieldsIsTrue.al b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/HasDiagnostic/InvocationWithInitPrimaryKeyFieldsIsTrue.al new file mode 100644 index 0000000..aed78b7 --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/HasDiagnostic/InvocationWithInitPrimaryKeyFieldsIsTrue.al @@ -0,0 +1,28 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + FromRec: Record MyTableA; + ToRec: Record MyTableB; + begin + [|ToRec.TransferFields(FromRec, true)|]; + end; +} + +table 50100 MyTableA +{ + fields + { + [|field(1; "Primary Key"; Code[20]) { }|] + field(2; MyField; Integer) { } + } +} + +table 50101 MyTableB +{ + fields + { + [|field(1; "Primary Key"; Integer) { }|] // Same ID (1) as in MyTableA, different type + field(2; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/NoDiagnostic/InvocationCodeToText.al b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/NoDiagnostic/InvocationCodeToText.al new file mode 100644 index 0000000..5c1689c --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/NoDiagnostic/InvocationCodeToText.al @@ -0,0 +1,28 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + FromRec: Record MyTableA; + ToRec: Record MyTableB; + begin + [|ToRec.TransferFields(FromRec)|]; + end; +} + +table 50100 MyTableA +{ + fields + { + field(1; "Primary Key"; Code[20]) { } + [|field(2; MyField; Code[20]) { }|] + } +} + +table 50101 MyTableB +{ + fields + { + field(1; "Primary Key"; Code[20]) { } + [|field(2; MyField; Text[20]) { }|] // Same ID (2) as in MyTableA, where a Code can safely convert into a Text + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/NoDiagnostic/InvocationWithInitPrimaryKeyFieldsIsFalse.al b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/NoDiagnostic/InvocationWithInitPrimaryKeyFieldsIsFalse.al new file mode 100644 index 0000000..40d218e --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/NoDiagnostic/InvocationWithInitPrimaryKeyFieldsIsFalse.al @@ -0,0 +1,28 @@ +codeunit 50100 MyCodeunit +{ + procedure MyProcedure() + var + FromRec: Record MyTableA; + ToRec: Record MyTableB; + begin + [|ToRec.TransferFields(FromRec, false)|]; + end; +} + +table 50100 MyTableA +{ + fields + { + [|field(1; "Primary Key"; Code[20]) { }|] + field(2; MyField; Integer) { } + } +} + +table 50101 MyTableB +{ + fields + { + [|field(1; "Primary Key"; Integer) { }|] // Same ID (1) as in MyTableA, different type + field(2; MyField; Integer) { } + } +} \ No newline at end of file diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/TransferFieldsTypeMismatch.cs b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/TransferFieldsTypeMismatch.cs index 7114250..bd7b580 100644 --- a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/TransferFieldsTypeMismatch.cs +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsTypeMismatch/TransferFieldsTypeMismatch.cs @@ -24,6 +24,7 @@ public void Setup() [TestCase("InvocationRecWithTable")] [TestCase("InvocationRecWithTablexRec")] [TestCase("InvocationSkipFieldsNotMatchingType")] + [TestCase("InvocationWithInitPrimaryKeyFieldsIsTrue")] [TestCase("InvocationWithReturnValue")] [TestCase("InvocationWithVarGlobals")] [TestCase("InvocationWithVarLocalAndGlobal")] @@ -50,7 +51,9 @@ public async Task HasDiagnostic(string testCase) [Test] [TestCase("BuiltInInvocation")] [TestCase("Invocation_Pragma")] + [TestCase("InvocationCodeToText")] [TestCase("InvocationSkipFieldsNotMatchingType")] + [TestCase("InvocationWithInitPrimaryKeyFieldsIsFalse")] [TestCase("InvocationWithType")] [TestCase("InvocationWithTypeLength")] [TestCase("TableExt_Paired_Extension_Pragma")] diff --git a/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs b/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs index 71d596d..1c98340 100644 --- a/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs +++ b/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs @@ -859,7 +859,7 @@ internal static string TransferFieldsTypeMismatchDescription { } /// - /// Looks up a localized string similar to Field with ID {0} has incompatible field types ({3} → {4}) between TransferFields-coupled tables {1} and {2}.. + /// Looks up a localized string similar to Fields {5} and {6} with ID {0} have a possible incompatible field type ({3} → {4}) between TransferFields-coupled tables {1} and {2}.. /// internal static string TransferFieldsTypeMismatchMessageFormat { get { diff --git a/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx b/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx index 59823b4..70c12ff 100644 --- a/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx +++ b/src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx @@ -385,7 +385,7 @@ Incompatible field types across TransferFields - Field with ID {0} has incompatible field types ({3} → {4}) between TransferFields-coupled tables {1} and {2}. + Fields {5} and {6} with ID {0} have a possible incompatible field type ({3} → {4}) between TransferFields-coupled tables {1} and {2}. Tables coupled via TransferFields must define matching field types for the same field ID. A type mismatch will result in a runtime error when TransferFields is executed, as incompatible field types cannot be safely copied. diff --git a/src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs b/src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs index 0ec9db1..02f4337 100644 --- a/src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs +++ b/src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs @@ -101,7 +101,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext ctx) var tableExtensions = GetCachedTableExtensions(ctx.Compilation); var sourceFields = BuildEffectiveFields(sourceTable, tableExtensions); - var targetFields = BuildEffectiveFields(targetTable, tableExtensions); + var targetFields = BuildEffectiveFields(targetTable, tableExtensions, IsInitPrimaryKeyFieldsEnabled(invocation)); if (sourceFields.IsEmpty || targetFields.IsEmpty) return; @@ -196,7 +196,9 @@ private static void AnalyzeInvocation(OperationAnalysisContext ctx) sourceDisplay, targetDisplay, GetToDisplayStringSafe(sourceById[minTypeMismatchId]), - GetToDisplayStringSafe(targetById[minTypeMismatchId]))); + GetToDisplayStringSafe(targetById[minTypeMismatchId]), + sourceById[minTypeMismatchId].Name.QuoteIdentifierIfNeededWithReflection(), + targetById[minTypeMismatchId].Name.QuoteIdentifierIfNeededWithReflection())); } } @@ -289,6 +291,15 @@ private static bool IsFieldSuppressed(string diagnosticId, IFieldSymbol field) return false; } + private static bool IsInitPrimaryKeyFieldsEnabled(IInvocationExpression invocation) + { + if (invocation.Arguments.Length < 2) + return true; // Default is true + + var constant = invocation.Arguments[1].Value.ConstantValue; + return constant.HasValue && constant.Value is true; + } + private static bool IsSkipFieldsNotMatchingTypeEnabled(IInvocationExpression invocation) { if (invocation.Arguments.Length < 3) @@ -313,39 +324,51 @@ private static bool IsSkipFieldsNotMatchingTypeEnabled(IInvocationExpression inv private static ImmutableArray BuildEffectiveFields( ITableTypeSymbol table, - ImmutableArray allTableExtensions) + ImmutableArray allTableExtensions, + bool includePrimaryKeyFields = true) { - var baseFields = table.Fields; + var pkFields = + !includePrimaryKeyFields && table.PrimaryKey is not null + ? table.PrimaryKey.Fields + : default; + + var baseBuilder = ImmutableArray.CreateBuilder(table.Fields.Length); + + foreach (var field in table.Fields) + { + var id = field.Id; + + if (id == 0 || id >= 2_000_000_000) + continue; + + if (!pkFields.IsDefaultOrEmpty && + pkFields.Any(pk => pk.Id == id)) + continue; + + baseBuilder.Add(field); + } var extensionFields = allTableExtensions .Where(ext => SameApplicationObject(ext.Target, table)) - .SelectMany(ext => ext.AddedFields) - .ToImmutableArray(); - - if (extensionFields.IsEmpty) - return baseFields; + .SelectMany(ext => ext.AddedFields); - return baseFields.AddRange(extensionFields); + return baseBuilder.ToImmutable(); } private static Dictionary BuildFieldMapById(IEnumerable fields) { - var map = new Dictionary(); + var map = fields is ICollection collection + ? new Dictionary(collection.Count) + : new Dictionary(); foreach (var field in fields) { - if (field is ISymbolWithId withId) - { - var id = (int)withId.Id; - if (id >= 2000000000) - continue; - - if (field.FieldClass != EnumProvider.FieldClassKind.Normal) - continue; + if (field.FieldClass != EnumProvider.FieldClassKind.Normal) + continue; - map[id] = field; - } + var id = field.Id; + map[id] = field; } return map; @@ -364,6 +387,16 @@ private static bool AreFieldTypesEquivalent(IFieldSymbol source, IFieldSymbol ta if (sourceType is null || targetType is null) return false; + var sourceKind = sourceType.GetNavTypeKindSafe(); + var targetKind = targetType.GetNavTypeKindSafe(); + + // Explicitly allow Enum → Integer assignments + if (sourceKind == EnumProvider.NavTypeKind.Enum && + targetKind == EnumProvider.NavTypeKind.Integer) + { + return true; + } + if (sourceType is IApplicationObjectTypeSymbol && targetType is IApplicationObjectTypeSymbol) { @@ -372,9 +405,6 @@ private static bool AreFieldTypesEquivalent(IFieldSymbol source, IFieldSymbol ta targetType.OriginalDefinition); } - var sourceKind = sourceType.GetNavTypeKindSafe(); - var targetKind = targetType.GetNavTypeKindSafe(); - if (IsNumeric(sourceKind) && IsNumeric(targetKind)) { return IsNumericAssignmentSafe(sourceKind, targetKind); @@ -386,7 +416,14 @@ private static bool AreFieldTypesEquivalent(IFieldSymbol source, IFieldSymbol ta return false; } - return sourceKind == targetKind; + // Explicitly allow Code → Text assignments + if (sourceKind == EnumProvider.NavTypeKind.Code && + targetKind == EnumProvider.NavTypeKind.Text) + { + return true; + } + + return sourceKind.Equals(targetKind); } private static bool IsNumeric(NavTypeKind kind) @@ -457,7 +494,9 @@ private static void ReportField( sourceDisplay, targetDisplay, GetToDisplayStringSafe(sourceField), - GetToDisplayStringSafe(targetField))); + GetToDisplayStringSafe(targetField), + sourceField.Name.QuoteIdentifierIfNeededWithReflection(), + targetField.Name.QuoteIdentifierIfNeededWithReflection())); return; } } @@ -504,7 +543,9 @@ private static void ReportField( sourceDisplay, targetDisplay, GetToDisplayStringSafe(sourceField), - GetToDisplayStringSafe(targetField))); + GetToDisplayStringSafe(targetField), + sourceField.Name.QuoteIdentifierIfNeededWithReflection(), + targetField.Name.QuoteIdentifierIfNeededWithReflection())); return; } } diff --git a/src/ALCops.PlatformCop/DiagnosticDescriptors.cs b/src/ALCops.PlatformCop/DiagnosticDescriptors.cs index 281cfae..cad4fb0 100644 --- a/src/ALCops.PlatformCop/DiagnosticDescriptors.cs +++ b/src/ALCops.PlatformCop/DiagnosticDescriptors.cs @@ -260,7 +260,7 @@ public static class DiagnosticDescriptors title: PlatformCopAnalyzers.TransferFieldsTypeMismatchTitle, messageFormat: PlatformCopAnalyzers.TransferFieldsTypeMismatchMessageFormat, category: Category.Design, - defaultSeverity: DiagnosticSeverity.Error, + defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: PlatformCopAnalyzers.TransferFieldsTypeMismatchDescription, helpLinkUri: GetHelpUri(DiagnosticIds.TransferFieldsTypeMismatch));