From 633657899c15c9388235839599131ab476dba530 Mon Sep 17 00:00:00 2001 From: David Obando Date: Fri, 24 Jul 2026 10:33:06 -0700 Subject: [PATCH 1/2] Guard imported MemberRef assembly identity Document the declaring-assembly invariant and cover colliding extension-host type names with metadata and runtime assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5a96d2bd-106a-4cb2-87ee-cef68a8a77f9 --- .../Emit/ImportedMemberRefFactory.cs | 4 + ...ortedExtensionAssemblyIdentityEmitTests.cs | 198 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 test/Compiler.Tests/Emit/Issue2801ImportedExtensionAssemblyIdentityEmitTests.cs diff --git a/src/Core/CodeAnalysis/Emit/ImportedMemberRefFactory.cs b/src/Core/CodeAnalysis/Emit/ImportedMemberRefFactory.cs index 6caca8ef..728fcefa 100644 --- a/src/Core/CodeAnalysis/Emit/ImportedMemberRefFactory.cs +++ b/src/Core/CodeAnalysis/Emit/ImportedMemberRefFactory.cs @@ -489,6 +489,10 @@ internal TypeReferenceHandle GetTypeReference(Type type) return existing; } + // Issue #2801: do not remap the reflected type through ReferenceResolver + // by FullName here. MemberRef parents must retain the declaring type's + // assembly identity when two references define the same type name. + // Nested types: resolution scope is the TypeRef of the declaring type, // namespace is empty, name is the short name only. Works for the // open generic definition of a nested generic type as well (Reflection diff --git a/test/Compiler.Tests/Emit/Issue2801ImportedExtensionAssemblyIdentityEmitTests.cs b/test/Compiler.Tests/Emit/Issue2801ImportedExtensionAssemblyIdentityEmitTests.cs new file mode 100644 index 00000000..9b6fd0d4 --- /dev/null +++ b/test/Compiler.Tests/Emit/Issue2801ImportedExtensionAssemblyIdentityEmitTests.cs @@ -0,0 +1,198 @@ +// +// Copyright (C) GSharp Authors. All rights reserved. +// + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace GSharp.Compiler.Tests.Emit; + +public sealed class Issue2801ImportedExtensionAssemblyIdentityEmitTests +{ + private const string PrimarySource = """ + namespace ClassLibrary1; + + public sealed class A { } + + public static class B + { + public static string Call(this A value) => "correct"; + } + """; + + private const string ShadowSource = """ + namespace ClassLibrary1; + + public static class B + { + public static string Shadow(this A value) => "shadow"; + } + """; + + private const string GSharpSource = """ + package MyApp + + import System + import ClassLibrary1 + + var a = A() + Console.WriteLine(a.Call()) + """; + + [Fact] + public void ImportedExtension_WithCollidingHostTypeName_ReferencesDeclaringAssemblyAndRuns() + { + using var artifacts = Compile(); + + IlVerifier.Verify( + artifacts.OutputPath, + additionalReferences: new[] { artifacts.PrimaryPath, artifacts.ShadowPath }); + + using var stream = File.OpenRead(artifacts.OutputPath); + using var peReader = new PEReader(stream); + var reader = peReader.GetMetadataReader(); + var callHandle = Assert.Single( + reader.MemberReferences, + handle => reader.GetString(reader.GetMemberReference(handle).Name) == "Call"); + var call = reader.GetMemberReference(callHandle); + var parent = reader.GetTypeReference((TypeReferenceHandle)call.Parent); + var assembly = reader.GetAssemblyReference((AssemblyReferenceHandle)parent.ResolutionScope); + + Assert.Equal("ClassLibrary1", reader.GetString(assembly.Name)); + Assert.Equal("correct\n", Run(artifacts.OutputPath)); + } + + private static CompilationArtifacts Compile() + { + var directory = Path.Combine( + AppContext.BaseDirectory, + "issue2801-artifacts", + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + + var primaryPath = Path.Combine(directory, "ClassLibrary1.dll"); + EmitFixture("ClassLibrary1", PrimarySource, primaryPath); + var shadowPath = Path.Combine(directory, "AClassLibrary3.dll"); + EmitFixture( + "AClassLibrary3", + ShadowSource, + shadowPath, + new[] { MetadataReference.CreateFromFile(primaryPath) }); + + var sourcePath = Path.Combine(directory, "Program.gs"); + var outputPath = Path.Combine(directory, "MyApp.dll"); + File.WriteAllText(sourcePath, GSharpSource); + + using var stdout = new StringWriter(); + using var stderr = new StringWriter(); + var previousOut = Console.Out; + var previousError = Console.Error; + Console.SetOut(stdout); + Console.SetError(stderr); + int exitCode; + try + { + exitCode = Program.Main( + new[] + { + "/out:" + outputPath, + "/target:exe", + "/targetframework:net10.0", + "/reference:" + shadowPath, + "/reference:" + primaryPath, + sourcePath, + }); + } + finally + { + Console.SetOut(previousOut); + Console.SetError(previousError); + } + + var diagnostics = stdout.ToString() + stderr.ToString(); + Assert.True(exitCode == 0, $"gsc failed:\n{diagnostics}"); + return new CompilationArtifacts(directory, primaryPath, shadowPath, outputPath); + } + + private static void EmitFixture( + string assemblyName, + string source, + string path, + MetadataReference[] additionalReferences = null) + { + var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) + .Split(Path.PathSeparator) + .Select(reference => MetadataReference.CreateFromFile(reference)) + .Concat(additionalReferences ?? Array.Empty()); + var compilation = CSharpCompilation.Create( + assemblyName, + new[] { CSharpSyntaxTree.ParseText(source) }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var result = compilation.Emit(path); + Assert.True(result.Success, string.Join(Environment.NewLine, result.Diagnostics)); + } + + private static string Run(string assemblyPath) + { + var startInfo = new ProcessStartInfo("dotnet") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + WorkingDirectory = Path.GetDirectoryName(assemblyPath)!, + }; + startInfo.ArgumentList.Add("exec"); + startInfo.ArgumentList.Add("--runtimeconfig"); + startInfo.ArgumentList.Add(Path.ChangeExtension(assemblyPath, ".runtimeconfig.json")); + startInfo.ArgumentList.Add(assemblyPath); + + using var process = Process.Start(startInfo)!; + var stdout = process.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + Assert.True(process.WaitForExit(30_000), "dotnet exec timed out."); + Assert.True(process.ExitCode == 0, $"exited {process.ExitCode}\n{stderr}"); + return stdout.Replace("\r\n", "\n", StringComparison.Ordinal); + } + + private sealed class CompilationArtifacts : IDisposable + { + public CompilationArtifacts( + string directory, + string primaryPath, + string shadowPath, + string outputPath) + { + Directory = directory; + PrimaryPath = primaryPath; + ShadowPath = shadowPath; + OutputPath = outputPath; + } + + public string Directory { get; } + + public string PrimaryPath { get; } + + public string ShadowPath { get; } + + public string OutputPath { get; } + + public void Dispose() + { + try + { + System.IO.Directory.Delete(Directory, recursive: true); + } + catch + { + } + } + } +} From 8db0dd5b8755fdc3d5d2360dfdc00d97a7305cd1 Mon Sep 17 00:00:00 2001 From: David Obando Date: Fri, 24 Jul 2026 11:06:31 -0700 Subject: [PATCH 2/2] Strengthen imported extension assembly invariant coverage Replace the issue-specific fix framing with reference-order-independent metadata and runtime coverage for colliding extension host type names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5a96d2bd-106a-4cb2-87ee-cef68a8a77f9 --- .../Emit/ImportedMemberRefFactory.cs | 4 --- ...ionDeclaringAssemblyInvariantEmitTests.cs} | 29 +++++++++++++------ 2 files changed, 20 insertions(+), 13 deletions(-) rename test/Compiler.Tests/Emit/{Issue2801ImportedExtensionAssemblyIdentityEmitTests.cs => ImportedExtensionDeclaringAssemblyInvariantEmitTests.cs} (83%) diff --git a/src/Core/CodeAnalysis/Emit/ImportedMemberRefFactory.cs b/src/Core/CodeAnalysis/Emit/ImportedMemberRefFactory.cs index 728fcefa..6caca8ef 100644 --- a/src/Core/CodeAnalysis/Emit/ImportedMemberRefFactory.cs +++ b/src/Core/CodeAnalysis/Emit/ImportedMemberRefFactory.cs @@ -489,10 +489,6 @@ internal TypeReferenceHandle GetTypeReference(Type type) return existing; } - // Issue #2801: do not remap the reflected type through ReferenceResolver - // by FullName here. MemberRef parents must retain the declaring type's - // assembly identity when two references define the same type name. - // Nested types: resolution scope is the TypeRef of the declaring type, // namespace is empty, name is the short name only. Works for the // open generic definition of a nested generic type as well (Reflection diff --git a/test/Compiler.Tests/Emit/Issue2801ImportedExtensionAssemblyIdentityEmitTests.cs b/test/Compiler.Tests/Emit/ImportedExtensionDeclaringAssemblyInvariantEmitTests.cs similarity index 83% rename from test/Compiler.Tests/Emit/Issue2801ImportedExtensionAssemblyIdentityEmitTests.cs rename to test/Compiler.Tests/Emit/ImportedExtensionDeclaringAssemblyInvariantEmitTests.cs index 9b6fd0d4..b8c0307a 100644 --- a/test/Compiler.Tests/Emit/Issue2801ImportedExtensionAssemblyIdentityEmitTests.cs +++ b/test/Compiler.Tests/Emit/ImportedExtensionDeclaringAssemblyInvariantEmitTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright (C) GSharp Authors. All rights reserved. // @@ -14,7 +14,12 @@ namespace GSharp.Compiler.Tests.Emit; -public sealed class Issue2801ImportedExtensionAssemblyIdentityEmitTests +/// +/// Verifies that an imported extension method's MemberRef retains its +/// assembly identity +/// when another reference defines the same fully-qualified host type. +/// +public sealed class ImportedExtensionDeclaringAssemblyInvariantEmitTests { private const string PrimarySource = """ namespace ClassLibrary1; @@ -46,10 +51,12 @@ import ClassLibrary1 Console.WriteLine(a.Call()) """; - [Fact] - public void ImportedExtension_WithCollidingHostTypeName_ReferencesDeclaringAssemblyAndRuns() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MemberRef_PreservesDeclaringAssembly_RegardlessOfReferenceOrder(bool shadowReferenceFirst) { - using var artifacts = Compile(); + using var artifacts = Compile(shadowReferenceFirst); IlVerifier.Verify( artifacts.OutputPath, @@ -62,18 +69,20 @@ public void ImportedExtension_WithCollidingHostTypeName_ReferencesDeclaringAssem reader.MemberReferences, handle => reader.GetString(reader.GetMemberReference(handle).Name) == "Call"); var call = reader.GetMemberReference(callHandle); + Assert.Equal(HandleKind.TypeReference, call.Parent.Kind); var parent = reader.GetTypeReference((TypeReferenceHandle)call.Parent); + Assert.Equal(HandleKind.AssemblyReference, parent.ResolutionScope.Kind); var assembly = reader.GetAssemblyReference((AssemblyReferenceHandle)parent.ResolutionScope); Assert.Equal("ClassLibrary1", reader.GetString(assembly.Name)); Assert.Equal("correct\n", Run(artifacts.OutputPath)); } - private static CompilationArtifacts Compile() + private static CompilationArtifacts Compile(bool shadowReferenceFirst) { var directory = Path.Combine( AppContext.BaseDirectory, - "issue2801-artifacts", + "imported-extension-assembly-identity-artifacts", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(directory); @@ -99,14 +108,16 @@ private static CompilationArtifacts Compile() int exitCode; try { + var firstReference = shadowReferenceFirst ? shadowPath : primaryPath; + var secondReference = shadowReferenceFirst ? primaryPath : shadowPath; exitCode = Program.Main( new[] { "/out:" + outputPath, "/target:exe", "/targetframework:net10.0", - "/reference:" + shadowPath, - "/reference:" + primaryPath, + "/reference:" + firstReference, + "/reference:" + secondReference, sourcePath, }); }