forked from ardalis/ApiEndpoints
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Code Analyzer one method per endpoint (ardalis#25)
* initial code analyzer template * EndpointHasExtraPublicMethodAnalyzer
- Loading branch information
Showing
16 changed files
with
1,516 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
src/Ardalis.ApiEndpoints.CodeAnalyzers/Ardalis.ApiEndpoints.CodeAnalyzers.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<IncludeBuildOutput>false</IncludeBuildOutput> | ||
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking> | ||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<PackageId>Ardalis.ApiEndpoints.CodeAnalyzers</PackageId> | ||
<Version>1.0.0</Version> | ||
<Authors>Steve Smith (@ardalis), Philip Pittle (@ppittle)</Authors> | ||
<PackageIconUrl>https://user-images.githubusercontent.com/782127/33497760-facf6550-d69c-11e7-94e4-b3856da259a9.png</PackageIconUrl> | ||
<Company>Ardalis.com</Company> | ||
<PackageProjectUrl>https://github.com/ardalis/ApiEndpoints</PackageProjectUrl> | ||
<Description>Code Analyzers supporting using Api Endpoints</Description> | ||
<Summary>Code Analyzers increasing productivity of developers using the Api Endpoints framework.</Summary> | ||
<RepositoryUrl>https://github.com/ardalis/ApiEndpoints</RepositoryUrl> | ||
<NoPackageAnalysis>true</NoPackageAnalysis> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="2.9.8" /> | ||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.3.1" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<None Update="tools\*.ps1" CopyToOutputDirectory="Always" Pack="true" PackagePath="" /> | ||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> | ||
</ItemGroup> | ||
|
||
</Project> |
100 changes: 100 additions & 0 deletions
100
src/Ardalis.ApiEndpoints.CodeAnalyzers/EndpointHasExtraPublicMethodAnalyzer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
|
||
namespace Ardalis.ApiEndpoints.CodeAnalyzers | ||
{ | ||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public class EndpointHasExtraPublicMethodAnalyzer : DiagnosticAnalyzer | ||
{ | ||
public const string DiagnosticId = "ApiEndpoints101"; | ||
|
||
internal static readonly LocalizableString Title = "Endpoint has more than one public method"; | ||
internal static readonly LocalizableString MessageFormat = "Endpoint {0} has additional public method {1}. Endpoints must have only one public method."; | ||
private static readonly LocalizableString Description = "MVC will interpret additional public methods on an Endpoint as Actions. Limit Endpoints to a single Action"; | ||
private const string Category = "Naming"; | ||
|
||
private static readonly DiagnosticDescriptor Rule = | ||
new DiagnosticDescriptor( | ||
DiagnosticId, | ||
Title, | ||
MessageFormat, | ||
Category, | ||
DiagnosticSeverity.Warning, | ||
isEnabledByDefault: true, | ||
description: Description); | ||
|
||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | ||
|
||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None); | ||
context.EnableConcurrentExecution(); | ||
context.RegisterSymbolAction(AnalyzeMethodDeclaration, SymbolKind.Method); | ||
} | ||
|
||
private void AnalyzeMethodDeclaration(SymbolAnalysisContext context) | ||
{ | ||
try | ||
{ | ||
var methodSymbol = context.Symbol as IMethodSymbol; | ||
|
||
if (null == methodSymbol) | ||
return; | ||
|
||
var isApiEndpoint = | ||
methodSymbol | ||
.ContainingType | ||
.GetAllBaseTypes() | ||
.Any(x => | ||
x.Name == "BaseEndpoint" || | ||
x.Name == "BaseAsyncEndpoint"); | ||
|
||
// not a type inheriting BaseEndpoint | ||
if (!isApiEndpoint) | ||
return; | ||
|
||
// isn't a new public method | ||
if (methodSymbol.IsOverride || methodSymbol.DeclaredAccessibility != Accessibility.Public) | ||
return; | ||
|
||
// at this point, we have a new public method on a BaseEndpoint that violates the rule | ||
var diagnostic = Diagnostic.Create( | ||
Rule, | ||
context.Symbol.Locations.FirstOrDefault(), | ||
methodSymbol.ContainingType.Name, | ||
methodSymbol.Name); | ||
|
||
context.ReportDiagnostic(diagnostic); | ||
} | ||
catch (Exception e) | ||
{ | ||
Debug.Write(e); | ||
|
||
if (Debugger.IsAttached) | ||
throw; | ||
} | ||
} | ||
} | ||
|
||
public static class NamedTypeSymbolExtensions | ||
{ | ||
public static List<INamedTypeSymbol> GetAllBaseTypes(this INamedTypeSymbol namedTypeSymbol) | ||
{ | ||
var baseTypes = new List<INamedTypeSymbol>(); | ||
|
||
for (var visitor = namedTypeSymbol.BaseType; visitor != null; visitor = visitor.BaseType) | ||
{ | ||
if (!baseTypes.Contains(visitor)) | ||
baseTypes.Add(visitor); | ||
} | ||
|
||
return baseTypes; | ||
} | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
src/Ardalis.ApiEndpoints.CodeAnalyzers/EndpointHasExtraPublicMethodCodeFixProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
using System; | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.CodeActions; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
||
namespace Ardalis.ApiEndpoints.CodeAnalyzers | ||
{ | ||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EndpointHasExtraPublicMethodCodeFixProvider)), Shared] | ||
public class EndpointHasExtraPublicMethodCodeFixProvider : CodeFixProvider | ||
{ | ||
private const string MakeInternalTitle = "Make additonal method internal."; | ||
private const string MakePrivateTitle = "Make additonal method private."; | ||
|
||
public sealed override ImmutableArray<string> FixableDiagnosticIds | ||
{ | ||
get { return ImmutableArray.Create(EndpointHasExtraPublicMethodAnalyzer.DiagnosticId); } | ||
} | ||
|
||
public sealed override FixAllProvider GetFixAllProvider() | ||
{ | ||
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers | ||
return WellKnownFixAllProviders.BatchFixer; | ||
} | ||
|
||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
|
||
var diagnostic = context.Diagnostics.First(); | ||
var diagnosticSpan = diagnostic.Location.SourceSpan; | ||
|
||
// Find the method declaration identified by the diagnostic. | ||
var declaration = | ||
root | ||
.FindToken(diagnosticSpan.Start) | ||
.Parent | ||
.AncestorsAndSelf() | ||
.OfType<MethodDeclarationSyntax>() | ||
.First(); | ||
|
||
// Register a code action that will invoke the fix. | ||
context.RegisterCodeFix( | ||
CodeAction.Create( | ||
title: MakeInternalTitle, | ||
createChangedDocument: c => ChangeMethodKindAsync(context.Document, declaration, SyntaxKind.InternalKeyword, c), | ||
equivalenceKey: MakeInternalTitle), | ||
diagnostic); | ||
|
||
context.RegisterCodeFix( | ||
CodeAction.Create( | ||
title: MakePrivateTitle, | ||
createChangedDocument: c => ChangeMethodKindAsync(context.Document, declaration, SyntaxKind.PrivateKeyword, c), | ||
equivalenceKey: MakePrivateTitle), | ||
diagnostic); | ||
} | ||
|
||
private async Task<Document> ChangeMethodKindAsync( | ||
Document document, | ||
MethodDeclarationSyntax method, | ||
SyntaxKind targetKind, | ||
CancellationToken cancellationToken) | ||
{ | ||
try | ||
{ | ||
var modifierList = | ||
// create the new modifier list, but replace the public token | ||
// goal is to preserve order | ||
method | ||
.Modifiers | ||
.Select(x => | ||
x.Kind() is SyntaxKind.PublicKeyword | ||
? SyntaxFactory.Token(targetKind) | ||
: x) | ||
.ToList(); | ||
|
||
// remove the public modifier | ||
var newMethod = method.WithModifiers(new SyntaxTokenList(modifierList)); | ||
|
||
var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); | ||
var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
var newRoot = root.ReplaceNode(method, newMethod); | ||
|
||
return document.WithSyntaxRoot(newRoot); | ||
} | ||
catch (Exception e) | ||
{ | ||
Debug.Write(e); | ||
|
||
if (Debugger.IsAttached) | ||
throw; | ||
|
||
return document; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
param($installPath, $toolsPath, $package, $project) | ||
|
||
if($project.Object.SupportsPackageDependencyResolution) | ||
{ | ||
if($project.Object.SupportsPackageDependencyResolution()) | ||
{ | ||
# Do not install analyzers via install.ps1, instead let the project system handle it. | ||
return | ||
} | ||
} | ||
|
||
$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve | ||
|
||
foreach($analyzersPath in $analyzersPaths) | ||
{ | ||
if (Test-Path $analyzersPath) | ||
{ | ||
# Install the language agnostic analyzers. | ||
foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) | ||
{ | ||
if($project.Object.AnalyzerReferences) | ||
{ | ||
$project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) | ||
} | ||
} | ||
} | ||
} | ||
|
||
# $project.Type gives the language name like (C# or VB.NET) | ||
$languageFolder = "" | ||
if($project.Type -eq "C#") | ||
{ | ||
$languageFolder = "cs" | ||
} | ||
if($project.Type -eq "VB.NET") | ||
{ | ||
$languageFolder = "vb" | ||
} | ||
if($languageFolder -eq "") | ||
{ | ||
return | ||
} | ||
|
||
foreach($analyzersPath in $analyzersPaths) | ||
{ | ||
# Install language specific analyzers. | ||
$languageAnalyzersPath = join-path $analyzersPath $languageFolder | ||
if (Test-Path $languageAnalyzersPath) | ||
{ | ||
foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) | ||
{ | ||
if($project.Object.AnalyzerReferences) | ||
{ | ||
$project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.