diff --git a/src/Microsoft.TestPlatform.AdapterUtilities/ManagedNameUtilities/ManagedNameParser.cs b/src/Microsoft.TestPlatform.AdapterUtilities/ManagedNameUtilities/ManagedNameParser.cs index 2bb5c83021..c8c1797d4c 100644 --- a/src/Microsoft.TestPlatform.AdapterUtilities/ManagedNameUtilities/ManagedNameParser.cs +++ b/src/Microsoft.TestPlatform.AdapterUtilities/ManagedNameUtilities/ManagedNameParser.cs @@ -82,12 +82,21 @@ private static int ParseMethodName(string managedMethodName, int start, out stri { var i = start; var quoted = false; + char? previousChar = null; + // Consume all characters that are in single quotes as is. Because F# methods wrapped in `` can have any text, like ``method name``. + // and will be emitted into CIL as 'method name'. + // Make sure you ignore \', because that is how F# will escape ' if it appears in the method name. for (; i < managedMethodName.Length; i++) { + if ((i - 1) > 0) + { + previousChar = managedMethodName[i - 1]; + } + var c = managedMethodName[i]; - if (c == '\'' || quoted) + if ((c == '\'' && previousChar != '\\') || quoted) { - quoted = c == '\'' ? !quoted : quoted; + quoted = (c == '\'' && previousChar != '\\') ? !quoted : quoted; continue; } diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/ManagedNameTests/SpecialNameTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/ManagedNameTests/SpecialNameTests.cs index c5c1a02ea0..4f48256f4f 100644 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/ManagedNameTests/SpecialNameTests.cs +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/ManagedNameTests/SpecialNameTests.cs @@ -1,6 +1,8 @@ ๏ปฟ// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Linq; using System.Reflection; using Microsoft.TestPlatform.AcceptanceTests; @@ -22,13 +24,24 @@ public void VerifyThatInvalidIdentifierNamesAreParsed() var assembly = Assembly.LoadFrom(asset); var types = assembly.GetTypes(); - foreach (var type in types) + // Some methods can be implemented directly on the module, and are not in any class. This is how we get them. + var modules = assembly.GetModules().Select(m => new { Module = m, Type = m.GetType() }); + var moduleTypes = modules.Select(m => { - var methods = type.GetMethods(); + var runtimeTypeProperty = m.Type.GetProperty("RuntimeType", BindingFlags.Instance | BindingFlags.NonPublic); + var moduleType = (Type?)runtimeTypeProperty!.GetValue(m.Module, null)!; + + return moduleType; + }); + + foreach (var type in types.Concat(moduleTypes).Where(t => t != null)) + { + var methods = type!.GetMethods(); foreach (var method in methods) { - if (method.DeclaringType != type) continue; + // Module methods have null Declaring type. + if (method.DeclaringType != null && method.DeclaringType != type) continue; ManagedNameHelper.GetManagedName(method, out var typeName, out var methodName); var methodInfo = ManagedNameHelper.GetMethod(assembly, typeName, methodName); diff --git a/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/ManagedNameUtilities/ManagedNameParserTests.cs b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/ManagedNameUtilities/ManagedNameParserTests.cs index 5550d3c801..9587f844c0 100644 --- a/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/ManagedNameUtilities/ManagedNameParserTests.cs +++ b/test/Microsoft.TestPlatform.AdapterUtilities.UnitTests/ManagedNameUtilities/ManagedNameParserTests.cs @@ -50,6 +50,15 @@ void AssertParse(string expectedMethod, int expectedArity, string[] expectedPara AssertParse("Method", 1, new string[] { "B", "List" }, "Method`1(B,List)"); AssertParse("Method", 0, new string[] { "B[]" }, "Method(B[])"); AssertParse("Method", 0, new string[] { "A[,]", "B[,,][]" }, "Method(A[,],B[,,][])"); + + // An F# method that would look like this in code: member _.``method that does not pass`` () = + // And like this in CIL: instance void 'method that does not pass' () cil managed + Assert.AreEqual(("Method that does not pass", 0, null), Parse("'Method that does not pass'()")); + + // An F# method that would look like this in code: member _.``method that doesn't pass`` () = + // And like this in CIL: instance void 'method that doesn\'t pass' () cil managed + // Notice the ' escaped by \. + Assert.AreEqual(("Method that doesn't pass", 0, null), Parse(@"'Method that doesn\'t pass'()")); } [TestMethod] diff --git a/test/TestAssets/CILProject/OrphanMethod.il b/test/TestAssets/CILProject/OrphanMethod.il index e13ddf6858..d7d9d1f9d5 100644 --- a/test/TestAssets/CILProject/OrphanMethod.il +++ b/test/TestAssets/CILProject/OrphanMethod.il @@ -2,24 +2,43 @@ .assembly extern mscorlib {} .assembly extern System.Core {} -.method static public int32 Add(int32 x, int32 y) cil managed +.class public CleanNamespaceName.ClassName { - .maxstack 2 + .method static public int32 Add(int32 x, int32 y) cil managed + { + .maxstack 2 - ldarg.0 - ldarg.1 - add - ret -} + ldarg.0 + ldarg.1 + add + ret + } -.method static public int32 'An awesome method to add ๐“งโž•๐“จ'(int32 '๐“ง', int32 '๐“จ') cil managed -{ - .maxstack 2 + .method static public int32 'An awesome method to add ๐“งโž•๐“จ'(int32 '๐“ง', int32 '๐“จ') cil managed + { + .maxstack 2 + + ldarg.0 + ldarg.1 + add + ret + } + + // has ' and no spaces, e.g. coming from F# method ``don't`` + .method static public void 'don\'t'() cil managed + { + .maxstack 1 + + ret + } - ldarg.0 - ldarg.1 - add - ret + // has ' and a space, e.g. coming from F# method ``don't pass`` + .method static public void 'don\'t pass'() cil managed + { + .maxstack 1 + + ret + } } .class public CleanNamespaceName.SecondLevel.'๐Œ๐ฒ ๐—ฎ๐˜„๐—ฒ๐˜€๐—ผ๐—บ๐—ฒ ๐˜ค๐˜ญ๐˜ข๐˜ด๐˜ด ๐˜ข๐˜ฏ ๐’Š๐’๐’‚๐’„๐’„๐’†๐’”๐’”๐’Š๐’ƒ๐’๐’† ๐™ฃ๐™–๐™ข๐™š ๐Ÿ˜ญ'