diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index dd1e8e8aef8e..083df614a49b 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -47,21 +47,48 @@ public void Initialize(IncrementalGeneratorInitializationContext context) throw new InvalidOperationException("Location cannot be null"); } - var fileName = $"{location.FilePath}-GeneratedBindingInterceptors-{location.Line}-{location.Column}.g.cs"; - var sanitizedFileName = fileName.Replace('/', '-').Replace('\\', '-').Replace(':', '-'); + var stableLocationId = CreateStableLocationId(location); + var hintName = CreateHintName(stableLocationId, location); var methodNamePrefix = binding.MethodType switch { InterceptedMethodType.SetBinding => "SetBinding", InterceptedMethodType.Create => "Create", _ => throw new NotSupportedException() }; - var uniqueId = (uint)Math.Abs(location.GetHashCode()); - var code = BindingCodeWriter.GenerateBinding(binding, $"{methodNamePrefix}{uniqueId}"); - spc.AddSource(sanitizedFileName, code); + var code = BindingCodeWriter.GenerateBinding(binding, $"{methodNamePrefix}{stableLocationId}"); + spc.AddSource(hintName, code); }); } + private static string CreateHintName(string stableLocationId, SimpleLocation location) + { + return $"BindingSourceGen-{stableLocationId}-{location.Line}-{location.Column}.g.cs"; + } + + private static string CreateStableLocationId(SimpleLocation location) + { + return ComputeStableHash($"{location.FilePath}|{location.Line}|{location.Column}"); + } + + private static string ComputeStableHash(string text) + { + const ulong offsetBasis = 14695981039346656037; + const ulong prime = 1099511628211; + + unchecked + { + var hash = offsetBasis; + foreach (var character in text) + { + hash ^= character; + hash *= prime; + } + + return hash.ToString("x16"); + } + } + private static bool IsSetBindingMethod(SyntaxNode node) { return node is InvocationExpressionSyntax invocation diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs index e98ae253a325..354f9646d6aa 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs @@ -20,7 +20,7 @@ public void GenerateSimpleBinding() var result = SourceGenHelpers.Run(source); Assert.NotNull(result.Binding); - var id = Math.Abs(result.Binding.SimpleLocation!.GetHashCode()); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( @@ -111,7 +111,46 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-3-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); + } + + [Fact] + public void GeneratedHintNameDoesNotIncludeSourceFilePath() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + var filePath = @"E:\GitHub\Esri\arcgis-toolkit-dotnet\src\Toolkit\Toolkit.Maui\Toolkit\UI\Controls\OfflineMapAreas\OfflineMapAreasView.Maui.cs"; + + var result = SourceGenHelpers.Run(new Dictionary { { filePath, source } }); + var generatedFile = SourceGenHelpers.GetGeneratedBindingFile(result); + var stableLocationId = SourceGenHelpers.GetStableBindingId(result.Binding); + + AssertExtensions.AssertNoDiagnostics(result); + Assert.StartsWith(SourceGenHelpers.BindingGeneratedSourceHintNamePrefix, generatedFile.Key, StringComparison.Ordinal); + Assert.Contains(stableLocationId, generatedFile.Key, StringComparison.Ordinal); + Assert.EndsWith("-3-7.g.cs", generatedFile.Key, StringComparison.Ordinal); + Assert.DoesNotContain("OfflineMapAreasView", generatedFile.Key, StringComparison.Ordinal); + Assert.DoesNotContain("GitHub", generatedFile.Key, StringComparison.Ordinal); + Assert.True(generatedFile.Key.Length < 80); + } + + [Fact] + public void GeneratedMethodNameUsesStableLocationId() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var result = SourceGenHelpers.Run(source); + var stableLocationId = SourceGenHelpers.GetStableBindingId(result.Binding); + + AssertExtensions.AssertNoDiagnostics(result); + Assert.Contains($"public static void SetBinding{stableLocationId}(", SourceGenHelpers.GetGeneratedBindingSource(result), StringComparison.Ordinal); } [Fact] @@ -126,7 +165,7 @@ public void GenerateSimpleBindingCreate() AssertExtensions.AssertNoDiagnostics(result); Assert.NotNull(result.Binding); - var id = Math.Abs(result.Binding.SimpleLocation!.GetHashCode()); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.CodeIsEqual( $$""" @@ -213,7 +252,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-2-27.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -302,7 +341,8 @@ public class B """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -397,7 +437,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-6-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -427,7 +467,8 @@ public class B """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -525,7 +566,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-7-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } public static IEnumerable GenerateSimpleBindingWhenNullableDisabledAndPropertyNullableData => @@ -658,7 +699,8 @@ public class C public void GenerateSimpleBindingWhenNullableDisabledAndPropertyNullable(string source) { var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -753,7 +795,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-7-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -797,7 +839,8 @@ public class D { """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -893,7 +936,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-7-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Theory] @@ -936,7 +979,8 @@ public class MyPropertyClass """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( @@ -1040,7 +1084,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-4-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -1081,7 +1125,8 @@ public class MyPropertyClass """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( @@ -1181,7 +1226,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-4-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -1213,7 +1258,8 @@ public class Wrapper """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( @@ -1308,7 +1354,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-3-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -1344,7 +1390,8 @@ public class MyPropertyClass """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( @@ -1449,7 +1496,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-6-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -1479,7 +1526,8 @@ public void SetBinding() """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -1575,7 +1623,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-17-23.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -1605,7 +1653,8 @@ public void SetBinding() """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -1700,7 +1749,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-17-23.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -1730,7 +1779,8 @@ public void SetBinding() """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -1824,7 +1874,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-17-23.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -1854,7 +1904,8 @@ public void SetBinding() """; var result = SourceGenHelpers.Run(source); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -1949,7 +2000,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-17-23.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -1973,10 +2024,11 @@ public class MyClass """; var result = SourceGenHelpers.Run(source); - var actual = result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-5-7.g.cs"]; + var actual = SourceGenHelpers.GetGeneratedBindingSource(result); AssertExtensions.AssertNoDiagnostics(result); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.CodeIsEqual( $$""" @@ -2092,7 +2144,8 @@ public void CreateBinding() """; var result = SourceGenHelpers.Run(source, [new BindingSourceGenerator(), new IncrementalGeneratorSlider()]); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result.SourceGeneratorDiagnostics, "Source generator"); AssertExtensions.AssertNoDiagnostics(result.GeneratedCodeCompilationDiagnostics, "Generated code compilation"); @@ -2182,7 +2235,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-13-13.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] @@ -2209,7 +2262,8 @@ public void CreateBinding() """; var result = SourceGenHelpers.Run(source, [new BindingSourceGenerator(), new IncrementalGeneratorSlider()]); - var id = Math.Abs(result.Binding!.SimpleLocation!.GetHashCode()); + Assert.NotNull(result.Binding); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); AssertExtensions.AssertNoDiagnostics(result.SourceGeneratorDiagnostics, "Source generator"); AssertExtensions.AssertNoDiagnostics(result.GeneratedCodeCompilationDiagnostics, "Generated code compilation"); @@ -2301,7 +2355,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-14-11.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Theory] @@ -2337,10 +2391,9 @@ public void SetBinding() var result = SourceGenHelpers.Run(source); AssertExtensions.AssertNoDiagnostics(result); - - // Find the interceptor file (not the common helper file) - var generatedCode = result.GeneratedFiles.First(kvp => kvp.Key.Contains("Path-To-Program", StringComparison.Ordinal)).Value; - + + var generatedCode = SourceGenHelpers.GetGeneratedBindingSource(result); + if (shouldUseUnsafeAccessor) { // Verify that UnsafeAccessor is generated for the setter and is called correctly @@ -2377,7 +2430,7 @@ class Bar var result = SourceGenHelpers.Run(source); Assert.NotNull(result.Binding); - var id = Math.Abs(result.Binding.SimpleLocation!.GetHashCode()); + var id = SourceGenHelpers.GetStableBindingId(result.Binding); // The key assertion: no CS8603 errors should be present AssertExtensions.AssertNoDiagnostics(result); @@ -2468,7 +2521,7 @@ internal static partial class GeneratedBindingInterceptors } } """, - result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-3-7.g.cs"]); + SourceGenHelpers.GetGeneratedBindingSource(result)); } [Fact] diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs index 3535e04c1bfc..949989f7ab4a 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -16,6 +16,8 @@ internal record CodeGeneratorResult( internal static class SourceGenHelpers { + internal const string BindingGeneratedSourceHintNamePrefix = "BindingSourceGen-"; + private static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Preview).WithFeatures( [new KeyValuePair("InterceptorsNamespaces", "Microsoft.Maui.Controls.Generated")]); @@ -34,13 +36,58 @@ internal static CodeGeneratorResult Run(string source) return Run(source, new[] { new BindingSourceGenerator() }); } + internal static CodeGeneratorResult Run(Dictionary sources) + { + return Run(CreateCompilation(sources), new[] { new BindingSourceGenerator() }); + } + + internal static string GetGeneratedBindingSource(CodeGeneratorResult result) + { + return GetGeneratedBindingFile(result).Value; + } + + internal static KeyValuePair GetGeneratedBindingFile(CodeGeneratorResult result) + { + return result.GeneratedFiles.Single(source => source.Key.StartsWith(BindingGeneratedSourceHintNamePrefix, StringComparison.Ordinal)); + } + + internal static string GetStableBindingId(BindingInvocationDescription? binding) + { + Assert.NotNull(binding); + Assert.NotNull(binding.SimpleLocation); + + return ComputeStableHash($"{binding.SimpleLocation.FilePath}|{binding.SimpleLocation.Line}|{binding.SimpleLocation.Column}"); + } + + private static string ComputeStableHash(string text) + { + const ulong offsetBasis = 14695981039346656037; + const ulong prime = 1099511628211; + + unchecked + { + var hash = offsetBasis; + foreach (var character in text) + { + hash ^= character; + hash *= prime; + } + + return hash.ToString("x16"); + } + } + internal static CodeGeneratorResult Run(string source, IEnumerable generators) { // Function assumes the first generator in a list is BindingSourceGenerator Assert.NotEmpty(generators); Assert.IsType(generators.First()); - var inputCompilation = CreateCompilation(source); + return Run(CreateCompilation(source), generators); + } + + private static CodeGeneratorResult Run(Compilation inputCompilation, IEnumerable generators) + { var driver = CreateDriver(generators.Select(g => g.AsSourceGenerator())); var result = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out Compilation compilation, out _).GetRunResult().Results.FirstOrDefault();