Skip to content

Commit 9479497

Browse files
committed
Add some analyzers
* [x] Complain when using MVC binding attributes with minimal action * [x] Complain if a route parameter is not used or cannot be bound.
1 parent bcfbd5c commit 9479497

22 files changed

+786
-24
lines changed

AspNetCore.sln

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 16
4-
VisualStudioVersion = 16.0.31320.298
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31606.5
55
MinimumVisualStudioVersion = 15.0.26124.0
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{C28A32F6-8314-412E-9F3B-CBD31C23E878}"
77
EndProject
@@ -1638,6 +1638,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.
16381638
EndProject
16391639
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpClientApp", "src\Servers\Kestrel\samples\HttpClientApp\HttpClientApp.csproj", "{514726D2-3D2E-44C1-B056-163E37DE3E8B}"
16401640
EndProject
1641+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.App.Analyzer", "src\Framework\Analyzer\src\Microsoft.AspNetCore.App.Analyzer.csproj", "{564CABB8-1B3F-4D9E-909D-260EF2B8614A}"
1642+
EndProject
1643+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Analyzer", "Analyzer", "{EE39397E-E4AF-4D3F-9B9C-D637F9222CDD}"
1644+
EndProject
1645+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.App.Analyzer.Test", "src\Framework\Analyzer\test\Microsoft.AspNetCore.App.Analyzer.Test.csproj", "{CF4CEC18-798D-46EC-B0A0-98D97496590F}"
1646+
EndProject
16411647
Global
16421648
GlobalSection(SolutionConfigurationPlatforms) = preSolution
16431649
Debug|Any CPU = Debug|Any CPU
@@ -7817,6 +7823,30 @@ Global
78177823
{514726D2-3D2E-44C1-B056-163E37DE3E8B}.Release|x64.Build.0 = Release|Any CPU
78187824
{514726D2-3D2E-44C1-B056-163E37DE3E8B}.Release|x86.ActiveCfg = Release|Any CPU
78197825
{514726D2-3D2E-44C1-B056-163E37DE3E8B}.Release|x86.Build.0 = Release|Any CPU
7826+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
7827+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Debug|Any CPU.Build.0 = Debug|Any CPU
7828+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Debug|x64.ActiveCfg = Debug|Any CPU
7829+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Debug|x64.Build.0 = Debug|Any CPU
7830+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Debug|x86.ActiveCfg = Debug|Any CPU
7831+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Debug|x86.Build.0 = Debug|Any CPU
7832+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Release|Any CPU.ActiveCfg = Release|Any CPU
7833+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Release|Any CPU.Build.0 = Release|Any CPU
7834+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Release|x64.ActiveCfg = Release|Any CPU
7835+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Release|x64.Build.0 = Release|Any CPU
7836+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Release|x86.ActiveCfg = Release|Any CPU
7837+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A}.Release|x86.Build.0 = Release|Any CPU
7838+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
7839+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Debug|Any CPU.Build.0 = Debug|Any CPU
7840+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Debug|x64.ActiveCfg = Debug|Any CPU
7841+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Debug|x64.Build.0 = Debug|Any CPU
7842+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Debug|x86.ActiveCfg = Debug|Any CPU
7843+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Debug|x86.Build.0 = Debug|Any CPU
7844+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Release|Any CPU.ActiveCfg = Release|Any CPU
7845+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Release|Any CPU.Build.0 = Release|Any CPU
7846+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Release|x64.ActiveCfg = Release|Any CPU
7847+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Release|x64.Build.0 = Release|Any CPU
7848+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Release|x86.ActiveCfg = Release|Any CPU
7849+
{CF4CEC18-798D-46EC-B0A0-98D97496590F}.Release|x86.Build.0 = Release|Any CPU
78207850
EndGlobalSection
78217851
GlobalSection(SolutionProperties) = preSolution
78227852
HideSolutionNode = FALSE
@@ -8628,7 +8658,9 @@ Global
86288658
{17459B97-1AA3-4154-83D3-C6BDC9FA3F85} = {022B4B80-E813-4256-8034-11A68146F4EF}
86298659
{247E7B6F-FBA2-41A9-BA03-C7C4DF28091C} = {B27FBAC2-ADA3-4A05-B232-64011B6B2DA3}
86308660
{514726D2-3D2E-44C1-B056-163E37DE3E8B} = {7B976D8F-EA31-4C0B-97BD-DFD9B3CC86FB}
8631-
{B2ACFA01-3046-4A32-B90A-F9537F51BF85} = {6126DCE4-9692-4EE2-B240-C65743572995}
8661+
{564CABB8-1B3F-4D9E-909D-260EF2B8614A} = {EE39397E-E4AF-4D3F-9B9C-D637F9222CDD}
8662+
{EE39397E-E4AF-4D3F-9B9C-D637F9222CDD} = {A4C26078-B6D8-4FD8-87A6-7C15A3482038}
8663+
{CF4CEC18-798D-46EC-B0A0-98D97496590F} = {EE39397E-E4AF-4D3F-9B9C-D637F9222CDD}
86328664
EndGlobalSection
86338665
GlobalSection(ExtensibilityGlobals) = postSolution
86348666
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

eng/Dependencies.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ and are generated based on the last package release.
191191
<LatestPackageReference Include="Serilog.Extensions.Logging" />
192192
<LatestPackageReference Include="Serilog.Sinks.File" />
193193
<LatestPackageReference Include="StackExchange.Redis" />
194+
<LatestPackageReference Include="System.Memory" />
194195
<LatestPackageReference Include="System.Reactive.Linq" />
195196
<LatestPackageReference Include="xunit.abstractions" />
196197
<LatestPackageReference Include="xunit.analyzers" />

eng/Versions.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@
190190
<MicrosoftBuildUtilitiesCoreVersion>16.9.0</MicrosoftBuildUtilitiesCoreVersion>
191191
<MicrosoftCodeAnalysisCommonVersion>4.0.0-2.21354.7</MicrosoftCodeAnalysisCommonVersion>
192192
<MicrosoftCodeAnalysisCSharpVersion>4.0.0-2.21354.7</MicrosoftCodeAnalysisCSharpVersion>
193-
<MicrosoftCodeAnalysisCSharpWorkspacesVersion>3.8.0</MicrosoftCodeAnalysisCSharpWorkspacesVersion>
193+
<MicrosoftCodeAnalysisCSharpWorkspacesVersion>4.0.0-2.21354.7</MicrosoftCodeAnalysisCSharpWorkspacesVersion>
194194
<MicrosoftCodeAnalysisPublicApiAnalyzersVersion>3.3.0</MicrosoftCodeAnalysisPublicApiAnalyzersVersion>
195195
<MicrosoftCssParserVersion>1.0.0-20200708.1</MicrosoftCssParserVersion>
196196
<MicrosoftIdentityModelLoggingVersion>6.10.0</MicrosoftIdentityModelLoggingVersion>
@@ -254,6 +254,7 @@
254254
<SerilogExtensionsLoggingVersion>1.4.0</SerilogExtensionsLoggingVersion>
255255
<SerilogSinksFileVersion>4.0.0</SerilogSinksFileVersion>
256256
<StackExchangeRedisVersion>2.2.4</StackExchangeRedisVersion>
257+
<SYstemMemoryVersion>4.5.4</SYstemMemoryVersion>
257258
<SystemReactiveLinqVersion>3.1.1</SystemReactiveLinqVersion>
258259
<SwashbuckleAspNetCoreVersion>6.1.5</SwashbuckleAspNetCoreVersion>
259260
<XunitAbstractionsVersion>2.0.3</XunitAbstractionsVersion>

src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/TestSource.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ public static TestSource Read(string rawSource)
2626
for (var i = 0; i < lines.Length; i++)
2727
{
2828
var line = lines[i];
29-
var markerStartIndex = line.IndexOf(MarkerStart, StringComparison.Ordinal);
30-
if (markerStartIndex != -1)
29+
30+
while (true)
3131
{
32+
var markerStartIndex = line.IndexOf(MarkerStart, StringComparison.Ordinal);
33+
if (markerStartIndex == -1)
34+
{
35+
break;
36+
}
37+
3238
var markerEndIndex = line.IndexOf(MarkerEnd, markerStartIndex, StringComparison.Ordinal);
3339
var markerName = line.Substring(markerStartIndex + 2, markerEndIndex - markerStartIndex - 2);
3440
var markerLocation = new DiagnosticLocation(i + 1, markerStartIndex + 1);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<Description>CSharp Analyzers for ASP.NET Core.</Description>
4+
<IsShippingPackage>false</IsShippingPackage>
5+
<AddPublicApiAnalyzers>false</AddPublicApiAnalyzers>
6+
<TargetFramework>netstandard2.0</TargetFramework>
7+
<IncludeBuildOutput>false</IncludeBuildOutput>
8+
<Nullable>Enable</Nullable>
9+
<RootNamespace>Microsoft.AspNetCore.Analyzers</RootNamespace>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<Reference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="All" />
14+
<Reference Include="System.Memory" />
15+
16+
<InternalsVisibleTo Include="Microsoft.AspNetCore.App.Analyzer.Test" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<Compile Include="$(SharedSourceRoot)IsExternalInit.cs" LinkBase="Shared" />
21+
<Compile Include="$(SharedSourceRoot)Roslyn\CodeAnalysisExtensions.cs" LinkBase="Shared" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.Collections.Immutable;
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace Microsoft.AspNetCore.Analyzers.MinimalActions
8+
{
9+
[System.Diagnostics.CodeAnalysis.SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]
10+
internal static class DiagnosticDescriptors
11+
{
12+
internal static readonly DiagnosticDescriptor DoNotUseModelBindingAttributesOnMinimalActionParameters = new(
13+
"ASP0003",
14+
"Do not use model binding attributes with minimal actions",
15+
"Attribute '{0}' should not be specified for a minimal action parameter",
16+
"Usage",
17+
DiagnosticSeverity.Warning,
18+
isEnabledByDefault: true,
19+
helpLinkUri: "https://aka.ms/minimal-action/analyzer");
20+
21+
internal static readonly DiagnosticDescriptor RouteValueIsUnused = new(
22+
"ASP0004",
23+
"Route value is unused",
24+
"The route value '{0}' does not get bound and can be removed",
25+
"Usage",
26+
DiagnosticSeverity.Warning,
27+
isEnabledByDefault: true,
28+
helpLinkUri: "https://aka.ms/minimal-action/analyzer");
29+
30+
internal static readonly DiagnosticDescriptor RouteParameterCannotBeBound = new(
31+
"ASP0005",
32+
"Route parameter is not bound",
33+
"Route parameter does not have a corresponding route token and cannot be bound",
34+
"Usage",
35+
DiagnosticSeverity.Warning,
36+
isEnabledByDefault: true,
37+
helpLinkUri: "https://aka.ms/minimal-action/analyzer");
38+
39+
40+
}
41+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.Diagnostics;
7+
8+
namespace Microsoft.AspNetCore.Analyzers.MinimalActions;
9+
10+
public partial class MinimalActionAnalyzer : DiagnosticAnalyzer
11+
{
12+
private static void DisallowMvcBindArgumentsOnParameters(
13+
in OperationAnalysisContext context,
14+
WellKnownTypes wellKnownTypes,
15+
IMethodSymbol methodSymbol)
16+
{
17+
foreach (var parameter in methodSymbol.Parameters)
18+
{
19+
var modelBindingAttribute = parameter.GetAttributes(wellKnownTypes.IBinderTypeProviderMetadata).FirstOrDefault() ??
20+
parameter.GetAttributes(wellKnownTypes.BindAttribute).FirstOrDefault();
21+
22+
if (modelBindingAttribute is not null)
23+
{
24+
var location = Location.None;
25+
if (!parameter.DeclaringSyntaxReferences.IsEmpty)
26+
{
27+
var syntax = parameter.DeclaringSyntaxReferences[0].GetSyntax(context.CancellationToken);
28+
location = syntax.GetLocation();
29+
}
30+
31+
context.ReportDiagnostic(Diagnostic.Create(
32+
DiagnosticDescriptors.DoNotUseModelBindingAttributesOnMinimalActionParameters,
33+
location,
34+
modelBindingAttribute.AttributeClass.Name));
35+
}
36+
}
37+
}
38+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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;
5+
using System.Collections.Immutable;
6+
using System.Linq;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using Microsoft.CodeAnalysis.Operations;
10+
11+
namespace Microsoft.AspNetCore.Analyzers.MinimalActions;
12+
13+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
14+
public partial class MinimalActionAnalyzer : DiagnosticAnalyzer
15+
{
16+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(new[]
17+
{
18+
DiagnosticDescriptors.DoNotUseModelBindingAttributesOnMinimalActionParameters,
19+
DiagnosticDescriptors.RouteValueIsUnused,
20+
DiagnosticDescriptors.RouteParameterCannotBeBound,
21+
});
22+
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
26+
context.EnableConcurrentExecution();
27+
28+
context.RegisterCompilationStartAction(static compilationStartAnalysisContext =>
29+
{
30+
var compilation = compilationStartAnalysisContext.Compilation;
31+
if (!WellKnownTypes.TryCreate(compilation, out var wellKnownTypes))
32+
{
33+
return;
34+
}
35+
36+
compilationStartAnalysisContext.RegisterOperationAction(operationAnalysisContext =>
37+
{
38+
var invocation = (IInvocationOperation)operationAnalysisContext.Operation;
39+
var targetMethod = invocation.TargetMethod;
40+
if (IsMapActionInvocation(wellKnownTypes, invocation, targetMethod))
41+
{
42+
return;
43+
}
44+
45+
var delegateCreation = invocation.Arguments[2].Descendants().OfType<IDelegateCreationOperation>().FirstOrDefault();
46+
if (delegateCreation is null)
47+
{
48+
return;
49+
}
50+
51+
if (delegateCreation.Target.Kind == OperationKind.AnonymousFunction)
52+
{
53+
var lambda = ((IAnonymousFunctionOperation)delegateCreation.Target);
54+
DisallowMvcBindArgumentsOnParameters(in operationAnalysisContext, wellKnownTypes, lambda.Symbol);
55+
RouteAttributeMismatch(in operationAnalysisContext, wellKnownTypes, invocation, lambda.Symbol);
56+
}
57+
else if (delegateCreation.Target.Kind == OperationKind.MethodReference)
58+
{
59+
var methodReference = (IMethodReferenceOperation)delegateCreation.Target;
60+
DisallowMvcBindArgumentsOnParameters(in operationAnalysisContext, wellKnownTypes, methodReference.Method);
61+
}
62+
}, OperationKind.Invocation);
63+
});
64+
}
65+
66+
private static bool IsMapActionInvocation(
67+
WellKnownTypes wellKnownTypes,
68+
IInvocationOperation invocation,
69+
IMethodSymbol targetMethod)
70+
{
71+
return !targetMethod.Name.StartsWith("Map", StringComparison.Ordinal) ||
72+
!SymbolEqualityComparer.Default.Equals(wellKnownTypes.MinimalActionEndpointRouteBuilderExtensions, targetMethod.ContainingType) ||
73+
invocation.Arguments.Length != 3;
74+
}
75+
}

0 commit comments

Comments
 (0)