Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/Sandbox/ConsoleApp.SourceGenerator.Tests/AssemblyResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;

namespace ConsoleApp.SourceGenerator.Tests
{
/// <summary>
/// The hoist's assembly resolver is process-wide, and a compiler server hosts the analyzers of every project
/// it ever built. So the resolver meets assemblies it was not built with: another build of the same generator,
/// or another generator that embeds a different version of the same dependency.
/// </summary>
public class AssemblyResolverTests
{
public AssemblyResolverTests()
{
// The resolver attaches in the hoist's module initializer.
RuntimeHelpers.RunModuleConstructor(typeof(ConsoleAppSourceGeneratorHoist).Module.ModuleHandle);
}

[Fact]
public void Embedded_Assembly_Resolves_When_An_Assembly_With_The_Same_Name_Was_Loaded_First()
{
// An older build of the generator, without embedded assemblies, takes the simple name first.
LoadInOwnContext(Emit("ShadowedGenerator", "1.0.0.0"));
byte[] dependency = Emit("ShadowedDependency", "2.0.0.0");
LoadInOwnContext(Emit("ShadowedGenerator", "2.0.0.0", ("SGF.Assembly::ShadowedDependency.dll", dependency)));

Assembly resolved = Assembly.Load(new AssemblyName("ShadowedDependency, Version=2.0.0.0"));

Assert.Equal(new Version(2, 0, 0, 0), resolved.GetName().Version);
}

[Fact]
public void Embedded_Assembly_Resolves_In_The_Requested_Version()
{
byte[] older = Emit("SharedDependency", "1.0.0.0");
byte[] newer = Emit("SharedDependency", "2.0.0.0");
LoadInOwnContext(Emit("OlderGenerator", "1.0.0.0", ("SGF.Assembly::SharedDependency.dll", older)));
LoadInOwnContext(Emit("NewerGenerator", "1.0.0.0", ("SGF.Assembly::SharedDependency.dll", newer)));

Assembly resolved = Assembly.Load(new AssemblyName("SharedDependency, Version=2.0.0.0"));

Assert.Equal(new Version(2, 0, 0, 0), resolved.GetName().Version);
}

[Fact]
public void Embedded_Assembly_Resolves_For_A_Request_Without_A_Version()
{
byte[] dependency = Emit("UnversionedDependency", "3.0.0.0");
LoadInOwnContext(Emit("UnversionedGenerator", "1.0.0.0", ("SGF.Assembly::UnversionedDependency.dll", dependency)));

Assembly resolved = Assembly.Load(new AssemblyName("UnversionedDependency"));

Assert.Equal(new Version(3, 0, 0, 0), resolved.GetName().Version);
}

[Fact]
public void Request_For_An_Older_Version_Resolves_To_The_Newer_One_Loaded()
{
byte[] dependency = Emit("UnifiedDependency", "2.0.0.0");
LoadInOwnContext(Emit("UnifyingGenerator", "1.0.0.0", ("SGF.Assembly::UnifiedDependency.dll", dependency)));

Assembly resolved = Assembly.Load(new AssemblyName("UnifiedDependency, Version=1.0.0.0"));

Assert.Equal(new Version(2, 0, 0, 0), resolved.GetName().Version);
}

[Fact]
public void Request_For_A_Newer_Version_Does_Not_Resolve_To_An_Older_One()
{
byte[] dependency = Emit("OutdatedDependency", "1.0.0.0");
LoadInOwnContext(Emit("OutdatedGenerator", "1.0.0.0", ("SGF.Assembly::OutdatedDependency.dll", dependency)));

Assert.Throws<FileNotFoundException>(() => Assembly.Load(new AssemblyName("OutdatedDependency, Version=2.0.0.0")));
}

private static byte[] Emit(string name, string version, params (string Name, byte[] Content)[] resources)
{
CSharpCompilation compilation = CSharpCompilation.Create(
name,
[CSharpSyntaxTree.ParseText($"[assembly: System.Reflection.AssemblyVersion(\"{version}\")]")],
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

using MemoryStream stream = new();
EmitResult result = compilation.Emit(
stream,
manifestResources: resources.Select(r => new ResourceDescription(r.Name, () => new MemoryStream(r.Content), isPublic: true)));
Assert.True(result.Success, string.Join(Environment.NewLine, result.Diagnostics));
return stream.ToArray();
}

// Roslyn loads each analyzer directory in a context of its own; the resolver sees them all through the AppDomain.
private static void LoadInOwnContext(byte[] image)
=> new AssemblyLoadContext(name: null).LoadFromStream(new MemoryStream(image));
}
}
7 changes: 5 additions & 2 deletions src/SourceGenerator.Foundations/Templates/AssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ public static SourceText Render(string @namespace) => SourceText.From($$"""
namespace {{@namespace}}
{
/// <summary>
/// Used to compare two <see cref="AssemblyName"/> to pull them out of the dictionary of types
/// Used to compare two <see cref="AssemblyName"/> to pull them out of the dictionary of types. Compares
/// name and version: one process (a compiler server, an IDE) hosts several generators, and several builds
/// of one, side by side. By name alone the first one loaded shadows the rest, and a generator gets a
/// dependency it was not built against, or none at all.
/// </summary>
internal class AssemblyNameComparer : IEqualityComparer<AssemblyName>
{
Expand All @@ -36,7 +39,7 @@ public bool Equals(AssemblyName? x, AssemblyName? y)
return false;
}

return string.Equals(GetName(x!), GetName(y!));
return string.Equals(GetName(x!), GetName(y!)) && x!.Version == y!.Version;
}

public int GetHashCode(AssemblyName obj)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ private static void AddAssembly(Assembly assembly)
return assembly;
}

// No exact version: the newest one loaded that still satisfies the request, as the runtime binds a
// reference to a newer version. Never an older one: it can lack what the caller was built against.
Assembly? newest = null;
Version newestVersion = new(0, 0);
foreach (KeyValuePair<AssemblyName, Assembly> loaded in s_loadedAssemblies)
{
Version version = loaded.Key.Version ?? new Version(0, 0);
bool sameName = string.Equals(loaded.Key.Name, assemblyName.Name);
bool satisfies = assemblyName.Version == null || version >= assemblyName.Version;
if (sameName && satisfies && (newest == null || version > newestVersion))
{
newest = loaded.Value;
newestVersion = version;
}
}

if (newest != null)
{
return newest;
}

foreach (Assembly loadedAssembly in s_assembliesWithResources)
{
string resourceName = $"SGF.Assembly::{assemblyName.Name}.dll";
Expand Down
Loading