diff --git a/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerFileReferenceTests.cs b/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerFileReferenceTests.cs index a599c96926ed3..ba3f65f6ce630 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerFileReferenceTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerFileReferenceTests.cs @@ -19,6 +19,13 @@ namespace Microsoft.CodeAnalysis.UnitTests { + [CollectionDefinition(Name)] + public class AssemblyLoadTestFixtureCollection : ICollectionFixture + { + public const string Name = nameof(AssemblyLoadTestFixtureCollection); + private AssemblyLoadTestFixtureCollection() { } + } + [Collection(AssemblyLoadTestFixtureCollection.Name)] public class AnalyzerFileReferenceTests : TestBase { diff --git a/src/Compilers/Core/CodeAnalysisTest/DefaultAnalyzerAssemblyLoaderTests.cs b/src/Compilers/Core/CodeAnalysisTest/DefaultAnalyzerAssemblyLoaderTests.cs index e6e9fbf1f2a99..9d71b5847fab4 100644 --- a/src/Compilers/Core/CodeAnalysisTest/DefaultAnalyzerAssemblyLoaderTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/DefaultAnalyzerAssemblyLoaderTests.cs @@ -30,63 +30,173 @@ namespace Microsoft.CodeAnalysis.UnitTests { - [CollectionDefinition(Name)] - public class AssemblyLoadTestFixtureCollection : ICollectionFixture - { - public const string Name = nameof(AssemblyLoadTestFixtureCollection); - private AssemblyLoadTestFixtureCollection() { } - } +#if NETCOREAPP - public sealed class DefaultAnalyzerAssemblyLoaderTests : TestBase + public sealed class InvokeUtil { -#if NETCOREAPP - private sealed class InvokeUtil + public void Exec(Action testOutputHelper, AssemblyLoadContext alc, bool shadowLoad, string typeName, string methodName) { - public void Exec(string typeName, string methodName) => InvokeTestCode(typeName, methodName); + // Ensure that the test did not load any of the test fixture assemblies into + // the default load context. That should never happen. Assemblies should either + // load into the compiler or directory load context. + // + // Not only is this bad behavior it also pollutes future test results. + var count = AssemblyLoadContext.Default.Assemblies.Count(); + using var fixture = new AssemblyLoadTestFixture(); + using var tempRoot = new TempRoot(); + var loader = shadowLoad + ? new ShadowCopyAnalyzerAssemblyLoader(alc, tempRoot.CreateDirectory().Path) + : new DefaultAnalyzerAssemblyLoader(alc); + try + { + DefaultAnalyzerAssemblyLoaderTests.InvokeTestCode(loader, fixture, typeName, methodName); + } + finally + { + testOutputHelper($"Test fixture root: {fixture.TempDirectory.Path}"); + + foreach (var context in loader.GetDirectoryLoadContextsSnapshot()) + { + testOutputHelper($"Directory context: {context.Directory}"); + foreach (var assembly in context.Assemblies) + { + testOutputHelper($"\t{assembly.FullName}"); + } + } + + if (loader is ShadowCopyAnalyzerAssemblyLoader shadowLoader) + { + testOutputHelper($"Shadow loader: {shadowLoader.BaseDirectory}"); + } + + testOutputHelper($"Loader path maps"); + foreach (var pair in loader.GetPathMapSnapshot()) + { + testOutputHelper($"\t{pair.OriginalAssemblyPath} -> {pair.RealAssemblyPath}"); + } + + Assert.Equal(count, AssemblyLoadContext.Default.Assemblies.Count()); + } } + } + #else - private sealed class InvokeUtil : MarshalByRefObject + + public sealed class InvokeUtil : MarshalByRefObject + { + public void Exec(ITestOutputHelper testOutputHelper, bool shadowLoad, string typeName, string methodName) { - public void Exec(string typeName, string methodName) + using var fixture = new AssemblyLoadTestFixture(); + using var tempRoot = new TempRoot(); + var loader = shadowLoad + ? new ShadowCopyAnalyzerAssemblyLoader(tempRoot.CreateDirectory().Path) + : new DefaultAnalyzerAssemblyLoader(); + + try { - try + DefaultAnalyzerAssemblyLoaderTests.InvokeTestCode(loader, fixture, typeName, methodName); + } + catch (TargetInvocationException ex) when (ex.InnerException is XunitException) + { + var inner = ex.InnerException; + throw new Exception(inner.Message + inner.StackTrace); + } + finally + { + testOutputHelper.WriteLine($"Test fixture root: {fixture.TempDirectory.Path}"); + + testOutputHelper.WriteLine($"Loaded Assemblies"); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().OrderByDescending(x => x.FullName)) { - InvokeTestCode(typeName, methodName); + testOutputHelper.WriteLine($"\t{assembly.FullName} -> {assembly.Location}"); } - catch (TargetInvocationException ex) when (ex.InnerException is XunitException) + + if (loader is ShadowCopyAnalyzerAssemblyLoader shadowLoader) { - var inner = ex.InnerException; - throw new Exception(inner.Message + inner.StackTrace); + testOutputHelper.WriteLine($"Shadow loader: {shadowLoader.BaseDirectory}"); + } + + testOutputHelper.WriteLine($"Loader path maps"); + foreach (var pair in loader.GetPathMapSnapshot()) + { + testOutputHelper.WriteLine($"\t{pair.OriginalAssemblyPath} -> {pair.RealAssemblyPath}"); } } } + } + #endif - private static readonly CSharpCompilationOptions s_dllWithMaxWarningLevel = new(OutputKind.DynamicallyLinkedLibrary, warningLevel: CodeAnalysis.Diagnostic.MaxWarningLevel); - private readonly ITestOutputHelper _output; + /// + /// Contains the bulk of our analyzer / generator loading tests. + /// + /// + /// These tests often have quirks associated with fundamental limitation issues around either + /// .NET Framework, .NET Core or our own legacy decisions. Rather than repeating a specific rationale + /// at all the tests that hit them, the common are outlined below and referenced with the following + /// comment style within the test. + /// + /// // See limitation 1 + /// + /// This allows us to provide central description of the limitations that can be easily referenced in the impacted + /// tests. For all the descriptions below assume that A.dll depends on B.dll. + /// + /// Limitation 1: .NET Framework probing path. + /// + /// The .NET Framework assembly loader will only call AppDomain.AssemblyResolve when it cannot satifisfy a load + /// request. One of the places the assembly loader will always consider when looking for dependencies of A.dll + /// is the directory that A.dll was loading from (it's added to the probing path). That means if B.dll is in the + /// same directory then the runtime will silently load it without a way for us to intervene. + /// + /// Note: this only applies when A.dll is in the Load or LoadFrom context which is always true for these tests + /// + /// Limitation 2: Dependency is already loaded. + /// + /// Similar to Limitation 1 is when the dependency, B.dll, is already present in the Load or LoadFrom context + /// then that will be used. The runtime will not attempt to load a better version (an exact match for example). + /// + /// Limitation 3: Shadow copy breaks up directories + /// + /// The shadow copy loader strategy is to put every analyzer dependency into a different shadow directory. That + /// means if A.dll and B.dll are in the same directory for a normal load, they are in different directories + /// during a shadow copy load. + /// + /// This causes significant issues in .NET Framework because we don't have the ability to know where a load + /// is coming from. The AppDomain.AssemblyResolve event just requests "B, Version=1.0.0.0" but gives no context + /// as to where the request is coming from. That means we often end up loading a different copy of B.dll in a + /// shadow load scenario. + /// + /// Long term this is something that needs to be addressed. Tracked by https://github.com/dotnet/roslyn/issues/66532 + /// + /// + public sealed class DefaultAnalyzerAssemblyLoaderTests : TestBase + { + public ITestOutputHelper TestOutputHelper { get; } - public DefaultAnalyzerAssemblyLoaderTests(ITestOutputHelper output) + public DefaultAnalyzerAssemblyLoaderTests(ITestOutputHelper testOutputHelper) { - _output = output; + TestOutputHelper = testOutputHelper; } - private void Run(Action action, [CallerMemberName] string? memberName = null) + private void Run(bool shadowLoad, Action action, [CallerMemberName] string? memberName = null) { #if NETCOREAPP var alc = AssemblyLoadContextUtils.Create($"Test {memberName}"); var assembly = alc.LoadFromAssemblyName(typeof(InvokeUtil).Assembly.GetName()); var util = assembly.CreateInstance(typeof(InvokeUtil).FullName)!; var method = util.GetType().GetMethod("Exec", BindingFlags.Public | BindingFlags.Instance)!; - method.Invoke(util, new object[] { action.Method.DeclaringType!.FullName!, action.Method.Name }); + var outputHelper = (string msg) => TestOutputHelper.WriteLine(msg); + method.Invoke(util, new object[] { outputHelper, alc, shadowLoad, action.Method.DeclaringType!.FullName!, action.Method.Name }); #else AppDomain? appDomain = null; try { appDomain = AppDomainUtils.Create($"Test {memberName}"); + var testOutputHelper = new AppDomainTestOutputHelper(TestOutputHelper); var type = typeof(InvokeUtil); var util = (InvokeUtil)appDomain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName); - util.Exec(action.Method.DeclaringType.FullName, action.Method.Name); + util.Exec(testOutputHelper, shadowLoad, action.Method.DeclaringType.FullName, action.Method.Name); } finally { @@ -100,7 +210,7 @@ private void Run(Action /// us back to the actual test code to execute. The intent is to invoke the lambda / static /// local func where the code exists. /// - private static void InvokeTestCode(string typeName, string methodName) + internal static void InvokeTestCode(DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture fixture, string typeName, string methodName) { var type = typeof(DefaultAnalyzerAssemblyLoaderTests).Assembly.GetType(typeName, throwOnError: false)!; var member = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)!; @@ -111,15 +221,15 @@ private static void InvokeTestCode(string typeName, string methodName) ? null : type.Assembly.CreateInstance(typeName); - using var fixture = new AssemblyLoadTestFixture(); - var loader = new DefaultAnalyzerAssemblyLoader(); member.Invoke(obj, new object[] { loader, fixture }); } - [Fact, WorkItem(32226, "https://github.com/dotnet/roslyn/issues/32226")] - public void LoadWithDependency() + [Theory] + [CombinatorialData] + [WorkItem(32226, "https://github.com/dotnet/roslyn/issues/32226")] + public void LoadWithDependency(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { var analyzerDependencyFile = testFixture.AnalyzerDependency; var analyzerMainFile = testFixture.AnalyzerWithDependency; @@ -140,30 +250,33 @@ public void LoadWithDependency() }); } - [Fact] - public void AddDependencyLocationThrowsOnNull() + [Theory] + [CombinatorialData] + public void AddDependencyLocationThrowsOnNull(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { Assert.Throws("fullPath", () => loader.AddDependencyLocation(null!)); Assert.Throws("fullPath", () => loader.AddDependencyLocation("a")); }); } - [Fact] - public void ThrowsForMissingFile() + [Theory] + [CombinatorialData] + public void ThrowsForMissingFile(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".dll"); Assert.ThrowsAny(() => loader.LoadFromPath(path)); }); } - [Fact] - public void BasicLoad() + [Theory] + [CombinatorialData] + public void BasicLoad(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { loader.AddDependencyLocation(testFixture.Alpha.Path); Assembly alpha = loader.LoadFromPath(testFixture.Alpha.Path); @@ -172,10 +285,11 @@ public void BasicLoad() }); } - [Fact] - public void AssemblyLoading() + [Theory] + [CombinatorialData] + public void AssemblyLoading_Multiple(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -204,44 +318,109 @@ public void AssemblyLoading() }); } - [ConditionalFact(typeof(CoreClrOnly))] - public void AssemblyLoading_AssemblyLocationNotAdded() + /// + /// The loaders should not actually look at the contents of the disk until a + /// call has occurred. This is historical behavior that doesn't have a clear reason for existing. There + /// is strong suspicion it's to delay loading of analyzers until absolutely necessary. As such we're + /// enshrining the behavior here so it is not _accidentally_ changed. + /// + [Theory] + [CombinatorialData] + public void AssemblyLoading_OverwriteBeforeLoad(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + { + loader.AddDependencyLocation(testFixture.Delta1.Path); + testFixture.Delta1.WriteAllBytes(testFixture.Delta2.ReadAllBytes()); + var assembly = loader.LoadFromPath(testFixture.Delta1.Path); + + var name = AssemblyName.GetAssemblyName(testFixture.Delta2.Path); + Assert.Equal(name.FullName, assembly.GetName().FullName); + + VerifyDependencyAssemblies( + loader, + testFixture.Delta1.Path); + }); + } + + [Theory] + [CombinatorialData] + public void AssemblyLoading_AssemblyLocationNotAdded(bool shadowLoad) + { + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { loader.AddDependencyLocation(testFixture.Gamma.Path); loader.AddDependencyLocation(testFixture.Delta1.Path); - Assert.Throws(() => loader.LoadFromPath(testFixture.Beta.Path)); + Assert.Throws(() => loader.LoadFromPath(testFixture.Beta.Path)); }); } - [ConditionalFact(typeof(CoreClrOnly))] - public void AssemblyLoading_DependencyLocationNotAdded() + [Theory] + [CombinatorialData] + public void AssemblyLoading_DependencyLocationNotAdded(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); - // We don't pass Alpha's path to AddDependencyLocation here, and therefore expect - // calling Beta.B.Write to fail. loader.AddDependencyLocation(testFixture.Gamma.Path); loader.AddDependencyLocation(testFixture.Beta.Path); Assembly beta = loader.LoadFromPath(testFixture.Beta.Path); var b = beta.CreateInstance("Beta.B")!; var writeMethod = b.GetType().GetMethod("Write")!; - var exception = Assert.Throws( - () => writeMethod.Invoke(b, new object[] { sb, "Test B" })); - Assert.IsAssignableFrom(exception.InnerException); - var actual = sb.ToString(); - Assert.Equal(@"", actual); + if (ExecutionConditionUtil.IsCoreClr || loader is ShadowCopyAnalyzerAssemblyLoader) + { + // We don't pass Alpha's path to AddDependencyLocation here, and therefore expect + // calling Beta.B.Write to fail because loader will prevent the load of Alpha + var exception = Assert.Throws( + () => writeMethod.Invoke(b, new object[] { sb, "Test B" })); + Assert.IsAssignableFrom(exception.InnerException); + + var actual = sb.ToString(); + Assert.Equal(@"", actual); + } + else + { + // See limitation 1 + writeMethod.Invoke(b, new object[] { sb, "Test B" }); + var actual = sb.ToString(); + Assert.Equal(@"Delta: Gamma: Beta: Test B +", actual); + } }); } - private static void VerifyAssemblies(IEnumerable assemblies, params (string simpleName, string version, string path)[] expected) + private static void VerifyAssemblies(DefaultAnalyzerAssemblyLoader loader, IEnumerable assemblies, params (string simpleName, string version, string path)[] expected) + { + expected = expected + .Select(x => (x.simpleName, x.version, loader.GetRealLoadPath(x.path))) + .ToArray(); + + Assert.Equal( + expected, + Roslyn.Utilities + .EnumerableExtensions + .Order(assemblies.Select(assembly => (assembly.GetName().Name!, assembly.GetName().Version!.ToString(), assembly.Location))) + .ToArray()); + + if (loader is ShadowCopyAnalyzerAssemblyLoader shadowLoader) + { + Assert.All(assemblies, x => x.Location.StartsWith(shadowLoader.BaseDirectory, StringComparison.Ordinal)); + } + } + + private static void VerifyAssemblies(DefaultAnalyzerAssemblyLoader loader, IEnumerable assemblies, params string[] assemblyPaths) { - Assert.Equal(expected, Roslyn.Utilities.EnumerableExtensions.Order(assemblies.Select(assembly => (assembly.GetName().Name!, assembly.GetName().Version!.ToString(), assembly.Location)))); + var data = assemblyPaths + .Select(x => + { + var name = AssemblyName.GetAssemblyName(x); + return (name.Name!, name.Version?.ToString() ?? "", x); + }) + .ToArray(); + VerifyAssemblies(loader, assemblies, data); } /// @@ -253,7 +432,7 @@ private static void VerifyDependencyAssemblies(DefaultAnalyzerAssemblyLoader loa #if NETCOREAPP // This verify only works where there is a single load context. - var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader); + var alcs = loader.GetDirectoryLoadContextsSnapshot(); Assert.Equal(1, alcs.Length); loadedAssemblies = alcs[0].Assemblies; @@ -292,27 +471,49 @@ static bool isInLoadFromContext(DefaultAnalyzerAssemblyLoader loader, Assembly a } #endif - var data = assemblyPaths - .Select(x => - { - var name = AssemblyName.GetAssemblyName(x); - return (name.Name!, name.Version?.ToString() ?? "", x); - }) - .ToArray(); + VerifyAssemblies(loader, loadedAssemblies, assemblyPaths); + } + + [Theory] + [CombinatorialData] + public void AssemblyLoading_Simple(bool shadowLoad) + { + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + { + using var temp = new TempRoot(); + StringBuilder sb = new StringBuilder(); + + loader.AddDependencyLocation(testFixture.Delta1.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); - VerifyAssemblies(loadedAssemblies, data); + VerifyDependencyAssemblies( + loader, + testFixture.Delta1.Path, + testFixture.Gamma.Path); + }); } - [Fact] - public void AssemblyLoading_DependencyInDifferentDirectory() + [ConditionalTheory(typeof(WindowsOnly), Reason = "https://github.com/dotnet/runtime/issues/81108")] + [CombinatorialData] + public void AssemblyLoading_DependencyInDifferentDirectory(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { using var temp = new TempRoot(); + var tempDir = temp.CreateDirectory(); StringBuilder sb = new StringBuilder(); - var deltaFile = temp.CreateDirectory().CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); - var gammaFile = temp.CreateDirectory().CreateFile("Gamma.dll").CopyContentFrom(testFixture.Gamma.Path); + var deltaFile = tempDir.CreateDirectory("a").CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); + var gammaFile = tempDir.CreateDirectory("b").CreateFile("Gamma.dll").CopyContentFrom(testFixture.Gamma.Path); loader.AddDependencyLocation(deltaFile.Path); loader.AddDependencyLocation(gammaFile.Path); @@ -337,24 +538,29 @@ public void AssemblyLoading_DependencyInDifferentDirectory() /// Similar to except want to validate /// a dependency in the same directory is preferred over one in a different directory. /// - [Fact] - public void AssemblyLoading_DependencyInDifferentDirectory2() + [Theory] + [CombinatorialData] + public void AssemblyLoading_DependencyInDifferentDirectory2(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { using var temp = new TempRoot(); - StringBuilder sb = new StringBuilder(); - - var deltaFile1 = temp.CreateDirectory().CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); var tempDir = temp.CreateDirectory(); - var gammaFile = tempDir.CreateFile("Gamma.dll").CopyContentFrom(testFixture.Gamma.Path); - var deltaFile2 = tempDir.CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); + + // It's important that we create these directories in a deterministic order so that + // our test has reliably output. Part of our resolution code will search the registered + // paths in a sorted order. + var deltaFile1 = tempDir.CreateDirectory("a").CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); + var tempSubDir = tempDir.CreateDirectory("b"); + var gammaFile = tempSubDir.CreateFile("Gamma.dll").CopyContentFrom(testFixture.Gamma.Path); + var deltaFile2 = tempSubDir.CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); loader.AddDependencyLocation(deltaFile1.Path); loader.AddDependencyLocation(deltaFile2.Path); loader.AddDependencyLocation(gammaFile.Path); Assembly gamma = loader.LoadFromPath(gammaFile.Path); + StringBuilder sb = new StringBuilder(); var b = gamma.CreateInstance("Gamma.G")!; var writeMethod = b.GetType().GetMethod("Write")!; writeMethod.Invoke(b, new object[] { sb, "Test G" }); @@ -363,10 +569,22 @@ public void AssemblyLoading_DependencyInDifferentDirectory2() Assert.Equal(@"Delta: Gamma: Test G ", actual); - VerifyDependencyAssemblies( - loader, - deltaFile2.Path, - gammaFile.Path); + if (ExecutionConditionUtil.IsDesktop && loader is ShadowCopyAnalyzerAssemblyLoader) + { + // See limitation 3 + VerifyDependencyAssemblies( + loader, + deltaFile1.Path, + gammaFile.Path); + } + else + { + VerifyDependencyAssemblies( + loader, + deltaFile2.Path, + gammaFile.Path); + + } }); } @@ -375,16 +593,18 @@ public void AssemblyLoading_DependencyInDifferentDirectory2() /// that we ensure the code does not prefer a dependency in the same directory if it's /// unregistered /// - [Fact] - public void AssemblyLoading_DependencyInDifferentDirectory3() + [Theory] + [CombinatorialData] + public void AssemblyLoading_DependencyInDifferentDirectory3(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { using var temp = new TempRoot(); + var tempDir = temp.CreateDirectory(); StringBuilder sb = new StringBuilder(); - var deltaFile = temp.CreateDirectory().CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); - var gammaFile = temp.CreateDirectory().CreateFile("Gamma.dll").CopyContentFrom(testFixture.Gamma.Path); + var deltaFile = tempDir.CreateDirectory("a").CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); + var gammaFile = tempDir.CreateDirectory("b").CreateFile("Gamma.dll").CopyContentFrom(testFixture.Gamma.Path); loader.AddDependencyLocation(deltaFile.Path); loader.AddDependencyLocation(gammaFile.Path); @@ -405,10 +625,42 @@ public void AssemblyLoading_DependencyInDifferentDirectory3() }); } - [Fact] - public void AssemblyLoading_MultipleVersions() + [Theory] + [CombinatorialData] + [WorkItem(32226, "https://github.com/dotnet/roslyn/issues/32226")] + public void AssemblyLoading_DependencyInDifferentDirectory4(bool shadowLoad) + { + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + { + var analyzerDependencyFile = testFixture.AnalyzerDependency; + var analyzerMainFile = testFixture.AnalyzerWithDependency; + + var analyzerMainReference = new AnalyzerFileReference(analyzerMainFile.Path, loader); + analyzerMainReference.AnalyzerLoadFailed += (_, e) => AssertEx.Fail(e.Exception!.Message); + var analyzerDependencyReference = new AnalyzerFileReference(analyzerDependencyFile.Path, loader); + analyzerDependencyReference.AnalyzerLoadFailed += (_, e) => AssertEx.Fail(e.Exception!.Message); + + Assert.True(loader.IsAnalyzerDependencyPath(analyzerMainFile.Path)); + Assert.True(loader.IsAnalyzerDependencyPath(analyzerDependencyFile.Path)); + + var analyzers = analyzerMainReference.GetAnalyzersForAllLanguages(); + Assert.Equal(1, analyzers.Length); + Assert.Equal("TestAnalyzer", analyzers[0].ToString()); + Assert.Equal(0, analyzerDependencyReference.GetAnalyzersForAllLanguages().Length); + Assert.NotNull(analyzerDependencyReference.GetAssembly()); + + VerifyDependencyAssemblies( + loader, + testFixture.AnalyzerWithDependency.Path, + testFixture.AnalyzerDependency.Path); + }); + } + + [Theory] + [CombinatorialData] + public void AssemblyLoading_MultipleVersions(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -426,16 +678,18 @@ public void AssemblyLoading_MultipleVersions() e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); #if NETCOREAPP - var alcs = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader); + var alcs = loader.GetDirectoryLoadContextsSnapshot(); Assert.Equal(2, alcs.Length); VerifyAssemblies( + loader, alcs[0].Assemblies, ("Delta", "1.0.0.0", testFixture.Delta1.Path), ("Gamma", "0.0.0.0", testFixture.Gamma.Path) ); VerifyAssemblies( + loader, alcs[1].Assemblies, ("Delta", "2.0.0.0", testFixture.Delta2.Path), ("Epsilon", "0.0.0.0", testFixture.Epsilon.Path)); @@ -461,10 +715,11 @@ public void AssemblyLoading_MultipleVersions() }); } - [Fact] - public void AssemblyLoading_MultipleVersions_NoExactMatch() + [Theory] + [CombinatorialData] + public void AssemblyLoading_MultipleVersions_NoExactMatch(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -477,11 +732,14 @@ public void AssemblyLoading_MultipleVersions_NoExactMatch() e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); var actual = sb.ToString(); - if (ExecutionConditionUtil.IsCoreClr) + if (ExecutionConditionUtil.IsCoreClr || loader is ShadowCopyAnalyzerAssemblyLoader) { // In .NET Core we have _full_ control over assembly loading and can prevent implicit // loads from probing paths. That means we can avoid implicitly loading the Delta v2 // next to Epsilon + // + // Similarly in the shadow copy scenarios the assemblies are not side by side so the + // load is controllable. VerifyDependencyAssemblies( loader, testFixture.Delta3.Path, @@ -493,9 +751,12 @@ public void AssemblyLoading_MultipleVersions_NoExactMatch() } else { - // The Epsilon.dll has Delta.dll (v2) next to it in the directory. The .NET Framework - // will implicitly load this due to normal probing rules. No way for us to intercept - // this and we end up with v2 here where it wasn't specified as a dependency. + // See limitation 1 + // The Epsilon.dll has Delta.dll (v2) next to it in the directory. + Assert.Throws(() => loader.GetRealLoadPath(testFixture.Delta2.Path)); + + // Fake the dependency so we can verify the rest of the load + loader.AddDependencyLocation(testFixture.Delta2.Path); VerifyDependencyAssemblies( loader, testFixture.Delta2.Path, @@ -509,14 +770,14 @@ public void AssemblyLoading_MultipleVersions_NoExactMatch() }); } - [Fact] - public void AssemblyLoading_MultipleVersions_MultipleEqualMatches() + [Theory] + [CombinatorialData] + public void AssemblyLoading_MultipleVersions_MultipleEqualMatches(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); - // Delta2B and Delta2 have the same version, but we prefer Delta2 because it's in the same directory as Epsilon. loader.AddDependencyLocation(testFixture.Delta2B.Path); loader.AddDependencyLocation(testFixture.Delta2.Path); loader.AddDependencyLocation(testFixture.Epsilon.Path); @@ -525,23 +786,45 @@ public void AssemblyLoading_MultipleVersions_MultipleEqualMatches() var e = epsilon.CreateInstance("Epsilon.E")!; e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); - VerifyDependencyAssemblies( - loader, - testFixture.Delta2.Path, - testFixture.Epsilon.Path); + if (ExecutionConditionUtil.IsDesktop && loader is ShadowCopyAnalyzerAssemblyLoader) + { + // Delta2B and Delta2 have the same version, but we prefer Delta2B because it's added first and + // in shadow loader we can't fall back to same directory because the runtime doesn't provide + // context for who requested the load. Just have to go to best version. + VerifyDependencyAssemblies( + loader, + testFixture.Delta2B.Path, + testFixture.Epsilon.Path); - var actual = sb.ToString(); - Assert.Equal( -@"Delta.2: Epsilon: Test E + var actual = sb.ToString(); + Assert.Equal( + @"Delta.2B: Epsilon: Test E ", - actual); + actual); + } + else + { + // See limitation 1 + // Delta2B and Delta2 have the same version, but we prefer Delta2 because it's in the same directory as Epsilon. + VerifyDependencyAssemblies( + loader, + testFixture.Delta2.Path, + testFixture.Epsilon.Path); + + var actual = sb.ToString(); + Assert.Equal( + @"Delta.2: Epsilon: Test E +", + actual); + } }); } - [Fact] - public void AssemblyLoading_MultipleVersions_MultipleVersionsOfSameAnalyzerItself() + [Theory] + [CombinatorialData] + public void AssemblyLoading_MultipleVersions_MultipleVersionsOfSameAnalyzerItself(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -557,11 +840,12 @@ public void AssemblyLoading_MultipleVersions_MultipleVersionsOfSameAnalyzerItsel // On Core, we're able to load both of these into separate AssemblyLoadContexts. Assert.NotEqual(delta2B.Location, delta2.Location); - Assert.Equal(testFixture.Delta2.Path, delta2.Location); - Assert.Equal(testFixture.Delta2B.Path, delta2B.Location); + Assert.Equal(loader.GetRealLoadPath(testFixture.Delta2.Path), delta2.Location); + Assert.Equal(loader.GetRealLoadPath(testFixture.Delta2B.Path), delta2B.Location); #else + // See limitation 2 // In non-core, we cache by assembly identity; since we don't use multiple AppDomains we have no // way to load different assemblies with the same identity, no matter what. Thus, we'll get the // same assembly for both of these. @@ -570,10 +854,11 @@ public void AssemblyLoading_MultipleVersions_MultipleVersionsOfSameAnalyzerItsel }); } - [Fact(Skip = "https://github.com/dotnet/roslyn/issues/60763")] - public void AssemblyLoading_MultipleVersions_ExactAndGreaterMatch() + [Theory] + [CombinatorialData] + public void AssemblyLoading_MultipleVersions_ExactAndGreaterMatch(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -585,14 +870,17 @@ public void AssemblyLoading_MultipleVersions_ExactAndGreaterMatch() var e = epsilon.CreateInstance("Epsilon.E")!; e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); - VerifyDependencyAssemblies( - loader, - testFixture.Delta2B.Path, - testFixture.Epsilon.Path); - var actual = sb.ToString(); - if (ExecutionConditionUtil.IsCoreClr) + if (ExecutionConditionUtil.IsCoreClr || loader is ShadowCopyAnalyzerAssemblyLoader) { + // This works in CoreClr because we have full control over assembly loading. It + // works in shadow copy because all the DLLs are put into different directories + // so everything is a AppDomain.AssemblyResolve event and we get full control there. + VerifyDependencyAssemblies( + loader, + testFixture.Delta2B.Path, + testFixture.Epsilon.Path); + Assert.Equal( @"Delta.2B: Epsilon: Test E ", @@ -600,18 +888,29 @@ public void AssemblyLoading_MultipleVersions_ExactAndGreaterMatch() } else { + // See limitation 2 + Assert.Throws(() => loader.GetRealLoadPath(testFixture.Delta2.Path)); + + // Fake the dependency so we can verify the rest of the load + loader.AddDependencyLocation(testFixture.Delta2.Path); + VerifyDependencyAssemblies( + loader, + testFixture.Delta2.Path, + testFixture.Epsilon.Path); + Assert.Equal( - @"Delta: Epsilon: Test E + @"Delta.2: Epsilon: Test E ", actual); } }); } - [Fact] - public void AssemblyLoading_MultipleVersions_WorseMatchInSameDirectory() + [Theory] + [CombinatorialData] + public void AssemblyLoading_MultipleVersions_WorseMatchInSameDirectory(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { using var temp = new TempRoot(); StringBuilder sb = new StringBuilder(); @@ -620,9 +919,6 @@ public void AssemblyLoading_MultipleVersions_WorseMatchInSameDirectory() 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. - // This is because the CLR will see it first and load it, without giving us any chance to redirect - // in the AssemblyResolve hook. loader.AddDependencyLocation(delta1File.Path); loader.AddDependencyLocation(testFixture.Delta2.Path); loader.AddDependencyLocation(epsilonFile.Path); @@ -631,23 +927,45 @@ public void AssemblyLoading_MultipleVersions_WorseMatchInSameDirectory() var e = epsilon.CreateInstance("Epsilon.E")!; e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); - VerifyDependencyAssemblies( - loader, - delta1File.Path, - epsilonFile.Path); + if (ExecutionConditionUtil.IsDesktop && loader is ShadowCopyAnalyzerAssemblyLoader) + { + // In desktop + shadow load the dependencies are in different directories with + // no context available when the load for Delta comes in. So we pick the best + // option. + // Epsilon wants Delta2, but since Delta1 is in the same directory, we prefer Delta1 over Delta2. + VerifyDependencyAssemblies( + loader, + testFixture.Delta2.Path, + epsilonFile.Path); - var actual = sb.ToString(); - Assert.Equal( - @"Delta: Epsilon: Test E + var actual = sb.ToString(); + Assert.Equal( + @"Delta.2: Epsilon: Test E ", - actual); + actual); + } + else + { + // See limitation 2 + VerifyDependencyAssemblies( + loader, + delta1File.Path, + epsilonFile.Path); + + var actual = sb.ToString(); + Assert.Equal( + @"Delta: Epsilon: Test E +", + actual); + } }); } - [Fact] - public void AssemblyLoading_MultipleVersions_MultipleLoaders() + [Theory] + [CombinatorialData] + public void AssemblyLoading_MultipleVersions_MultipleLoaders(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader1, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader1, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -667,18 +985,20 @@ public void AssemblyLoading_MultipleVersions_MultipleLoaders() e.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); #if NETCOREAPP - var alcs1 = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader1); + var alcs1 = loader1.GetDirectoryLoadContextsSnapshot(); Assert.Equal(1, alcs1.Length); VerifyAssemblies( + loader1, alcs1[0].Assemblies, ("Delta", "1.0.0.0", testFixture.Delta1.Path), ("Gamma", "0.0.0.0", testFixture.Gamma.Path)); - var alcs2 = DefaultAnalyzerAssemblyLoader.TestAccessor.GetOrderedLoadContexts(loader2); + var alcs2 = loader2.GetDirectoryLoadContextsSnapshot(); Assert.Equal(1, alcs2.Length); VerifyAssemblies( + loader2, alcs2[0].Assemblies, ("Delta", "2.0.0.0", testFixture.Delta2.Path), ("Epsilon", "0.0.0.0", testFixture.Epsilon.Path)); @@ -704,10 +1024,11 @@ public void AssemblyLoading_MultipleVersions_MultipleLoaders() }); } - [Fact] - public void AssemblyLoading_MultipleVersions_MissingVersion() + [Theory] + [CombinatorialData] + public void AssemblyLoading_MultipleVersions_MissingVersion(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -732,10 +1053,11 @@ public void AssemblyLoading_MultipleVersions_MissingVersion() }); } - [Fact] - public void AssemblyLoading_UnifyToHighest() + [Theory] + [CombinatorialData] + public void AssemblyLoading_UnifyToHighest(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { var sb = new StringBuilder(); @@ -766,10 +1088,11 @@ public void AssemblyLoading_UnifyToHighest() }); } - [Fact] - public void AssemblyLoading_CanLoadDifferentVersionsDirectly() + [Theory] + [CombinatorialData] + public void AssemblyLoading_CanLoadDifferentVersionsDirectly(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { var sb = new StringBuilder(); @@ -795,10 +1118,11 @@ public void AssemblyLoading_CanLoadDifferentVersionsDirectly() }); } - [Fact] - public void AssemblyLoading_AnalyzerReferencesSystemCollectionsImmutable_01() + [Theory] + [CombinatorialData] + public void AssemblyLoading_AnalyzerReferencesSystemCollectionsImmutable_01(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -821,10 +1145,11 @@ public void AssemblyLoading_AnalyzerReferencesSystemCollectionsImmutable_01() }); } - [Fact] - public void AssemblyLoading_AnalyzerReferencesSystemCollectionsImmutable_02() + [Theory] + [CombinatorialData] + public void AssemblyLoading_AnalyzerReferencesSystemCollectionsImmutable_02(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { StringBuilder sb = new StringBuilder(); @@ -838,10 +1163,11 @@ public void AssemblyLoading_AnalyzerReferencesSystemCollectionsImmutable_02() }); } - [Fact(Skip = "https://github.com/dotnet/roslyn/issues/66104")] - public void AssemblyLoading_CompilerDependencyDuplicated() + [Theory] + [CombinatorialData] + public void AssemblyLoading_CompilerDependencyDuplicated(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { var assembly = typeof(ImmutableArray).Assembly; @@ -858,10 +1184,11 @@ public void AssemblyLoading_CompilerDependencyDuplicated() }); } - [ConditionalFact(typeof(WindowsOnly))] - public void AssemblyLoading_NativeDependency() + [ConditionalTheory(typeof(WindowsOnly))] + [CombinatorialData] + public void AssemblyLoading_NativeDependency(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { const int INVALID_FILE_ATTRIBUTES = -1; loader.AddDependencyLocation(testFixture.AnalyzerWithNativeDependency.Path); @@ -874,10 +1201,33 @@ public void AssemblyLoading_NativeDependency() }); } - [Fact] - public void AssemblyLoading_Delete() + [Theory] + [CombinatorialData] + public void AssemblyLoading_DeleteAfterLoad1(bool shadowLoad) { - Run(static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + { + StringBuilder sb = new StringBuilder(); + + loader.AddDependencyLocation(testFixture.Delta1.Path); + _ = loader.LoadFromPath(testFixture.Delta1.Path); + + if (loader is ShadowCopyAnalyzerAssemblyLoader || !ExecutionConditionUtil.IsWindows) + { + File.Delete(testFixture.Delta1.Path); + } + else + { + Assert.Throws(() => File.Delete(testFixture.Delta1.Path)); + } + }); + } + + [Theory] + [CombinatorialData] + public void AssemblyLoading_DeleteAfterLoad2(bool shadowLoad) + { + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { using var temp = new TempRoot(); StringBuilder sb = new StringBuilder(); @@ -885,20 +1235,14 @@ public void AssemblyLoading_Delete() var tempDir = temp.CreateDirectory(); var deltaCopy = tempDir.CreateFile("Delta.dll").CopyContentFrom(testFixture.Delta1.Path); loader.AddDependencyLocation(deltaCopy.Path); - Assembly delta = loader.LoadFromPath(deltaCopy.Path); + Assembly? delta = loader.LoadFromPath(deltaCopy.Path); - try + if (loader is ShadowCopyAnalyzerAssemblyLoader || !ExecutionConditionUtil.IsWindows) { File.Delete(deltaCopy.Path); } - catch (UnauthorizedAccessException) - { - return; - } - - // The above call may or may not throw depending on the platform configuration. - // If it doesn't throw, we might as well check that things are still functioning reasonably. + // Ensure everything is functioning still var d = delta.CreateInstance("Delta.D"); d!.GetType().GetMethod("Write")!.Invoke(d, new object[] { sb, "Test D" }); @@ -910,128 +1254,73 @@ public void AssemblyLoading_Delete() }); } -#if NETCOREAPP - [Fact] - public void VerifyCompilerAssemblySimpleNames() + [Theory] + [CombinatorialData] + public void AssemblyLoading_DeleteAfterLoad3(bool shadowLoad) { - var caAssembly = typeof(Microsoft.CodeAnalysis.SyntaxNode).Assembly; - var caReferences = caAssembly.GetReferencedAssemblies(); - var allReferenceSimpleNames = ArrayBuilder.GetInstance(); - allReferenceSimpleNames.Add(caAssembly.GetName().Name ?? throw new InvalidOperationException()); - foreach (var reference in caReferences) + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => { - allReferenceSimpleNames.Add(reference.Name ?? throw new InvalidOperationException()); - } + using var temp = new TempRoot(); + var tempDir = temp.CreateDirectory(); + var sb = new StringBuilder(); - var csAssembly = typeof(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode).Assembly; - allReferenceSimpleNames.Add(csAssembly.GetName().Name ?? throw new InvalidOperationException()); - var csReferences = csAssembly.GetReferencedAssemblies(); - foreach (var reference in csReferences) - { - var name = reference.Name ?? throw new InvalidOperationException(); - if (!allReferenceSimpleNames.Contains(name, StringComparer.OrdinalIgnoreCase)) - { - allReferenceSimpleNames.Add(name); - } - } + var tempDir1 = tempDir.CreateDirectory("a"); + var tempDir2 = tempDir.CreateDirectory("b"); + var tempDir3 = tempDir.CreateDirectory("c"); - var vbAssembly = typeof(Microsoft.CodeAnalysis.VisualBasic.VisualBasicSyntaxNode).Assembly; - var vbReferences = vbAssembly.GetReferencedAssemblies(); - allReferenceSimpleNames.Add(vbAssembly.GetName().Name ?? throw new InvalidOperationException()); - foreach (var reference in vbReferences) - { - var name = reference.Name ?? throw new InvalidOperationException(); - if (!allReferenceSimpleNames.Contains(name, StringComparer.OrdinalIgnoreCase)) + 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" }); + + if (loader is ShadowCopyAnalyzerAssemblyLoader) { - allReferenceSimpleNames.Add(name); + File.Delete(delta1File.Path); + File.Delete(delta2File.Path); + File.Delete(gammaFile.Path); } - } - if (!DefaultAnalyzerAssemblyLoader.CompilerAssemblySimpleNames.SetEquals(allReferenceSimpleNames)) - { - allReferenceSimpleNames.Sort(); - var allNames = string.Join(",\r\n ", allReferenceSimpleNames.Select(name => $@"""{name}""")); - _output.WriteLine(" internal static readonly ImmutableHashSet CompilerAssemblySimpleNames ="); - _output.WriteLine(" ImmutableHashSet.Create("); - _output.WriteLine(" StringComparer.OrdinalIgnoreCase,"); - _output.WriteLine($" {allNames});"); - allReferenceSimpleNames.Free(); - Assert.True(false, $"{nameof(DefaultAnalyzerAssemblyLoader)}.{nameof(DefaultAnalyzerAssemblyLoader.CompilerAssemblySimpleNames)} is not up to date. Paste in the standard output of this test to update it."); - } - else - { - allReferenceSimpleNames.Free(); - } + var actual = sb.ToString(); + Assert.Equal(@"Delta: Gamma: Test G +", actual); + }); } - [Fact] - public void AssemblyLoadingInNonDefaultContext_AnalyzerReferencesSystemCollectionsImmutable() - { - using var testFixture = new AssemblyLoadTestFixture(); - - // Create a separate ALC as the compiler context, load the compiler assembly and a modified version of S.C.I into it, - // then use that to load and run `AssemblyLoadingInNonDefaultContextHelper1` below. We expect the analyzer running in - // its own `DirectoryLoadContext` would use the bogus S.C.I loaded in the compiler load context instead of the real one - // in the default context. - var compilerContext = new System.Runtime.Loader.AssemblyLoadContext("compilerContext"); - _ = compilerContext.LoadFromAssemblyPath(testFixture.UserSystemCollectionsImmutable.Path); - _ = compilerContext.LoadFromAssemblyPath(typeof(DefaultAnalyzerAssemblyLoader).GetTypeInfo().Assembly.Location); - - var testAssembly = compilerContext.LoadFromAssemblyPath(typeof(DefaultAnalyzerAssemblyLoaderTests).GetTypeInfo().Assembly.Location); - var testObject = testAssembly.CreateInstance(typeof(DefaultAnalyzerAssemblyLoaderTests).FullName!, - ignoreCase: false, BindingFlags.Default, binder: null, args: new object[] { _output, }, null, null)!; - - StringBuilder sb = new StringBuilder(); - testObject.GetType().GetMethod(nameof(AssemblyLoadingInNonDefaultContextHelper1), BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(testObject, new object[] { sb }); - Assert.Equal("42", sb.ToString()); - } +#if NETCOREAPP - // This helper does the same thing as in `AssemblyLoading_AnalyzerReferencesSystemCollectionsImmutable_01` test above except the assertions. - private void AssemblyLoadingInNonDefaultContextHelper1(StringBuilder sb) + [Theory] + [CombinatorialData] + public void AssemblyLoadingInNonDefaultContext_AnalyzerReferencesSystemCollectionsImmutable(bool shadowLoad) { - using var testFixture = new AssemblyLoadTestFixture(); - var loader = new DefaultAnalyzerAssemblyLoader(); - loader.AddDependencyLocation(testFixture.UserSystemCollectionsImmutable.Path); - loader.AddDependencyLocation(testFixture.AnalyzerReferencesSystemCollectionsImmutable1.Path); - - Assembly analyzerAssembly = loader.LoadFromPath(testFixture.AnalyzerReferencesSystemCollectionsImmutable1.Path); - var analyzer = analyzerAssembly.CreateInstance("Analyzer")!; - analyzer.GetType().GetMethod("Method")!.Invoke(analyzer, new object[] { sb }); - } + Run(shadowLoad, static (DefaultAnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => + { + // Create a separate ALC as the compiler context, load the compiler assembly and a modified version of S.C.I into it, + // then use that to load and run `AssemblyLoadingInNonDefaultContextHelper1` below. We expect the analyzer running in + // its own `DirectoryLoadContext` would use the bogus S.C.I loaded in the compiler load context instead of the real one + // in the default context. + var compilerContext = loader.CompilerLoadContext; + _ = compilerContext.LoadFromAssemblyPath(testFixture.UserSystemCollectionsImmutable.Path); + _ = compilerContext.LoadFromAssemblyPath(typeof(DefaultAnalyzerAssemblyLoader).GetTypeInfo().Assembly.Location); - [Fact] - public void AssemblyLoadingInNonDefaultContext_AnalyzerReferencesNonCompilerAssemblyUsedByDefaultContext() - { - using var testFixture = new AssemblyLoadTestFixture(); - // Load the V2 of Delta to default ALC, then create a separate ALC for compiler and load compiler assembly. - // Next use compiler context to load and run `AssemblyLoadingInNonDefaultContextHelper2` below. We expect the analyzer running in - // its own `DirectoryLoadContext` would load and use Delta V1 located in its directory instead of V2 already loaded in the default context. - _ = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(testFixture.Delta2.Path); - var compilerContext = new System.Runtime.Loader.AssemblyLoadContext("compilerContext"); - _ = compilerContext.LoadFromAssemblyPath(typeof(DefaultAnalyzerAssemblyLoader).GetTypeInfo().Assembly.Location); - - var testAssembly = compilerContext.LoadFromAssemblyPath(typeof(DefaultAnalyzerAssemblyLoaderTests).GetTypeInfo().Assembly.Location); - var testObject = testAssembly.CreateInstance(typeof(DefaultAnalyzerAssemblyLoaderTests).FullName!, - ignoreCase: false, BindingFlags.Default, binder: null, args: new object[] { _output }, null, null)!; - - StringBuilder sb = new StringBuilder(); - testObject.GetType().GetMethod(nameof(AssemblyLoadingInNonDefaultContextHelper2), BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(testObject, new object[] { sb }); - Assert.Equal( -@"Delta: Hello -", - sb.ToString()); - } + StringBuilder sb = new StringBuilder(); - private void AssemblyLoadingInNonDefaultContextHelper2(StringBuilder sb) - { - using var testFixture = new AssemblyLoadTestFixture(); - var loader = new DefaultAnalyzerAssemblyLoader(); - loader.AddDependencyLocation(testFixture.AnalyzerReferencesDelta1.Path); - loader.AddDependencyLocation(testFixture.Delta1.Path); - - Assembly analyzerAssembly = loader.LoadFromPath(testFixture.AnalyzerReferencesDelta1.Path); - var analyzer = analyzerAssembly.CreateInstance("Analyzer")!; - analyzer.GetType().GetMethod("Method")!.Invoke(analyzer, new object[] { sb }); + loader.AddDependencyLocation(testFixture.UserSystemCollectionsImmutable.Path); + loader.AddDependencyLocation(testFixture.AnalyzerReferencesSystemCollectionsImmutable1.Path); + + Assembly analyzerAssembly = loader.LoadFromPath(testFixture.AnalyzerReferencesSystemCollectionsImmutable1.Path); + var analyzer = analyzerAssembly.CreateInstance("Analyzer")!; + analyzer.GetType().GetMethod("Method")!.Invoke(analyzer, new object[] { sb }); + + Assert.Equal("42", sb.ToString()); + }); } #endif } diff --git a/src/Compilers/Core/CodeAnalysisTest/ShadowCopyAnalyzerAssemblyLoaderTests.cs b/src/Compilers/Core/CodeAnalysisTest/ShadowCopyAnalyzerAssemblyLoaderTests.cs deleted file mode 100644 index 02525eb8c552d..0000000000000 --- a/src/Compilers/Core/CodeAnalysisTest/ShadowCopyAnalyzerAssemblyLoaderTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -// 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.Collections.Immutable; -using System.IO; -using System.Reflection; -using System.Text; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Test.Utilities; -using Roslyn.Test.Utilities; -using Xunit; - -namespace Microsoft.CodeAnalysis.UnitTests -{ - [Collection(AssemblyLoadTestFixtureCollection.Name)] - public sealed class ShadowCopyAnalyzerAssemblyLoaderTests : TestBase - { - private static readonly CSharpCompilationOptions s_dllWithMaxWarningLevel = new(OutputKind.DynamicallyLinkedLibrary, warningLevel: CodeAnalysis.Diagnostic.MaxWarningLevel); - private readonly AssemblyLoadTestFixture _testFixture; - public ShadowCopyAnalyzerAssemblyLoaderTests(AssemblyLoadTestFixture testFixture) - { - _testFixture = testFixture; - } - - [Fact, WorkItem(32226, "https://github.com/dotnet/roslyn/issues/32226")] - public void LoadWithDependency() - { - var analyzerDependencyFile = _testFixture.AnalyzerDependency; - var analyzerMainFile = _testFixture.AnalyzerWithDependency; - var loader = new ShadowCopyAnalyzerAssemblyLoader(); - loader.AddDependencyLocation(analyzerDependencyFile.Path); - - var analyzerMainReference = new AnalyzerFileReference(analyzerMainFile.Path, loader); - analyzerMainReference.AnalyzerLoadFailed += (_, e) => AssertEx.Fail(e.Exception!.Message); - var analyzerDependencyReference = new AnalyzerFileReference(analyzerDependencyFile.Path, loader); - analyzerDependencyReference.AnalyzerLoadFailed += (_, e) => AssertEx.Fail(e.Exception!.Message); - - var analyzers = analyzerMainReference.GetAnalyzersForAllLanguages(); - Assert.Equal(1, analyzers.Length); - Assert.Equal("TestAnalyzer", analyzers[0].ToString()); - - Assert.Equal(0, analyzerDependencyReference.GetAnalyzersForAllLanguages().Length); - - Assert.NotNull(analyzerDependencyReference.GetAssembly()); - } - - [Fact] - public void AssemblyLoading_MultipleVersions() - { - StringBuilder sb = new StringBuilder(); - - var loader = new ShadowCopyAnalyzerAssemblyLoader(); - loader.AddDependencyLocation(_testFixture.Gamma.Path); - loader.AddDependencyLocation(_testFixture.Delta1.Path); - loader.AddDependencyLocation(_testFixture.Epsilon.Path); - loader.AddDependencyLocation(_testFixture.Delta2.Path); - - Assembly gamma = loader.LoadFromPath(_testFixture.Gamma.Path); - var g = gamma.CreateInstance("Gamma.G"); - g!.GetType().GetMethod("Write")!.Invoke(g, new object[] { sb, "Test G" }); - - Assembly epsilon = loader.LoadFromPath(_testFixture.Epsilon.Path); - var e = epsilon.CreateInstance("Epsilon.E"); - e!.GetType().GetMethod("Write")!.Invoke(e, new object[] { sb, "Test E" }); - - var actual = sb.ToString(); - if (ExecutionConditionUtil.IsCoreClr) - { - Assert.Equal( -@"Delta: Gamma: Test G -Delta.2: Epsilon: Test E -", - actual); - } - else - { - Assert.Equal( -@"Delta: Gamma: Test G -Delta: Epsilon: Test E -", - actual); - } - } - - [Fact] - public void AssemblyLoading_Delete() - { - StringBuilder sb = new StringBuilder(); - - var loader = new ShadowCopyAnalyzerAssemblyLoader(); - - var tempDir = Temp.CreateDirectory(); - var gammaCopy = tempDir.CreateFile("Gamma.dll").CopyContentFrom(_testFixture.Gamma.Path); - var deltaCopy = tempDir.CreateFile("Delta.dll").CopyContentFrom(_testFixture.Delta1.Path); - loader.AddDependencyLocation(deltaCopy.Path); - loader.AddDependencyLocation(gammaCopy.Path); - - Assembly gamma = loader.LoadFromPath(gammaCopy.Path); - var g = gamma.CreateInstance("Gamma.G"); - g!.GetType().GetMethod("Write")!.Invoke(g, new object[] { sb, "Test G" }); - - File.Delete(gammaCopy.Path); - File.Delete(deltaCopy.Path); - - var actual = sb.ToString(); - Assert.Equal( -@"Delta: Gamma: Test G -", - 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 92693c1d4ba10..0ab092ea97caa 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Reflection; using Roslyn.Utilities; @@ -23,18 +25,40 @@ internal abstract class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader { private readonly object _guard = new(); - // lock _guard to read/write - private readonly Dictionary _loadedAssembliesByPath = new(); + /// + /// Set of analyzer dependencies original full paths to the data calculated for that path + /// + /// + /// Access must be guarded by + /// + private readonly Dictionary _analyzerAssemblyInfoMap = new(); - // maps file name to a full path (lock _guard to read/write): + /// + /// Maps analyzer dependency simple names to the set of original full paths it was loaded from. This _only_ + /// tracks the paths provided to the analyzer as it's a place to look for indirect loads. + /// + /// + /// Access must be guarded by + /// private readonly Dictionary> _knownAssemblyPathsBySimpleName = new(StringComparer.OrdinalIgnoreCase); /// - /// Implemented by derived types to actually perform the load for an assembly that doesn't have a cached result. + /// The implementation needs to load an with the specified . The + /// parameter is the original path. It may be different than + /// as that is empty on .NET Core. /// - protected abstract Assembly LoadFromPathUncheckedImpl(string fullPath); + /// + /// This method should return an instance or throw. + /// + protected abstract Assembly Load(AssemblyName assemblyName, string assemblyOriginalPath); - #region Public API + internal bool IsAnalyzerDependencyPath(string fullPath) + { + lock (_guard) + { + return _analyzerAssemblyInfoMap.ContainsKey(fullPath); + } + } public void AddDependencyLocation(string fullPath) { @@ -52,57 +76,141 @@ public void AddDependencyLocation(string fullPath) { _knownAssemblyPathsBySimpleName[simpleName] = paths.Add(fullPath); } + + // Ensure that there is no cached Assembly information about this location. Long + // lived processes like VS and VBCSCompiler will see the same location added as + // a dependency many times. Each time have to assume there is new content on disk + // that needs to be considered. + _analyzerAssemblyInfoMap[fullPath] = null; } } - public Assembly LoadFromPath(string fullPath) + public Assembly LoadFromPath(string originalAnalyzerPath) { - CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath)); - return LoadFromPathUnchecked(fullPath); - } + CompilerPathUtilities.RequireAbsolutePath(originalAnalyzerPath, nameof(originalAnalyzerPath)); - #endregion + (AssemblyName? assemblyName, string _) = GetAssemblyInfoForPath(originalAnalyzerPath); + + // Not a managed assembly, nothing else to do + if (assemblyName is null) + { + throw new ArgumentException($"Not a valid assembly: {originalAnalyzerPath}"); + } + + try + { + return Load(assemblyName, originalAnalyzerPath); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unable to load {assemblyName.Name}", ex); + } + } /// - /// Returns the cached assembly for fullPath if we've done a load for this path before, or calls if - /// it needs to be loaded. This method skips the check in release builds that the path is an absolute path, hence the "Unchecked" in the name. + /// Get the and the path it should be loaded from for the given original + /// analyzer path /// - protected Assembly LoadFromPathUnchecked(string fullPath) + /// + /// This is used in the implementation of the loader instead of + /// because we only want information for registered paths. Using unregistered paths inside the + /// implementation should result in errors. + /// + protected (AssemblyName? AssemblyName, string RealAssemblyPath) GetAssemblyInfoForPath(string originalAnalyzerPath) { - Debug.Assert(PathUtilities.IsAbsolute(fullPath)); - - // Check if we have already loaded an assembly from the given path. - Assembly? loadedAssembly = null; lock (_guard) { - if (_loadedAssembliesByPath.TryGetValue(fullPath, out var existingAssembly)) + if (!_analyzerAssemblyInfoMap.TryGetValue(originalAnalyzerPath, out var tuple)) { - loadedAssembly = existingAssembly; + throw new InvalidOperationException(); + } + + if (tuple is { } info) + { + return info; } } - // Otherwise, load the assembly. - if (loadedAssembly == null) + string realPath = PreparePathToLoad(originalAnalyzerPath); + AssemblyName? assemblyName; + try { - loadedAssembly = LoadFromPathUncheckedImpl(fullPath); + assemblyName = AssemblyName.GetAssemblyName(realPath); + } + catch + { + // The above can fail when the assembly doesn't exist because it's corrupted, + // doesn't exist on disk, or is a native DLL. Those failures are handled when + // the actual load is attempted. Just record the failure now. + assemblyName = null; } - // Add the loaded assembly to the path cache. lock (_guard) { - _loadedAssembliesByPath[fullPath] = loadedAssembly; + _analyzerAssemblyInfoMap[originalAnalyzerPath] = (assemblyName, realPath); } - return loadedAssembly; + return (assemblyName, realPath); } - protected ImmutableHashSet? GetPaths(string simpleName) + /// + /// Return the best path for loading an assembly with the specified . This + /// return is a real path to load, not an original path. + /// + protected string? GetBestPath(AssemblyName assemblyName) { + if (assemblyName.Name is null) + { + return null; + } + + ImmutableHashSet? paths; lock (_guard) { - _knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out var paths); - return paths; + if (!_knownAssemblyPathsBySimpleName.TryGetValue(assemblyName.Name, out paths)) + { + return null; + } } + + // Sort the candidate paths by ordinal, to ensure determinism with the same inputs if you were to have + // multiple assemblies providing the same version. + string? bestPath = null; + AssemblyName? bestName = null; + foreach (var candidateOriginalPath in paths.OrderBy(StringComparer.Ordinal)) + { + (AssemblyName? candidateName, string candidateRealPath) = GetAssemblyInfoForPath(candidateOriginalPath); + if (candidateName is null) + { + continue; + } + + bool isMatch; +#if NETCOREAPP + isMatch = candidateName.Name == assemblyName.Name; +#else + isMatch = + candidateName.Name == assemblyName.Name && + candidateName.Version >= assemblyName.Version && + candidateName.GetPublicKeyToken().AsSpan().SequenceEqual(assemblyName.GetPublicKeyToken().AsSpan()); +#endif + + if (isMatch) + { + if (candidateName.Version == assemblyName.Version) + { + return candidateRealPath; + } + + if (bestName is null || candidateName.Version > bestName.Version) + { + bestPath = candidateRealPath; + bestName = candidateName; + } + } + } + + return bestPath; } /// @@ -110,9 +218,34 @@ protected Assembly LoadFromPathUnchecked(string fullPath) /// identified the context to load an assembly in, but before the assembly is actually /// loaded from disk. This is used to substitute out the original path with the shadow-copied version. /// - protected virtual string GetPathToLoad(string fullPath) + protected virtual string PreparePathToLoad(string fullPath) => fullPath; + + /// + /// When is overriden this returns the most recent + /// real path calculated for the + /// + internal string GetRealLoadPath(string originalFullPath) + { + lock (_guard) + { + if (!_analyzerAssemblyInfoMap.TryGetValue(originalFullPath, out var tuple)) + { + throw new InvalidOperationException($"Invalid original path: {originalFullPath}"); + } + + return tuple is { } value ? value.RealAssemblyPath : originalFullPath; + } + } + + internal (string OriginalAssemblyPath, string RealAssemblyPath)[] GetPathMapSnapshot() { - return fullPath; + lock (_guard) + { + return _analyzerAssemblyInfoMap + .Select(x => (x.Key, x.Value?.RealAssemblyPath ?? "")) + .OrderBy(x => x.Key) + .ToArray(); + } } } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Core.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Core.cs index de511b693541a..932ed5467726f 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Core.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Core.cs @@ -17,95 +17,43 @@ namespace Microsoft.CodeAnalysis { internal class DefaultAnalyzerAssemblyLoader : AnalyzerAssemblyLoader { - /// - ///

Typically a user analyzer has a reference to the compiler and some of the compiler's - /// dependencies such as System.Collections.Immutable. For the analyzer to correctly - /// interoperate with the compiler that created it, we need to ensure that we always use the - /// compiler's version of a given assembly over the analyzer's version.

- /// - ///

If we neglect to do this, then in the case where the user ships the compiler or its - /// dependencies in the analyzer's bin directory, we could end up loading a separate - /// instance of those assemblies in the process of loading the analyzer, which will surface - /// as a failure to load the analyzer.

- ///
- internal static readonly ImmutableHashSet CompilerAssemblySimpleNames = - ImmutableHashSet.Create( - StringComparer.OrdinalIgnoreCase, - "Microsoft.CodeAnalysis", - "Microsoft.CodeAnalysis.CSharp", - "Microsoft.CodeAnalysis.VisualBasic", - "System.Collections", - "System.Collections.Concurrent", - "System.Collections.Immutable", - "System.Console", - "System.Diagnostics.Debug", - "System.Diagnostics.StackTrace", - "System.Diagnostics.Tracing", - "System.IO.Compression", - "System.IO.FileSystem", - "System.Linq", - "System.Linq.Expressions", - "System.Memory", - "System.Reflection.Metadata", - "System.Reflection.Primitives", - "System.Resources.ResourceManager", - "System.Runtime", - "System.Runtime.CompilerServices.Unsafe", - "System.Runtime.Extensions", - "System.Runtime.InteropServices", - "System.Runtime.InteropServices.RuntimeInformation", - "System.Runtime.Loader", - "System.Runtime.Numerics", - "System.Runtime.Serialization.Primitives", - "System.Security.Cryptography.Algorithms", - "System.Security.Cryptography.Primitives", - "System.Text.Encoding.CodePages", - "System.Text.Encoding.Extensions", - "System.Text.RegularExpressions", - "System.Threading", - "System.Threading.Tasks", - "System.Threading.Tasks.Parallel", - "System.Threading.Thread", - "System.Threading.ThreadPool", - "System.Xml.ReaderWriter", - "System.Xml.XDocument", - "System.Xml.XPath.XDocument"); - - internal virtual ImmutableHashSet AssemblySimpleNamesToBeLoadedInCompilerContext => CompilerAssemblySimpleNames; - - // This is the context where compiler (and some of its dependencies) are being loaded into, which might be different from AssemblyLoadContext.Default. - private static readonly AssemblyLoadContext s_compilerLoadContext = AssemblyLoadContext.GetLoadContext(typeof(DefaultAnalyzerAssemblyLoader).GetTypeInfo().Assembly)!; - + private readonly AssemblyLoadContext _compilerLoadContext; private readonly object _guard = new object(); private readonly Dictionary _loadContextByDirectory = new Dictionary(StringComparer.Ordinal); - protected override Assembly LoadFromPathUncheckedImpl(string fullPath) + internal AssemblyLoadContext CompilerLoadContext => _compilerLoadContext; + + internal DefaultAnalyzerAssemblyLoader(AssemblyLoadContext? compilerLoadContext = null) + { + _compilerLoadContext = compilerLoadContext ?? AssemblyLoadContext.GetLoadContext(typeof(DefaultAnalyzerAssemblyLoader).GetTypeInfo().Assembly)!; + } + + protected override Assembly Load(AssemblyName assemblyName, string assemblyOriginalPath) { DirectoryLoadContext? loadContext; - var fullDirectoryPath = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException(message: null, paramName: nameof(fullPath)); + var fullDirectoryPath = Path.GetDirectoryName(assemblyOriginalPath) ?? throw new ArgumentException(message: null, paramName: nameof(assemblyOriginalPath)); lock (_guard) { if (!_loadContextByDirectory.TryGetValue(fullDirectoryPath, out loadContext)) { - loadContext = new DirectoryLoadContext(fullDirectoryPath, this, s_compilerLoadContext); + loadContext = new DirectoryLoadContext(fullDirectoryPath, this, _compilerLoadContext); _loadContextByDirectory[fullDirectoryPath] = loadContext; } } - var name = AssemblyName.GetAssemblyName(fullPath); - return loadContext.LoadFromAssemblyName(name); + return loadContext.LoadFromAssemblyName(assemblyName); } - internal static class TestAccessor + internal DirectoryLoadContext[] GetDirectoryLoadContextsSnapshot() { - public static AssemblyLoadContext[] GetOrderedLoadContexts(DefaultAnalyzerAssemblyLoader loader) + lock (_guard) { - return loader._loadContextByDirectory.Values.OrderBy(v => v.Directory).ToArray(); + return _loadContextByDirectory.Values.OrderBy(v => v.Directory).ToArray(); } } - private sealed class DirectoryLoadContext : AssemblyLoadContext + internal sealed class DirectoryLoadContext : AssemblyLoadContext { internal string Directory { get; } private readonly DefaultAnalyzerAssemblyLoader _loader; @@ -121,71 +69,50 @@ public DirectoryLoadContext(string directory, DefaultAnalyzerAssemblyLoader load protected override Assembly? Load(AssemblyName assemblyName) { var simpleName = assemblyName.Name!; - if (_loader.AssemblySimpleNamesToBeLoadedInCompilerContext.Contains(simpleName)) + try { - // Delegate to the compiler's load context to load the compiler or anything - // referenced by the compiler - return _compilerLoadContext.LoadFromAssemblyName(assemblyName); + if (_compilerLoadContext.LoadFromAssemblyName(assemblyName) is { } compilerAssembly) + { + return compilerAssembly; + } } - - var assemblyPath = Path.Combine(Directory, simpleName + ".dll"); - var paths = _loader.GetPaths(simpleName); - if (paths is null) + catch { - // The analyzer didn't explicitly register this dependency. Most likely the - // assembly we're trying to load here is netstandard or a similar framework - // assembly. In this case, we want to load it in compiler's ALC to avoid any - // potential type mismatch issue. Otherwise, if this is truly an unknown assembly, - // we assume both compiler and default ALC will fail to load it. - return _compilerLoadContext.LoadFromAssemblyName(assemblyName); + // Expected to happen when the assembly cannot be resolved in the compiler / host + // AssemblyLoadContext. } - Debug.Assert(paths.Any()); - // A matching assembly in this directory was specified via /analyzer. - if (paths.Contains(assemblyPath)) + // Prefer registered dependencies in the same directory first. + var assemblyPath = Path.Combine(Directory, simpleName + ".dll"); + if (_loader.IsAnalyzerDependencyPath(assemblyPath)) { - return LoadFromAssemblyPath(_loader.GetPathToLoad(assemblyPath)); + (_, var loadPath) = _loader.GetAssemblyInfoForPath(assemblyPath); + return LoadFromAssemblyPath(loadPath); } - 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) + // Next prefer registered dependencies from other directories. Ideally this would not + // be necessary but msbuild target defaults have caused a number of customers to + // fall into this path. See discussion here for where it comes up + // https://github.com/dotnet/roslyn/issues/56442 + if (_loader.GetBestPath(assemblyName) is string bestRealPath) { - // 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; - } + return LoadFromAssemblyPath(bestRealPath); } - Debug.Assert(bestCandidateName != null); - Debug.Assert(bestCandidatePath != null); - - return LoadFromAssemblyPath(_loader.GetPathToLoad(bestCandidatePath)); + // No analyzer registered this dependency. Time to fail + return null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { var assemblyPath = Path.Combine(Directory, unmanagedDllName + ".dll"); - var paths = _loader.GetPaths(unmanagedDllName); - if (paths is null || !paths.Contains(assemblyPath)) + if (_loader.IsAnalyzerDependencyPath(assemblyPath)) { - return IntPtr.Zero; + (_, var loadPath) = _loader.GetAssemblyInfoForPath(assemblyPath); + return LoadUnmanagedDllFromPath(loadPath); } - var pathToLoad = _loader.GetPathToLoad(assemblyPath); - return LoadUnmanagedDllFromPath(pathToLoad); + return IntPtr.Zero; } } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Desktop.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Desktop.cs index 64bd380497fb2..9e2685eb414fe 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Desktop.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.Desktop.cs @@ -5,11 +5,7 @@ #if !NETCOREAPP using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.IO; using System.Reflection; using System.Threading; using Roslyn.Utilities; @@ -30,46 +26,17 @@ namespace Microsoft.CodeAnalysis internal class DefaultAnalyzerAssemblyLoader : AnalyzerAssemblyLoader { private readonly object _guard = new(); - - private readonly Dictionary _loadedAssembliesByIdentity = new(); - private readonly Dictionary _loadedAssemblyIdentitiesByPath = new(); private bool _hookedAssemblyResolve; - protected override Assembly LoadFromPathUncheckedImpl(string fullPath) + internal DefaultAnalyzerAssemblyLoader() { - EnsureResolvedHooked(); - - AssemblyIdentity? identity; - - lock (_guard) - { - identity = GetOrAddAssemblyIdentity(fullPath); - if (identity != null && _loadedAssembliesByIdentity.TryGetValue(identity, out var existingAssembly)) - { - return existingAssembly; - } - } - - var pathToLoad = GetPathToLoad(fullPath); - var loadedAssembly = Assembly.LoadFrom(pathToLoad); - - lock (_guard) - { - identity ??= identity ?? AssemblyIdentity.FromAssemblyDefinition(loadedAssembly); + } - // The same assembly may be loaded from two different full paths (e.g. when loaded from GAC, etc.), - // or another thread might have loaded the assembly after we checked above. - if (_loadedAssembliesByIdentity.TryGetValue(identity, out var existingAssembly)) - { - loadedAssembly = existingAssembly; - } - else - { - _loadedAssembliesByIdentity.Add(identity, loadedAssembly); - } + protected override Assembly? Load(AssemblyName assemblyName, string _) + { + EnsureResolvedHooked(); - return loadedAssembly; - } + return AppDomain.CurrentDomain.Load(assemblyName); } internal bool EnsureResolvedHooked() @@ -78,7 +45,7 @@ internal bool EnsureResolvedHooked() { if (!_hookedAssemblyResolve) { - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve; _hookedAssemblyResolve = true; return true; } @@ -93,7 +60,7 @@ internal bool EnsureResolvedUnhooked() { if (_hookedAssemblyResolve) { - AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve; + AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolve; _hookedAssemblyResolve = false; return true; } @@ -102,106 +69,27 @@ internal bool EnsureResolvedUnhooked() return false; } - private Assembly? CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + private Assembly? AssemblyResolve(object sender, ResolveEventArgs args) { - // In the .NET Framework, if a handler to AssemblyResolve throws an exception, other handlers - // are not called. To avoid any bug in our handler breaking other handlers running in the same process - // we catch exceptions here. We do not expect exceptions to be thrown though. try { - return GetOrLoad(AppDomain.CurrentDomain.ApplyPolicy(args.Name)); - } - catch - { - return null; - } - } - - private AssemblyIdentity? GetOrAddAssemblyIdentity(string fullPath) - { - Debug.Assert(PathUtilities.IsAbsolute(fullPath)); - - lock (_guard) - { - if (_loadedAssemblyIdentitiesByPath.TryGetValue(fullPath, out var existingIdentity)) + var displayName = AppDomain.CurrentDomain.ApplyPolicy(args.Name); + var assemblyName = new AssemblyName(displayName); + string? bestPath = GetBestPath(assemblyName); + if (bestPath is not null) { - return existingIdentity; + return Assembly.LoadFrom(bestPath); } - } - - var identity = AssemblyIdentityUtils.TryGetAssemblyIdentity(fullPath); - lock (_guard) - { - if (_loadedAssemblyIdentitiesByPath.TryGetValue(fullPath, out var existingIdentity) && existingIdentity != null) - { - // Somebody else beat us, so used the cached value - identity = existingIdentity; - } - else - { - _loadedAssemblyIdentitiesByPath[fullPath] = identity; - } - } - - return identity; - } - - private Assembly? GetOrLoad(string displayName) - { - if (!AssemblyIdentity.TryParseDisplayName(displayName, out var requestedIdentity)) - { return null; } - - ImmutableHashSet candidatePaths; - lock (_guard) - { - - // First, check if this loader already loaded the requested assembly: - if (_loadedAssembliesByIdentity.TryGetValue(requestedIdentity, out var existingAssembly)) - { - return existingAssembly; - } - // Second, check if an assembly file of the same simple name was registered with the loader: - candidatePaths = GetPaths(requestedIdentity.Name); - if (candidatePaths is null) - { - return null; - } - - Debug.Assert(candidatePaths.Count > 0); - } - - // Find the highest version that satisfies the original request. We'll match for the highest version we can, assuming it - // actually matches the original request - string? bestPath = null; - Version? bestIdentityVersion = null; - - // Sort the candidate paths by ordinal, to ensure determinism with the same inputs if you were to have multiple assemblies - // providing the same version. - foreach (var candidatePath in candidatePaths.OrderBy(StringComparer.Ordinal)) - { - var candidateIdentity = GetOrAddAssemblyIdentity(candidatePath); - - if (candidateIdentity is not null && - candidateIdentity.Version >= requestedIdentity.Version && - candidateIdentity.PublicKeyToken.SequenceEqual(requestedIdentity.PublicKeyToken)) - { - if (bestIdentityVersion is null || candidateIdentity.Version > bestIdentityVersion) - { - bestPath = candidatePath; - bestIdentityVersion = candidateIdentity.Version; - } - } - } - - if (bestPath != null) + catch { - return LoadFromPathUnchecked(bestPath); + // In the .NET Framework, if a handler to AssemblyResolve throws an exception, other handlers + // are not called. To avoid any bug in our handler breaking other handlers running in the same process + // we catch exceptions here. We do not expect exceptions to be thrown though. + return null; } - - return null; } } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs index efe3c5e762e83..bba9e50f5d4af 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs @@ -3,11 +3,17 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +#if NETCOREAPP +using System.Runtime.Loader; +#endif + namespace Microsoft.CodeAnalysis { internal sealed class ShadowCopyAnalyzerAssemblyLoader : DefaultAnalyzerAssemblyLoader @@ -33,7 +39,19 @@ internal sealed class ShadowCopyAnalyzerAssemblyLoader : DefaultAnalyzerAssembly /// private int _assemblyDirectoryId; + internal string BaseDirectory => _baseDirectory; + +#if NETCOREAPP + public ShadowCopyAnalyzerAssemblyLoader(string? baseDirectory = null) + : this(null, baseDirectory) + { + } + + public ShadowCopyAnalyzerAssemblyLoader(AssemblyLoadContext? compilerLoadContext, string? baseDirectory = null) + : base(compilerLoadContext) +#else public ShadowCopyAnalyzerAssemblyLoader(string? baseDirectory = null) +#endif { if (baseDirectory != null) { @@ -95,10 +113,10 @@ private void DeleteLeftoverDirectories() } } - protected override string GetPathToLoad(string fullPath) + protected override string PreparePathToLoad(string originalFullPath) { string assemblyDirectory = CreateUniqueDirectoryForAssembly(); - string shadowCopyPath = CopyFileAndResources(fullPath, assemblyDirectory); + string shadowCopyPath = CopyFileAndResources(originalFullPath, assemblyDirectory); return shadowCopyPath; } diff --git a/src/Compilers/Core/Portable/FileSystem/CompilerPathUtilities.cs b/src/Compilers/Core/Portable/FileSystem/CompilerPathUtilities.cs index 1fed15a0f352f..cc28931b20c23 100644 --- a/src/Compilers/Core/Portable/FileSystem/CompilerPathUtilities.cs +++ b/src/Compilers/Core/Portable/FileSystem/CompilerPathUtilities.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; namespace Roslyn.Utilities diff --git a/src/Compilers/Server/VBCSCompiler/AnalyzerConsistencyChecker.cs b/src/Compilers/Server/VBCSCompiler/AnalyzerConsistencyChecker.cs index e9f5c24b08600..243509d75a2a2 100644 --- a/src/Compilers/Server/VBCSCompiler/AnalyzerConsistencyChecker.cs +++ b/src/Compilers/Server/VBCSCompiler/AnalyzerConsistencyChecker.cs @@ -56,8 +56,7 @@ private static bool CheckCore( IEnumerable analyzerReferences, IAnalyzerAssemblyLoader loader, ICompilerServerLogger? logger, - [NotNullWhen(false)] - out List? errorMessages) + [NotNullWhen(false)] out List? errorMessages) { errorMessages = null; var resolvedPaths = new List(); diff --git a/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs b/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs index 296e8d6ff91e6..f4a40e59aa6d4 100644 --- a/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs +++ b/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs @@ -41,7 +41,7 @@ public RunRequest(Guid requestId, string language, string? workingDirectory, str internal sealed class CompilerServerHost : ICompilerServerHost { - public IAnalyzerAssemblyLoader AnalyzerAssemblyLoader { get; } = new ShadowCopyAnalyzerAssemblyLoader(Path.Combine(Path.GetTempPath(), "VBCSCompiler", "AnalyzerAssemblyLoader")); + public IAnalyzerAssemblyLoader AnalyzerAssemblyLoader { get; } = new ShadowCopyAnalyzerAssemblyLoader(baseDirectory: Path.Combine(Path.GetTempPath(), "VBCSCompiler", "AnalyzerAssemblyLoader")); public static Func SharedAssemblyReferenceProvider { get; } = (path, properties) => new CachingMetadataReference(path, properties); diff --git a/src/Compilers/Server/VBCSCompilerTests/VBCSCompilerServerTests.cs b/src/Compilers/Server/VBCSCompilerTests/VBCSCompilerServerTests.cs index f16131c698c1f..9ab19b4b2724a 100644 --- a/src/Compilers/Server/VBCSCompilerTests/VBCSCompilerServerTests.cs +++ b/src/Compilers/Server/VBCSCompilerTests/VBCSCompilerServerTests.cs @@ -38,7 +38,7 @@ public class StartupTests : VBCSCompilerServerTests public async Task ShadowCopyAnalyzerAssemblyLoaderMissingDirectory() { var baseDirectory = Path.Combine(Path.GetTempPath(), TestBase.GetUniqueName()); - var loader = new ShadowCopyAnalyzerAssemblyLoader(baseDirectory); + var loader = new ShadowCopyAnalyzerAssemblyLoader(baseDirectory: baseDirectory); var task = loader.DeleteLeftoverDirectoriesTask; await task; Assert.False(task.IsFaulted); diff --git a/src/Compilers/Test/Core/AssemblyLoadTestFixture.cs b/src/Compilers/Test/Core/AssemblyLoadTestFixture.cs index f69db8712ea87..0a2b84e7a37d9 100644 --- a/src/Compilers/Test/Core/AssemblyLoadTestFixture.cs +++ b/src/Compilers/Test/Core/AssemblyLoadTestFixture.cs @@ -19,6 +19,8 @@ public sealed class AssemblyLoadTestFixture : IDisposable private readonly TempRoot _temp; private readonly TempDirectory _directory; + public TempDirectory TempDirectory => _directory; + /// /// An assembly with no references, assembly version 1. /// diff --git a/src/Compilers/Test/Core/Platform/Desktop/AppDomainTestOutputHelper.cs b/src/Compilers/Test/Core/Platform/Desktop/AppDomainTestOutputHelper.cs new file mode 100644 index 0000000000000..c704a80a2c1c8 --- /dev/null +++ b/src/Compilers/Test/Core/Platform/Desktop/AppDomainTestOutputHelper.cs @@ -0,0 +1,33 @@ +// 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. + +#if NET472 +using System; +using System.IO; +using System.Reflection; +using Xunit.Abstractions; + +namespace Roslyn.Test.Utilities.Desktop; + +/// +/// Allows using an across +/// instances +/// +public sealed class AppDomainTestOutputHelper : MarshalByRefObject, ITestOutputHelper +{ + public ITestOutputHelper TestOutputHelper { get; } + + public AppDomainTestOutputHelper(ITestOutputHelper testOutputHelper) + { + TestOutputHelper = testOutputHelper; + } + + public void WriteLine(string message) => + TestOutputHelper.WriteLine(message); + + public void WriteLine(string format, params object[] args) => + TestOutputHelper.WriteLine(format, args); +} + +#endif diff --git a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs index 756088babd255..f49d5859f0a70 100644 --- a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs +++ b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs @@ -705,8 +705,8 @@ private static AnalyzerFileReference CreateShadowCopiedAnalyzerReference(TempRoo private class MissingAnalyzerLoader : AnalyzerAssemblyLoader { - protected override Assembly LoadFromPathUncheckedImpl(string fullPath) - => throw new FileNotFoundException(fullPath); + protected override Assembly Load(AssemblyName assemblyName, string assemblyOriginalPath) => + throw new FileNotFoundException(assemblyOriginalPath); } private class MissingMetadataReference : PortableExecutableReference diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs index 9df2a92ed1292..4aebeac233b94 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs @@ -21,30 +21,10 @@ public RemoteAnalyzerAssemblyLoader(string baseDirectory) _baseDirectory = baseDirectory; } - protected override string GetPathToLoad(string fullPath) + protected override string PreparePathToLoad(string fullPath) { var fixedPath = Path.GetFullPath(Path.Combine(_baseDirectory, Path.GetFileName(fullPath))); return File.Exists(fixedPath) ? fixedPath : fullPath; } - -#if NETCOREAPP - - // The following are special assemblies since they contain IDE analyzers and/or their dependencies, - // but in the meantime, they also contain the host of compiler in remote process. Therefore on coreclr, - // we must ensure they are only loaded once and in the same ALC compiler asemblies are loaded into. - // Otherwise these analyzers will fail to interoperate with the host due to mismatch in assembly identity. - private static readonly ImmutableHashSet s_ideAssemblySimpleNames = - CompilerAssemblySimpleNames.Union(new[] - { - "Microsoft.CodeAnalysis.Features", - "Microsoft.CodeAnalysis.CSharp.Features", - "Microsoft.CodeAnalysis.VisualBasic.Features", - "Microsoft.CodeAnalysis.Workspaces", - "Microsoft.CodeAnalysis.CSharp.Workspaces", - "Microsoft.CodeAnalysis.VisualBasic.Workspaces", - }); - - internal override ImmutableHashSet AssemblySimpleNamesToBeLoadedInCompilerContext => s_ideAssemblySimpleNames; -#endif } } diff --git a/src/Workspaces/Remote/ServiceHubTest/RemoteAnalyzerAssemblyLoaderTests.cs b/src/Workspaces/Remote/ServiceHubTest/RemoteAnalyzerAssemblyLoaderTests.cs index c0cfdeeafa5c0..944d020fba37b 100644 --- a/src/Workspaces/Remote/ServiceHubTest/RemoteAnalyzerAssemblyLoaderTests.cs +++ b/src/Workspaces/Remote/ServiceHubTest/RemoteAnalyzerAssemblyLoaderTests.cs @@ -7,6 +7,7 @@ using System.Runtime.Loader; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Remote.Diagnostics; +using Microsoft.CodeAnalysis.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.Remote.UnitTests @@ -16,6 +17,7 @@ public class RemoteAnalyzerAssemblyLoaderTests [Fact] public void NonIdeAnalyzerAssemblyShouldBeLoadedInSeparateALC() { + using var testFixture = new AssemblyLoadTestFixture(); var remoteAssemblyInCurrentAlc = typeof(RemoteAnalyzerAssemblyLoader).GetTypeInfo().Assembly; var remoteAssemblyLocation = remoteAssemblyInCurrentAlc.Location; @@ -24,8 +26,8 @@ public void NonIdeAnalyzerAssemblyShouldBeLoadedInSeparateALC() // Try to load MS.CA.Remote.ServiceHub.dll as an analyzer assembly via RemoteAnalyzerAssemblyLoader // since it's not one of the special assemblies listed in RemoteAnalyzerAssemblyLoader, // RemoteAnalyzerAssemblyLoader should loaded in a spearate DirectoryLoadContext. - loader.AddDependencyLocation(remoteAssemblyLocation); - var remoteAssemblyLoadedViaRemoteLoader = loader.LoadFromPath(remoteAssemblyLocation); + loader.AddDependencyLocation(testFixture.Delta1.Path); + var remoteAssemblyLoadedViaRemoteLoader = loader.LoadFromPath(testFixture.Delta1.Path); var alc1 = AssemblyLoadContext.GetLoadContext(remoteAssemblyInCurrentAlc); var alc2 = AssemblyLoadContext.GetLoadContext(remoteAssemblyLoadedViaRemoteLoader);