Skip to content

Commit

Permalink
Add Analyzers (#2)
Browse files Browse the repository at this point in the history
* Add additional generator tests
* Add MissingHandlerAttribute analyzer
* Add Invalid [Authorize] and [AllowAnonymous] analyzers
* Clean up Test facts
  • Loading branch information
viceroypenguin authored Mar 19, 2024
1 parent d9887aa commit a3f1952
Show file tree
Hide file tree
Showing 25 changed files with 825 additions and 75 deletions.
9 changes: 8 additions & 1 deletion Immediate.Apis.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immediate.Apis.Shared", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immediate.Apis.FunctionalTests", "tests\Immediate.Apis.FunctionalTests\Immediate.Apis.FunctionalTests.csproj", "{4D9D5A85-102D-4F71-9E8A-FECBFE47EBA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Immediate.Apis.Tests", "tests\Immediate.Apis.Tests\Immediate.Apis.Tests.csproj", "{058EFC02-6DDA-486F-8C4F-08BA816061CE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immediate.Apis.Tests", "tests\Immediate.Apis.Tests\Immediate.Apis.Tests.csproj", "{058EFC02-6DDA-486F-8C4F-08BA816061CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Immediate.Apis.Analyzers", "src\Immediate.Apis.Analyzers\Immediate.Apis.Analyzers.csproj", "{2E3B32C4-7A45-43C5-AAE7-D9001EEABD23}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -59,6 +61,10 @@ Global
{058EFC02-6DDA-486F-8C4F-08BA816061CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{058EFC02-6DDA-486F-8C4F-08BA816061CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{058EFC02-6DDA-486F-8C4F-08BA816061CE}.Release|Any CPU.Build.0 = Release|Any CPU
{2E3B32C4-7A45-43C5-AAE7-D9001EEABD23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E3B32C4-7A45-43C5-AAE7-D9001EEABD23}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E3B32C4-7A45-43C5-AAE7-D9001EEABD23}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E3B32C4-7A45-43C5-AAE7-D9001EEABD23}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -70,6 +76,7 @@ Global
{0EF463C2-6237-4491-A446-80860ACD280D} = {68142973-97B4-45D0-B6C7-BE884990AB7E}
{4D9D5A85-102D-4F71-9E8A-FECBFE47EBA3} = {4A2C6A96-653A-40CC-9354-535F92A0BBD8}
{058EFC02-6DDA-486F-8C4F-08BA816061CE} = {4A2C6A96-653A-40CC-9354-535F92A0BBD8}
{2E3B32C4-7A45-43C5-AAE7-D9001EEABD23} = {68142973-97B4-45D0-B6C7-BE884990AB7E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E947E0E4-B196-46E5-8CFE-A6AE390B8897}
Expand Down
6 changes: 6 additions & 0 deletions src/Immediate.Apis.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Release 1.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
7 changes: 7 additions & 0 deletions src/Immediate.Apis.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
IAPI0001 | ImmediateApis | Error | InvalidHandlerAttributeAnalyzer
IAPI0002 | ImmediateApis | Error | InvalidAuthorizeAttributeAnalyzer
IAPI0003 | ImmediateApis | Warning | InvalidAuthorizeAttributeAnalyzer
8 changes: 8 additions & 0 deletions src/Immediate.Apis.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Immediate.Apis.Analyzers;

internal static class DiagnosticIds
{
public const string IAPI0001MissingHandlerAttribute = "IAPI0001";
public const string IAPI0002InvalidAuthorizeParameter = "IAPI0002";
public const string IAPI0003UsedBothAuthorizeAndAnonymous = "IAPI0003";
}
21 changes: 21 additions & 0 deletions src/Immediate.Apis.Analyzers/Immediate.Apis.Analyzers.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="MinVer" PrivateAssets="all" />
</ItemGroup>

<PropertyGroup Label="MinVer">
<MinVerAutoIncrement>minor</MinVerAutoIncrement>
<MinVerDefaultPreReleaseIdentifiers>preview.0</MinVerDefaultPreReleaseIdentifiers>
<MinVerTagPrefix>v</MinVerTagPrefix>
</PropertyGroup>

</Project>
13 changes: 13 additions & 0 deletions src/Immediate.Apis.Analyzers/Immediate.Apis.Analyzers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Immediate.Api.Analyzers

## IAPI0001: [Handler] must be used

The `[Handler]` attribute must be applied for registrations to be generated by `Immediate.Apis`.

| Item | Value |
|----------|------------------|
| Category | ImmediateHandler |
| Enabled | True |
| Severity | Error |
| CodeFix | False |
---
108 changes: 108 additions & 0 deletions src/Immediate.Apis.Analyzers/InvalidAuthorizeAttributeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Immediate.Apis.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidAuthorizeAttributeAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor InvalidAuthorizeParameter =
new(
id: DiagnosticIds.IAPI0002InvalidAuthorizeParameter,
title: "Must use `Policies` parameter for [Authorize]",
messageFormat: "[Authorize] was used with invalid parameter {0}",
category: "ImmediateApis",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Only policy authorization is supported with Immediate.Apis."
);

public static readonly DiagnosticDescriptor UsedBothAuthorizeAndAnonymous =
new(
id: DiagnosticIds.IAPI0003UsedBothAuthorizeAndAnonymous,
title: "Only use one of [AllowAnonymous] and [Authorize]",
messageFormat: "Both [AllowAnonymous] and [Authorize] were used, but only one will be applied to the endpoint",
category: "ImmediateApis",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Only one of [AllowAnonymous] and [Authorize] can be used for a single endpoint."
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
[
InvalidAuthorizeParameter,
UsedBothAuthorizeAndAnonymous,
]);

public override void Initialize(AnalysisContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));

context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
var token = context.CancellationToken;
token.ThrowIfCancellationRequested();

if (context.Symbol is not INamedTypeSymbol namedTypeSymbol)
return;

var attributeNames = namedTypeSymbol
.GetAttributes()
.Select(a => a.AttributeClass?.ToString() ?? "")
.ToList();

if (!attributeNames.Any(x => Utility.ValidAttributes.Contains(x)))
{
return;
}

token.ThrowIfCancellationRequested();

var allowAnonymous = attributeNames.Contains("Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute");

var authorizeIndex = attributeNames.IndexOf("Microsoft.AspNetCore.Authorization.AuthorizeAttribute");
var authorize = authorizeIndex >= 0;

if (allowAnonymous && authorize)
{
context.ReportDiagnostic(
Diagnostic.Create(
UsedBothAuthorizeAndAnonymous,
namedTypeSymbol.Locations[0]
)
);
}

if (authorize)
{
var authorizeAttribute = namedTypeSymbol.GetAttributes()[authorizeIndex];
if (authorizeAttribute.NamedArguments.Length > 0)
{
foreach (var argument in authorizeAttribute.NamedArguments)
{
if (argument.Key != "Policy")
{
context.ReportDiagnostic(
Diagnostic.Create(
InvalidAuthorizeParameter,
authorizeAttribute.ApplicationSyntaxReference
?.GetSyntax()
.GetLocation(),
argument.Key
)
);
}
}
}
}
}
}
70 changes: 70 additions & 0 deletions src/Immediate.Apis.Analyzers/MissingHandlerAttributeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Immediate.Apis.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MissingHandlerAttributeAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor MissingHandlerAttribute =
new(
id: DiagnosticIds.IAPI0001MissingHandlerAttribute,
title: "[Handler] must be used",
messageFormat: "Handler `{0}` must be marked with [Handler]",
category: "ImmediateApis",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "An endpoint registration can only be generated for an Immediate.Handlers handler."
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
[
MissingHandlerAttribute,
]);

public override void Initialize(AnalysisContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));

context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
var token = context.CancellationToken;
token.ThrowIfCancellationRequested();

if (context.Symbol is not INamedTypeSymbol namedTypeSymbol)
return;

if (!namedTypeSymbol
.GetAttributes()
.Any(x => Utility.ValidAttributes.Contains(x.AttributeClass?.ToString()))
)
{
return;
}

token.ThrowIfCancellationRequested();

if (!namedTypeSymbol
.GetAttributes()
.Any(x => x.AttributeClass?.ToString() == "Immediate.Handlers.Shared.HandlerAttribute")
)
{
context.ReportDiagnostic(
Diagnostic.Create(
MissingHandlerAttribute,
namedTypeSymbol.Locations[0],
namedTypeSymbol.Name
)
);
}
}
}
8 changes: 8 additions & 0 deletions src/Immediate.Apis.Analyzers/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"profiles": {
"Immediate.Apis.FunctionalTests": {
"commandName": "DebugRoslynComponent",
"targetProject": "../../tests/Immediate.Apis.FunctionalTests/Immediate.Apis.FunctionalTests.csproj"
}
}
}
12 changes: 12 additions & 0 deletions src/Immediate.Apis.Analyzers/Utility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Immediate.Apis.Analyzers;
internal static class Utility
{
public static readonly string[] ValidAttributes =
[
"Immediate.Apis.Shared.MapGetAttribute",
"Immediate.Apis.Shared.MapPostAttribute",
"Immediate.Apis.Shared.MapPutAttribute",
"Immediate.Apis.Shared.MapPatchAttribute",
"Immediate.Apis.Shared.MapDeleteAttribute",
];
}
33 changes: 33 additions & 0 deletions tests/Immediate.Apis.Tests/AnalyzerTests/AnalyzerTestHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;

namespace Immediate.Apis.Tests.AnalyzerTests;

public static class AnalyzerTestHelpers
{
public static CSharpAnalyzerTest<TAnalyzer, DefaultVerifier> CreateAnalyzerTest<TAnalyzer>(
string inputSource
)
where TAnalyzer : DiagnosticAnalyzer, new()
{
var csTest = new CSharpAnalyzerTest<TAnalyzer, DefaultVerifier>
{
TestState =
{
Sources = { inputSource },
ReferenceAssemblies = new ReferenceAssemblies(
"net8.0",
new PackageIdentity(
"Microsoft.NETCore.App.Ref",
"8.0.0"),
Path.Combine("ref", "net8.0")),
},
};

csTest.TestState.AdditionalReferences
.AddRange(Utility.GetMetadataReferences());

return csTest;
}
}
Loading

0 comments on commit a3f1952

Please sign in to comment.