File: InvokeAsyncOfObjectAnalyzerTest.cs
Web Access
Project: src\src\Components\Analyzers\test\Microsoft.AspNetCore.Components.Analyzers.Tests.csproj (Microsoft.AspNetCore.Components.Analyzers.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using TestHelper;
 
namespace Microsoft.AspNetCore.Components.Analyzers.Test;
 
public class InvokeAsyncOfObjectAnalyzerTest : DiagnosticVerifier
{
    protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new InvokeAsyncOfObjectAnalyzer();
 
    private static readonly string JSInteropDeclarations = @"
    namespace Microsoft.JSInterop
    {
        using System.Threading;
        using System.Threading.Tasks;
 
        public interface IJSRuntime
        {
            ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args);
            ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args);
        }
 
        public interface IJSObjectReference : System.IAsyncDisposable
        {
            ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args);
            ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args);
        }
 
        public interface IJSInProcessRuntime : IJSRuntime
        {
            TValue Invoke<TValue>(string identifier, params object[] args);
        }
 
        public interface IJSInProcessObjectReference : IJSObjectReference
        {
            TValue Invoke<TValue>(string identifier, params object[] args);
        }
 
        public static class JSRuntimeExtensions
        {
            public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, params object[] args)
                => default;
            public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, params object[] args)
                => default;
            public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args)
                => default;
            public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args)
                => default;
        }
 
        public static class JSObjectReferenceExtensions
        {
            public static ValueTask InvokeVoidAsync(this IJSObjectReference jsObjectReference, string identifier, params object[] args)
                => default;
            public static ValueTask<TValue> InvokeAsync<TValue>(this IJSObjectReference jsObjectReference, string identifier, params object[] args)
                => default;
            public static ValueTask<TValue> InvokeAsync<TValue>(this IJSObjectReference jsObjectReference, string identifier, CancellationToken cancellationToken, params object[] args)
                => default;
            public static ValueTask InvokeVoidAsync(this IJSObjectReference jsObjectReference, string identifier, CancellationToken cancellationToken, params object[] args)
                => default;
        }
    }
";
 
    [Fact]
    public void NoDiagnosticForInvokeVoidAsync()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task TestMethod()
            {
                await _jsRuntime.InvokeVoidAsync(""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        VerifyCSharpDiagnostic(test);
    }
 
    [Fact]
    public void NoDiagnosticForInvokeAsyncWithTypedReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task<string> TestMethod()
            {
                return await _jsRuntime.InvokeAsync<string>(""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        VerifyCSharpDiagnostic(test);
    }
 
    [Fact]
    public void DiagnosticForInvokeAsyncWithObjectReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task TestMethod()
            {
                await _jsRuntime.InvokeAsync<object>(""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        var expected = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 13, 23)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected);
    }
 
    [Fact]
    public void DiagnosticForExtensionMethodInvokeAsyncWithObjectReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task TestMethod()
            {
                await JSRuntimeExtensions.InvokeAsync<object>(_jsRuntime, ""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        var expected = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 13, 23)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected);
    }
 
    [Fact]
    public void DiagnosticForIJSObjectReferenceInvokeAsyncWithObjectReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSObjectReference _jsObjectReference;
 
            public async Task TestMethod()
            {
                await _jsObjectReference.InvokeAsync<object>(""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        var expected = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 13, 23)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected);
    }
 
    [Fact]
    public void DiagnosticForInvokeAsyncWithCancellationTokenAndObjectReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task TestMethod(CancellationToken ct)
            {
                await _jsRuntime.InvokeAsync<object>(""myFunction"", ct, null);
            }
        }
    }" + JSInteropDeclarations;
 
        var expected = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 14, 23)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected);
    }
 
    [Fact]
    public void NoDiagnosticForNonJSInteropInvokeAsync()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using System.Threading.Tasks;
 
        class TestClass
        {
            public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
                => default;
 
            public async Task TestMethod()
            {
                await InvokeAsync<object>(""myFunction"", null);
            }
        }
    }";
 
        VerifyCSharpDiagnostic(test);
    }
 
    [Fact]
    public void NoDiagnosticForInvokeAsyncWithIntReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task<int> TestMethod()
            {
                return await _jsRuntime.InvokeAsync<int>(""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        VerifyCSharpDiagnostic(test);
    }
 
    [Fact]
    public void NoDiagnosticForInvokeAsyncWithCustomClassReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class MyResult { }
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task<MyResult> TestMethod()
            {
                return await _jsRuntime.InvokeAsync<MyResult>(""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        VerifyCSharpDiagnostic(test);
    }
 
    [Fact]
    public void DiagnosticForIJSInProcessRuntimeInvokeAsyncWithObjectReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSInProcessRuntime _jsRuntime;
 
            public async Task TestMethod()
            {
                await _jsRuntime.InvokeAsync<object>(""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        var expected = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 13, 23)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected);
    }
 
    [Fact]
    public void DiagnosticForIJSInProcessObjectReferenceInvokeAsyncWithObjectReturn()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSInProcessObjectReference _jsObjectRef;
 
            public async Task TestMethod()
            {
                await _jsObjectRef.InvokeAsync<object>(""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        var expected = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 13, 23)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected);
    }
 
    [Fact]
    public void MultipleInvocationsReportMultipleDiagnostics()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task TestMethod()
            {
                await _jsRuntime.InvokeAsync<object>(""myFunction1"");
                await _jsRuntime.InvokeAsync<object>(""myFunction2"");
            }
        }
    }" + JSInteropDeclarations;
 
        var expected1 = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 13, 23)
            }
        };
 
        var expected2 = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 14, 23)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected1, expected2);
    }
 
    [Fact]
    public void DiagnosticForJSObjectReferenceExtensionMethod()
    {
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSObjectReference _jsObjectRef;
 
            public async Task TestMethod()
            {
                await JSObjectReferenceExtensions.InvokeAsync<object>(_jsObjectRef, ""myFunction"");
            }
        }
    }" + JSInteropDeclarations;
 
        var expected = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 13, 23)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected);
    }
 
    [Fact]
    public void DiagnosticForInvokeAsyncWithObjectReturnAssignedToVariable()
    {
        // This test confirms that the diagnostic still fires when the result is assigned to a variable.
        // Using InvokeAsync<object> is problematic because 'object' cannot be properly deserialized from JSON -
        // the result will either be null or cause serialization errors if JavaScript returns a non-serializable value.
        var test = @"
    namespace ConsoleApplication1
    {
        using Microsoft.JSInterop;
        using System.Threading.Tasks;
 
        class TestClass
        {
            private IJSRuntime _jsRuntime;
 
            public async Task TestMethod()
            {
                var result = await _jsRuntime.InvokeAsync<object>(""myFunction"");
                System.Console.WriteLine(result);
            }
        }
    }" + JSInteropDeclarations;
 
        var expected = new DiagnosticResult
        {
            Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id,
            Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync<object>'. Return values of type 'object' cannot be deserialized and may cause serialization errors if the JavaScript function returns a non-serializable value.",
            Severity = DiagnosticSeverity.Warning,
            Locations = new[]
            {
                new DiagnosticResultLocation("Test0.cs", 13, 36)
            }
        };
 
        VerifyCSharpDiagnostic(test, expected);
    }
}