Introduce Mvc analyzers

This commit is contained in:
Pranav K 2017-10-02 08:26:44 -07:00
parent 61d42825d7
commit c6c77dd4d3
29 changed files with 2548 additions and 2 deletions

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.10
VisualStudioVersion = 15.0.27130.2020
MinimumVisualStudioVersion = 15.0.26730.03
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -104,6 +104,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{44546170-35BF-448F-88F5-4331AE67AEAE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Analyzers", "src\Microsoft.AspNetCore.Mvc.Analyzers\Microsoft.AspNetCore.Mvc.Analyzers.csproj", "{29454949-4AE0-4B3B-9157-BFC28B7ECD97}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Analyzers.Test", "test\Microsoft.AspNetCore.Mvc.Analyzers.Test\Microsoft.AspNetCore.Mvc.Analyzers.Test.csproj", "{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -490,6 +494,30 @@ Global
{28D4DA20-6E13-47F9-80AE-D6AA7699CC35}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{28D4DA20-6E13-47F9-80AE-D6AA7699CC35}.Release|x86.ActiveCfg = Release|Any CPU
{28D4DA20-6E13-47F9-80AE-D6AA7699CC35}.Release|x86.Build.0 = Release|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Debug|x86.ActiveCfg = Debug|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Debug|x86.Build.0 = Debug|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Release|Any CPU.Build.0 = Release|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Release|x86.ActiveCfg = Release|Any CPU
{29454949-4AE0-4B3B-9157-BFC28B7ECD97}.Release|x86.Build.0 = Release|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Debug|x86.ActiveCfg = Debug|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Debug|x86.Build.0 = Debug|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Release|Any CPU.Build.0 = Release|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Release|x86.ActiveCfg = Release|Any CPU
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -528,6 +556,8 @@ Global
{CF322BE1-E1FE-4CFD-8FCA-16A14B905D53} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{0AB46520-F441-4E01-B444-08F4D23F8B1B} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{28D4DA20-6E13-47F9-80AE-D6AA7699CC35} = {44546170-35BF-448F-88F5-4331AE67AEAE}
{29454949-4AE0-4B3B-9157-BFC28B7ECD97} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{2E6CDE10-8F96-4B75-B0D9-808F6A01B8BD} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D003597F-372F-4068-A2F0-353BE3C3B39A}

32
Mvc.sln
View File

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26831.3000
MinimumVisualStudioVersion = 15.0.26730.03
@ -161,6 +161,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorBuildWebSite.Precompil
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorBuildWebSite.Views", "test\WebSites\RazorBuildWebSite.Views\RazorBuildWebSite.Views.csproj", "{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Mvc.Analyzers", "src\Microsoft.AspNetCore.Mvc.Analyzers\Microsoft.AspNetCore.Mvc.Analyzers.csproj", "{87A3E227-C45E-4141-A59F-402908E651FD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Mvc.Analyzers.Test", "test\Microsoft.AspNetCore.Mvc.Analyzers.Test\Microsoft.AspNetCore.Mvc.Analyzers.Test.csproj", "{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -833,6 +837,30 @@ Global
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|x86.ActiveCfg = Release|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|x86.Build.0 = Release|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Debug|x86.ActiveCfg = Debug|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Debug|x86.Build.0 = Debug|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Release|Any CPU.Build.0 = Release|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Release|x86.ActiveCfg = Release|Any CPU
{87A3E227-C45E-4141-A59F-402908E651FD}.Release|x86.Build.0 = Release|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Debug|x86.ActiveCfg = Debug|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Debug|x86.Build.0 = Debug|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Release|Any CPU.Build.0 = Release|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Release|x86.ActiveCfg = Release|Any CPU
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -898,6 +926,8 @@ Global
{BF8A3392-C3D2-4813-855A-E906564600E1} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{856D7E25-E033-477D-9ABD-0B50CF428C80} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{87A3E227-C45E-4141-A59F-402908E651FD} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{E3E09D2F-1FCF-4396-9B09-5A62CA8CC831} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}

View File

@ -42,6 +42,7 @@
<MicrosoftAspNetCoreWebUtilitiesPackageVersion>2.1.0-preview1-28124</MicrosoftAspNetCoreWebUtilitiesPackageVersion>
<MicrosoftAspNetWebApiClientPackageVersion>5.2.4-preview1</MicrosoftAspNetWebApiClientPackageVersion>
<MicrosoftCodeAnalysisCSharpPackageVersion>2.6.1</MicrosoftCodeAnalysisCSharpPackageVersion>
<MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>2.6.1</MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>
<MicrosoftCodeAnalysisRazorPackageVersion>2.1.0-preview1-28124</MicrosoftCodeAnalysisRazorPackageVersion>
<MicrosoftExtensionsCachingMemoryPackageVersion>2.1.0-preview1-28124</MicrosoftExtensionsCachingMemoryPackageVersion>
<MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>2.1.0-preview1-28124</MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ActionsMustNotBeAsyncVoidAnalyzer : ControllerAnalyzerBase
{
public static readonly string ReturnTypeKey = "ReturnType";
public ActionsMustNotBeAsyncVoidAnalyzer()
: base(DiagnosticDescriptors.MVC1003_ActionsMustNotBeAsyncVoid)
{
}
protected override void InitializeWorker(ControllerAnalyzerContext analyzerContext)
{
analyzerContext.Context.RegisterSyntaxNodeAction(context =>
{
var methodSyntax = (MethodDeclarationSyntax)context.Node;
var method = context.SemanticModel.GetDeclaredSymbol(methodSyntax, context.CancellationToken);
if (!analyzerContext.IsControllerAction(method))
{
return;
}
if (!method.IsAsync || !method.ReturnsVoid)
{
return;
}
var returnType = analyzerContext.SystemThreadingTask.ToMinimalDisplayString(
context.SemanticModel,
methodSyntax.ReturnType.SpanStart);
var properties = ImmutableDictionary.Create<string, string>(StringComparer.Ordinal)
.Add(ReturnTypeKey, returnType);
var location = methodSyntax.ReturnType.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(
SupportedDiagnostic,
location,
properties: properties));
}, SyntaxKind.MethodDeclaration);
}
}
}

View File

@ -0,0 +1,58 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Editing;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public class ActionsMustNotBeAsyncVoidFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(DiagnosticDescriptors.MVC1003_ActionsMustNotBeAsyncVoid.Id);
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
if (context.Diagnostics.Length == 0)
{
return;
}
if (!context.Diagnostics[0].Properties.TryGetValue(ActionsMustNotBeAsyncVoidAnalyzer.ReturnTypeKey, out var returnTypeName))
{
return;
}
var rootNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
const string title = "Fix async void usage.";
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: CreateChangedDocumentAsync,
equivalenceKey: title),
context.Diagnostics);
async Task<Document> CreateChangedDocumentAsync(CancellationToken cancellationToken)
{
var returnTypeSyntax = rootNode.FindNode(context.Span);
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken).ConfigureAwait(false);
editor.ReplaceNode(returnTypeSyntax, SyntaxFactory.IdentifierName(returnTypeName));
return editor.GetChangedDocument();
}
}
}
}

View File

@ -0,0 +1,52 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ApiActionsAreAttributeRoutedAnalyzer : ApiControllerAnalyzerBase
{
internal const string MethodNameKey = "MethodName";
public ApiActionsAreAttributeRoutedAnalyzer()
: base(DiagnosticDescriptors.MVC1000_ApiActionsMustBeAttributeRouted)
{
}
protected override void InitializeWorker(ApiControllerAnalyzerContext analyzerContext)
{
analyzerContext.Context.RegisterSymbolAction(context =>
{
var method = (IMethodSymbol)context.Symbol;
if (!analyzerContext.IsApiAction(method))
{
return;
}
foreach (var attribute in method.GetAttributes())
{
if (attribute.AttributeClass.IsAssignableFrom(analyzerContext.RouteAttribute))
{
return;
}
}
var properties = ImmutableDictionary.Create<string, string>(StringComparer.Ordinal)
.Add(MethodNameKey, method.Name);
var location = method.Locations.Length > 0 ? method.Locations[0] : Location.None;
context.ReportDiagnostic(Diagnostic.Create(
SupportedDiagnostic,
location,
properties: properties));
}, SymbolKind.Method);
}
}
}

View File

@ -0,0 +1,187 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public class ApiActionsAreAttributeRoutedFixProvider : CodeFixProvider
{
private static readonly RouteAttributeInfo[] RouteAttributes = new[]
{
new RouteAttributeInfo("HttpGet", TypeNames.HttpGetAttribute, new[] { "Get", "Find" }),
new RouteAttributeInfo("HttpPost", TypeNames.HttpPostAttribute, new[] { "Post", "Create", "Update" }),
new RouteAttributeInfo("HttpDelete", TypeNames.HttpDeleteAttribute, new[] { "Delete", "Remove" }),
new RouteAttributeInfo("HttpPut", TypeNames.HttpPutAttribute, new[] { "Put", "Create", "Update" }),
};
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(DiagnosticDescriptors.MVC1000_ApiActionsMustBeAttributeRouted.Id);
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
if (context.Diagnostics.Length == 0)
{
return;
}
var rootNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
Debug.Assert(context.Diagnostics.Length == 1);
var diagnostic = context.Diagnostics[0];
var methodName = diagnostic.Properties[ApiActionsAreAttributeRoutedAnalyzer.MethodNameKey];
var matchedByKeyword = false;
foreach (var routeInfo in RouteAttributes)
{
foreach (var keyword in routeInfo.KeyWords)
{
// Determine if the method starts with a conventional key and only show relevant routes.
// For e.g. FindPetByCategory would result in HttpGet attribute.
if (methodName.StartsWith(keyword, StringComparison.Ordinal))
{
matchedByKeyword = true;
var title = $"Add {routeInfo.Name} attribute";
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: cancellationToken => CreateChangedDocumentAsync(routeInfo.Type, cancellationToken),
equivalenceKey: title),
context.Diagnostics);
}
}
}
if (!matchedByKeyword)
{
foreach (var routeInfo in RouteAttributes)
{
var title = $"Add {routeInfo.Name} attribute";
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: cancellationToken => CreateChangedDocumentAsync(routeInfo.Type, cancellationToken),
equivalenceKey: title),
context.Diagnostics);
}
}
async Task<Document> CreateChangedDocumentAsync(string attributeName, CancellationToken cancellationToken)
{
var methodNode = (MethodDeclarationSyntax)rootNode.FindNode(context.Span);
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken).ConfigureAwait(false);
var compilation = editor.SemanticModel.Compilation;
var attributeMetadata = compilation.GetTypeByMetadataName(attributeName);
var fromRouteAttribute = compilation.GetTypeByMetadataName(TypeNames.FromRouteAttribute);
attributeName = attributeMetadata.ToMinimalDisplayString(editor.SemanticModel, methodNode.SpanStart);
// Remove the Attribute suffix from type names e.g. "HttpGetAttribute" -> "HttpGet"
if (attributeName.EndsWith("Attribute", StringComparison.Ordinal))
{
attributeName = attributeName.Substring(0, attributeName.Length - "Attribute".Length);
}
var method = editor.SemanticModel.GetDeclaredSymbol(methodNode);
var attribute = SyntaxFactory.Attribute(
SyntaxFactory.ParseName(attributeName));
var route = GetRoute(fromRouteAttribute, method);
if (!string.IsNullOrEmpty(route))
{
attribute = attribute.AddArgumentListArguments(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(route))));
}
editor.AddAttribute(methodNode, attribute);
return editor.GetChangedDocument();
}
}
private static string GetRoute(ITypeSymbol fromRouteAttribute, IMethodSymbol method)
{
StringBuilder routeNameBuilder = null;
foreach (var parameter in method.Parameters)
{
if (IsIdParameter(parameter.Name) || parameter.HasAttribute(fromRouteAttribute))
{
if (routeNameBuilder == null)
{
routeNameBuilder = new StringBuilder(parameter.Name.Length + 2);
}
else
{
routeNameBuilder.Append("/");
}
routeNameBuilder
.Append("{")
.Append(parameter.Name)
.Append("}");
}
}
return routeNameBuilder?.ToString();
}
private static bool IsIdParameter(string name)
{
// Check if the parameter is named "id" (e.g. int id) or ends in Id (e.g. personId)
if (name == null || name.Length < 2)
{
return false;
}
if (string.Equals("id", name, StringComparison.Ordinal))
{
return true;
}
if (name.Length > 3 && name.EndsWith("Id", StringComparison.Ordinal) && char.IsLower(name[name.Length - 3]))
{
return true;
}
return false;
}
private struct RouteAttributeInfo
{
public RouteAttributeInfo(string name, string type, string[] keywords)
{
Name = name;
Type = type;
KeyWords = keywords;
}
public string Name { get; }
public string Type { get; }
public string[] KeyWords { get; }
}
}
}

View File

@ -0,0 +1,94 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer : ApiControllerAnalyzerBase
{
public ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer()
: base(DiagnosticDescriptors.MVC1001_ApiActionsHaveBadModelStateFilter)
{
}
protected override void InitializeWorker(ApiControllerAnalyzerContext analyzerContext)
{
analyzerContext.Context.RegisterSyntaxNodeAction(context =>
{
var methodSyntax = (MethodDeclarationSyntax)context.Node;
if (methodSyntax.Body == null)
{
// Ignore expression bodied methods.
return;
}
var method = context.SemanticModel.GetDeclaredSymbol(methodSyntax, context.CancellationToken);
if (!analyzerContext.IsApiAction(method))
{
return;
}
if (method.ReturnsVoid || method.ReturnType == analyzerContext.SystemThreadingTaskOfT)
{
// Void or Task returning methods. We don't have to check anything here since we're specifically
// looking for return BadRequest(..);
return;
}
// Only look for top level statements that look like "if (!ModelState.IsValid)"
foreach (var memberAccessSyntax in methodSyntax.Body.DescendantNodes().OfType<MemberAccessExpressionSyntax>())
{
var ancestorIfStatement = memberAccessSyntax.FirstAncestorOrSelf<IfStatementSyntax>();
if (ancestorIfStatement == null)
{
// Node's not in an if statement.
continue;
}
var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccessSyntax, context.CancellationToken);
if (!(symbolInfo.Symbol is IPropertySymbol property) ||
(property.ContainingType != analyzerContext.ModelStateDictionary) ||
!string.Equals(property.Name, "IsValid", StringComparison.Ordinal) ||
!IsFalseExpression(memberAccessSyntax))
{
continue;
}
var containingBlock = (SyntaxNode)ancestorIfStatement;
if (containingBlock.Parent.Kind() == SyntaxKind.ElseClause)
{
containingBlock = containingBlock.Parent;
}
context.ReportDiagnostic(Diagnostic.Create(SupportedDiagnostic, containingBlock.GetLocation()));
return;
}
}, SyntaxKind.MethodDeclaration);
}
private static bool IsFalseExpression(MemberAccessExpressionSyntax memberAccessSyntax)
{
switch (memberAccessSyntax.Parent.Kind())
{
case SyntaxKind.LogicalNotExpression:
// !ModelState.IsValid
return true;
case SyntaxKind.EqualsExpression:
var binaryExpression = (BinaryExpressionSyntax)memberAccessSyntax.Parent;
// ModelState.IsValid == false
// false == ModelState.IsValid
return binaryExpression.Left.Kind() == SyntaxKind.FalseLiteralExpression ||
binaryExpression.Right.Kind() == SyntaxKind.FalseLiteralExpression;
}
return false;
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public class ApiActionsDoNotRequireExplicitModelValidationCheckCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(DiagnosticDescriptors.MVC1001_ApiActionsHaveBadModelStateFilter.Id);
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
const string title = "Remove ModelState.IsValid check";
var rootNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: CreateChangedDocumentAsync,
equivalenceKey: title),
context.Diagnostics);
async Task<Document> CreateChangedDocumentAsync(CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken).ConfigureAwait(false);
var node = rootNode.FindNode(context.Span);
editor.RemoveNode(node);
return editor.GetChangedDocument();
}
}
}
}

View File

@ -0,0 +1,102 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ApiActionsShouldUseActionResultOfTAnalyzer : ApiControllerAnalyzerBase
{
public static readonly string ReturnTypeKey = "ReturnType";
public ApiActionsShouldUseActionResultOfTAnalyzer()
: base(DiagnosticDescriptors.MVC1002_ApiActionsShouldReturnActionResultOf)
{
}
protected override void InitializeWorker(ApiControllerAnalyzerContext analyzerContext)
{
analyzerContext.Context.RegisterSyntaxNodeAction(context =>
{
var methodSyntax = (MethodDeclarationSyntax)context.Node;
if (methodSyntax.Body == null)
{
// Ignore expression bodied methods.
}
var method = context.SemanticModel.GetDeclaredSymbol(methodSyntax, context.CancellationToken);
if (!analyzerContext.IsApiAction(method))
{
return;
}
if (method.ReturnsVoid || method.ReturnType.Kind != SymbolKind.NamedType)
{
return;
}
var declaredReturnType = method.ReturnType;
var namedReturnType = (INamedTypeSymbol)method.ReturnType;
var isTaskOActionResult = false;
if (namedReturnType.ConstructedFrom?.IsAssignableFrom(analyzerContext.SystemThreadingTaskOfT) ?? false)
{
// Unwrap Task<T>.
isTaskOActionResult = true;
declaredReturnType = namedReturnType.TypeArguments[0];
}
if (!declaredReturnType.IsAssignableFrom(analyzerContext.IActionResult))
{
// Method signature does not look like IActionResult MyAction or SomeAwaitable<IActionResult>.
// Nothing to do here.
return;
}
// Method returns an IActionResult. Determine if the method block returns an ObjectResult
foreach (var returnStatement in methodSyntax.DescendantNodes().OfType<ReturnStatementSyntax>())
{
var returnType = context.SemanticModel.GetTypeInfo(returnStatement.Expression, context.CancellationToken);
if (returnType.Type == null || returnType.Type.Kind == SymbolKind.ErrorType)
{
continue;
}
ImmutableDictionary<string, string> properties = null;
if (returnType.Type.IsAssignableFrom(analyzerContext.ObjectResult))
{
// Check if the method signature looks like "return Ok(userModelInstance)". If so, we can infer the type of userModelInstance
if (returnStatement.Expression is InvocationExpressionSyntax invocation &&
invocation.ArgumentList.Arguments.Count == 1)
{
var typeInfo = context.SemanticModel.GetTypeInfo(invocation.ArgumentList.Arguments[0].Expression);
var desiredReturnType = analyzerContext.ActionResultOfT.Construct(typeInfo.Type);
if (isTaskOActionResult)
{
desiredReturnType = analyzerContext.SystemThreadingTaskOfT.Construct(desiredReturnType);
}
var desiredReturnTypeString = desiredReturnType.ToMinimalDisplayString(
context.SemanticModel,
methodSyntax.ReturnType.SpanStart);
properties = ImmutableDictionary.Create<string, string>(StringComparer.Ordinal)
.Add(ReturnTypeKey, desiredReturnTypeString);
}
context.ReportDiagnostic(Diagnostic.Create(
SupportedDiagnostic,
methodSyntax.ReturnType.GetLocation(),
properties: properties));
}
}
}, SyntaxKind.MethodDeclaration);
}
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Editing;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public class ApiActionsShouldUseActionResultOfTCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(DiagnosticDescriptors.MVC1002_ApiActionsShouldReturnActionResultOf.Id);
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var rootNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
foreach (var diagnostic in context.Diagnostics)
{
if (diagnostic.Properties.TryGetValue("ReturnType", out var returnTypeName))
{
var title = $"Make return type {returnTypeName}";
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: cancellationToken => CreateChangedDocumentAsync(returnTypeName, cancellationToken),
equivalenceKey: title),
context.Diagnostics);
}
}
async Task<Document> CreateChangedDocumentAsync(string returnTypeName, CancellationToken cancellationToken)
{
var returnType = rootNode.FindNode(context.Span);
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken).ConfigureAwait(false);
editor.ReplaceNode(returnType, SyntaxFactory.IdentifierName(returnTypeName));
return editor.GetChangedDocument();
}
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public abstract class ApiControllerAnalyzerBase : DiagnosticAnalyzer
{
public ApiControllerAnalyzerBase(DiagnosticDescriptor diagnostic)
{
SupportedDiagnostics = ImmutableArray.Create(diagnostic);
}
protected DiagnosticDescriptor SupportedDiagnostic => SupportedDiagnostics[0];
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
public sealed override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(compilationContext =>
{
var analyzerContext = new ApiControllerAnalyzerContext(compilationContext);
// Only do work if ApiControllerAttribute is defined.
if (analyzerContext.ApiControllerAttribute == null)
{
return;
}
InitializeWorker(analyzerContext);
});
}
protected abstract void InitializeWorker(ApiControllerAnalyzerContext analyzerContext);
}
}

View File

@ -0,0 +1,63 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class ApiControllerAnalyzerContext
{
#pragma warning disable RS1012 // Start action has no registered actions.
public ApiControllerAnalyzerContext(CompilationStartAnalysisContext context)
#pragma warning restore RS1012 // Start action has no registered actions.
{
Context = context;
ApiControllerAttribute = context.Compilation.GetTypeByMetadataName(TypeNames.ApiControllerAttribute);
}
public CompilationStartAnalysisContext Context { get; }
public INamedTypeSymbol ApiControllerAttribute { get; }
private INamedTypeSymbol _routeAttribute;
public INamedTypeSymbol RouteAttribute => GetType(TypeNames.IRouteTemplateProvider, ref _routeAttribute);
private INamedTypeSymbol _actionResultOfT;
public INamedTypeSymbol ActionResultOfT => GetType(TypeNames.ActionResultOfT, ref _actionResultOfT);
private INamedTypeSymbol _systemThreadingTask;
public INamedTypeSymbol SystemThreadingTask => GetType(TypeNames.Task, ref _systemThreadingTask);
private INamedTypeSymbol _systemThreadingTaskOfT;
public INamedTypeSymbol SystemThreadingTaskOfT => GetType(TypeNames.TaskOfT, ref _systemThreadingTaskOfT);
private INamedTypeSymbol _objectResult;
public INamedTypeSymbol ObjectResult => GetType(TypeNames.ObjectResult, ref _objectResult);
private INamedTypeSymbol _iActionResult;
public INamedTypeSymbol IActionResult => GetType(TypeNames.IActionResult, ref _iActionResult);
public INamedTypeSymbol _modelState;
public INamedTypeSymbol ModelStateDictionary => GetType(TypeNames.ModelStateDictionary, ref _modelState);
public INamedTypeSymbol _nonActionAttribute;
public INamedTypeSymbol NonActionAttribute => GetType(TypeNames.NonActionAttribute, ref _nonActionAttribute);
private INamedTypeSymbol GetType(string name, ref INamedTypeSymbol cache) =>
cache = cache ?? Context.Compilation.GetTypeByMetadataName(name);
public bool IsApiAction(IMethodSymbol method)
{
return
method.ContainingType.HasAttribute(ApiControllerAttribute, inherit: true) &&
method.DeclaredAccessibility == Accessibility.Public &&
!method.IsGenericMethod &&
!method.IsAbstract &&
!method.IsStatic &&
!method.HasAttribute(NonActionAttribute);
}
}
}

View File

@ -0,0 +1,78 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
internal static class CodeAnalysisExtensions
{
public static bool HasAttribute(this ITypeSymbol typeSymbol, ITypeSymbol attribute, bool inherit)
{
while (typeSymbol != null)
{
if (typeSymbol.HasAttribute(attribute))
{
return true;
}
typeSymbol = typeSymbol.BaseType;
}
return false;
}
public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attribute)
{
Debug.Assert(symbol != null);
Debug.Assert(attribute != null);
foreach (var declaredAttribute in symbol.GetAttributes())
{
if (declaredAttribute.AttributeClass == attribute)
{
return true;
}
}
return false;
}
public static bool IsAssignableFrom(this ITypeSymbol source, INamedTypeSymbol target)
{
Debug.Assert(source != null);
Debug.Assert(target != null);
if (source == target)
{
return true;
}
if (target.TypeKind == TypeKind.Interface)
{
foreach (var @interface in source.AllInterfaces)
{
if (@interface == target)
{
return true;
}
}
return false;
}
do
{
if (source == target)
{
return true;
}
source = source.BaseType;
} while (source != null);
return false;
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public abstract class ControllerAnalyzerBase : DiagnosticAnalyzer
{
public ControllerAnalyzerBase(DiagnosticDescriptor diagnostic)
{
SupportedDiagnostics = ImmutableArray.Create(diagnostic);
}
protected DiagnosticDescriptor SupportedDiagnostic => SupportedDiagnostics[0];
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
public sealed override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(compilationContext =>
{
var analyzerContext = new ControllerAnalyzerContext(compilationContext);
// Only do work if ControllerAttribute is defined.
if (analyzerContext.ControllerAttribute == null)
{
return;
}
InitializeWorker(analyzerContext);
});
}
protected abstract void InitializeWorker(ControllerAnalyzerContext analyzerContext);
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class ControllerAnalyzerContext
{
#pragma warning disable RS1012 // Start action has no registered actions.
public ControllerAnalyzerContext(CompilationStartAnalysisContext context)
#pragma warning restore RS1012 // Start action has no registered actions.
{
Context = context;
ControllerAttribute = Context.Compilation.GetTypeByMetadataName(TypeNames.ControllerAttribute);
}
public CompilationStartAnalysisContext Context { get; }
public INamedTypeSymbol ControllerAttribute { get; }
private INamedTypeSymbol _systemThreadingTask;
public INamedTypeSymbol SystemThreadingTask => GetType(TypeNames.Task, ref _systemThreadingTask);
private INamedTypeSymbol _systemThreadingTaskOfT;
public INamedTypeSymbol SystemThreadingTaskOfT => GetType(TypeNames.TaskOfT, ref _systemThreadingTaskOfT);
public INamedTypeSymbol _nonActionAttribute;
public INamedTypeSymbol NonActionAttribute => GetType(TypeNames.NonActionAttribute, ref _nonActionAttribute);
private INamedTypeSymbol GetType(string name, ref INamedTypeSymbol cache) =>
cache = cache ?? Context.Compilation.GetTypeByMetadataName(name);
public bool IsControllerAction(IMethodSymbol method)
{
return
method.ContainingType.HasAttribute(ControllerAttribute, inherit: true) &&
method.DeclaredAccessibility == Accessibility.Public &&
!method.IsGenericMethod &&
!method.IsAbstract &&
!method.IsStatic &&
!method.HasAttribute(NonActionAttribute);
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public static class DiagnosticDescriptors
{
public static readonly DiagnosticDescriptor MVC1000_ApiActionsMustBeAttributeRouted =
new DiagnosticDescriptor(
"MVC1000",
"Actions on types annotated with ApiControllerAttribute must be attribute routed.",
"Actions on types annotated with ApiControllerAttribute must be attribute routed.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor MVC1001_ApiActionsHaveBadModelStateFilter =
new DiagnosticDescriptor(
"MVC1001",
"Actions on types annotated with ApiControllerAttribute do not require explicit ModelState validity check.",
"Actions on types annotated with ApiControllerAttribute do not require explicit ModelState validity check.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor MVC1002_ApiActionsShouldReturnActionResultOf =
new DiagnosticDescriptor(
"MVC1002",
"Actions on types annotated with ApiControllerAttribute should return ActionResult<T>.",
"Actions on types annotated with ApiControllerAttribute should return ActionResult<T>.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor MVC1003_ActionsMustNotBeAsyncVoid =
new DiagnosticDescriptor(
"MVC2001",
"Controller actions must not have async void signature.",
"Controller actions must not have async void signature.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>CSharp Analyzers for ASP.NET Core MVC.</Description>
<PackageTags>aspnetcore;aspnetcoremvc</PackageTags>
<VerifyVersion>false</VerifyVersion>
<VersionPrefix>$(ExperimentalVersionPrefix)</VersionPrefix>
<VersionSuffix>$(ExperimentalVersionSuffix)</VersionSuffix>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnableApiCheck>false</EnableApiCheck>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion)" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers\dotnet\cs\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
internal static class TypeNames
{
public const string ControllerAttribute = "Microsoft.AspNetCore.Mvc.ControllerAttribute";
public const string ApiControllerAttribute = "Microsoft.AspNetCore.Mvc.ApiControllerAttribute";
public const string NonActionAttribute = "Microsoft.AspNetCore.Mvc.NonActionAttribute";
public const string IRouteTemplateProvider = "Microsoft.AspNetCore.Mvc.Routing.IRouteTemplateProvider";
public const string ActionResultOfT = "Microsoft.AspNetCore.Mvc.ActionResult`1";
public const string Task = "System.Threading.Tasks.Task";
public const string TaskOfT = "System.Threading.Tasks.Task`1";
public const string ObjectResult = "Microsoft.AspNetCore.Mvc.ObjectResult";
public const string IActionResult = "Microsoft.AspNetCore.Mvc.IActionResult";
public const string ModelStateDictionary = "Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary";
public const string HttpGetAttribute = "Microsoft.AspNetCore.Mvc.HttpGetAttribute";
public const string HttpPostAttribute = "Microsoft.AspNetCore.Mvc.HttpPostAttribute";
public const string HttpPutAttribute = "Microsoft.AspNetCore.Mvc.HttpPutAttribute";
public const string HttpDeleteAttribute = "Microsoft.AspNetCore.Mvc.HttpDeleteAttribute";
public const string FromRouteAttribute = "Microsoft.AspNetCore.Mvc.FromRouteAttribute";
}
}

View File

@ -0,0 +1,174 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class ActionsMustNotBeAsyncVoidFacts : AnalyzerTestBase
{
protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; }
= new ActionsMustNotBeAsyncVoidAnalyzer();
protected override CodeFixProvider CodeFixProvider { get; }
= new ActionsMustNotBeAsyncVoidFixProvider();
[Fact]
public async Task NoDiagnosticsAreReturned_FoEmptyScenarios()
{
// Arrange
var test = @"";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenMethodIsNotAControllerAction()
{
// Arrange
var test =
@"
using System.Threading.Tasks;
public class UserViewModel
{
public async void Index() => await Task.Delay(10);
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task DiagnosticsAreReturned_WhenMethodIsAControllerAction()
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC2001",
Message = "Controller actions must not have async void signature.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 7, 18) }
};
var test =
@"
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
public class HomeController : Controller
{
public async void Index()
{
await Response.Body.FlushAsync();
}
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
public class HomeController : Controller
{
public async Task Index()
{
await Response.Body.FlushAsync();
}
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Fact]
public async Task DiagnosticsAreReturned_WhenActionMethodIsExpressionBodied()
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC2001",
Message = "Controller actions must not have async void signature.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 7, 18) }
};
var test =
@"
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
public class HomeController : Controller
{
public async void Index() => await Response.Body.FlushAsync();
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
public class HomeController : Controller
{
public async Task Index() => await Response.Body.FlushAsync();
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Fact]
public async Task CodeFix_ProducesFullyQualifiedNamespaces()
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC2001",
Message = "Controller actions must not have async void signature.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 6, 18) }
};
var test =
@"
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
public async void Index() => await Response.Body.FlushAsync();
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
public async System.Threading.Tasks.Task Index() => await Response.Body.FlushAsync();
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
}
}

View File

@ -0,0 +1,361 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class ApiActionsDoNotRequireExplicitModelValidationCheckFacts : AnalyzerTestBase
{
protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; }
= new ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer();
protected override CodeFixProvider CodeFixProvider { get; }
= new ApiActionsDoNotRequireExplicitModelValidationCheckCodeFixProvider();
[Fact]
public async Task NoDiagnosticsAreReturned_FoEmptyScenarios()
{
// Arrange
var test = @"";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenTypeIsNotApiController()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
public IActionResult Index() => View();
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenActionDoesNotHaveModelStateCheck()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : Controller
{
public IActionResult GetPetId()
{
return Ok(new object());
}
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenAActionsUseExpressionBodies()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : Controller
{
public IActionResult GetPetId() => ModelState.IsVald ? OK() : BadResult();
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_ForNonActions()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
private int GetPetIdPrivate() => 0;
protected int GetPetIdProtected() => 0;
public static IActionResult FindPetByStatus(int status) => null;
[NonAction]
public object Reset(int state) => null;
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public Task DiagnosticsAndCodeFixes_WhenActionHasModelStateIsValidCheck()
{
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
public IActionResult GetPetId()
{
if (!ModelState.IsValid)
{
return BadRequest();
}
return Ok();
}
}";
// Act & Assert
return VerifyAsync(test);
}
[Fact]
public Task DiagnosticsAndCodeFixes_WhenActionHasModelStateIsValidCheck_UsingComparisonToFalse()
{
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
public IActionResult GetPetId()
{
if (ModelState.IsValid == false)
{
return BadRequest();
}
return Ok();
}
}";
// Act & Assert
return VerifyAsync(test);
}
[Fact]
public Task DiagnosticsAndCodeFixes_WhenActionHasModelStateIsValidCheck_WithoutBraces()
{
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
public IActionResult GetPetId()
{
if (!ModelState.IsValid)
return BadRequest();
return Ok();
}
}";
return VerifyAsync(test);
}
private async Task VerifyAsync(string test)
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC1001",
Message = "Actions on types annotated with ApiControllerAttribute do not require explicit ModelState validity check.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 9, 9) }
};
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
public IActionResult GetPetId()
{
return Ok();
}
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Fact]
public async Task DiagnosticsAndCodeFixes_WhenModelStateIsInElseIf()
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC1001",
Message = "Actions on types annotated with ApiControllerAttribute do not require explicit ModelState validity check.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 13, 9) }
};
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
public IActionResult GetPetId()
{
if (User == null)
{
return Unauthorized();
}
else if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return Ok();
}
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
public IActionResult GetPetId()
{
if (User == null)
{
return Unauthorized();
}
return Ok();
}
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Fact]
public async Task DiagnosticsAndCodeFixes_WhenModelStateIsInNestedBlock()
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC1001",
Message = "Actions on types annotated with ApiControllerAttribute do not require explicit ModelState validity check.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 15, 13) }
};
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
public IActionResult GetPetId()
{
if (User == null)
{
return Unauthorized();
}
else
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Debug.Assert(ModelState.Count == 0);
}
return Ok();
}
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : ControllerBase
{
public IActionResult GetPetId()
{
if (User == null)
{
return Unauthorized();
}
else
{
Debug.Assert(ModelState.Count == 0);
}
return Ok();
}
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
}
}

View File

@ -0,0 +1,272 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class ApiActionsAreAttributeRoutedFacts : AnalyzerTestBase
{
protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; }
= new ApiActionsAreAttributeRoutedAnalyzer();
protected override CodeFixProvider CodeFixProvider { get; }
= new ApiActionsAreAttributeRoutedFixProvider();
[Fact]
public async Task NoDiagnosticsAreReturned_FoEmptyScenarios()
{
// Arrange
var test = @"";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenTypeIsNotApiController()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
public IActionResult Index() => View();
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenApiControllerActionHasAttribute()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : Controller
{
[HttpGet]
public int GetPetId() => 0;
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_ForNonActions()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController : Controller
{
private int GetPetIdPrivate() => 0;
protected int GetPetIdProtected() => 0;
public static IActionResult FindPetByStatus(int status) => null;
[NonAction]
public object Reset(int state) => null;
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task DiagnosticsAndCodeFixes_WhenApiControllerActionDoesNotHaveAttribute()
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC1000",
Message = "Actions on types annotated with ApiControllerAttribute must be attribute routed.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 8, 16) }
};
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route]
public class PetController : Controller
{
public int GetPetId() => 0;
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route]
public class PetController : Controller
{
[HttpGet]
public int GetPetId() => 0;
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Fact]
public async Task CodeFixes_ApplyFullyQualifiedNames()
{
// Arrange
var test =
@"
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route]
public class PetController
{
public object GetPet() => null;
}";
var expectedFix =
@"
[Microsoft.AspNetCore.Mvc.ApiController]
[Microsoft.AspNetCore.Mvc.Route]
public class PetController
{
[Microsoft.AspNetCore.Mvc.HttpGet]
public object GetPet() => null;
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Theory]
[InlineData("id")]
[InlineData("petId")]
public async Task CodeFixes_WithIdParameter(string idParameter)
{
// Arrange
var test =
$@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route]
public class PetController
{{
public IActionResult Post(string notid, int {idParameter}) => null;
}}";
var expectedFix =
$@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route]
public class PetController
{{
[HttpPost(""{{{idParameter}}}"")]
public IActionResult Post(string notid, int {idParameter}) => null;
}}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Fact]
public async Task CodeFixes_WithRouteParameter()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route]
public class PetController
{
public IActionResult DeletePetByStatus([FromRoute] Status status, [FromRoute] Category category) => null;
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route]
public class PetController
{
[HttpDelete(""{status}/{category}"")]
public IActionResult DeletePetByStatus([FromRoute] Status status, [FromRoute] Category category) => null;
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Fact]
public async Task CodeFixes_WhenAttributeCannotBeInferred()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route]
public class PetController
{
public IActionResult ModifyPet() => null;
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route]
public class PetController
{
[HttpPut]
public IActionResult ModifyPet() => null;
}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
// There isn't a good way to test all fixes simultaneously. We'll pick the last one to verify when we
// expect to have 4 fixes.
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics, codeFixIndex: 3);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
}
}

View File

@ -0,0 +1,257 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class ApiActionsShouldUseActionResultOfTFacts : AnalyzerTestBase
{
protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; }
= new ApiActionsShouldUseActionResultOfTAnalyzer();
protected override CodeFixProvider CodeFixProvider { get; }
= new ApiActionsShouldUseActionResultOfTCodeFixProvider();
[Fact]
public async Task NoDiagnosticsAreReturned_FoEmptyScenarios()
{
// Arrange
var test = @"";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenTypeIsNotApiController()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
public class HomeController: ControllerBase
{
public IActionResult Index() => View();
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_ForNonActions()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController: ControllerBaseBase
{
private int GetPetIdPrivate() => 0;
protected int GetPetIdProtected() => 0;
public static IActionResult FindPetByStatus(int status) => null;
[NonAction]
public object Reset(int state) => null;
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenActionAreExpressionBodiedMembers()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class PetController: ControllerBase
{
public IActionResult GetPetId() => ModelState.IsValid ? OK(new object()) : BadResult();
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Theory]
[InlineData("Pet")]
[InlineData("List<Pet>")]
[InlineData("System.Threading.Task<Pet>")]
public async Task NoDiagnosticsAreReturned_WhenTypeReturnsNonObjectResult(string returnType)
{
// Arrange
var test =
$@"
using Microsoft.AspNetCore.Mvc;
public class Pet {{ }}
[ApiController]
public class PetController: ControllerBase
{{
public {returnType} GetPetId() => null;
}}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task NoDiagnosticsAreReturned_WhenTypeReturnsActionResultOfT()
{
// Arrange
var test =
@"
using Microsoft.AspNetCore.Mvc;
public class Pet { }
[ApiController]
public class PetController: ControllerBase
{
public ActionResult<Pet> GetPetId() => null;
}";
var project = CreateProject(test);
// Act
var result = await GetDiagnosticAsync(project);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task DiagnosticsAreReturned_WhenActionsReturnIActionResult()
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC1002",
Message = "Actions on types annotated with ApiControllerAttribute should return ActionResult<T>.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 9, 12) }
};
var test =
@"
using Microsoft.AspNetCore.Mvc;
public class Pet {}
[ApiController]
public class PetController: ControllerBase
{
public IActionResult GetPet()
{
return Ok(new Pet());
}
}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
public class Pet {}
[ApiController]
public class PetController: ControllerBase
{
public ActionResult<Pet> GetPet()
{
return Ok(new Pet());
}
}";
var project = CreateProject(test);
// Act
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
[Fact]
public async Task DiagnosticsAreReturned_WhenActionReturnsAsyncIActionResult()
{
// Arrange
var expectedDiagnostic = new DiagnosticResult
{
Id = "MVC1002",
Message = "Actions on types annotated with ApiControllerAttribute should return ActionResult<T>.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test.cs", 8, 18) }
};
var test =
@"
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
[ApiController]
public class PetController: ControllerBase
{
public async Task<IActionResult> GetPet()
{
await Task.Delay(0);
return Ok(new Pet());
}
}
public class Pet {}";
var expectedFix =
@"
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
[ApiController]
public class PetController: ControllerBase
{
public async Task<ActionResult<Pet>> GetPet()
{
await Task.Delay(0);
return Ok(new Pet());
}
}
public class Pet {}";
var project = CreateProject(test);
// Act & Assert
var actualDiagnostics = await GetDiagnosticAsync(project);
Assert.DiagnosticsEqual(new[] { expectedDiagnostic }, actualDiagnostics);
var actualFix = await ApplyCodeFixAsync(project, actualDiagnostics);
Assert.Equal(expectedFix, actualFix, ignoreLineEndingDifferences: true);
}
}
}

View File

@ -0,0 +1,132 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.DependencyModel;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure
{
public abstract class AnalyzerTestBase : IDisposable
{
private static readonly object WorkspaceLock = new object();
public Workspace Workspace { get; private set; }
protected abstract DiagnosticAnalyzer DiagnosticAnalyzer { get; }
protected virtual CodeFixProvider CodeFixProvider { get; }
protected Project CreateProject(string source)
{
var projectId = ProjectId.CreateNewId(debugName: "TestProject");
var newFileName = "Test.cs";
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
var metadataReferences = DependencyContext.Load(GetType().Assembly)
.CompileLibraries
.SelectMany(c => c.ResolveReferencePaths())
.Select(path => MetadataReference.CreateFromFile(path))
.Cast<MetadataReference>()
.ToList();
lock (WorkspaceLock)
{
if (Workspace == null)
{
Workspace = new AdhocWorkspace();
}
}
var solution = Workspace
.CurrentSolution
.AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp)
.AddMetadataReferences(projectId, metadataReferences)
.AddDocument(documentId, newFileName, SourceText.From(source));
return solution.GetProject(projectId);
}
protected async Task<Diagnostic[]> GetDiagnosticAsync(Project project)
{
var compilation = await project.GetCompilationAsync();
var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(DiagnosticAnalyzer));
var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
}
protected Task<string> ApplyCodeFixAsync(
Project project,
Diagnostic[] analyzerDiagnostic,
int codeFixIndex = 0)
{
var diagnostic = analyzerDiagnostic.Single();
return ApplyCodeFixAsync(project, diagnostic, codeFixIndex);
}
protected async Task<string> ApplyCodeFixAsync(
Project project,
Diagnostic analyzerDiagnostic,
int codeFixIndex = 0)
{
if (CodeFixProvider == null)
{
throw new InvalidOperationException($"{nameof(CodeFixProvider)} has not been assigned.");
}
var document = project.Documents.Single();
var actions = new List<CodeAction>();
var context = new CodeFixContext(document, analyzerDiagnostic, (a, d) => actions.Add(a), CancellationToken.None);
await CodeFixProvider.RegisterCodeFixesAsync(context);
if (actions.Count == 0)
{
throw new InvalidOperationException("CodeFix produced no actions to apply.");
}
var updatedSolution = await ApplyFixAsync(actions[codeFixIndex]);
// Todo: figure out why this doesn't work.
// var updatedProject = updatedSolution.GetProject(project.Id);
// await EnsureCompilable(updatedProject);
var updatedDocument = updatedSolution.GetDocument(document.Id);
var sourceText = await updatedDocument.GetTextAsync();
return sourceText.ToString();
}
private static async Task EnsureCompilable(Project project)
{
var compilation = await project
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.GetCompilationAsync();
var diagnostics = compilation.GetDiagnostics();
if (diagnostics.Length != 0)
{
var message = string.Join(
Environment.NewLine,
diagnostics.Select(d => CSharpDiagnosticFormatter.Instance.Format(d)));
throw new InvalidOperationException($"Compilation failed:{Environment.NewLine}{message}");
}
}
private static async Task<Solution> ApplyFixAsync(CodeAction codeAction)
{
var operations = await codeAction.GetOperationsAsync(CancellationToken.None);
return Assert.Single(operations.OfType<ApplyChangesOperation>()).ChangedSolution;
}
public void Dispose()
{
Workspace?.Dispose();
}
}
}

View File

@ -0,0 +1,168 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
internal class Assert : Xunit.Assert
{
public static void DiagnosticsEqual(IEnumerable<DiagnosticResult> expected, IEnumerable<Diagnostic> actual)
{
var expectedCount = expected.Count();
var actualCount = actual.Count();
if (expectedCount != actualCount)
{
throw new DiagnosticsAssertException(
expected,
actual,
$"Mismatch between number of diagnostics returned, expected \"{expectedCount}\" actual \"{actualCount}.");
}
foreach (var (expectedItem, actualItem) in expected.Zip(actual, (a, b) => (a, b)))
{
if (expectedItem.Line == -1 && expectedItem.Column == -1)
{
if (actualItem.Location != Location.None)
{
throw new DiagnosticAssertException(
expectedItem,
actualItem,
$"Expected: A project diagnostic with no location. Actual {actualItem.Location}.");
}
}
else
{
VerifyDiagnosticLocation(expectedItem, actualItem);
}
if (actualItem.Id != expectedItem.Id)
{
throw new DiagnosticAssertException(
expectedItem,
actualItem,
$"Expected: Expected id: {expectedItem.Id}. Actual id: {actualItem.Id}.");
}
if (actualItem.Severity != expectedItem.Severity)
{
throw new DiagnosticAssertException(
expectedItem,
actualItem,
$"Expected: Expected severity: {expectedItem.Severity}. Actual severity: {actualItem.Severity}.");
}
if (actualItem.GetMessage() != expectedItem.Message)
{
throw new DiagnosticAssertException(
expectedItem,
actualItem,
$"Expected: Expected message: {expectedItem.Message}. Actual message: {actualItem.GetMessage()}.");
}
}
}
private static void VerifyDiagnosticLocation(DiagnosticResult expected, Diagnostic actual)
{
var actualSpan = actual.Location.GetLineSpan();
var actualLinePosition = actualSpan.StartLinePosition;
// Only check line position if there is an actual line in the real diagnostic
if (actualLinePosition.Line > 0)
{
if (actualLinePosition.Line + 1 != expected.Line)
{
throw new DiagnosticAssertException(
expected,
actual,
$"Expected diagnostic to be on line \"{expected.Line}\" was actually on line \"{actualLinePosition.Line + 1}\"");
}
}
// Only check column position if there is an actual column position in the real diagnostic
if (actualLinePosition.Character > 0)
{
if (actualLinePosition.Character + 1 != expected.Column)
{
throw new DiagnosticAssertException(
expected,
actual,
$"Expected diagnostic to start at column \"{expected.Column}\" was actually on line \"{actualLinePosition.Character + 1}\"");
}
}
}
private static string FormatDiagnostics(IEnumerable<Diagnostic> diagnostics)
{
return string.Join(Environment.NewLine, diagnostics.Select(FormatDiagnostic));
}
private static string FormatDiagnostic(Diagnostic diagnostic)
{
var builder = new StringBuilder();
builder.AppendLine(diagnostic.ToString());
var location = diagnostic.Location;
if (location == Location.None)
{
builder.Append($"Location unknown: ({diagnostic.Id})");
}
else
{
True(location.IsInSource,
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostic}");
var linePosition = location.GetLineSpan().StartLinePosition;
builder.Append($"({(linePosition.Line + 1)}, {(linePosition.Character + 1)}, {diagnostic.Id})");
}
return builder.ToString();
}
private static async Task<string> GetStringFromDocumentAsync(Document document)
{
var simplifiedDoc = await Simplifier.ReduceAsync(document, Simplifier.Annotation);
var root = await simplifiedDoc.GetSyntaxRootAsync();
root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace);
return root.GetText().ToString();
}
private class DiagnosticsAssertException : Xunit.Sdk.EqualException
{
public DiagnosticsAssertException(
IEnumerable<DiagnosticResult> expected,
IEnumerable<Diagnostic> actual,
string message)
: base(expected, actual)
{
Message = message + Environment.NewLine + FormatDiagnostics(actual);
}
public override string Message { get; }
}
private class DiagnosticAssertException : Xunit.Sdk.EqualException
{
public DiagnosticAssertException(
DiagnosticResult expected,
Diagnostic actual,
string message)
: base(expected, actual)
{
Message = message + Environment.NewLine + FormatDiagnostic(actual);
}
public override string Message { get; }
}
}
}

View File

@ -0,0 +1,70 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure
{
/// <summary>
/// Location where the diagnostic appears, as determined by path, line number, and column number.
/// </summary>
public struct DiagnosticResultLocation
{
public DiagnosticResultLocation(string path, int line, int column)
{
if (line < -1)
{
throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
}
if (column < -1)
{
throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
}
Path = path;
Line = line;
Column = column;
}
public string Path { get; }
public int Line { get; }
public int Column { get; }
}
/// <summary>
/// Struct that stores information about a Diagnostic appearing in a source
/// </summary>
public struct DiagnosticResult
{
private DiagnosticResultLocation[] _locations;
public DiagnosticResultLocation[] Locations
{
get
{
if (_locations == null)
{
_locations = new DiagnosticResultLocation[] { };
}
return _locations;
}
set => _locations = value;
}
public DiagnosticSeverity Severity { get; set; }
public string Id { get; set; }
public string Message { get; set; }
public string Path => Locations.Length > 0 ? Locations[0].Path : "";
public int Line => Locations.Length > 0 ? Locations[0].Line : -1;
public int Column => Locations.Length > 0 ? Locations[0].Column : -1;
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
<ItemGroup>
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Mvc.Analyzers\Microsoft.AspNetCore.Mvc.Analyzers.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Mvc\Microsoft.AspNetCore.Mvc.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,3 @@
{
"shadowCopy": false
}

View File

@ -2,9 +2,15 @@
<PropertyGroup>
<VersionPrefix>2.1.0</VersionPrefix>
<VersionSuffix>preview1</VersionSuffix>
<ExperimentalVersionPrefix>0.1.0</ExperimentalVersionPrefix>
<ExperimentalVersionSuffix>alpha1</ExperimentalVersionSuffix>
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
<BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
<ExperimentalVersionSuffix Condition="'$(ExperimentalVersionSuffix)' != '' And '$(BuildNumber)' != ''">$(ExperimentalVersionSuffix)-$(BuildNumber)</ExperimentalVersionSuffix>
</PropertyGroup>
</Project>