Skip to content

Commit 8330349

Browse files
authored
Add analyzer for detecting misplaced attributes on delegates
1 parent ca89594 commit 8330349

File tree

4 files changed

+227
-0
lines changed

4 files changed

+227
-0
lines changed

Diff for: src/Framework/Analyzer/src/DelegateEndpoints/DelegateEndpointAnalyzer.cs

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public partial class DelegateEndpointAnalyzer : DiagnosticAnalyzer
1818
{
1919
DiagnosticDescriptors.DoNotUseModelBindingAttributesOnDelegateEndpointParameters,
2020
DiagnosticDescriptors.DoNotReturnActionResultsFromMapActions,
21+
DiagnosticDescriptors.DetectMisplacedLambdaAttribute
2122
});
2223

2324
public override void Initialize(AnalysisContext context)
@@ -54,6 +55,7 @@ public override void Initialize(AnalysisContext context)
5455
var lambda = ((IAnonymousFunctionOperation)delegateCreation.Target);
5556
DisallowMvcBindArgumentsOnParameters(in operationAnalysisContext, wellKnownTypes, invocation, lambda.Symbol);
5657
DisallowReturningActionResultFromMapMethods(in operationAnalysisContext, wellKnownTypes, invocation, lambda);
58+
DetectMisplacedLambdaAttribute(operationAnalysisContext, invocation, lambda);
5759
}
5860
else if (delegateCreation.Target.Kind == OperationKind.MethodReference)
5961
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using Microsoft.CodeAnalysis.Operations;
9+
10+
namespace Microsoft.AspNetCore.Analyzers.DelegateEndpoints;
11+
12+
public partial class DelegateEndpointAnalyzer : DiagnosticAnalyzer
13+
{
14+
private static void DetectMisplacedLambdaAttribute(
15+
in OperationAnalysisContext context,
16+
IInvocationOperation invocation,
17+
IAnonymousFunctionOperation lambda)
18+
{
19+
// This analyzer will only process invocations that are immediate children of the
20+
// AnonymousFunctionOperation provided as the delegate endpoint. We'll support checking
21+
// for misplaced attributes in `() => Hello()` and `() => { return Hello(); }` but not in:
22+
// () => {
23+
// Hello();
24+
// return "foo";
25+
// }
26+
InvocationExpressionSyntax? targetInvocation = null;
27+
28+
// () => Hello() has a single child which is a BlockOperation so we check to see if
29+
// expression associated with that operation is an invocation expression
30+
if (lambda.Children.First().Syntax is InvocationExpressionSyntax innerInvocation)
31+
{
32+
targetInvocation = innerInvocation;
33+
}
34+
35+
if (lambda.Children.First().Children.First() is IReturnOperation returnOperation
36+
&& returnOperation.ReturnedValue is IInvocationOperation returnedInvocation)
37+
38+
{
39+
targetInvocation = (InvocationExpressionSyntax)returnedInvocation.Syntax;
40+
}
41+
42+
if (targetInvocation is null)
43+
{
44+
return;
45+
}
46+
47+
48+
var methodOperation = invocation.SemanticModel.GetSymbolInfo(targetInvocation);
49+
50+
var attributes = methodOperation.Symbol.GetAttributes();
51+
var location = lambda.Syntax.GetLocation();
52+
53+
foreach (var attribute in attributes)
54+
{
55+
context.ReportDiagnostic(Diagnostic.Create(
56+
DiagnosticDescriptors.DetectMisplacedLambdaAttribute,
57+
location,
58+
attribute.AttributeClass?.Name,
59+
methodOperation.Symbol.Name));
60+
}
61+
}
62+
}

Diff for: src/Framework/Analyzer/src/DelegateEndpoints/DiagnosticDescriptors.cs

+9
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,14 @@ internal static class DiagnosticDescriptors
2525
DiagnosticSeverity.Warning,
2626
isEnabledByDefault: true,
2727
helpLinkUri: "https://aka.ms/aspnet/analyzers");
28+
29+
internal static readonly DiagnosticDescriptor DetectMisplacedLambdaAttribute = new(
30+
"ASP0005",
31+
"Do not place attribute on invoked method",
32+
"'{0}' should not be placed on '{1}'. Place '{0}' on the endpoint delegate instead.",
33+
"Usage",
34+
DiagnosticSeverity.Warning,
35+
isEnabledByDefault: true,
36+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
2837
}
2938
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using Microsoft.AspNetCore.Analyzer.Testing;
6+
using Xunit;
7+
8+
namespace Microsoft.AspNetCore.Analyzers.DelegateEndpoints;
9+
10+
public partial class DetectMisplacedLambdaAttributeTest
11+
{
12+
private TestDiagnosticAnalyzerRunner Runner { get; } = new(new DelegateEndpointAnalyzer());
13+
14+
[Fact]
15+
public async Task MinimalAction_WithCorrectlyPlacedAttribute_Works()
16+
{
17+
// Arrange
18+
var source = @"
19+
using Microsoft.AspNetCore.Authorization;
20+
using Microsoft.AspNetCore.Builder;
21+
var app = WebApplication.Create();
22+
app.MapGet(""/"", [Authorize] () => Hello());
23+
void Hello() { }
24+
";
25+
// Act
26+
var diagnostics = await Runner.GetDiagnosticsAsync(source);
27+
28+
// Assert
29+
Assert.Empty(diagnostics);
30+
}
31+
32+
[Fact]
33+
public async Task MinimalAction_WithMisplacedAttribute_ProducesDiagnostics()
34+
{
35+
// Arrange
36+
var source = TestSource.Read(@"
37+
using Microsoft.AspNetCore.Authorization;
38+
using Microsoft.AspNetCore.Builder;
39+
var app = WebApplication.Create();
40+
app.MapGet(""/"", /*MM*/() => Hello());
41+
[Authorize]
42+
void Hello() { }
43+
");
44+
// Act
45+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
46+
47+
// Assert
48+
var diagnostic = Assert.Single(diagnostics);
49+
Assert.Same(DiagnosticDescriptors.DetectMisplacedLambdaAttribute, diagnostic.Descriptor);
50+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
51+
Assert.Equal("'AuthorizeAttribute' should not be placed on 'Hello'. Place 'AuthorizeAttribute' on the endpoint delegate instead.", diagnostic.GetMessage(CultureInfo.InvariantCulture));
52+
}
53+
54+
[Fact]
55+
public async Task MinimalAction_WithMisplacedAttributeAndBlockSyntax_ProducesDiagnostics()
56+
{
57+
// Arrange
58+
var source = TestSource.Read(@"
59+
using Microsoft.AspNetCore.Authorization;
60+
using Microsoft.AspNetCore.Builder;
61+
var app = WebApplication.Create();
62+
app.MapGet(""/"", /*MM*/() => { return Hello(); });
63+
[Authorize]
64+
string Hello() { return ""foo""; }
65+
");
66+
// Act
67+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
68+
69+
// Assert
70+
var diagnostic = Assert.Single(diagnostics);
71+
Assert.Same(DiagnosticDescriptors.DetectMisplacedLambdaAttribute, diagnostic.Descriptor);
72+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
73+
Assert.Equal("'AuthorizeAttribute' should not be placed on 'Hello'. Place 'AuthorizeAttribute' on the endpoint delegate instead.", diagnostic.GetMessage(CultureInfo.InvariantCulture));
74+
}
75+
76+
[Fact]
77+
public async Task MinimalAction_WithMultipleMisplacedAttributes_ProducesDiagnostics()
78+
{
79+
// Arrange
80+
var source = TestSource.Read(@"
81+
using Microsoft.AspNetCore.Authorization;
82+
using Microsoft.AspNetCore.Builder;
83+
using Microsoft.AspNetCore.Mvc;
84+
var app = WebApplication.Create();
85+
app.MapGet(""/"", /*MM*/() => Hello());
86+
[Authorize]
87+
[Produces(""application/xml"")]
88+
void Hello() { }
89+
");
90+
// Act
91+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
92+
93+
// Assert
94+
Assert.Collection(diagnostics,
95+
diagnostic => {
96+
Assert.Same(DiagnosticDescriptors.DetectMisplacedLambdaAttribute, diagnostic.Descriptor);
97+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
98+
Assert.Equal("'AuthorizeAttribute' should not be placed on 'Hello'. Place 'AuthorizeAttribute' on the endpoint delegate instead.", diagnostic.GetMessage(CultureInfo.InvariantCulture));
99+
},
100+
diagnostic => {
101+
Assert.Same(DiagnosticDescriptors.DetectMisplacedLambdaAttribute, diagnostic.Descriptor);
102+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
103+
Assert.Equal("'ProducesAttribute' should not be placed on 'Hello'. Place 'ProducesAttribute' on the endpoint delegate instead.", diagnostic.GetMessage(CultureInfo.InvariantCulture));
104+
}
105+
);
106+
}
107+
108+
[Fact]
109+
public async Task MinimalAction_WithSingleMisplacedAttribute_ProducesDiagnostics()
110+
{
111+
// Arrange
112+
var source = TestSource.Read(@"
113+
using Microsoft.AspNetCore.Authorization;
114+
using Microsoft.AspNetCore.Builder;
115+
using Microsoft.AspNetCore.Mvc;
116+
var app = WebApplication.Create();
117+
app.MapGet(""/"", /*MM*/[Authorize]() => Hello());
118+
[Produces(""application/xml"")]
119+
void Hello() { }
120+
");
121+
// Act
122+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
123+
124+
// Assert
125+
Assert.Collection(diagnostics,
126+
diagnostic => {
127+
Assert.Same(DiagnosticDescriptors.DetectMisplacedLambdaAttribute, diagnostic.Descriptor);
128+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
129+
Assert.Equal("'ProducesAttribute' should not be placed on 'Hello'. Place 'ProducesAttribute' on the endpoint delegate instead.", diagnostic.GetMessage(CultureInfo.InvariantCulture));
130+
}
131+
);
132+
}
133+
134+
[Fact]
135+
public async Task MinimalAction_DoesNotWarnOnNonReturningMethods()
136+
{
137+
// Arrange
138+
var source = @"
139+
using System;
140+
using Microsoft.AspNetCore.Authorization;
141+
using Microsoft.AspNetCore.Builder;
142+
var app = WebApplication.Create();
143+
app.MapGet(""/"", /*MM*/() => {
144+
Console.WriteLine(""foo"");
145+
return ""foo"";
146+
});";
147+
// Act
148+
var diagnostics = await Runner.GetDiagnosticsAsync(source);
149+
150+
// Assert
151+
Assert.Empty(diagnostics);
152+
}
153+
}
154+

0 commit comments

Comments
 (0)