Create an analyzer to warn users not to decorate filters on page handler methods

Fixes #7684
This commit is contained in:
Pranav K 2018-06-01 14:29:34 -07:00
parent 839223756b
commit 592ed3b4f5
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
20 changed files with 621 additions and 1 deletions

View File

@ -0,0 +1,166 @@
// 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 AttributesShouldNotBeAppliedToPageModelAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
DiagnosticDescriptors.MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods,
DiagnosticDescriptors.MVC1002_RouteAttributesShouldNotBeAppliedToPageHandlerMethods,
DiagnosticDescriptors.MVC1003_RouteAttributesShouldNotBeAppliedToPageModels);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(compilationStartAnalysisContext =>
{
var typeCache = new TypeCache(compilationStartAnalysisContext.Compilation);
if (typeCache.PageModelAttribute == null || typeCache.PageModelAttribute.TypeKind == TypeKind.Error)
{
// No-op if we can't find types we care about.
return;
}
InitializeWorker(compilationStartAnalysisContext, typeCache);
});
}
private void InitializeWorker(CompilationStartAnalysisContext compilationStartAnalysisContext, TypeCache typeCache)
{
compilationStartAnalysisContext.RegisterSymbolAction(symbolAnalysisContext =>
{
var method = (IMethodSymbol)symbolAnalysisContext.Symbol;
var declaringType = method.ContainingType;
if (!IsPageModel(declaringType, typeCache.PageModelAttribute) || !IsPageHandlerMethod(method))
{
return;
}
ReportFilterDiagnostic(ref symbolAnalysisContext, method, typeCache.IFilterMetadata);
ReportFilterDiagnostic(ref symbolAnalysisContext, method, typeCache.AuthorizeAttribute);
ReportFilterDiagnostic(ref symbolAnalysisContext, method, typeCache.AllowAnonymousAttribute);
ReportRouteDiagnostic(ref symbolAnalysisContext, method, typeCache.IRouteTemplateProvider);
}, SymbolKind.Method);
compilationStartAnalysisContext.RegisterSymbolAction(symbolAnalysisContext =>
{
var type = (INamedTypeSymbol)symbolAnalysisContext.Symbol;
if (!IsPageModel(type, typeCache.PageModelAttribute))
{
return;
}
ReportRouteDiagnosticOnModel(ref symbolAnalysisContext, type, typeCache.IRouteTemplateProvider);
}, SymbolKind.NamedType);
}
private bool IsPageHandlerMethod(IMethodSymbol method)
{
return method.MethodKind == MethodKind.Ordinary &&
!method.IsStatic &&
!method.IsGenericMethod &&
method.DeclaredAccessibility == Accessibility.Public;
}
private static bool IsPageModel(INamedTypeSymbol type, INamedTypeSymbol pageAttributeModel)
{
return type.TypeKind == TypeKind.Class &&
!type.IsStatic &&
type.HasAttribute(pageAttributeModel, inherit: true);
}
private static void ReportRouteDiagnosticOnModel(ref SymbolAnalysisContext symbolAnalysisContext, INamedTypeSymbol typeSymbol, INamedTypeSymbol routeAttribute)
{
var attribute = GetAttribute(typeSymbol, routeAttribute);
if (attribute != null)
{
var location = GetAttributeLocation(ref symbolAnalysisContext, attribute);
symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.MVC1003_RouteAttributesShouldNotBeAppliedToPageModels,
location,
attribute.AttributeClass.Name));
}
}
private static void ReportRouteDiagnostic(ref SymbolAnalysisContext symbolAnalysisContext, IMethodSymbol method, INamedTypeSymbol routeAttribute)
{
var attribute = GetAttribute(method, routeAttribute);
if (attribute != null)
{
var location = GetAttributeLocation(ref symbolAnalysisContext, attribute);
symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.MVC1002_RouteAttributesShouldNotBeAppliedToPageHandlerMethods,
location,
attribute.AttributeClass.Name));
}
}
private static void ReportFilterDiagnostic(ref SymbolAnalysisContext symbolAnalysisContext, IMethodSymbol method, INamedTypeSymbol filterAttribute)
{
var attribute = GetAttribute(method, filterAttribute);
if (attribute != null)
{
var location = GetAttributeLocation(ref symbolAnalysisContext, attribute);
symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods,
location,
attribute.AttributeClass.Name));
}
}
private static AttributeData GetAttribute(ISymbol symbol, INamedTypeSymbol attributeType)
{
foreach (var attribute in symbol.GetAttributes())
{
if (attributeType.IsAssignableFrom(attribute.AttributeClass))
{
return attribute;
}
}
return null;
}
private static Location GetAttributeLocation(ref SymbolAnalysisContext symbolAnalysisContext, AttributeData attribute)
{
var syntax = attribute.ApplicationSyntaxReference.GetSyntax(symbolAnalysisContext.CancellationToken);
return syntax?.GetLocation() ?? Location.None;
}
private class TypeCache
{
public TypeCache(Compilation compilation)
{
PageModelAttribute = compilation.GetTypeByMetadataName(SymbolNames.PageModelAttributeType);
IFilterMetadata = compilation.GetTypeByMetadataName(SymbolNames.IFilterMetadataType);
AuthorizeAttribute = compilation.GetTypeByMetadataName(SymbolNames.AuthorizeAttribute);
AllowAnonymousAttribute = compilation.GetTypeByMetadataName(SymbolNames.AllowAnonymousAttribute);
IRouteTemplateProvider = compilation.GetTypeByMetadataName(SymbolNames.IRouteTemplateProvider);
}
public INamedTypeSymbol PageModelAttribute { get; }
public INamedTypeSymbol IFilterMetadata { get; }
public INamedTypeSymbol AuthorizeAttribute { get; }
public INamedTypeSymbol AllowAnonymousAttribute { get; }
public INamedTypeSymbol IRouteTemplateProvider { get; }
}
}
}

View File

@ -0,0 +1,80 @@
// 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.Generic;
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)
{
foreach (var type in typeSymbol.GetTypeHierarchy())
{
if (type.HasAttribute(attribute))
{
return true;
}
}
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.TypeKind == TypeKind.Interface)
{
foreach (var @interface in target.AllInterfaces)
{
if (source == @interface)
{
return true;
}
}
return false;
}
foreach (var type in target.GetTypeHierarchy())
{
if (source == type)
{
return true;
}
}
return false;
}
private static IEnumerable<ITypeSymbol> GetTypeHierarchy(this ITypeSymbol typeSymbol)
{
while (typeSymbol != null)
{
yield return typeSymbol;
typeSymbol = typeSymbol.BaseType;
}
}
}
}

View File

@ -15,5 +15,32 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods =
new DiagnosticDescriptor(
"MVC1001",
"Filters cannot be applied to page handler methods.",
"'{0}' cannot be applied to Razor Page handler methods. It may be applied either to the Razor Page model or applied globally.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor MVC1002_RouteAttributesShouldNotBeAppliedToPageHandlerMethods =
new DiagnosticDescriptor(
"MVC1002",
"Route attributes cannot be applied to page handler methods.",
"'{0}' cannot be applied to Razor Page handler methods. Routes for Razor Pages must be declared using the @page directive or using conventions.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor MVC1003_RouteAttributesShouldNotBeAppliedToPageModels =
new DiagnosticDescriptor(
"MVC1003",
"Route attributes cannot be applied to page models.",
"'{0}' cannot be applied to a Razor Page model. Routes for Razor Pages must be declared using the @page directive or using conventions.",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
}

View File

@ -5,12 +5,22 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
{
internal static class SymbolNames
{
public const string IHtmlHelperType = "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper";
public const string AllowAnonymousAttribute = "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute";
public const string AuthorizeAttribute = "Microsoft.AspNetCore.Authorization.AuthorizeAttribute";
public const string IFilterMetadataType = "Microsoft.AspNetCore.Mvc.Filters.IFilterMetadata";
public const string HtmlHelperPartialExtensionsType = "Microsoft.AspNetCore.Mvc.Rendering.HtmlHelperPartialExtensions";
public const string IHtmlHelperType = "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper";
public const string PageModelAttributeType = "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageModelAttribute";
public const string PartialMethod = "Partial";
public const string RenderPartialMethod = "RenderPartial";
public const string IRouteTemplateProvider = "Microsoft.AspNetCore.Mvc.Routing.IRouteTemplateProvider";
}
}

View File

@ -0,0 +1,120 @@
// 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.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Analyzer.Testing;
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class AttributesShouldNotBeAppliedToPageModelAnalyzerTest
{
private MvcDiagnosticAnalyzerRunner Executor { get; } = new MvcDiagnosticAnalyzerRunner(new AttributesShouldNotBeAppliedToPageModelAnalyzer());
[Fact]
public async Task NoDiagnosticsAreReturned_FoEmptyScenarios()
{
// Act
var result = await Executor.GetDiagnosticsAsync(source: string.Empty);
// Assert
Assert.Empty(result);
}
[Fact]
public Task NoDiagnosticsAreReturned_ForControllerBaseActions()
=> VerifyNoDiagnosticsAreReturned();
[Fact]
public Task NoDiagnosticsAreReturned_ForControllerActions()
=> VerifyNoDiagnosticsAreReturned();
[Fact]
public Task NoDiagnosticsAreReturned_ForPageHandlersWithNonFilterAttributes()
=> VerifyNoDiagnosticsAreReturned();
[Fact]
public Task NoDiagnosticsAreReturned_IfFiltersAreAppliedToPageModel()
=> VerifyNoDiagnosticsAreReturned();
[Fact]
public Task NoDiagnosticsAreReturned_IfAuthorizeAttributeIsAppliedToPageModel()
=> VerifyNoDiagnosticsAreReturned();
[Fact]
public Task NoDiagnosticsAreReturned_IfAllowAnonymousIsAppliedToPageModel()
=> VerifyNoDiagnosticsAreReturned();
[Fact]
public Task NoDiagnosticsAreReturned_ForNonHandlerMethodsWithAttributes()
=> VerifyNoDiagnosticsAreReturned();
[Fact]
public Task DiagnosticsAreReturned_IfFiltersAreAppliedToPageHandlerMethod()
=> VerifyDefault(DiagnosticDescriptors.MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods);
[Fact]
public Task DiagnosticsAreReturned_IfFiltersAreAppliedToPageHandlerMethodDerivingFromCustomModel()
=> VerifyDefault(DiagnosticDescriptors.MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods);
[Fact]
public Task DiagnosticsAreReturned_IfAuthorizeAttributeIsAppliedToPageHandlerMethod()
=> VerifyDefault(DiagnosticDescriptors.MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods);
[Fact]
public Task DiagnosticsAreReturned_IfFiltersAreAppliedToPageHandlerMethodForTypeWithPageModelAttribute()
=> VerifyDefault(DiagnosticDescriptors.MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods);
[Fact]
public Task DiagnosticsAreReturned_IfAttributeIsAppliedToBaseType()
=> VerifyDefault(DiagnosticDescriptors.MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods);
[Fact]
public Task DiagnosticsAreReturned_IfRouteAttributesAreAppliedToPageHandlerMethod()
=> VerifyDefault(DiagnosticDescriptors.MVC1002_RouteAttributesShouldNotBeAppliedToPageHandlerMethods);
[Fact]
public Task DiagnosticsAreReturned_IfAllowAnonymousIsAppliedToPageHandlerMethod()
=> VerifyDefault(DiagnosticDescriptors.MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods);
[Fact]
public Task DiagnosticsAreReturned_IfRouteAttribute_IsAppliedToPageModel()
=> VerifyDefault(DiagnosticDescriptors.MVC1003_RouteAttributesShouldNotBeAppliedToPageModels);
private async Task VerifyNoDiagnosticsAreReturned([CallerMemberName] string testMethod = "")
{
// Arrange
var source = MvcTestSource.Read(GetType().Name, testMethod);
// Act
var result = await Executor.GetDiagnosticsAsync(source.Source);
// Assert
Assert.Empty(result);
}
private async Task VerifyDefault(DiagnosticDescriptor descriptor, [CallerMemberName] string testMethod = "")
{
// Arrange
var testSource = MvcTestSource.Read(GetType().Name, testMethod);
var expectedLocation = testSource.DefaultMarkerLocation;
// Act
var result = await Executor.GetDiagnosticsAsync(testSource.Source);
// Assert
Assert.Collection(
result,
diagnostic =>
{
Assert.Equal(descriptor.Id, diagnostic.Id);
Assert.Same(descriptor, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(expectedLocation, diagnostic.Location);
});
}
}
}

View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class DiagnosticsAreReturned_IfAllowAnonymousIsAppliedToPageHandlerMethod : PageModel
{
[/*MM*/AllowAnonymous]
public void OnGet()
{
}
public void OnPost()
{
}
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
[PageModel]
public abstract class DiagnosticsAreReturned_IfAttributeIsAppliedToBaseTypeBase
{
[/*MM*/Authorize]
public void OnGet() { }
}
public class DiagnosticsAreReturned_IfAttributeIsAppliedToBaseType : DiagnosticsAreReturned_IfAttributeIsAppliedToBaseTypeBase
{
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class DiagnosticsAreReturned_IfAuthorizeAttributeIsAppliedToPageHandlerMethod : PageModel
{
[/*MM*/Authorize]
public void OnPost()
{
}
}
}

View File

@ -0,0 +1,13 @@
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class DiagnosticsAreReturned_IfFiltersAreAppliedToPageHandlerMethod : PageModel
{
[/*MM*/ServiceFilter(typeof(object))]
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,20 @@
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
[PageModel]
public abstract class CustomPageModel
{
}
public class DiagnosticsAreReturned_IfFiltersAreAppliedToPageHandlerMethodDerivingFromCustomModel : CustomPageModel
{
[/*MM*/ServiceFilter(typeof(object))]
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
[PageModel]
public class DiagnosticsAreReturned_IfFiltersAreAppliedToPageHandlerMethodForTypeWithPageModelAttribute
{
[/*MM*/ServiceFilter(typeof(object))]
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
[/*MM*/Route("/mypage")]
public class DiagnosticsAreReturned_IfRouteAttribute_IsAppliedToPageModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class DiagnosticsAreReturned_IfRouteAttributesAreAppliedToPageHandlerMethod : PageModel
{
[/*MM*/HttpHead]
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class NoDiagnosticsAreReturned_ForControllerActions : Controller
{
[Authorize]
public IActionResult AuthorizeAttribute() => null;
[ServiceFilter(typeof(object))]
public IActionResult ServiceFilter() => null;
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class NoDiagnosticsAreReturned_ForControllerBaseActions : ControllerBase
{
[Authorize]
public IActionResult AuthorizeAttribute() => null;
[ServiceFilter(typeof(object))]
public IActionResult ServiceFilter() => null;
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class NoDiagnosticsAreReturned_ForNonHandlerMethodsWithAttributes : PageModel
{
[Authorize]
private void OnGetPrivate() { }
[TypeFilter(typeof(object))]
internal IActionResult OnPost() => null;
[AllowAnonymous]
public void OnGet<T>() { }
[ServiceFilter(typeof(object))]
public static void OnPostStatic() { }
}
}

View File

@ -0,0 +1,13 @@
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
public class NoDiagnosticsAreReturned_ForPageHandlersWithNonFilterAttributes : PageModel
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
[AllowAnonymous]
public class NoDiagnosticsAreReturned_IfAllowAnonymousIsAppliedToPageModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
[Authorize]
public class NoDiagnosticsAreReturned_IfAuthorizeAttributeIsAppliedToPageModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.Analyzers.Test
{
[ServiceFilter(typeof(object))]
public class NoDiagnosticsAreReturned_IfFiltersAreAppliedToPageModel : PageModel
{
public void OnGet()
{
}
}
}