diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectId.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectId.cs index fa08ca56e7319..4b1f14104ae9c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectId.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectId.cs @@ -22,7 +22,7 @@ namespace Microsoft.CodeAnalysis; #pragma warning restore CA1200 // Avoid using cref tags with a prefix [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] [DataContract] -public sealed class ProjectId : IEquatable +public sealed class ProjectId : IEquatable, IComparable { /// /// Checksum of this ProjectId, built only from . @@ -37,8 +37,8 @@ public sealed class ProjectId : IEquatable /// /// An optional name to show only for debugger-display purposes. This must not be used for any other - /// purpose. Importantly, it must not be part of the equality/hashing contract of this type (including ). + /// purpose. Importantly, it must not be part of the equality/hashing/comparable contract of this type (including + /// ). /// [DataMember(Order = 1)] private readonly string? _debugName; @@ -114,4 +114,12 @@ internal Checksum Checksum writer.WriteString(nameof(ProjectId)); writer.WriteGuid(@this.Id); }), this); + + int IComparable.CompareTo(ProjectId? other) + { + if (other is null) + return 1; + + return this.Id.CompareTo(other.Id); + } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratorExecutionVersion.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratorExecutionVersion.cs index 765e3cae91379..8b87aa885a8c3 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratorExecutionVersion.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratorExecutionVersion.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Collections; @@ -48,14 +49,14 @@ public static SourceGeneratorExecutionVersion ReadFrom(ObjectReader reader) /// Helper construct to allow a mapping from s to . /// Limited to just the surface area the workspace needs. /// -internal sealed class SourceGeneratorExecutionVersionMap(ImmutableSegmentedDictionary map) +internal sealed class SourceGeneratorExecutionVersionMap(ImmutableSortedDictionary map) { public static readonly SourceGeneratorExecutionVersionMap Empty = new(); - public ImmutableSegmentedDictionary Map { get; } = map; + public ImmutableSortedDictionary Map { get; } = map; public SourceGeneratorExecutionVersionMap() - : this(ImmutableSegmentedDictionary.Empty) + : this(ImmutableSortedDictionary.Empty) { } @@ -75,6 +76,8 @@ public override bool Equals([NotNullWhen(true)] object? obj) public void WriteTo(ObjectWriter writer) { + // Writing out the dictionary in order is fine. That's because it's a sorted dictionary, and ProjectIds are + // naturally comparable. writer.WriteInt32(Map.Count); foreach (var (projectId, version) in Map) { @@ -86,7 +89,7 @@ public void WriteTo(ObjectWriter writer) public static SourceGeneratorExecutionVersionMap Deserialize(ObjectReader reader) { var count = reader.ReadInt32(); - var builder = ImmutableSegmentedDictionary.CreateBuilder(); + var builder = ImmutableSortedDictionary.CreateBuilder(); for (var i = 0; i < count; i++) { var projectId = ProjectId.ReadFrom(reader); diff --git a/src/Workspaces/Core/Portable/Workspace/Workspace_SourceGeneration.cs b/src/Workspaces/Core/Portable/Workspace/Workspace_SourceGeneration.cs index 2d7ea17c29427..9070ead22dcdb 100644 --- a/src/Workspaces/Core/Portable/Workspace/Workspace_SourceGeneration.cs +++ b/src/Workspaces/Core/Portable/Workspace/Workspace_SourceGeneration.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Frozen; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis; @@ -69,7 +68,7 @@ static SourceGeneratorExecutionVersionMap GetUpdatedSourceGeneratorVersions( // projects that transitively depend on that project, so that their generators will run as well when next // asked. var dependencyGraph = solution.GetProjectDependencyGraph(); - var result = ImmutableSegmentedDictionary.CreateBuilder(); + var result = ImmutableSortedDictionary.CreateBuilder(); // Determine if we want a major solution change, forcing regeneration of all projects. var solutionMajor = projectIds.Any(t => t.projectId is null && t.forceRegeneration); diff --git a/src/Workspaces/CoreTest/SolutionTests/SourceGeneratorExecutionVersionMapTests.cs b/src/Workspaces/CoreTest/SolutionTests/SourceGeneratorExecutionVersionMapTests.cs new file mode 100644 index 0000000000000..00f06cf10e63f --- /dev/null +++ b/src/Workspaces/CoreTest/SolutionTests/SourceGeneratorExecutionVersionMapTests.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Roslyn.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests; + +public sealed class SourceGeneratorExecutionVersionMapTests +{ + [Fact] + public void TestOrderingDoesNotMatter() + { + var projectId1 = ProjectId.CreateNewId(); + var projectId2 = ProjectId.CreateNewId(); + Assert.NotEqual(projectId1, projectId2); + + var project1Kvp = new KeyValuePair(projectId1, new(MajorVersion: 1, MinorVersion: 1)); + var project2Kvp = new KeyValuePair(projectId2, new(MajorVersion: 2, MinorVersion: 2)); + + var map1 = new SourceGeneratorExecutionVersionMap(ImmutableSortedDictionary.CreateRange([project1Kvp, project2Kvp])); + var map2 = new SourceGeneratorExecutionVersionMap(ImmutableSortedDictionary.CreateRange([project2Kvp, project1Kvp])); + Assert.True(map1.Map.SequenceEqual(map2.Map)); + + using var memoryStream1 = SerializableBytes.CreateWritableStream(); + using var memoryStream2 = SerializableBytes.CreateWritableStream(); + { + using var writer1 = new ObjectWriter(memoryStream1, leaveOpen: true); + { + map1.WriteTo(writer1); + } + + using var writer2 = new ObjectWriter(memoryStream2, leaveOpen: true); + { + map2.WriteTo(writer2); + } + + memoryStream1.Position = 0; + memoryStream2.Position = 0; + + var array1 = memoryStream1.ToArray(); + var array2 = memoryStream2.ToArray(); + + Assert.Equal(array1.Length, array2.Length); + Assert.True(array1.Length > 0); + + Assert.True(array1.AsSpan().SequenceEqual(array2)); + + memoryStream1.Position = 0; + memoryStream2.Position = 0; + + var rehydrated1 = SourceGeneratorExecutionVersionMap.Deserialize(ObjectReader.GetReader(memoryStream1, leaveOpen: true)); + var rehydrated2 = SourceGeneratorExecutionVersionMap.Deserialize(ObjectReader.GetReader(memoryStream2, leaveOpen: true)); + + Assert.True(rehydrated1.Map.SequenceEqual(rehydrated2.Map)); + Assert.True(rehydrated1.Map.SequenceEqual(map1.Map)); + } + } +}