From eaf4387273c903d770433c61f74839d76a254a50 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Fri, 26 Jun 2026 12:15:34 +0200 Subject: [PATCH] fix(PC0021): match TransferFields relations case-insensitively for namespaces AL namespaces and object identifiers are case-insensitive, but the runtime-resolved namespace casing (symbol.ContainingNamespace.QualifiedName) is not stable across compilations. The same logical namespace ships with different literal casing across Microsoft apps (e.g. Microsoft.EServices.EDocument vs Microsoft.eServices.EDocument). TransferFieldsRelations.Matches compared the stored namespace and object name with StringComparer.Ordinal, so a casing difference made the relation lookup fail. On the table-extension path (AnalyzeTableExtension -> TryFindBySource) this silently dropped the schema-compatibility diagnostic (false negative). - Matches: compare both namespace and Name with SemanticFacts.IsSameName (repo convention; internally OrdinalIgnoreCase), keeping the empty-namespace fallback. - TransferFieldsSchemaCompatibility: same fix for the table-extension name lookups. - Add regression test TableExt_NamespaceCasingMismatch proving the diagnostic fires regardless of namespace casing (fails before the fix, passes after). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TableExt_NamespaceCasingMismatch.al | 37 +++++++++++++++++++ .../TransferFieldsNameMismatch.cs | 3 +- .../Analyzers/TransferFieldsRelations.cs | 11 ++++-- .../TransferFieldsSchemaCompatibility.cs | 4 +- 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/HasDiagnostic/TableExt_NamespaceCasingMismatch.al diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/HasDiagnostic/TableExt_NamespaceCasingMismatch.al b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/HasDiagnostic/TableExt_NamespaceCasingMismatch.al new file mode 100644 index 00000000..1904243f --- /dev/null +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/HasDiagnostic/TableExt_NamespaceCasingMismatch.al @@ -0,0 +1,37 @@ +// The stored relation is Microsoft.Sales.History/"Sales Cr.Memo Header" -> +// Microsoft.EServices.EDocument/"E-Invoice Export Header". Here the namespace is declared +// with different literal casing ("microsoft.sales.history"). AL namespaces are +// case-insensitive, so the relation must still match and the diagnostic must still fire. +namespace microsoft.sales.history; + +table 50140 "Sales Cr.Memo Header" +{ + fields + { + field(1; "No."; Code[20]) { } + } +} + +table 50141 "E-Invoice Export Header" +{ + fields + { + field(1; "No."; Code[20]) { } + } +} + +tableextension 50142 MyCrMemoExt extends "Sales Cr.Memo Header" +{ + fields + { + [|field(50100; MyFieldA; Integer) { }|] + } +} + +tableextension 50143 MyEInvExt extends "E-Invoice Export Header" +{ + fields + { + [|field(50100; MyFieldB; Integer) { }|] // Same ID (50100) as in MyCrMemoExt, different name + } +} diff --git a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/TransferFieldsNameMismatch.cs b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/TransferFieldsNameMismatch.cs index cce6120f..f7088e4a 100644 --- a/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/TransferFieldsNameMismatch.cs +++ b/src/ALCops.PlatformCop.Test/Rules/TransferFieldsNameMismatch/TransferFieldsNameMismatch.cs @@ -33,10 +33,11 @@ public void Setup() [TestCase("InvocationWithTableExtension")] [TestCase("TableExt_Multiple_SameBase")] [TestCase("TableExtension")] + [TestCase("TableExt_NamespaceCasingMismatch")] public async Task HasDiagnostic(string testCase) { SkipTestIfVersionIsTooLow( - ["InvocationWithTableExtension", "TableExt_Multiple_SameBase", "TableExtension", "TableExtensionTypeWithLength"], + ["InvocationWithTableExtension", "TableExt_Multiple_SameBase", "TableExtension", "TableExtensionTypeWithLength", "TableExt_NamespaceCasingMismatch"], testCase, "13.0", "No support for tableextensions when target itself is already declared in the same module"); diff --git a/src/ALCops.PlatformCop/Analyzers/TransferFieldsRelations.cs b/src/ALCops.PlatformCop/Analyzers/TransferFieldsRelations.cs index 1553edf9..8eba2cdf 100644 --- a/src/ALCops.PlatformCop/Analyzers/TransferFieldsRelations.cs +++ b/src/ALCops.PlatformCop/Analyzers/TransferFieldsRelations.cs @@ -38,13 +38,16 @@ private static bool Matches(ObjectName configured, ITableTypeSymbol table) var ns = table.GetContainingNamespaceQualifiedNameWithReflection() ?? string.Empty; var name = table.Name ?? string.Empty; - if (StringComparer.Ordinal.Equals(configured.Name, name) && - StringComparer.Ordinal.Equals(configured.Namespace, ns)) + // AL namespaces and object identifiers are case-insensitive, but the runtime-resolved + // namespace casing (symbol.ContainingNamespace.QualifiedName) is not stable across + // compilations, so both comparisons must be case-insensitive. + if (SemanticFacts.IsSameName(configured.Name, name) && + SemanticFacts.IsSameName(configured.Namespace, ns)) return true; - // Keep backwards comptibility for objects without namespace + // Keep backwards compatibility for objects without namespace if (string.IsNullOrEmpty(ns) && - StringComparer.Ordinal.Equals(configured.Name, name)) + SemanticFacts.IsSameName(configured.Name, name)) return true; return false; diff --git a/src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs b/src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs index d4b9194f..dd599944 100644 --- a/src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs +++ b/src/ALCops.PlatformCop/Analyzers/TransferFieldsSchemaCompatibility.cs @@ -220,12 +220,12 @@ private static void AnalyzeTableExtensionForRelation(SymbolAnalysisContext ctx, var sourceTableExtensions = tableExtensions - .Where(te => te.Target is not null && te.Target.Name.Equals(relation.Source.Name)) + .Where(te => te.Target is not null && SemanticFacts.IsSameName(te.Target.Name, relation.Source.Name)) .SelectMany(x => x.AddedFields); var targetTableExtensions = tableExtensions - .Where(te => te.Target is not null && te.Target.Name.Equals(relation.Target.Name)) + .Where(te => te.Target is not null && SemanticFacts.IsSameName(te.Target.Name, relation.Target.Name)) .SelectMany(x => x.AddedFields); var sourceById = BuildFieldMapById(sourceTableExtensions);