Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Create an analyzer to warn users not to decorate filters on page hand…
Browse files Browse the repository at this point in the history
…ler methods

Fixes #7684
  • Loading branch information
pranavkm committed Jun 4, 2018
1 parent f0c552e commit 118e6f0
Show file tree
Hide file tree
Showing 19 changed files with 576 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// 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
{
[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)
{
#if !DEBUG
context.EnableConcurrentExecution();
#endif
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

TypeCache typeCache = null;
context.RegisterCompilationStartAction(compilationStartAnalysisContext =>
{
typeCache = 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))
{
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 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; }
}
}
}
80 changes: 80 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
27 changes: 27 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Analyzers/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,32 @@ public static class DiagnosticDescriptors
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor MVC1001_FiltersShouldNotBeAppliedToPageHandlerMethods =
new DiagnosticDescriptor(
"MVC1001",
"Filters cannot be applied to page handler methods.",
"'{0}' cannoted be applied to a Razor Page handler methods. It may be applied either to a PageModel or 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 a 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);
}
}
12 changes: 11 additions & 1 deletion src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Analyzers\Microsoft.AspNetCore.Mvc.Analyzers.csproj" PrivateAssets="None" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Analyzers\Microsoft.AspNetCore.Mvc.Analyzers.csproj" PrivateAssets="contentfiles;build" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.ApiExplorer\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Cors\Microsoft.AspNetCore.Mvc.Cors.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.DataAnnotations\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" />
Expand Down
Loading

0 comments on commit 118e6f0

Please sign in to comment.