// Copyright (c) 2026 Siegfried Pammer // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System.Collections.Generic; using System.Linq; using System.Reflection.Metadata; using ICSharpCode.Decompiler.Metadata; using NUnit.Framework; namespace ICSharpCode.Decompiler.Tests.Metadata { [TestFixture] public class SignatureBlobComparerTests { static MetadataReader Metadata => TypeSystem.TypeSystemLoaderTests.TestAssembly.Metadata; #region Helper methods /// /// Finds all MethodDefinitionHandles for methods with the given name in the given type. /// static List FindMethods(string typeName, string methodName) { var results = new List(); foreach (var typeHandle in Metadata.TypeDefinitions) { var typeDef = Metadata.GetTypeDefinition(typeHandle); if (!Metadata.StringComparer.Equals(typeDef.Name, typeName)) continue; foreach (var methodHandle in typeDef.GetMethods()) { var methodDef = Metadata.GetMethodDefinition(methodHandle); if (Metadata.StringComparer.Equals(methodDef.Name, methodName)) results.Add(methodHandle); } } return results; } /// /// Finds a single MethodDefinitionHandle for a method with the given name in the given type. /// static MethodDefinitionHandle FindMethod(string typeName, string methodName) { var results = FindMethods(typeName, methodName); Assert.That(results.Count, Is.EqualTo(1), $"Expected exactly one method '{methodName}' in '{typeName}', found {results.Count}"); return results[0]; } /// /// Finds a MethodDefinitionHandle matching on name and parameter count. /// static MethodDefinitionHandle FindMethod(string typeName, string methodName, int parameterCount) { var results = FindMethods(typeName, methodName); var filtered = results.Where(h => { var m = Metadata.GetMethodDefinition(h); return m.GetParameters().Count == parameterCount; }).ToList(); Assert.That(filtered.Count, Is.EqualTo(1), $"Expected exactly one method '{methodName}' with {parameterCount} params in '{typeName}', found {filtered.Count}"); return filtered[0]; } static BlobReader GetSignatureBlob(MethodDefinitionHandle handle) { var method = Metadata.GetMethodDefinition(handle); return Metadata.GetBlobReader(method.Signature); } static bool CompareSignatures(MethodDefinitionHandle a, MethodDefinitionHandle b) { return SignatureBlobComparer.EqualsMethodSignature( GetSignatureBlob(a), GetSignatureBlob(b), Metadata, Metadata); } #endregion #region Identity / reflexive tests [Test] public void SameMethod_IsEqual() { // SimplePublicClass.Method() compared to itself var method = FindMethod("SimplePublicClass", "Method"); Assert.That(CompareSignatures(method, method), Is.True); } [Test] public void Constructor_ComparedToItself_IsEqual() { var ctor = FindMethod("SimplePublicClass", ".ctor"); Assert.That(CompareSignatures(ctor, ctor), Is.True); } [Test] public void StaticMethod_ComparedToItself_IsEqual() { // StaticClass.Extension(this object) — it's static at the IL level var method = FindMethod("StaticClass", "Extension"); Assert.That(CompareSignatures(method, method), Is.True); } #endregion #region Same name, different signatures [Test] public void DifferentParameterTypes_NotEqual() { // MethodWithOptionalParameter(int) vs MethodWithParamsArray(object[]) var intParam = FindMethod("ParameterTests", "MethodWithOptionalParameter"); var arrayParam = FindMethod("ParameterTests", "MethodWithParamsArray"); Assert.That(CompareSignatures(intParam, arrayParam), Is.False); } [Test] public void RefVsOut_SameAtSignatureLevel() { // ref and out are both ELEMENT_TYPE_BYREF at the blob level; // the out modifier is only represented via attributes. var methodRef = FindMethod("ParameterTests", "MethodWithRefParameter"); var methodOut = FindMethod("ParameterTests", "MethodWithOutParameter"); Assert.That(CompareSignatures(methodRef, methodOut), Is.True); } [Test] public void RefVsIn_SameAtSignatureLevel() { // ref and in are both ELEMENT_TYPE_BYREF at the blob level; // the in modifier is only represented via IsReadOnlyAttribute. var methodRef = FindMethod("ParameterTests", "MethodWithRefParameter"); var methodIn = FindMethod("ParameterTests", "MethodWithInParameter"); Assert.That(CompareSignatures(methodRef, methodIn), Is.True); } [Test] public void ByRefVsNonByRef_NotEqual() { // MethodWithRefParameter(ref int) vs MethodWithOptionalParameter(int) // byref int vs plain int should differ at the blob level var byRef = FindMethod("ParameterTests", "MethodWithRefParameter"); var plain = FindMethod("ParameterTests", "MethodWithOptionalParameter"); Assert.That(CompareSignatures(byRef, plain), Is.False); } [Test] public void DifferentParameterCount_NotEqual() { // Compare a method with 0 params to one with 1 param var method0 = FindMethod("SimplePublicClass", "Method"); // 0 params var method1 = FindMethod("ParameterTests", "MethodWithOutParameter"); // 1 param Assert.That(CompareSignatures(method0, method1), Is.False); } [Test] public void DifferentReturnType_NotEqual() { // SimplePublicClass.Method() returns void // PropertyTest has a getter that returns string (get_Item) var voidMethod = FindMethod("SimplePublicClass", "Method"); var stringGetter = FindMethod("PropertyTest", "get_Item"); Assert.That(CompareSignatures(voidMethod, stringGetter), Is.False); } #endregion #region Generic methods [Test] public void GenericMethod_ComparedToItself_IsEqual() { // GenericClass.TestMethod(string) var method = FindMethod("GenericClass`2", "TestMethod"); Assert.That(CompareSignatures(method, method), Is.True); } [Test] public void GenericMethod_DifferentArity_NotEqual() { // TestMethod(string) has 2 type params // GetIndex(T) has 1 type param var testMethod = FindMethod("GenericClass`2", "TestMethod"); var getIndex = FindMethod("GenericClass`2", "GetIndex"); Assert.That(CompareSignatures(testMethod, getIndex), Is.False); } [Test] public void GenericMethodWithOneTypeParam_ComparedToItself_IsEqual() { var method = FindMethod("GenericClass`2", "GetIndex"); Assert.That(CompareSignatures(method, method), Is.True); } [Test] public void NonGenericVsGenericMethod_NotEqual() { // SimplePublicClass.Method() — non-generic // GenericClass.GetIndex(T) — generic var nonGeneric = FindMethod("SimplePublicClass", "Method"); var generic = FindMethod("GenericClass`2", "GetIndex"); Assert.That(CompareSignatures(nonGeneric, generic), Is.False); } #endregion #region Instance vs static [Test] public void InstanceVsStatic_DifferentCallingConvention_NotEqual() { // An instance method vs a static method — calling convention differs in the signature header // StaticClass.Extension is static; SimplePublicClass.Method is instance var staticMethod = FindMethod("StaticClass", "Extension"); var instanceMethod = FindMethod("SimplePublicClass", "Method"); Assert.That(CompareSignatures(staticMethod, instanceMethod), Is.False); } #endregion #region Methods with same signature across different types [Test] public void SameSignatureInDifferentTypes_IsEqual() { // Both SimplePublicClass and ParameterTests have parameterless instance constructors var ctor1 = FindMethod("SimplePublicClass", ".ctor"); var ctor2 = FindMethod("ParameterTests", ".ctor"); Assert.That(CompareSignatures(ctor1, ctor2), Is.True); } #endregion #region Methods involving complex types [Test] public void MethodWithArrayParam_ComparedToItself_IsEqual() { // ParameterTests.MethodWithParamsArray(params object[]) var method = FindMethod("ParameterTests", "MethodWithParamsArray"); Assert.That(CompareSignatures(method, method), Is.True); } [Test] public void MethodWithOptionalParam_ComparedToNonOptional_IsEqual() { // At the signature blob level, optional vs required doesn't matter — // the signatures are the same if the types match. // MethodWithOptionalParameter(int) vs MethodWithEnumOptionalParameter(StringComparison) // These have different parameter types, so they should NOT be equal. var optionalInt = FindMethod("ParameterTests", "MethodWithOptionalParameter"); var optionalEnum = FindMethod("ParameterTests", "MethodWithEnumOptionalParameter"); Assert.That(CompareSignatures(optionalInt, optionalEnum), Is.False); } [Test] public void VarArgsMethod_ComparedToItself_IsEqual() { var method = FindMethod("ParameterTests", "VarArgsMethod"); Assert.That(CompareSignatures(method, method), Is.True); } [Test] public void VarArgsMethod_VsNormalMethod_NotEqual() { // VarArgsMethod uses __arglist calling convention var varArgs = FindMethod("ParameterTests", "VarArgsMethod"); var normal = FindMethod("SimplePublicClass", "Method"); Assert.That(CompareSignatures(varArgs, normal), Is.False); } #endregion #region Indexer accessors [Test] public void IndexerGetter_ComparedToItself_IsEqual() { var getter = FindMethod("PropertyTest", "get_Item"); Assert.That(CompareSignatures(getter, getter), Is.True); } [Test] public void IndexerGetter_VsSetter_NotEqual() { var getter = FindMethod("PropertyTest", "get_Item"); var setter = FindMethod("PropertyTest", "set_Item"); Assert.That(CompareSignatures(getter, setter), Is.False); } #endregion #region DllImport / extern methods [Test] public void DllImportMethod_ComparedToItself_IsEqual() { var method = FindMethod("NonCustomAttributes", "DllMethod"); Assert.That(CompareSignatures(method, method), Is.True); } #endregion #region Explicit interface implementation [Test] public void ExplicitImpl_VsRegularMethod_SameSignature() { // ExplicitImplementationTests has both M(int) and IExplicitImplementationTests.M(int) // Their signatures should be identical at the blob level var methods = FindMethods("ExplicitImplementationTests", "M"); Assert.That(methods.Count, Is.EqualTo(1), "Explicit impl has a mangled name, so only one 'M' should be found"); } #endregion #region Symmetric property [Test] public void Comparison_IsSymmetric() { var method1 = FindMethod("SimplePublicClass", "Method"); var method2 = FindMethod("ParameterTests", "MethodWithOutParameter"); bool forward = CompareSignatures(method1, method2); bool backward = CompareSignatures(method2, method1); Assert.That(forward, Is.EqualTo(backward), "Comparison should be symmetric"); } [Test] public void Comparison_IsSymmetric_WhenEqual() { var ctor1 = FindMethod("SimplePublicClass", ".ctor"); var ctor2 = FindMethod("ParameterTests", ".ctor"); bool forward = CompareSignatures(ctor1, ctor2); bool backward = CompareSignatures(ctor2, ctor1); Assert.That(forward, Is.True); Assert.That(backward, Is.True); } #endregion } }