From 52ce103ff1ea44c3b1d49922e5b3331020d85e68 Mon Sep 17 00:00:00 2001 From: Ivan Migalev Date: Sat, 20 Dec 2025 17:39:17 +0100 Subject: [PATCH 1/6] (#54) Add test for a type explicitly implementing an internal interface Co-authored-by: Alexander Ulitin --- tests/Refasmer.Tests/IntegrationTests.cs | 1 + .../InternalInteraceImplBug.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/RefasmerTestAssembly/InternalInteraceImplBug.cs diff --git a/tests/Refasmer.Tests/IntegrationTests.cs b/tests/Refasmer.Tests/IntegrationTests.cs index 1b826d3..45b56be 100644 --- a/tests/Refasmer.Tests/IntegrationTests.cs +++ b/tests/Refasmer.Tests/IntegrationTests.cs @@ -32,6 +32,7 @@ public async Task CheckRefasmedType(string typeName) [TestCase("RefasmerTestAssembly.CustomEnumerable")] [TestCase("RefasmerTestAssembly.EnumType")] [TestCase("RefasmerTestAssembly.InternalEnumType")] + [TestCase("RefasmerTestAssembly.InternalInterfaceImplBug")] public async Task CheckRefasmedTypeOmitNonApi(string typeName) { var assemblyPath = await BuildTestAssembly(); diff --git a/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs b/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs new file mode 100644 index 0000000..421ef6e --- /dev/null +++ b/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs @@ -0,0 +1,15 @@ +namespace RefasmerTestAssembly; + +public class Class1 : IInternalInterace +{ + void IInternalInterace.Method(string x, Internal2 y) + { + } +} + +internal class Internal2{} + +internal interface IInternalInterace +{ + void Method(T x, Internal2 y); +} From 8ce191b7de7b1b627a3f3c3e2ae31bf549f09457 Mon Sep 17 00:00:00 2001 From: Ivan Migalev Date: Sat, 20 Dec 2025 17:57:27 +0100 Subject: [PATCH 2/6] (#54) Renames + improve test logging --- tests/Refasmer.Tests/ExitCodeTests.cs | 7 +++++-- tests/Refasmer.Tests/IntegrationTestBase.cs | 16 +++++++++++----- tests/Refasmer.Tests/IntegrationTests.cs | 2 +- .../InternalInteraceImplBug.cs | 6 +++--- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/tests/Refasmer.Tests/ExitCodeTests.cs b/tests/Refasmer.Tests/ExitCodeTests.cs index ee847f8..de52b7f 100644 --- a/tests/Refasmer.Tests/ExitCodeTests.cs +++ b/tests/Refasmer.Tests/ExitCodeTests.cs @@ -20,10 +20,13 @@ private async Task DoTest(int expectedCode, params string[] additionalArgs) var outputPath = Path.GetTempFileName(); using var collector = CollectConsoleOutput(); var exitCode = ExecuteRefasmAndGetExitCode(assemblyPath, outputPath, additionalArgs); + if (exitCode != expectedCode) + { + await TestContext.Out.WriteLineAsync($"StdOut:\n{collector.StdOut}\nStdErr: {collector.StdErr}"); + } Assert.That( exitCode, Is.EqualTo(expectedCode), - $"Refasmer returned exit code {exitCode}, while {expectedCode} was expected." + - $" StdOut:\n{collector.StdOut}\nStdErr: {collector.StdErr}"); + $"Refasmer returned exit code {exitCode}, while {expectedCode} was expected."); } } diff --git a/tests/Refasmer.Tests/IntegrationTestBase.cs b/tests/Refasmer.Tests/IntegrationTestBase.cs index 91a4b7e..c1ad42b 100644 --- a/tests/Refasmer.Tests/IntegrationTestBase.cs +++ b/tests/Refasmer.Tests/IntegrationTestBase.cs @@ -16,10 +16,15 @@ protected static async Task BuildTestAssembly() Console.WriteLine($"Building project {testProject}…"); var result = await Command.Run("dotnet", "build", testProject, "--configuration", "Release").Task; + if (result.ExitCode != 0) + { + await TestContext.Out.WriteLineAsync($"StdOut:\n{result.StandardOutput}\nStdErr: {result.StandardError}"); + } + Assert.That( result.ExitCode, Is.EqualTo(0), - $"Failed to build test assembly, exit code {result.ExitCode}. StdOut:\n{result.StandardOutput}\nStdErr: {result.StandardError}"); + $"Failed to build test assembly, exit code {result.ExitCode}."); return Path.Combine( root, @@ -67,10 +72,11 @@ protected static string RefasmTestAssembly(string assemblyPath, bool omitNonApiM var options = new[]{ "--omit-non-api-members", omitNonApiMembers.ToString(CultureInfo.InvariantCulture) }; using var collector = CollectConsoleOutput(); var exitCode = ExecuteRefasmAndGetExitCode(assemblyPath, outputPath, options); - Assert.That( - exitCode, - Is.EqualTo(0), - $"Refasmer returned exit code {exitCode}. StdOut:\n{collector.StdOut}\nStdErr: {collector.StdErr}"); + if (exitCode != 0) + { + TestContext.Out.WriteLine($"Refasmer returned exit code {exitCode}. StdOut:\n{collector.StdOut}\nStdErr: {collector.StdErr}"); + } + Assert.That(exitCode, Is.EqualTo(0)); return outputPath; } diff --git a/tests/Refasmer.Tests/IntegrationTests.cs b/tests/Refasmer.Tests/IntegrationTests.cs index 45b56be..3431017 100644 --- a/tests/Refasmer.Tests/IntegrationTests.cs +++ b/tests/Refasmer.Tests/IntegrationTests.cs @@ -32,7 +32,7 @@ public async Task CheckRefasmedType(string typeName) [TestCase("RefasmerTestAssembly.CustomEnumerable")] [TestCase("RefasmerTestAssembly.EnumType")] [TestCase("RefasmerTestAssembly.InternalEnumType")] - [TestCase("RefasmerTestAssembly.InternalInterfaceImplBug")] + [TestCase("RefasmerTestAssembly.ExplicitImplOfInternalInterface")] public async Task CheckRefasmedTypeOmitNonApi(string typeName) { var assemblyPath = await BuildTestAssembly(); diff --git a/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs b/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs index 421ef6e..7f3a26a 100644 --- a/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs +++ b/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs @@ -1,15 +1,15 @@ namespace RefasmerTestAssembly; -public class Class1 : IInternalInterace +public class ExplicitImplOfInternalInterface : IInternalInterface { - void IInternalInterace.Method(string x, Internal2 y) + void IInternalInterface.Method(string x, Internal2 y) { } } internal class Internal2{} -internal interface IInternalInterace +internal interface IInternalInterface { void Method(T x, Internal2 y); } From 032cf5e751db8b99d2996fd6328032990aa83ff5 Mon Sep 17 00:00:00 2001 From: Ivan Migalev Date: Sat, 20 Dec 2025 19:14:13 +0100 Subject: [PATCH 3/6] .gitignore: add Claude code --- src/.gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/.gitignore b/src/.gitignore index 6a74613..b4d8c30 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1 +1,4 @@ +# Claude code +.claude/ + *.dll From ae34bbfb5afd62e387917c894b6f9d69022a96e5 Mon Sep 17 00:00:00 2001 From: Ivan Migalev Date: Sat, 20 Dec 2025 19:18:36 +0100 Subject: [PATCH 4/6] (#54) ImportLogic: ignore excluded interfaces' methods' bodies --- src/Refasmer/Importer/ImportLogic.cs | 10 +++++++++- ...sembly.ExplicitImplOfInternalInterface.verified.txt | 4 ++++ tests/RefasmerTestAssembly/InternalInteraceImplBug.cs | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.ExplicitImplOfInternalInterface.verified.txt diff --git a/src/Refasmer/Importer/ImportLogic.cs b/src/Refasmer/Importer/ImportLogic.cs index d8b0b60..9975844 100644 --- a/src/Refasmer/Importer/ImportLogic.cs +++ b/src/Refasmer/Importer/ImportLogic.cs @@ -178,9 +178,17 @@ private void ImportTypeDefinitionAccessories( TypeDefinitionHandle srcHandle, Ty srcImpl => { var body = Import(srcImpl.MethodBody); + + // If the method body wasn't imported (e.g., private explicit interface implementation), + // skip importing the declaration. For generic interfaces, the declaration is a + // MemberReference whose import triggers signature processing. If the signature contains + // internal types that weren't preserved, this would throw UnknownTypeInSignature. + // See: https://github.com/JetBrains/Refasmer/issues/54 + if (body.IsNil) + return default; var decl = Import(srcImpl.MethodDeclaration); - return body.IsNil || decl.IsNil + return decl.IsNil ? default : _builder.AddMethodImplementation(dstHandle, body, decl); }, diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.ExplicitImplOfInternalInterface.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.ExplicitImplOfInternalInterface.verified.txt new file mode 100644 index 0000000..86ea06e --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.ExplicitImplOfInternalInterface.verified.txt @@ -0,0 +1,4 @@ +public class: RefasmerTestAssembly.ExplicitImplOfInternalInterface + - interface impl: RefasmerTestAssembly.IInternalInterface`1 +methods: +- .ctor(): System.Void: diff --git a/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs b/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs index 7f3a26a..d113e86 100644 --- a/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs +++ b/tests/RefasmerTestAssembly/InternalInteraceImplBug.cs @@ -1,5 +1,6 @@ namespace RefasmerTestAssembly; +// See #53 and #54. public class ExplicitImplOfInternalInterface : IInternalInterface { void IInternalInterface.Method(string x, Internal2 y) From 845e9d9908895566355296a64bf6d076f35cf196 Mon Sep 17 00:00:00 2001 From: Ivan Migalev Date: Sat, 20 Dec 2025 20:07:40 +0100 Subject: [PATCH 5/6] (#54) IntegrationTests: add a correct test that verifies that the internal interface is preserved --- tests/Refasmer.Tests/IntegrationTests.cs | 2 +- ..._mainClassName=ExplicitImplOfInternalInterface.verified.txt} | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename tests/Refasmer.Tests/data/{IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.ExplicitImplOfInternalInterface.verified.txt => IntegrationTests.InternalTypeInPublicApi_mainClassName=ExplicitImplOfInternalInterface.verified.txt} (74%) diff --git a/tests/Refasmer.Tests/IntegrationTests.cs b/tests/Refasmer.Tests/IntegrationTests.cs index 3431017..0755406 100644 --- a/tests/Refasmer.Tests/IntegrationTests.cs +++ b/tests/Refasmer.Tests/IntegrationTests.cs @@ -32,7 +32,6 @@ public async Task CheckRefasmedType(string typeName) [TestCase("RefasmerTestAssembly.CustomEnumerable")] [TestCase("RefasmerTestAssembly.EnumType")] [TestCase("RefasmerTestAssembly.InternalEnumType")] - [TestCase("RefasmerTestAssembly.ExplicitImplOfInternalInterface")] public async Task CheckRefasmedTypeOmitNonApi(string typeName) { var assemblyPath = await BuildTestAssembly(); @@ -58,6 +57,7 @@ await VerifyTypeContents( [TestCase("PublicClassImplementingInternal", "IInterface1ToBeMarkedInternal")] [TestCase("PublicClassWithInternalInterfaceImpl", "Class3ToBeMarkedInternal,IInterface2ToBeMarkedInternal`1")] [TestCase("PublicClassWithInternalTypeInExplicitImpl", "IInterface3")] + [TestCase("ExplicitImplOfInternalInterface", "IInternalInterface`1")] public async Task InternalTypeInPublicApi(string mainClassName, string auxiliaryClassNames) { var assemblyPath = await BuildTestAssemblyWithInternalTypeInPublicApi(); diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.ExplicitImplOfInternalInterface.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.InternalTypeInPublicApi_mainClassName=ExplicitImplOfInternalInterface.verified.txt similarity index 74% rename from tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.ExplicitImplOfInternalInterface.verified.txt rename to tests/Refasmer.Tests/data/IntegrationTests.InternalTypeInPublicApi_mainClassName=ExplicitImplOfInternalInterface.verified.txt index 86ea06e..33e06a4 100644 --- a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.ExplicitImplOfInternalInterface.verified.txt +++ b/tests/Refasmer.Tests/data/IntegrationTests.InternalTypeInPublicApi_mainClassName=ExplicitImplOfInternalInterface.verified.txt @@ -2,3 +2,4 @@ - interface impl: RefasmerTestAssembly.IInternalInterface`1 methods: - .ctor(): System.Void: +internal interface: RefasmerTestAssembly.IInternalInterface`1 From 1eba18281751839dcb543255c27ccb035d71a917 Mon Sep 17 00:00:00 2001 From: Ivan Migalev Date: Sun, 21 Dec 2025 18:59:28 +0100 Subject: [PATCH 6/6] (#54) ImportLogic: clarify the comments --- src/Refasmer/Importer/ImportLogic.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Refasmer/Importer/ImportLogic.cs b/src/Refasmer/Importer/ImportLogic.cs index 9975844..7fb7022 100644 --- a/src/Refasmer/Importer/ImportLogic.cs +++ b/src/Refasmer/Importer/ImportLogic.cs @@ -184,6 +184,10 @@ private void ImportTypeDefinitionAccessories( TypeDefinitionHandle srcHandle, Ty // MemberReference whose import triggers signature processing. If the signature contains // internal types that weren't preserved, this would throw UnknownTypeInSignature. // See: https://github.com/JetBrains/Refasmer/issues/54 + // + // Note: a non-generic interface method implementation is not a MemberReference, but a + // MethodDefinition which doesn't trigger signature processing (should be already in the method + // definition cache). if (body.IsNil) return default; var decl = Import(srcImpl.MethodDeclaration);