From eb42a34de9fcdb4c844203c455cdf8190575d325 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:53:20 +0000 Subject: [PATCH 1/4] Initial plan From 33c41656868723184920c01b84b9d53e09c8169c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:05:12 +0000 Subject: [PATCH 2/4] Add BL0010 analyzer: Recommend InvokeVoidAsync over InvokeAsync Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Analyzers/src/DiagnosticDescriptors.cs | 9 + .../src/InvokeAsyncOfObjectAnalyzer.cs | 150 ++++++ src/Components/Analyzers/src/Resources.resx | 9 + .../test/InvokeAsyncOfObjectAnalyzerTest.cs | 469 ++++++++++++++++++ 4 files changed, 637 insertions(+) create mode 100644 src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs create mode 100644 src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs diff --git a/src/Components/Analyzers/src/DiagnosticDescriptors.cs b/src/Components/Analyzers/src/DiagnosticDescriptors.cs index 5f67edaf8447..5259cbd0ba6c 100644 --- a/src/Components/Analyzers/src/DiagnosticDescriptors.cs +++ b/src/Components/Analyzers/src/DiagnosticDescriptors.cs @@ -92,4 +92,13 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Warning, isEnabledByDefault: true, description: CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Description))); + + public static readonly DiagnosticDescriptor UseInvokeVoidAsyncForObjectReturn = new( + "BL0010", + CreateLocalizableResourceString(nameof(Resources.UseInvokeVoidAsyncForObjectReturn_Title)), + CreateLocalizableResourceString(nameof(Resources.UseInvokeVoidAsyncForObjectReturn_Format)), + Usage, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: CreateLocalizableResourceString(nameof(Resources.UseInvokeVoidAsyncForObjectReturn_Description))); } diff --git a/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs b/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs new file mode 100644 index 000000000000..4e862ec1fe84 --- /dev/null +++ b/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Components.Analyzers; + +/// +/// Analyzer that detects usage of InvokeAsync<object> and recommends using InvokeVoidAsync instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvokeAsyncOfObjectAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + + context.RegisterCompilationStartAction(compilationContext => + { + var ijsRuntimeType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSRuntime"); + var ijsObjectReferenceType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSObjectReference"); + var ijsInProcessRuntimeType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSInProcessRuntime"); + var ijsInProcessObjectReferenceType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSInProcessObjectReference"); + var objectType = compilationContext.Compilation.GetSpecialType(SpecialType.System_Object); + + if (ijsRuntimeType is null && ijsObjectReferenceType is null) + { + // JSInterop types are not available + return; + } + + compilationContext.RegisterOperationAction(operationContext => + { + var invocation = (IInvocationOperation)operationContext.Operation; + var targetMethod = invocation.TargetMethod; + + // Check if the method is named InvokeAsync and is generic + if (targetMethod.Name != "InvokeAsync" || !targetMethod.IsGenericMethod) + { + return; + } + + // Check if the type argument is object + if (targetMethod.TypeArguments.Length != 1 || + !SymbolEqualityComparer.Default.Equals(targetMethod.TypeArguments[0], objectType)) + { + return; + } + + // Check if the method is on IJSRuntime, IJSObjectReference, or their in-process variants + // This includes extension methods on these types + var containingType = targetMethod.ContainingType; + var receiverType = GetReceiverType(invocation); + + if (!IsJSInteropType(receiverType, ijsRuntimeType, ijsObjectReferenceType, ijsInProcessRuntimeType, ijsInProcessObjectReferenceType) && + !IsJSInteropExtensionClass(containingType)) + { + return; + } + + operationContext.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn, + invocation.Syntax.GetLocation())); + }, OperationKind.Invocation); + }); + } + + private static ITypeSymbol? GetReceiverType(IInvocationOperation invocation) + { + // For extension methods, the first argument is the receiver + if (invocation.TargetMethod.IsExtensionMethod && invocation.Arguments.Length > 0) + { + return invocation.Arguments[0].Value.Type; + } + + // For instance methods + return invocation.Instance?.Type; + } + + private static bool IsJSInteropType( + ITypeSymbol? type, + INamedTypeSymbol? ijsRuntimeType, + INamedTypeSymbol? ijsObjectReferenceType, + INamedTypeSymbol? ijsInProcessRuntimeType, + INamedTypeSymbol? ijsInProcessObjectReferenceType) + { + if (type is null) + { + return false; + } + + // Check if the type implements any of the JSInterop interfaces + if (ijsRuntimeType is not null && ImplementsInterface(type, ijsRuntimeType)) + { + return true; + } + + if (ijsObjectReferenceType is not null && ImplementsInterface(type, ijsObjectReferenceType)) + { + return true; + } + + if (ijsInProcessRuntimeType is not null && ImplementsInterface(type, ijsInProcessRuntimeType)) + { + return true; + } + + if (ijsInProcessObjectReferenceType is not null && ImplementsInterface(type, ijsInProcessObjectReferenceType)) + { + return true; + } + + return false; + } + + private static bool ImplementsInterface(ITypeSymbol type, INamedTypeSymbol interfaceType) + { + if (SymbolEqualityComparer.Default.Equals(type, interfaceType)) + { + return true; + } + + foreach (var iface in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(iface, interfaceType)) + { + return true; + } + } + + return false; + } + + private static bool IsJSInteropExtensionClass(INamedTypeSymbol containingType) + { + // Check for the extension classes that provide InvokeAsync methods + var fullName = containingType.ToDisplayString(); + return fullName == "Microsoft.JSInterop.JSRuntimeExtensions" || + fullName == "Microsoft.JSInterop.JSObjectReferenceExtensions" || + fullName == "Microsoft.JSInterop.JSInProcessRuntimeExtensions" || + fullName == "Microsoft.JSInterop.JSInProcessObjectReferenceExtensions"; + } +} diff --git a/src/Components/Analyzers/src/Resources.resx b/src/Components/Analyzers/src/Resources.resx index 6a23211094aa..5c193fd5c376 100644 --- a/src/Components/Analyzers/src/Resources.resx +++ b/src/Components/Analyzers/src/Resources.resx @@ -198,4 +198,13 @@ Property with [PersistentState] should not have initializer + + JavaScript interop calls that do not expect a return value should use InvokeVoidAsync instead of InvokeAsync<object>. Using InvokeAsync<object> may cause serialization issues with non-serializable JavaScript values. + + + 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. + + + Use InvokeVoidAsync instead of InvokeAsync<object> + \ No newline at end of file diff --git a/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs b/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs new file mode 100644 index 000000000000..b9a9b7f24ee1 --- /dev/null +++ b/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs @@ -0,0 +1,469 @@ +// 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 InvokeAsync(string identifier, object[] args); + ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args); + } + + public interface IJSObjectReference : System.IAsyncDisposable + { + ValueTask InvokeAsync(string identifier, object[] args); + ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args); + } + + public interface IJSInProcessRuntime : IJSRuntime + { + TValue Invoke(string identifier, params object[] args); + } + + public interface IJSInProcessObjectReference : IJSObjectReference + { + TValue Invoke(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 InvokeAsync(this IJSRuntime jsRuntime, string identifier, params object[] args) + => default; + public static ValueTask InvokeAsync(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 InvokeAsync(this IJSObjectReference jsObjectReference, string identifier, params object[] args) + => default; + public static ValueTask InvokeAsync(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 TestMethod() + { + return await _jsRuntime.InvokeAsync(""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(""myFunction""); + } + } + }" + JSInteropDeclarations; + + var expected = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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(_jsRuntime, ""myFunction""); + } + } + }" + JSInteropDeclarations; + + var expected = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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(""myFunction""); + } + } + }" + JSInteropDeclarations; + + var expected = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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(""myFunction"", ct, null); + } + } + }" + JSInteropDeclarations; + + var expected = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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 InvokeAsync(string identifier, object[] args) + => default; + + public async Task TestMethod() + { + await InvokeAsync(""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 TestMethod() + { + return await _jsRuntime.InvokeAsync(""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 TestMethod() + { + return await _jsRuntime.InvokeAsync(""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(""myFunction""); + } + } + }" + JSInteropDeclarations; + + var expected = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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(""myFunction""); + } + } + }" + JSInteropDeclarations; + + var expected = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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(""myFunction1""); + await _jsRuntime.InvokeAsync(""myFunction2""); + } + } + }" + JSInteropDeclarations; + + var expected1 = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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'. 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(_jsObjectRef, ""myFunction""); + } + } + }" + JSInteropDeclarations; + + var expected = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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); + } +} From ef2960badd753aadd604f9e3c52273965b57b817 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:07:52 +0000 Subject: [PATCH 3/4] Address code review feedback: use symbol comparison instead of string comparison Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../src/InvokeAsyncOfObjectAnalyzer.cs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs b/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs index 4e862ec1fe84..a31ddd4ffde9 100644 --- a/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs +++ b/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs @@ -14,6 +14,11 @@ namespace Microsoft.AspNetCore.Components.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class InvokeAsyncOfObjectAnalyzer : DiagnosticAnalyzer { + private const string JSRuntimeExtensionsTypeName = "Microsoft.JSInterop.JSRuntimeExtensions"; + private const string JSObjectReferenceExtensionsTypeName = "Microsoft.JSInterop.JSObjectReferenceExtensions"; + private const string JSInProcessRuntimeExtensionsTypeName = "Microsoft.JSInterop.JSInProcessRuntimeExtensions"; + private const string JSInProcessObjectReferenceExtensionsTypeName = "Microsoft.JSInterop.JSInProcessObjectReferenceExtensions"; + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn); @@ -24,10 +29,15 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(compilationContext => { + // Cache type lookups once per compilation var ijsRuntimeType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSRuntime"); var ijsObjectReferenceType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSObjectReference"); var ijsInProcessRuntimeType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSInProcessRuntime"); var ijsInProcessObjectReferenceType = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.JSInterop.IJSInProcessObjectReference"); + var jsRuntimeExtensionsType = compilationContext.Compilation.GetTypeByMetadataName(JSRuntimeExtensionsTypeName); + var jsObjectReferenceExtensionsType = compilationContext.Compilation.GetTypeByMetadataName(JSObjectReferenceExtensionsTypeName); + var jsInProcessRuntimeExtensionsType = compilationContext.Compilation.GetTypeByMetadataName(JSInProcessRuntimeExtensionsTypeName); + var jsInProcessObjectReferenceExtensionsType = compilationContext.Compilation.GetTypeByMetadataName(JSInProcessObjectReferenceExtensionsTypeName); var objectType = compilationContext.Compilation.GetSpecialType(SpecialType.System_Object); if (ijsRuntimeType is null && ijsObjectReferenceType is null) @@ -60,7 +70,7 @@ public override void Initialize(AnalysisContext context) var receiverType = GetReceiverType(invocation); if (!IsJSInteropType(receiverType, ijsRuntimeType, ijsObjectReferenceType, ijsInProcessRuntimeType, ijsInProcessObjectReferenceType) && - !IsJSInteropExtensionClass(containingType)) + !IsJSInteropExtensionClass(containingType, jsRuntimeExtensionsType, jsObjectReferenceExtensionsType, jsInProcessRuntimeExtensionsType, jsInProcessObjectReferenceExtensionsType)) { return; } @@ -138,13 +148,17 @@ private static bool ImplementsInterface(ITypeSymbol type, INamedTypeSymbol inter return false; } - private static bool IsJSInteropExtensionClass(INamedTypeSymbol containingType) + private static bool IsJSInteropExtensionClass( + INamedTypeSymbol containingType, + INamedTypeSymbol? jsRuntimeExtensionsType, + INamedTypeSymbol? jsObjectReferenceExtensionsType, + INamedTypeSymbol? jsInProcessRuntimeExtensionsType, + INamedTypeSymbol? jsInProcessObjectReferenceExtensionsType) { - // Check for the extension classes that provide InvokeAsync methods - var fullName = containingType.ToDisplayString(); - return fullName == "Microsoft.JSInterop.JSRuntimeExtensions" || - fullName == "Microsoft.JSInterop.JSObjectReferenceExtensions" || - fullName == "Microsoft.JSInterop.JSInProcessRuntimeExtensions" || - fullName == "Microsoft.JSInterop.JSInProcessObjectReferenceExtensions"; + // Use symbol equality comparison instead of string comparison + return SymbolEqualityComparer.Default.Equals(containingType, jsRuntimeExtensionsType) || + SymbolEqualityComparer.Default.Equals(containingType, jsObjectReferenceExtensionsType) || + SymbolEqualityComparer.Default.Equals(containingType, jsInProcessRuntimeExtensionsType) || + SymbolEqualityComparer.Default.Equals(containingType, jsInProcessObjectReferenceExtensionsType); } } From 61397d9b1214c6ae1c2764501d8c1ef2ae15e904 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 18:12:43 +0000 Subject: [PATCH 4/4] Add nullable enable directive and test for assigned result scenario Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../src/InvokeAsyncOfObjectAnalyzer.cs | 2 + .../test/InvokeAsyncOfObjectAnalyzerTest.cs | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs b/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs index a31ddd4ffde9..fa668db8357a 100644 --- a/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs +++ b/src/Components/Analyzers/src/InvokeAsyncOfObjectAnalyzer.cs @@ -6,6 +6,8 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +#nullable enable + namespace Microsoft.AspNetCore.Components.Analyzers; /// diff --git a/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs b/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs index b9a9b7f24ee1..a8fa25afa05b 100644 --- a/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs +++ b/src/Components/Analyzers/test/InvokeAsyncOfObjectAnalyzerTest.cs @@ -466,4 +466,42 @@ public async Task TestMethod() 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 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(""myFunction""); + System.Console.WriteLine(result); + } + } + }" + JSInteropDeclarations; + + var expected = new DiagnosticResult + { + Id = DiagnosticDescriptors.UseInvokeVoidAsyncForObjectReturn.Id, + Message = "Use 'InvokeVoidAsync' instead of 'InvokeAsync'. 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); + } }