Introduce Mvc analyzers
This commit is contained in:
parent
61d42825d7
commit
c6c77dd4d3
|
|
@ -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
32
Mvc.sln
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"shadowCopy": false
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue