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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ public class SampleCommand : ICommand { }
Assert.That(diagnostics, Does.Not.Contain("IL2026"));
}

[Test]
public void SuppressesIL3050ForAddHandler()
{
var source = """
using System.Threading.Tasks;
using NServiceBus;

public class Test
{
public void Configure(EndpointConfiguration cfg)
{
cfg.AddHandler<SampleHandler>();
}
}

public class SampleHandler : IHandleMessages<SampleCommand>
{
public Task Handle(SampleCommand cmd, IMessageHandlerContext context) => Task.CompletedTask;
}

public class SampleCommand : ICommand { }
""";

var result = SourceGeneratorTest.ForIncrementalGenerator<AddHandlerInterceptor>()
.WithSource(source, "test.cs")
.WithAnalyzer<MockTrimmingAnalyzer>()
.WithSuppressor<AddHandlerInterceptorSuppressor>()
.Run();

var diagnostics = result.GetCompilationOutput();

Assert.That(diagnostics, Does.Not.Contain("IL3050"));
}

[Test]
public void DoesNotSuppressIL2026ForNonAddHandlerCalls()
{
Expand Down Expand Up @@ -74,4 +108,37 @@ public void SomeOtherMethod() { }

Assert.That(diagnostics, Does.Contain("IL2026"));
}

[Test]
public void DoesNotSuppressIL3050ForNonAddHandlerCalls()
{
var source = """
using System.Diagnostics.CodeAnalysis;
using NServiceBus;

public class Test
{
public void Configure(EndpointConfiguration cfg)
{
// This call should still produce IL3050 since it's not intercepted
SomeOtherMethod();
}

[RequiresDynamicCode("Test method")]
public void SomeOtherMethod() { }
}
""";

var result = SourceGeneratorTest.ForIncrementalGenerator<AddHandlerInterceptor>()
.WithSource(source, "test.cs")
.WithAnalyzer<MockTrimmingAnalyzer>()
.WithSuppressor<AddHandlerInterceptorSuppressor>()
.SuppressDiagnosticErrors()
.SuppressCompilationErrors()
.Run();

var diagnostics = result.GetCompilationOutput();

Assert.That(diagnostics, Does.Contain("IL3050"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace NServiceBus.Core.Analyzer.Tests.Helpers;
using Microsoft.CodeAnalysis.Diagnostics;

// Currently, this mock analyzer does not support all trimming-related warnings.
// It only supports IL2026 for method invocations for now.
// It only supports IL2026 and IL3050 for method invocations for now.
#pragma warning disable RS1001 // Yes we don't want it to be found
class MockTrimmingAnalyzer : DiagnosticAnalyzer
#pragma warning restore RS1001
Expand All @@ -21,7 +21,17 @@ class MockTrimmingAnalyzer : DiagnosticAnalyzer
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [IL2026Descriptor];
static readonly DiagnosticDescriptor IL3050Descriptor = new(
#pragma warning disable RS2008
id: "IL3050",
#pragma warning restore RS2008
title: "Using member with RequiresDynamicCodeAttribute",
messageFormat: "Using member '{0}' which has 'RequiresDynamicCodeAttribute'",
category: "AOT",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [IL2026Descriptor, IL3050Descriptor];

public override void Initialize(AnalysisContext context)
{
Expand All @@ -36,13 +46,16 @@ public override void Initialize(AnalysisContext context)
}

var method = invocation.TargetMethod;
if (!method.GetAttributes().Any(attr => attr.AttributeClass?.Name == "RequiresUnreferencedCodeAttribute"))
var attributes = method.GetAttributes();
if (attributes.Any(attr => attr.AttributeClass?.Name == "RequiresUnreferencedCodeAttribute"))
{
return;
operationContext.ReportDiagnostic(Diagnostic.Create(IL2026Descriptor, invocation.Syntax.GetLocation(), method.Name));
}

var diagnostic = Diagnostic.Create(IL2026Descriptor, invocation.Syntax.GetLocation(), method.Name);
operationContext.ReportDiagnostic(diagnostic);
if (attributes.Any(attr => attr.AttributeClass?.Name == "RequiresDynamicCodeAttribute"))
{
operationContext.ReportDiagnostic(Diagnostic.Create(IL3050Descriptor, invocation.Syntax.GetLocation(), method.Name));
}
}, OperationKind.Invocation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,55 @@ public class SampleCommand : ICommand
Assert.That(diagnostics, Does.Not.Contain("IL2026"));
}

[Test]
public void SuppressesIL3050ForAddSaga()
{
var source = """
using System.Threading.Tasks;
using NServiceBus;

public class Test
{
public void Configure(EndpointConfiguration cfg)
{
cfg.AddSaga<SampleSaga>();
}
}

public class SampleSaga : Saga<SampleSagaData>,
IAmStartedByMessages<SampleCommand>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<SampleSagaData> mapper)
{
mapper.MapSaga(saga => saga.CorrelationId)
.ToMessage<SampleCommand>(msg => msg.CorrelationId);
}

public Task Handle(SampleCommand cmd, IMessageHandlerContext context) => Task.CompletedTask;
}

public class SampleSagaData : ContainSagaData
{
public string CorrelationId { get; set; }
}

public class SampleCommand : ICommand
{
public string CorrelationId { get; set; }
}
""";

var result = SourceGeneratorTest.ForIncrementalGenerator<AddSagaInterceptor>()
.WithSource(source, "test.cs")
.WithAnalyzer<MockTrimmingAnalyzer>()
.WithSuppressor<AddSagaInterceptorSuppressor>()
.Run();

var diagnostics = result.GetCompilationOutput();

Assert.That(diagnostics, Does.Not.Contain("IL3050"));
}

[Test]
public void SuppressesIL2026ForFinderOnlySaga()
{
Expand Down Expand Up @@ -111,6 +160,60 @@ public class StartSagaMessage : IMessage;
Assert.That(diagnostics, Does.Not.Contain("IL2026"));
}

[Test]
public void SuppressesIL3050ForFinderOnlySaga()
{
var source = """
using System.Threading;
using System.Threading.Tasks;
using NServiceBus;
using NServiceBus.Persistence;
using NServiceBus.Extensibility;
using NServiceBus.Sagas;

public class Test
{
public void Configure(EndpointConfiguration cfg)
{
cfg.AddSaga<FinderOnlySaga>();
}
}

public class FinderOnlySaga : Saga<FinderOnlySagaData>,
IAmStartedByMessages<StartSagaMessage>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<FinderOnlySagaData> mapper)
{
mapper.ConfigureFinderMapping<StartSagaMessage, FinderOnlyFinder>();
}

public Task Handle(StartSagaMessage message, IMessageHandlerContext context) => Task.CompletedTask;
}

public class FinderOnlySagaData : ContainSagaData
{
public string Property { get; set; }
}

public class FinderOnlyFinder : ISagaFinder<FinderOnlySagaData, StartSagaMessage>
{
public Task<FinderOnlySagaData> FindBy(StartSagaMessage message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(default(FinderOnlySagaData));
}

public class StartSagaMessage : IMessage;
""";

var result = SourceGeneratorTest.ForIncrementalGenerator<AddSagaInterceptor>()
.WithSource(source, "test.cs")
.WithAnalyzer<MockTrimmingAnalyzer>()
.WithSuppressor<AddSagaInterceptorSuppressor>()
.Run();

var diagnostics = result.GetCompilationOutput();

Assert.That(diagnostics, Does.Not.Contain("IL3050"));
}

[Test]
public void DoesNotSuppressIL2026ForNonAddSagaCalls()
{
Expand Down Expand Up @@ -143,4 +246,37 @@ public void SomeOtherMethod() { }

Assert.That(diagnostics, Does.Contain("IL2026"));
}

[Test]
public void DoesNotSuppressIL3050ForNonAddSagaCalls()
{
var source = """
using System.Diagnostics.CodeAnalysis;
using NServiceBus;

public class Test
{
public void Configure(EndpointConfiguration cfg)
{
// This call should still produce IL3050 since it's not intercepted
SomeOtherMethod();
}

[RequiresDynamicCode("Test method")]
public void SomeOtherMethod() { }
}
""";

var result = SourceGeneratorTest.ForIncrementalGenerator<AddSagaInterceptor>()
.WithSource(source, "test.cs")
.WithAnalyzer<MockTrimmingAnalyzer>()
.WithSuppressor<AddSagaInterceptorSuppressor>()
.SuppressDiagnosticErrors()
.SuppressCompilationErrors()
.Run();

var diagnostics = result.GetCompilationOutput();

Assert.That(diagnostics, Does.Contain("IL3050"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AddHandlerInterceptorSuppressor : DiagnosticSuppressor
{
const string Justification = "The AddHandler method has been intercepted by a statically generated variant.";

static readonly SuppressionDescriptor SuppressRUCDiagnostic = new(
SupressionIds.AddHandlerInterceptorSuppression,
suppressedDiagnosticId: "IL2026",
justification: "The AddHandler method has been intercepted by a statically generated variant.");
justification: Justification);

static readonly SuppressionDescriptor SuppressRDCDiagnostic = new(
SupressionIds.AddHandlerInterceptorAotSuppression,
suppressedDiagnosticId: "IL3050",
justification: Justification);

public override void ReportSuppressions(SuppressionAnalysisContext context)
{
foreach (var diagnostic in context.ReportedDiagnostics)
{
if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId)
if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId && diagnostic.Id != SuppressRDCDiagnostic.SuppressedDiagnosticId)
{
continue;
}
Expand Down Expand Up @@ -47,10 +54,11 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
var operation = semanticModel.GetOperation(node, context.CancellationToken);
if (operation is IInvocationOperation { TargetMethod: { } methodSymbol } && AddHandlerInterceptor.Parser.IsAddHandlerMethod(methodSymbol))
{
context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic));
var targetSuppression = diagnostic.Id == SuppressRUCDiagnostic.SuppressedDiagnosticId ? SuppressRUCDiagnostic : SuppressRDCDiagnostic;
context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic));
}
}
}

public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => [SuppressRUCDiagnostic];
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => [SuppressRUCDiagnostic, SuppressRDCDiagnostic];
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,23 @@
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AddSagaInterceptorSuppressor : DiagnosticSuppressor
{
const string Justification = "The AddSaga method has been intercepted by a statically generated variant.";

static readonly SuppressionDescriptor SuppressRUCDiagnostic = new(
SupressionIds.AddSagaInterceptorSuppression,
suppressedDiagnosticId: "IL2026",
justification: "The AddSaga method has been intercepted by a statically generated variant.");
justification: Justification);

static readonly SuppressionDescriptor SuppressRDCDiagnostic = new(
SupressionIds.AddSagaInterceptorAotSuppression,
suppressedDiagnosticId: "IL3050",
justification: Justification);

public override void ReportSuppressions(SuppressionAnalysisContext context)
{
foreach (var diagnostic in context.ReportedDiagnostics)
{
if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId)
if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId && diagnostic.Id != SuppressRDCDiagnostic.SuppressedDiagnosticId)
{
continue;
}
Expand Down Expand Up @@ -53,17 +60,18 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)

// Only suppress when an interceptor can actually be emitted for this call site. A saga that cannot be
// parsed (no Saga<TSagaData> base, abstract, or otherwise unsupported) keeps the RequiresUnreferencedCode
// fallback warning.
// and RequiresDynamicCode fallback warnings.
if (methodSymbol.TypeArguments[0] is not INamedTypeSymbol sagaType ||
!HandlerKnownTypes.TryGet(context.Compilation, out var knownTypes) ||
Sagas.Parser.Parse(semanticModel, sagaType, knownTypes, context.CancellationToken) is null)
{
continue;
}

context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic));
var targetSuppression = diagnostic.Id == SuppressRUCDiagnostic.SuppressedDiagnosticId ? SuppressRUCDiagnostic : SuppressRDCDiagnostic;
context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic));
}
}

public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => [SuppressRUCDiagnostic];
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => [SuppressRUCDiagnostic, SuppressRDCDiagnostic];
}
2 changes: 2 additions & 0 deletions src/NServiceBus.Core.Analyzer/SupressionIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ public static class SupressionIds
public const string AddHandlerInterceptorSuppression = "NSBS0001";
public const string AddSagaInterceptorSuppression = "NSBS0002";
public const string AddMessageTypeInterceptorSuppression = "NSBS0003";
public const string AddHandlerInterceptorAotSuppression = "NSBS0004";
public const string AddSagaInterceptorAotSuppression = "NSBS0005";
}
Loading