Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions src/Common/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,30 @@ typeSymbol is INamedTypeSymbol
public bool IsHandlerAttribute =>
typeSymbol is INamedTypeSymbol
{
Arity: 0,
Name: "HandlerAttribute",
ContainingNamespace.IsImmediateHandlersShared: true,
};

public bool IsHandler =>
typeSymbol is INamedTypeSymbol
&& typeSymbol.GetAttributes().Any(a => a.AttributeClass.IsHandlerAttribute);

public bool IsCacheForAttribute =>
typeSymbol is INamedTypeSymbol
{
Arity: 1,
Name: "CacheForAttribute",
ContainingNamespace.IsImmediateCacheShared: true,
};
}

extension(INamedTypeSymbol typeSymbol)
{
public bool GetValidHandleMethod([NotNullWhen(true)] out ITypeSymbol? requestType, [NotNullWhen(true)] out ITypeSymbol? responseType)
public IMethodSymbol? GetHandleMethod()
{
requestType = null;
responseType = null;

if (!typeSymbol.GetAttributes().Any(a => a.AttributeClass.IsHandlerAttribute))
return false;
if (!typeSymbol.IsHandler)
return null;

if (typeSymbol
.GetMembers()
Expand All @@ -40,9 +50,20 @@ public bool GetValidHandleMethod([NotNullWhen(true)] out ITypeSymbol? requestTyp
.Take(2)
.ToList() is not [var handleMethod])
{
return false;
return null;
}

return handleMethod;
}

public bool GetValidHandleMethod([NotNullWhen(true)] out ITypeSymbol? requestType, [NotNullWhen(true)] out ITypeSymbol? responseType)
{
requestType = null;
responseType = null;

if (typeSymbol.GetHandleMethod() is not { } handleMethod)
return false;

// must have request type
if (handleMethod.Parameters is not [{ Type: ITypeSymbol parameterType }, ..])
return false;
Expand Down
9 changes: 9 additions & 0 deletions src/Immediate.Cache.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Release 2.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
IC0001 | ImmediateCache | Error | CacheForUsageAnalyzer
IC0002 | ImmediateCache | Error | CacheForUsageAnalyzer
IC0003 | ImmediateCache | Error | CacheForUsageAnalyzer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

134 changes: 134 additions & 0 deletions src/Immediate.Cache.Analyzers/CacheForUsageAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Immediate.Cache.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CacheForUsageAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor CacheMustNotBeNested =
new(
id: DiagnosticIds.IC0001CacheMustNotBeNested,
title: "Cache nesting is not allowed",
messageFormat: "Cache '{0}' must not be nested in another type",
category: "ImmediateCache",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Cache classes must not be nested in another type.",
customTags: [WellKnownDiagnosticTags.NotConfigurable]
);

public static readonly DiagnosticDescriptor TargetMustBeHandler =
new(
id: DiagnosticIds.IC0002TargetMustBeHandler,
title: "Cache Target must be a `[Handler]`",
messageFormat: "Cache Target class '{0}' is not marked as `[Handler]`",
category: "ImmediateCache",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "IC Caches explicitly wrap IH Handlers only.",
customTags: [WellKnownDiagnosticTags.NotConfigurable]
);

public static readonly DiagnosticDescriptor TargetHandlerMustReturnValue =
new(
id: DiagnosticIds.IC0003TargetHandlerMustReturnValue,
title: "Cache Target Handler must have return value",
messageFormat: "Cache Target class '{0}' must return a value",
category: "ImmediateCache",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Caching a non-value does not .",
customTags: [WellKnownDiagnosticTags.NotConfigurable]
);

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

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();

var cacheSymbol = (INamedTypeSymbol)context.Symbol;

if (cacheSymbol.GetCacheTargetHandler() is not { } targetTypeSymbol)
return;

if (cacheSymbol.ContainingType is not null)
{
context.ReportDiagnostic(
Diagnostic.Create(
CacheMustNotBeNested,
cacheSymbol.Locations[0],
cacheSymbol.Name)
);
}

if (targetTypeSymbol is not INamedTypeSymbol { IsHandler: true } handlerSymbol)
{
context.ReportDiagnostic(
Diagnostic.Create(
TargetMustBeHandler,
cacheSymbol.Locations[0],
targetTypeSymbol.Name
)
);

return;
}

if (
handlerSymbol.GetHandleMethod() is not
{
ReturnType: INamedTypeSymbol
{
Arity: 1,
Name: "ValueTask",
ContainingNamespace.IsSystemThreadingTasks: true,
TypeArguments: [ITypeSymbol],
},
}
)
{
context.ReportDiagnostic(
Diagnostic.Create(
TargetHandlerMustReturnValue,
cacheSymbol.Locations[0],
targetTypeSymbol.Name
)
);
}
}
}

file static class Extensions
{
public static ITypeSymbol? GetCacheTargetHandler(this INamedTypeSymbol typeSymbol)
{
foreach (var attribute in typeSymbol.GetAttributes())
{
if (attribute.AttributeClass is { IsCacheForAttribute: true, TypeArguments: [var targetTypeSymbol] })
return targetTypeSymbol;
}

return null;
}
}
8 changes: 8 additions & 0 deletions src/Immediate.Cache.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Immediate.Cache.Analyzers;

internal static class DiagnosticIds
{
public const string IC0001CacheMustNotBeNested = "IC0001";
public const string IC0002TargetMustBeHandler = "IC0002";
public const string IC0003TargetHandlerMustReturnValue = "IC0003";
}
4 changes: 2 additions & 2 deletions tests/Immediate.Cache.FunctionalTests/DelayGetValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Immediate.Cache.FunctionalTests;

[Handler]
public static partial class DelayGetValue
public sealed partial class DelayGetValue
{
public sealed class Query
{
Expand All @@ -23,7 +23,7 @@ public sealed record Response(int Value, bool ExecutedHandler, Guid RandomValue)

private static readonly Lock Lock = new();

private static async ValueTask<Response> HandleAsync(
private async ValueTask<Response> HandleAsync(
Query query,
CancellationToken token
)
Expand Down
11 changes: 2 additions & 9 deletions tests/Immediate.Cache.FunctionalTests/DelayGetValueCache.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Immediate.Cache.Shared;
using Immediate.Handlers.Shared;
using Microsoft.Extensions.Caching.Memory;

namespace Immediate.Cache.FunctionalTests;

public sealed class DelayGetValueCache(
IMemoryCache memoryCache,
Owned<IHandler<DelayGetValue.Query, DelayGetValue.Response>> ownedHandler
) : ApplicationCache<DelayGetValue.Query, DelayGetValue.Response>(
memoryCache,
ownedHandler
)
[CacheFor<DelayGetValue>]
public sealed partial class DelayGetValueCache
{
[SuppressMessage(
"Design",
Expand Down
4 changes: 2 additions & 2 deletions tests/Immediate.Cache.FunctionalTests/GetValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
namespace Immediate.Cache.FunctionalTests;

[Handler]
public static partial class GetValue
public sealed partial class GetValue
{
public sealed record Query(int Value);
public sealed record Response(int Value, bool ExecutedHandler);

private static ValueTask<Response> HandleAsync(
private ValueTask<Response> HandleAsync(
Query query,
CancellationToken _
) => ValueTask.FromResult(new Response(query.Value, ExecutedHandler: true));
Expand Down
11 changes: 2 additions & 9 deletions tests/Immediate.Cache.FunctionalTests/GetValueCache.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Immediate.Cache.Shared;
using Immediate.Handlers.Shared;
using Microsoft.Extensions.Caching.Memory;

namespace Immediate.Cache.FunctionalTests;

public sealed class GetValueCache(
IMemoryCache memoryCache,
Owned<IHandler<GetValue.Query, GetValue.Response>> ownedHandler
) : ApplicationCache<GetValue.Query, GetValue.Response>(
memoryCache,
ownedHandler
)
[CacheFor<GetValue>]
public sealed partial class GetValueCache
{
[SuppressMessage(
"Design",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Immediate.Cache.Analyzers\Immediate.Cache.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\src\Immediate.Cache.Generators\Immediate.Cache.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\src\Immediate.Cache.Shared\Immediate.Cache.Shared.csproj" />
</ItemGroup>

Expand Down
38 changes: 38 additions & 0 deletions tests/Immediate.Cache.Tests/AnalyzerTests/AnalyzerTestHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Diagnostics.CodeAnalysis;
using Immediate.Cache.Generators;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;

namespace Immediate.Cache.Tests.AnalyzerTests;

public static class AnalyzerTestHelpers
{
public static CSharpAnalyzerTest<TAnalyzer, DefaultVerifier> CreateAnalyzerTest<TAnalyzer>(
[StringSyntax("c#-test")] string inputSource
)
where TAnalyzer : DiagnosticAnalyzer, new()
{
var csTest = new ImmediateCacheGeneratorAnalyzerTest<TAnalyzer>
{
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
TestState =
{
Sources = { inputSource },
ReferenceAssemblies = Utility.ReferenceAssemblies,
},
};

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

return csTest;
}

private sealed class ImmediateCacheGeneratorAnalyzerTest<TAnalyzer> : CSharpAnalyzerTest<TAnalyzer, DefaultVerifier>
where TAnalyzer : DiagnosticAnalyzer, new()
{
protected override IEnumerable<Type> GetSourceGenerators() =>
[typeof(ImmediateCacheGenerator)];
}
}
Loading