From 32360c3b62293b2f6f5c042a5f3d9a428bb53747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Wed, 31 Jul 2024 21:58:56 -0400 Subject: [PATCH] Better implementation for helpers in async state machine --- .../CallerContext.cs | 59 ++++++++- .../FileEditor.cs | 32 ++--- ...tou.Framework.InlineSnapshotTesting.csproj | 4 +- .../InlineSnapshotTests.cs | 122 ++++++++++++++++++ 4 files changed, 191 insertions(+), 26 deletions(-) diff --git a/src/Meziantou.Framework.InlineSnapshotTesting/CallerContext.cs b/src/Meziantou.Framework.InlineSnapshotTesting/CallerContext.cs index 89953c39..2ce650f0 100644 --- a/src/Meziantou.Framework.InlineSnapshotTesting/CallerContext.cs +++ b/src/Meziantou.Framework.InlineSnapshotTesting/CallerContext.cs @@ -35,15 +35,17 @@ public static CallerContext Get(InlineSnapshotSettings settings, string? filePat if (frame is null) continue; - var methodInfo = frame.GetMethod(); - if (methodInfo == null) + var method = frame.GetMethod(); + if (method == null) continue; - var attribute = methodInfo.GetCustomAttribute(); + method = GetActualMethod(method); + + var attribute = method.GetCustomAttribute(); if (attribute is null) continue; - methodName = methodInfo.Name; + methodName = method.Name; if (ParseLocalFunctionName(methodName, out var localFunctionName)) { methodName = localFunctionName; @@ -52,7 +54,7 @@ public static CallerContext Get(InlineSnapshotSettings settings, string? filePat parameterName = attribute.ParameterName; if (parameterName is not null) { - var parameters = methodInfo.GetParameters(); + var parameters = method.GetParameters(); for (var j = 0; j < parameterName.Length; j++) { if (parameters[j].Name == parameterName) @@ -179,6 +181,53 @@ public readonly CSharpStringFormats FilterFormats(CSharpStringFormats formats) return formats; } + private static MethodBase GetActualMethod(MethodBase method) + { + if (method.DeclaringType.IsAssignableTo(typeof(IAsyncStateMachine))) + { + var parentType = method.DeclaringType.DeclaringType; + if (parentType is not null) + { + static MethodInfo[] GetDeclaredMethods(Type type) => type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); + var methods = GetDeclaredMethods(parentType); + if (methods is not null) + { + foreach (var candidateMethod in methods) + { + var attributes = candidateMethod.GetCustomAttributes(inherit: false); + // ReSharper disable once ConditionIsAlwaysTrueOrFalse - Taken from CoreFX + if (attributes is null) + { + continue; + } + + bool foundAttribute = false, foundIteratorAttribute = false; + foreach (var asma in attributes) + { + if (asma.StateMachineType == method.DeclaringType) + { + foundAttribute = true; + foundIteratorAttribute |= asma is IteratorStateMachineAttribute + || typeof(System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute) != null + && typeof(System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute).IsInstanceOfType(asma); + } + } + + if (foundAttribute) + { + // If this is an iterator (sync or async), mark the iterator as changed, so it gets the + annotation + // of the original method. Non-iterator async state machines resolve directly to their builder methods + // so aren't marked as changed. + return candidateMethod; + } + } + } + } + } + + return method; + } + private static Version? GetCSharpLanguageVersionFromAssemblyLocation(string assemblyLocation) { try diff --git a/src/Meziantou.Framework.InlineSnapshotTesting/FileEditor.cs b/src/Meziantou.Framework.InlineSnapshotTesting/FileEditor.cs index 303bbce0..526f93c7 100644 --- a/src/Meziantou.Framework.InlineSnapshotTesting/FileEditor.cs +++ b/src/Meziantou.Framework.InlineSnapshotTesting/FileEditor.cs @@ -83,29 +83,23 @@ public static void UpdateFile(CallerContext context, InlineSnapshotSettings sett span = new TextSpan(span.Start + context.ColumnNumber, 1); } - // It can be hard to find the method name from the call stack (compiler state machine from async/await, iterators, async iterators, etc.) - // If there is only one invocation, let's use it - var nodes = FindInvocations(root, span).ToArray(); - if (nodes.Length > 1) + var nodes = FindInvocations(root, span).Where(invocation => { - nodes = FindInvocations(root, span).Where(invocation => - { - // Dummy.MethodName() - if (invocation.Expression is MemberAccessExpressionSyntax { Name.Identifier.Text: string memberName } && memberName == context.MethodName) - return true; + // Dummy.MethodName() + if (invocation.Expression is MemberAccessExpressionSyntax { Name.Identifier.Text: string memberName } && memberName == context.MethodName) + return true; - // Dummy.MethodName() - if (invocation.Expression is GenericNameSyntax { Identifier.Text: string memberName2 } && memberName2 == context.MethodName) - return true; + // Dummy.MethodName() + if (invocation.Expression is GenericNameSyntax { Identifier.Text: string memberName2 } && memberName2 == context.MethodName) + return true; - // MethodName() - if (invocation.Expression is IdentifierNameSyntax { Identifier.Text: string identifierName } && identifierName == context.MethodName) - return true; + // MethodName() + if (invocation.Expression is IdentifierNameSyntax { Identifier.Text: string identifierName } && identifierName == context.MethodName) + return true; - return false; - }) - .ToArray(); - } + return false; + }) + .ToArray(); if (nodes.Length == 0) throw new InlineSnapshotException("Cannot find the SyntaxNode to update"); diff --git a/src/Meziantou.Framework.InlineSnapshotTesting/Meziantou.Framework.InlineSnapshotTesting.csproj b/src/Meziantou.Framework.InlineSnapshotTesting/Meziantou.Framework.InlineSnapshotTesting.csproj index 359ed17f..edac5dfb 100644 --- a/src/Meziantou.Framework.InlineSnapshotTesting/Meziantou.Framework.InlineSnapshotTesting.csproj +++ b/src/Meziantou.Framework.InlineSnapshotTesting/Meziantou.Framework.InlineSnapshotTesting.csproj @@ -5,7 +5,7 @@ Enables verification of objects using inline snapshots $(DefineConstants);DEBUG_TaskDialogPrompt - 3.0.6 + 3.0.7 $(NoWarn);NU5100 @@ -27,7 +27,7 @@ - + diff --git a/tests/Meziantou.Framework.InlineSnapshotTesting.Tests/InlineSnapshotTests.cs b/tests/Meziantou.Framework.InlineSnapshotTesting.Tests/InlineSnapshotTests.cs index e48f717b..8f5460c1 100644 --- a/tests/Meziantou.Framework.InlineSnapshotTesting.Tests/InlineSnapshotTests.cs +++ b/tests/Meziantou.Framework.InlineSnapshotTesting.Tests/InlineSnapshotTests.cs @@ -246,6 +246,128 @@ static async System.Threading.Tasks.Task Helper(string expected, [CallerFilePath """"); } + [Fact] + public async Task SupportAsyncHelperMethods_WithAsyncCodeAndMultipleInvocation() + { + await AssertSnapshot( + $$"""" + await Helper("", GetValue()); + + string GetValue() => ""; + + [InlineSnapshotAssertion(nameof(expected))] + static async System.Threading.Tasks.Task Helper(string expected, string dummy, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + await System.Threading.Tasks.Task.Yield(); + var data = new + { + FirstName = "Gérald", + LastName = "Barré", + NickName = "meziantou", + }; + {{nameof(InlineSnapshot)}}.{{nameof(InlineSnapshot.Validate)}}(data, expected, filePath, lineNumber); + } + """", + $$"""" + await Helper(""" + FirstName: Gérald + LastName: Barré + NickName: meziantou + """, GetValue()); + + string GetValue() => ""; + + [InlineSnapshotAssertion(nameof(expected))] + static async System.Threading.Tasks.Task Helper(string expected, string dummy, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + await System.Threading.Tasks.Task.Yield(); + var data = new + { + FirstName = "Gérald", + LastName = "Barré", + NickName = "meziantou", + }; + {{nameof(InlineSnapshot)}}.{{nameof(InlineSnapshot.Validate)}}(data, expected, filePath, lineNumber); + } + """"); + } + + [Fact] + public async Task SupportMultipleAsyncHelperMethods_WithAsyncCode() + { + await AssertSnapshot( + $$"""" + await Helper1(""); + + await Helper2(""); + + [InlineSnapshotAssertion(nameof(expected))] + static async System.Threading.Tasks.Task Helper1(string expected, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + await System.Threading.Tasks.Task.Yield(); + var data = new + { + FirstName = "Gérald", + LastName = "Barré", + NickName = "meziantou", + }; + {{nameof(InlineSnapshot)}}.{{nameof(InlineSnapshot.Validate)}}(data, expected, filePath, lineNumber); + } + + [InlineSnapshotAssertion(nameof(expected))] + static async System.Threading.Tasks.Task Helper2(string expected, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + await System.Threading.Tasks.Task.Yield(); + var data = new + { + FirstName = "Gérald", + LastName = "Barré", + NickName = "meziantou", + }; + {{nameof(InlineSnapshot)}}.{{nameof(InlineSnapshot.Validate)}}(data, expected, filePath, lineNumber); + } + """", + $$"""" + await Helper1(""" + FirstName: Gérald + LastName: Barré + NickName: meziantou + """); + + await Helper2(""" + FirstName: Gérald + LastName: Barré + NickName: meziantou + """); + + [InlineSnapshotAssertion(nameof(expected))] + static async System.Threading.Tasks.Task Helper1(string expected, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + await System.Threading.Tasks.Task.Yield(); + var data = new + { + FirstName = "Gérald", + LastName = "Barré", + NickName = "meziantou", + }; + {{nameof(InlineSnapshot)}}.{{nameof(InlineSnapshot.Validate)}}(data, expected, filePath, lineNumber); + } + + [InlineSnapshotAssertion(nameof(expected))] + static async System.Threading.Tasks.Task Helper2(string expected, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + await System.Threading.Tasks.Task.Yield(); + var data = new + { + FirstName = "Gérald", + LastName = "Barré", + NickName = "meziantou", + }; + {{nameof(InlineSnapshot)}}.{{nameof(InlineSnapshot.Validate)}}(data, expected, filePath, lineNumber); + } + """"); + } + [Fact] public async Task SupportAsyncGenericHelperMethods() {