diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ReferenceAssemblies.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ReferenceAssemblies.cs index c842bc515..198ee8609 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ReferenceAssemblies.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ReferenceAssemblies.cs @@ -460,14 +460,58 @@ await PackageExtractor.ExtractPackageAsync( } } - // Prefer assemblies from the reference assembly package to ones otherwise provided + // Prefer newer assemblies when more than one have the same name if (ReferenceAssemblyPackage is not null) { - var referenceAssemblies = new HashSet(resolvedAssemblies.Where(resolved => resolved.StartsWith(referenceAssemblyInstalledPath!))); + var comparer = new FrameworkPrecedenceSorter(DefaultFrameworkNameProvider.Instance, allEquivalent: false); + var assembliesByName = resolvedAssemblies.GroupBy(Path.GetFileNameWithoutExtension, StringComparer.OrdinalIgnoreCase); - // Suppression due to https://github.com/dotnet/roslyn/issues/44735 - var referenceAssemblyNames = new HashSet(referenceAssemblies.Select((Func)Path.GetFileNameWithoutExtension!)); - resolvedAssemblies.RemoveWhere(resolved => referenceAssemblyNames.Contains(Path.GetFileNameWithoutExtension(resolved)) && !referenceAssemblies.Contains(resolved)); + // Keep track of assemblies to remove from resolvedAssemblies. Defer the actual removal to the end + // of this block for ease in future debugging scenarios. + var assembliesToRemove = new List(); + foreach (var assemblyNameGroup in assembliesByName) + { + var assembliesByPrecedence = assemblyNameGroup + .Select(static name => (name, framework: GetFrameworkNameFromPath(name))) + .OrderBy(static x => x.framework, comparer) + .ThenByDescending(static x => x.framework, new NuGetFrameworkSorter()) + .ToArray(); + for (var i = 1; i < assembliesByPrecedence.Length; i++) + { + // We want to keep the last reference listed for the most recent supported target framework. + // Typically, if more than one item has the most recent supported target framework, it will + // be a case where the reference assembly package provides the assembly and a newer version + // is provided explicitly. For example: + // + // Microsoft.NETCore.App.Ref 6.0.0 provides System.Collections.Immutable in the net6.0 folder + // System.Collections.Immutable 8.0.0 provides System.Collections.Immutable in the net6.0 folder + // + // In this example, the Microsoft.NETCore.App.Ref package is resolved first, so by taking + // the last net6.0 assembly, we ensure the assembly from System.Collections.Immutable 8.0.0 + // is resolved. + if (Equals(assembliesByPrecedence[0].framework, assembliesByPrecedence[i].framework)) + { + assembliesToRemove.Add(assembliesByPrecedence[i - 1].name); + } + else + { + assembliesToRemove.Add(assembliesByPrecedence[i].name); + } + } + + static NuGetFramework GetFrameworkNameFromPath(string path) + { + var frameworkFolder = Path.GetFileName(Path.GetDirectoryName(path)); + if (frameworkFolder is null) + { + return NuGetFramework.UnsupportedFramework; + } + + return NuGetFramework.ParseFolder(frameworkFolder); + } + } + + resolvedAssemblies.ExceptWith(assembliesToRemove); } // Add the facade assemblies diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MetadataReferenceTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MetadataReferenceTests.cs index 90cbf8b74..4c2fc666f 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MetadataReferenceTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MetadataReferenceTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -649,6 +650,37 @@ class TestClass { }.RunAsync(); } + [Theory] + [InlineData("net462")] + [InlineData("net47")] + [InlineData("net471")] + [InlineData("net472")] + [InlineData("net48")] +#if !(NETCOREAPP1_1 || NET46) + [InlineData("net6.0")] + [InlineData("net7.0")] + [InlineData("net8.0")] +#endif + [InlineData("netstandard2.0")] + [InlineData("netstandard2.1")] + public async Task ResolveSystemCollectionsImmutable8(string targetFramework) + { + var testCode = @" +using System.Collections.Frozen; + +class TestClass { + FrozenSet TestMethod() => throw null; +} +"; + + await new CSharpTest() + { + TestCode = testCode, + ReferenceAssemblies = ReferenceAssembliesForTargetFramework(targetFramework) + .AddPackages(ImmutableArray.Create(new PackageIdentity("System.Collections.Immutable", "8.0.0"))), + }.RunAsync(); + } + internal static ReferenceAssemblies ReferenceAssembliesForTargetFramework(string targetFramework) { return targetFramework switch