Skip to content

Commit c6507b8

Browse files
committed
Add MEN019 exclusion for JSInvokable methods
1 parent 9560418 commit c6507b8

File tree

5 files changed

+63
-6
lines changed

5 files changed

+63
-6
lines changed

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<AssemblyOriginatorKeyFile>../Analyzers.snk</AssemblyOriginatorKeyFile>
2222

2323
<!-- NOTE: Change the version in Vsix\source.extension.vsixmanifest to match this! -->
24-
<Version>3.3.4</Version>
24+
<Version>3.3.5</Version>
2525

2626
<RoslynVersion>3.11.0</RoslynVersion>
2727
</PropertyGroup>

src/Menees.Analyzers.Vsix/source.extension.vsixmanifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
33
<Metadata>
4-
<Identity Id="Menees.Analyzers.Vsix" Version="3.3.4" Language="en-US" Publisher="Bill Menees"/>
4+
<Identity Id="Menees.Analyzers.Vsix" Version="3.3.5" Language="en-US" Publisher="Bill Menees"/>
55
<DisplayName>Menees.Analyzers.Vsix</DisplayName>
66
<Description xml:space="preserve">Provides analyzers for validating that tabs are used for indentation, that the lengths of lines, methods, properties, and files are acceptable, and that #regions are used within long files and files that contain multiple types.</Description>
77
<License>License.txt</License>

src/Menees.Analyzers/Men019SupportAsyncCancellationToken.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ private sealed class NestedAnalyzer(
6464
INamedTypeSymbol cancellationTokenType,
6565
INamedTypeSymbol[] fixedTaskTypes,
6666
INamedTypeSymbol[] genericTaskTypes,
67-
INamedTypeSymbol asyncMethodBuilderAttributeType)
67+
INamedTypeSymbol asyncMethodBuilderAttributeType,
68+
INamedTypeSymbol? jsInvokableAttributeType)
6869
{
6970
#region Private Data Members
7071

@@ -73,6 +74,7 @@ private sealed class NestedAnalyzer(
7374
private readonly INamedTypeSymbol[] fixedTaskTypes = fixedTaskTypes;
7475
private readonly INamedTypeSymbol[] genericTaskTypes = genericTaskTypes;
7576
private readonly INamedTypeSymbol asyncMethodBuilderAttributeType = asyncMethodBuilderAttributeType;
77+
private readonly INamedTypeSymbol? jsInvokableAttributeType = jsInvokableAttributeType;
7678
private readonly ConcurrentDictionary<(ITypeSymbol Source, ITypeSymbol Target), bool> canAccessCancellationTokenProperty
7779
= new(TypeAccessComparer.Instance);
7880

@@ -89,9 +91,13 @@ private sealed class NestedAnalyzer(
8991
INamedTypeSymbol? valueTask1Type = compilation.GetTypeByMetadataName(typeof(ValueTask<>).FullName);
9092
INamedTypeSymbol? asyncMethodBuilderAttributeType = compilation.GetTypeByMetadataName(typeof(AsyncMethodBuilderAttribute).FullName);
9193

94+
// This may be null if the project doesn't reference Microsoft.JSInterop.
95+
// That's fine; the HasJSInvokableAttribute check will simply be skipped.
96+
INamedTypeSymbol? jsInvokableAttributeType = compilation.GetTypeByMetadataName("Microsoft.JSInterop.JSInvokableAttribute");
97+
9298
NestedAnalyzer? result = cancellationTokenType != null && taskType != null && task1Type != null
9399
&& valueTaskType != null && valueTask1Type != null && asyncMethodBuilderAttributeType != null
94-
? new(caller, cancellationTokenType, [taskType, valueTaskType], [task1Type, valueTask1Type], asyncMethodBuilderAttributeType)
100+
? new(caller, cancellationTokenType, [taskType, valueTaskType], [task1Type, valueTask1Type], asyncMethodBuilderAttributeType, jsInvokableAttributeType)
95101
: null;
96102
return result;
97103
}
@@ -134,7 +140,8 @@ public void HandleNamedTypeSymbol(SymbolAnalysisContext context)
134140
&& method.ExplicitInterfaceImplementations.IsDefaultOrEmpty
135141
&& !IsImplicitInterfaceImplementation(method)
136142
&& !settings.IsUnitTestMethod(method)
137-
&& !IsAssemblyEntryPoint(method, ref context))
143+
&& !IsAssemblyEntryPoint(method, ref context)
144+
&& !HasJSInvokableAttribute(method))
138145
{
139146
eligibleMethods.Add(method);
140147

@@ -263,6 +270,15 @@ private bool IsImplicitInterfaceImplementation(IMethodSymbol method)
263270
return result;
264271
}
265272

273+
private bool HasJSInvokableAttribute(IMethodSymbol method)
274+
{
275+
// JSInvokable methods in Blazor must be public and are called from JavaScript.
276+
// JavaScript can't pass a CancellationToken.
277+
bool result = this.jsInvokableAttributeType != null
278+
&& method.GetAttributes().Any(attr => SymbolComparer.Equals(attr.AttributeClass, this.jsInvokableAttributeType));
279+
return result;
280+
}
281+
266282
private static bool IsAssemblyEntryPoint(IMethodSymbol method, ref SymbolAnalysisContext context)
267283
{
268284
bool result = method.IsStatic

tests/Menees.Analyzers.Test/Men019UnitTests.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ private class Nested
7777

7878
protected override CodeFixProvider? CSharpCodeFixProvider => new Men019SupportAsyncCancellationTokenFixer();
7979

80-
protected override IEnumerable<Type> AssemblyRequiredTypes => [typeof(ValueTask), typeof(TestMethodAttribute)];
80+
protected override IEnumerable<Type> AssemblyRequiredTypes =>
81+
#if NET
82+
[typeof(ValueTask), typeof(TestMethodAttribute), typeof(Microsoft.JSInterop.JSInvokableAttribute)];
83+
#else
84+
[typeof(ValueTask), typeof(TestMethodAttribute)];
85+
#endif
8186

8287
#endregion
8388

@@ -165,6 +170,38 @@ public sealed class DerivedJob : Job
165170

166171
#endregion
167172

173+
#if NET
174+
#region ValidJSInvokableCodeTest
175+
176+
[TestMethod]
177+
public void ValidJSInvokableCodeTest()
178+
{
179+
string test = @"using System;
180+
using System.Runtime.CompilerServices;
181+
using System.Threading;
182+
using System.Threading.Tasks;
183+
using Microsoft.JSInterop;
184+
185+
public class BlazorComponent
186+
{
187+
[JSInvokable]
188+
public Task NotifyFromJs() => Task.CompletedTask;
189+
190+
[JSInvokable(""CustomIdentifier"")]
191+
public Task<string> GetDataForJs() => Task.FromResult(""data"");
192+
193+
[JSInvokable]
194+
public ValueTask<int> ComputeForJs() => new(42);
195+
}
196+
"
197+
+ SharedCode;
198+
199+
this.VerifyCSharpDiagnostic(test);
200+
}
201+
202+
#endregion
203+
#endif
204+
168205
#region InvalidCodeTest
169206

170207
[TestMethod]

tests/Menees.Analyzers.Test/Menees.Analyzers.Test.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
<None Include="..\Directory.runsettings" Link="Directory.runsettings" />
1818
</ItemGroup>
1919

20+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
21+
<PackageReference Include="Microsoft.JSInterop" Version="8.0.0" />
22+
</ItemGroup>
23+
2024
<ItemGroup>
2125
<ProjectReference Include="..\..\src\Menees.Analyzers.CodeFixes\Menees.Analyzers.CodeFixes.csproj" />
2226
</ItemGroup>

0 commit comments

Comments
 (0)