Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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) { }
}
}
Original file line number Diff line number Diff line change
@@ -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) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public void Setup()
[TestCase("InvocationRecWithTable")]
[TestCase("InvocationRecWithTablexRec")]
[TestCase("InvocationSkipFieldsNotMatchingType")]
[TestCase("InvocationWithInitPrimaryKeyFieldsIsTrue")]
[TestCase("InvocationWithReturnValue")]
[TestCase("InvocationWithVarGlobals")]
[TestCase("InvocationWithVarLocalAndGlobal")]
Expand All @@ -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")]
Expand Down
Original file line number Diff line number Diff line change
@@ -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) { }
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public void Setup()
[TestCase("InvocationRecWithTable")]
[TestCase("InvocationRecWithTablexRec")]
[TestCase("InvocationSkipFieldsNotMatchingType")]
[TestCase("InvocationWithInitPrimaryKeyFieldsIsTrue")]
[TestCase("InvocationWithReturnValue")]
[TestCase("InvocationWithVarGlobals")]
[TestCase("InvocationWithVarLocalAndGlobal")]
Expand All @@ -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")]
Expand Down
2 changes: 1 addition & 1 deletion src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ internal static string TransferFieldsTypeMismatchDescription {
}

/// <summary>
/// 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}..
/// </summary>
internal static string TransferFieldsTypeMismatchMessageFormat {
get {
Expand Down
2 changes: 1 addition & 1 deletion src/ALCops.PlatformCop/ALCops.PlatformCopAnalyzers.resx
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@
<value>Incompatible field types across TransferFields</value>
</data>
<data name="TransferFieldsTypeMismatchMessageFormat" xml:space="preserve">
<value>Field with ID {0} has incompatible field types ({3} → {4}) between TransferFields-coupled tables {1} and {2}.</value>
<value>Fields {5} and {6} with ID {0} have a possible incompatible field type ({3} → {4}) between TransferFields-coupled tables {1} and {2}.</value>
</data>
<data name="TransferFieldsTypeMismatchDescription" xml:space="preserve">
<value>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.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax;
using Microsoft.Dynamics.Nav.CodeAnalysis.Text;
#if NETSTANDARD2_1
using Microsoft.Dynamics.Nav.CodeAnalysis.Symbols;

Check warning on line 11 in src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs

View workflow job for this annotation

GitHub Actions / build-and-test / Build

The using directive for 'Microsoft.Dynamics.Nav.CodeAnalysis.Symbols' appeared previously in this namespace

Check warning on line 11 in src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs

View workflow job for this annotation

GitHub Actions / build-and-test / Build

The using directive for 'Microsoft.Dynamics.Nav.CodeAnalysis.Symbols' appeared previously in this namespace
#endif
using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities;
using static ALCops.PlatformCop.Helpers.TransferFieldsRelations;
Expand Down Expand Up @@ -101,7 +101,7 @@

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;
Expand Down Expand Up @@ -196,7 +196,9 @@
sourceDisplay,
targetDisplay,
GetToDisplayStringSafe(sourceById[minTypeMismatchId]),
GetToDisplayStringSafe(targetById[minTypeMismatchId])));
GetToDisplayStringSafe(targetById[minTypeMismatchId]),
sourceById[minTypeMismatchId].Name.QuoteIdentifierIfNeededWithReflection(),
targetById[minTypeMismatchId].Name.QuoteIdentifierIfNeededWithReflection()));
}
}

Expand Down Expand Up @@ -289,6 +291,15 @@
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)
Expand All @@ -313,39 +324,51 @@

private static ImmutableArray<IFieldSymbol> BuildEffectiveFields(
ITableTypeSymbol table,
ImmutableArray<ITableExtensionTypeSymbol> allTableExtensions)
ImmutableArray<ITableExtensionTypeSymbol> allTableExtensions,
bool includePrimaryKeyFields = true)
{
var baseFields = table.Fields;
var pkFields =
!includePrimaryKeyFields && table.PrimaryKey is not null
? table.PrimaryKey.Fields
: default;

var baseBuilder = ImmutableArray.CreateBuilder<IFieldSymbol>(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<int, IFieldSymbol> BuildFieldMapById(IEnumerable<IFieldSymbol> fields)
{
var map = new Dictionary<int, IFieldSymbol>();
var map = fields is ICollection<IFieldSymbol> collection
? new Dictionary<int, IFieldSymbol>(collection.Count)
: new Dictionary<int, IFieldSymbol>();

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;
Expand All @@ -364,6 +387,16 @@
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)
{
Expand All @@ -372,9 +405,6 @@
targetType.OriginalDefinition);
}

var sourceKind = sourceType.GetNavTypeKindSafe();
var targetKind = targetType.GetNavTypeKindSafe();

if (IsNumeric(sourceKind) && IsNumeric(targetKind))
{
return IsNumericAssignmentSafe(sourceKind, targetKind);
Expand All @@ -386,7 +416,14 @@
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)
Expand Down Expand Up @@ -457,7 +494,9 @@
sourceDisplay,
targetDisplay,
GetToDisplayStringSafe(sourceField),
GetToDisplayStringSafe(targetField)));
GetToDisplayStringSafe(targetField),
sourceField.Name.QuoteIdentifierIfNeededWithReflection(),
targetField.Name.QuoteIdentifierIfNeededWithReflection()));
return;
}
}
Expand Down Expand Up @@ -504,7 +543,9 @@
sourceDisplay,
targetDisplay,
GetToDisplayStringSafe(sourceField),
GetToDisplayStringSafe(targetField)));
GetToDisplayStringSafe(targetField),
sourceField.Name.QuoteIdentifierIfNeededWithReflection(),
targetField.Name.QuoteIdentifierIfNeededWithReflection()));
return;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/ALCops.PlatformCop/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down