diff --git a/src/Compilers/Core/CodeAnalysisTest/DefaultAnalyzerAssemblyLoaderTests.cs b/src/Compilers/Core/CodeAnalysisTest/DefaultAnalyzerAssemblyLoaderTests.cs index b4aa5e6bb7814..723a496a22974 100644 --- a/src/Compilers/Core/CodeAnalysisTest/DefaultAnalyzerAssemblyLoaderTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/DefaultAnalyzerAssemblyLoaderTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; @@ -150,6 +151,44 @@ public void AssemblyLoading_DependencyLocationNotAdded() Assert.Equal(@"", actual); } + private static void VerifyAssemblies(IEnumerable assemblies, params (string simpleName, string version, string path)[] expected) + { + Assert.Equal(expected, assemblies.Select(assembly => (assembly.GetName().Name!, assembly.GetName().Version!.ToString(), assembly.Location)).Order()); + } + + [ConditionalFact(typeof(CoreClrOnly))] + public void AssemblyLoading_DependencyInDifferentDirectory() + { + StringBuilder sb = new StringBuilder(); + var loader = new DefaultAnalyzerAssemblyLoader(); + + var tempDir = Temp.CreateDirectory(); + + var deltaFile = tempDir.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta1.Path); + loader.AddDependencyLocation(deltaFile.Path); + loader.AddDependencyLocation(_testFixture.Gamma.Path); + Assembly gamma = loader.LoadFromPath(_testFixture.Gamma.Path); + + var b = gamma.CreateInstance("Gamma.G")!; + var writeMethod = b.GetType().GetMethod("Write")!; + writeMethod.Invoke(b, new object[] { sb, "Test G" }); + + var actual = sb.ToString(); + Assert.Equal(@"Delta: Gamma: Test G +", actual); + +#if NETCOREAPP + var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader); + Assert.Equal(1, alcs.Length); + + VerifyAssemblies( + alcs[0].Assemblies, + ("Delta", "1.0.0.0", deltaFile.Path), + ("Gamma", "0.0.0.0", _testFixture.Gamma.Path) + ); +#endif + } + [Fact] public void AssemblyLoading_MultipleVersions() { @@ -173,15 +212,16 @@ public void AssemblyLoading_MultipleVersions() var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader); Assert.Equal(2, alcs.Length); - Assert.Equal(new[] { + VerifyAssemblies( + alcs[0].Assemblies, ("Delta", "1.0.0.0", _testFixture.Delta1.Path), ("Gamma", "0.0.0.0", _testFixture.Gamma.Path) - }, alcs[0].Assemblies.Select(a => (a.GetName().Name!, a.GetName().Version!.ToString(), a.Location)).Order()); + ); - Assert.Equal(new[] { + VerifyAssemblies( + alcs[1].Assemblies, ("Delta", "2.0.0.0", _testFixture.Delta2.Path), - ("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path) - }, alcs[1].Assemblies.Select(a => (a.GetName().Name!, a.GetName().Version!.ToString(), a.Location)).Order()); + ("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path)); #endif var actual = sb.ToString(); @@ -203,6 +243,166 @@ public void AssemblyLoading_MultipleVersions() } } + [Fact] + public void AssemblyLoading_MultipleVersions_NoExactMatch() + { + StringBuilder sb = new StringBuilder(); + + var loader = new DefaultAnalyzerAssemblyLoader(); + loader.AddDependencyLocation(_testFixture.Delta1.Path); + loader.AddDependencyLocation(_testFixture.Epsilon.Path); + loader.AddDependencyLocation(_testFixture.Delta3.Path); + + Assembly epsilon = loader.LoadFromPath(_testFixture.Epsilon.Path); + var e = epsilon.CreateInstance("Epsilon.E")!; + e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); + +#if NETCOREAPP + var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader); + Assert.Equal(1, alcs.Length); + + VerifyAssemblies( + alcs[0].Assemblies, + ("Delta", "3.0.0.0", _testFixture.Delta3.Path), + ("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path)); +#endif + + var actual = sb.ToString(); + if (ExecutionConditionUtil.IsCoreClr) + { + Assert.Equal( +@"Delta.3: Epsilon: Test E +", + actual); + } + else + { + Assert.Equal( +@"Delta: Epsilon: Test E +", + actual); + } + } + + [Fact] + public void AssemblyLoading_MultipleVersions_MultipleEqualMatches() + { + StringBuilder sb = new StringBuilder(); + + // Delta2B and Delta2 have the same version, but we prefer Delta2 because it's in the same directory as Epsilon. + var loader = new DefaultAnalyzerAssemblyLoader(); + loader.AddDependencyLocation(_testFixture.Delta2B.Path); + loader.AddDependencyLocation(_testFixture.Delta2.Path); + loader.AddDependencyLocation(_testFixture.Epsilon.Path); + + Assembly epsilon = loader.LoadFromPath(_testFixture.Epsilon.Path); + var e = epsilon.CreateInstance("Epsilon.E")!; + e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); + +#if NETCOREAPP + var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader); + Assert.Equal(1, alcs.Length); + + VerifyAssemblies( + alcs[0].Assemblies, + ("Delta", "2.0.0.0", _testFixture.Delta2.Path), + ("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path)); +#endif + + var actual = sb.ToString(); + if (ExecutionConditionUtil.IsCoreClr) + { + Assert.Equal( +@"Delta.2: Epsilon: Test E +", + actual); + } + else + { + Assert.Equal( +@"Delta: Epsilon: Test E +", + actual); + } + } + + [Fact] + public void AssemblyLoading_MultipleVersions_ExactAndGreaterMatch() + { + StringBuilder sb = new StringBuilder(); + + var loader = new DefaultAnalyzerAssemblyLoader(); + loader.AddDependencyLocation(_testFixture.Delta2B.Path); + loader.AddDependencyLocation(_testFixture.Delta3.Path); + loader.AddDependencyLocation(_testFixture.Epsilon.Path); + + Assembly epsilon = loader.LoadFromPath(_testFixture.Epsilon.Path); + var e = epsilon.CreateInstance("Epsilon.E")!; + e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); + +#if NETCOREAPP + var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader); + Assert.Equal(1, alcs.Length); + + VerifyAssemblies( + alcs[0].Assemblies, + ("Delta", "2.0.0.0", _testFixture.Delta2B.Path), + ("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path)); +#endif + + var actual = sb.ToString(); + if (ExecutionConditionUtil.IsCoreClr) + { + Assert.Equal( +@"Delta.2B: Epsilon: Test E +", + actual); + } + else + { + Assert.Equal( +@"Delta: Epsilon: Test E +", + actual); + } + } + + [Fact] + public void AssemblyLoading_MultipleVersions_WorseMatchInSameDirectory() + { + StringBuilder sb = new StringBuilder(); + + var tempDir = Temp.CreateDirectory(); + var epsilonFile = tempDir.CreateFile("Epsilon.dll").CopyContentFrom(_testFixture.Epsilon.Path); + var delta1File = tempDir.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta1.Path); + + // Epsilon wants Delta2, but since Delta1 is in the same directory, we prefer Delta1 over Delta2. + var loader = new DefaultAnalyzerAssemblyLoader(); + loader.AddDependencyLocation(delta1File.Path); + loader.AddDependencyLocation(_testFixture.Delta2.Path); + loader.AddDependencyLocation(epsilonFile.Path); + + Assembly epsilon = loader.LoadFromPath(epsilonFile.Path); + var e = epsilon.CreateInstance("Epsilon.E")!; + e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); + +#if NETCOREAPP + var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader); + Assert.Equal(1, alcs.Length); + + VerifyAssemblies( + alcs[0].Assemblies, + ("Delta", "1.0.0.0", delta1File.Path), + ("Epsilon", "0.0.0.0", epsilonFile.Path)); +#endif + + var actual = sb.ToString(); + Assert.Equal( +@"Delta: Epsilon: Test E +", + actual); + } + [Fact] public void AssemblyLoading_MultipleVersions_MultipleLoaders() { @@ -228,18 +428,18 @@ public void AssemblyLoading_MultipleVersions_MultipleLoaders() var alcs1 = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader1); Assert.Equal(1, alcs1.Length); - Assert.Equal(new[] { + VerifyAssemblies( + alcs1[0].Assemblies, ("Delta", "1.0.0.0", _testFixture.Delta1.Path), - ("Gamma", "0.0.0.0", _testFixture.Gamma.Path) - }, alcs1[0].Assemblies.Select(a => (a.GetName().Name!, a.GetName().Version!.ToString(), a.Location)).Order()); + ("Gamma", "0.0.0.0", _testFixture.Gamma.Path)); var alcs2 = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader2); Assert.Equal(1, alcs2.Length); - Assert.Equal(new[] { + VerifyAssemblies( + alcs2[0].Assemblies, ("Delta", "2.0.0.0", _testFixture.Delta2.Path), - ("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path) - }, alcs2[0].Assemblies.Select(a => (a.GetName().Name!, a.GetName().Version!.ToString(), a.Location)).Order()); + ("Epsilon", "0.0.0.0", _testFixture.Epsilon.Path)); #endif var actual = sb.ToString(); @@ -280,19 +480,11 @@ public void AssemblyLoading_MultipleVersions_MissingVersion() var eWrite = e.GetType().GetMethod("Write")!; var actual = sb.ToString(); - if (ExecutionConditionUtil.IsCoreClr) - { - var exception = Assert.Throws(() => eWrite.Invoke(e, new object[] { sb, "Test E" })); - Assert.IsAssignableFrom(exception.InnerException); - } - else - { - eWrite.Invoke(e, new object[] { sb, "Test E" }); - Assert.Equal( + eWrite.Invoke(e, new object[] { sb, "Test E" }); + Assert.Equal( @"Delta: Gamma: Test G ", - actual); - } + actual); } [Fact] diff --git a/src/Compilers/Core/CodeAnalysisTest/ShadowCopyAnalyzerAssemblyLoaderTests.cs b/src/Compilers/Core/CodeAnalysisTest/ShadowCopyAnalyzerAssemblyLoaderTests.cs index 90b10ae46592a..02525eb8c552d 100644 --- a/src/Compilers/Core/CodeAnalysisTest/ShadowCopyAnalyzerAssemblyLoaderTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/ShadowCopyAnalyzerAssemblyLoaderTests.cs @@ -110,5 +110,37 @@ public void AssemblyLoading_Delete() ", actual); } + + [ConditionalFact(typeof(CoreClrOnly))] + public void AssemblyLoading_DependencyInDifferentDirectory_Delete() + { + StringBuilder sb = new StringBuilder(); + var loader = new ShadowCopyAnalyzerAssemblyLoader(); + + var tempDir1 = Temp.CreateDirectory(); + var tempDir2 = Temp.CreateDirectory(); + var tempDir3 = Temp.CreateDirectory(); + + var delta1File = tempDir1.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta1.Path); + var delta2File = tempDir2.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta2.Path); + var gammaFile = tempDir3.CreateFile("Gamma.dll").CopyContentFrom(_testFixture.Gamma.Path); + + loader.AddDependencyLocation(delta1File.Path); + loader.AddDependencyLocation(delta2File.Path); + loader.AddDependencyLocation(gammaFile.Path); + Assembly gamma = loader.LoadFromPath(gammaFile.Path); + + var b = gamma.CreateInstance("Gamma.G")!; + var writeMethod = b.GetType().GetMethod("Write")!; + writeMethod.Invoke(b, new object[] { sb, "Test G" }); + + File.Delete(delta1File.Path); + File.Delete(delta2File.Path); + File.Delete(gammaFile.Path); + + var actual = sb.ToString(); + Assert.Equal(@"Delta: Gamma: Test G +", actual); + } } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs index fd24d12239cb0..d3b117b4a8344 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs @@ -154,21 +154,10 @@ private AssemblyIdentity AddToCache(string fullPath, AssemblyIdentity identity) } #nullable enable - protected bool IsKnownDependencyLocation(string fullPath) + protected HashSet? GetPaths(string simpleName) { - CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath)); - var simpleName = PathUtilities.GetFileName(fullPath, includeExtension: false); - if (!_knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out var paths)) - { - return false; - } - - if (!paths.Contains(fullPath)) - { - return false; - } - - return true; + _knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out var paths); + return paths; } /// diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Core.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Core.cs index 16fcc1fcc718a..0b3a04e6b1c40 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Core.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Core.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -126,7 +127,8 @@ public DirectoryLoadContext(string directory, DefaultAnalyzerAssemblyLoader load } var assemblyPath = Path.Combine(Directory, simpleName + ".dll"); - if (!_loader.IsKnownDependencyLocation(assemblyPath)) + var paths = _loader.GetPaths(simpleName); + if (paths is null) { // The analyzer didn't explicitly register this dependency. Most likely the // assembly we're trying to load here is netstandard or a similar framework @@ -136,14 +138,46 @@ public DirectoryLoadContext(string directory, DefaultAnalyzerAssemblyLoader load return _compilerLoadContext.LoadFromAssemblyName(assemblyName); } - var pathToLoad = _loader.GetPathToLoad(assemblyPath); - return LoadFromAssemblyPath(pathToLoad); + Debug.Assert(paths.Any()); + // A matching assembly in this directory was specified via /analyzer. + if (paths.Contains(assemblyPath)) + { + return LoadFromAssemblyPath(_loader.GetPathToLoad(assemblyPath)); + } + + AssemblyName? bestCandidateName = null; + string? bestCandidatePath = null; + // The assembly isn't expected to be found at 'assemblyPath', + // but some assembly with the same simple name is known to the loader. + foreach (var candidatePath in paths) + { + // Note: we assume that the assembly really can be found at 'candidatePath' + // (without 'GetPathToLoad'), and that calling GetAssemblyName doesn't cause us + // to hold a lock on the file. This prevents unnecessary shadow copies. + var candidateName = AssemblyName.GetAssemblyName(candidatePath); + // Checking FullName ensures that version and PublicKeyToken match exactly. + if (candidateName.FullName.Equals(assemblyName.FullName, StringComparison.OrdinalIgnoreCase)) + { + return LoadFromAssemblyPath(_loader.GetPathToLoad(candidatePath)); + } + else if (bestCandidateName is null || bestCandidateName.Version < candidateName.Version) + { + bestCandidateName = candidateName; + bestCandidatePath = candidatePath; + } + } + + Debug.Assert(bestCandidateName != null); + Debug.Assert(bestCandidatePath != null); + + return LoadFromAssemblyPath(_loader.GetPathToLoad(bestCandidatePath)); } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { var assemblyPath = Path.Combine(Directory, unmanagedDllName + ".dll"); - if (!_loader.IsKnownDependencyLocation(assemblyPath)) + var paths = _loader.GetPaths(unmanagedDllName); + if (paths is null || !paths.Contains(assemblyPath)) { return IntPtr.Zero; } diff --git a/src/Compilers/Test/Core/AssemblyLoadTestFixture.cs b/src/Compilers/Test/Core/AssemblyLoadTestFixture.cs index 0ccb901d7359f..9fb7487c5e4f0 100644 --- a/src/Compilers/Test/Core/AssemblyLoadTestFixture.cs +++ b/src/Compilers/Test/Core/AssemblyLoadTestFixture.cs @@ -28,6 +28,9 @@ public sealed class AssemblyLoadTestFixture : IDisposable public TempFile Delta2 { get; } public TempFile Epsilon { get; } + public TempFile Delta2B { get; } + public TempFile Delta3 { get; } + public TempFile UserSystemCollectionsImmutable { get; } /// @@ -165,6 +168,44 @@ public void Write(StringBuilder sb, string s) } ", delta2Reference); + var v2BDirectory = _directory.CreateDirectory("Version2B"); + Delta2B = GenerateDll("Delta", v2BDirectory, @" +using System.Text; + +[assembly: System.Reflection.AssemblyTitle(""Delta"")] +[assembly: System.Reflection.AssemblyVersion(""2.0.0.0"")] + +namespace Delta +{ + public class D + { + public void Write(StringBuilder sb, string s) + { + sb.AppendLine(""Delta.2B: "" + s); + } + } +} +"); + + var v3Directory = _directory.CreateDirectory("Version3"); + Delta3 = GenerateDll("Delta", v3Directory, @" +using System.Text; + +[assembly: System.Reflection.AssemblyTitle(""Delta"")] +[assembly: System.Reflection.AssemblyVersion(""3.0.0.0"")] + +namespace Delta +{ + public class D + { + public void Write(StringBuilder sb, string s) + { + sb.AppendLine(""Delta.3: "" + s); + } + } +} +"); + var sciUserDirectory = _directory.CreateDirectory("SCIUser"); var compilerReference = MetadataReference.CreateFromFile(typeof(Microsoft.CodeAnalysis.SyntaxNode).Assembly.Location);