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));