Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Rosylyn analyzer for irrelevant APIs #717

Merged
merged 25 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
214 changes: 214 additions & 0 deletions src/NServiceBus.AzureFunctions.Analyzer.Tests/AnalyzerTestFixture.cs
jpalac marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
namespace NServiceBus.AzureFunctions.Analyzer.Tests
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

public class AnalyzerTestFixture<TAnalyzer> where TAnalyzer : DiagnosticAnalyzer, new()
{
protected virtual LanguageVersion AnalyzerLanguageVersion => LanguageVersion.CSharp7;

protected Task Assert(string markupCode, CancellationToken cancellationToken = default) =>
Assert(Array.Empty<string>(), markupCode, Array.Empty<string>(), cancellationToken);

protected Task Assert(string expectedDiagnosticId, string markupCode, CancellationToken cancellationToken = default) =>
Assert(new[] { expectedDiagnosticId }, markupCode, Array.Empty<string>(), cancellationToken);

protected async Task Assert(string[] expectedDiagnosticIds, string markupCode, string[] ignoreDiagnosticIds, CancellationToken cancellationToken = default)
{
var (code, markupSpans) = Parse(markupCode);

var project = CreateProject(code);
await WriteCode(project);

var compilerDiagnostics = (await Task.WhenAll(project.Documents
.Select(doc => doc.GetCompilerDiagnostics(cancellationToken))))
.SelectMany(diagnostics => diagnostics);

WriteCompilerDiagnostics(compilerDiagnostics);

var compilation = await project.GetCompilationAsync(cancellationToken);
compilation.Compile();

var analyzerDiagnostics = (await compilation.GetAnalyzerDiagnostics(new TAnalyzer(), cancellationToken))
.Where(d => !ignoreDiagnosticIds.Contains(d.Id))
.ToList();
WriteAnalyzerDiagnostics(analyzerDiagnostics);

var expectedSpansAndIds = expectedDiagnosticIds
.SelectMany(id => markupSpans.Select(span => (span.file, span.span, id)))
.OrderBy(item => item.span)
.ThenBy(item => item.id)
.ToList();

var actualSpansAndIds = analyzerDiagnostics
.Select(diagnostic => (diagnostic.Location.SourceTree.FilePath, diagnostic.Location.SourceSpan, diagnostic.Id))
.ToList();

NUnit.Framework.CollectionAssert.AreEqual(expectedSpansAndIds, actualSpansAndIds);
}

protected static async Task WriteCode(Project project)
{
if (!VerboseLogging)
{
return;
}

foreach (var document in project.Documents)
{
Console.WriteLine(document.Name);
var code = await document.GetCode();
foreach (var (line, index) in code.Replace("\r\n", "\n").Split('\n')
.Select((line, index) => (line, index)))
{
Console.WriteLine($" {index + 1,3}: {line}");
}
}

}

static readonly ImmutableDictionary<string, ReportDiagnostic> DiagnosticOptions = new Dictionary<string, ReportDiagnostic>
{
{ "CS1701", ReportDiagnostic.Hidden }
}
.ToImmutableDictionary();

protected Project CreateProject(string[] code)
{
var workspace = new AdhocWorkspace();
var project = workspace.AddProject("TestProject", LanguageNames.CSharp)
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithSpecificDiagnosticOptions(DiagnosticOptions))
.WithParseOptions(new CSharpParseOptions(AnalyzerLanguageVersion))
.AddMetadataReferences(ProjectReferences);

for (int i = 0; i < code.Length; i++)
{
project = project.AddDocument($"TestDocument{i}", code[i]).Project;
}

return project;
}

static AnalyzerTestFixture()
{
ProjectReferences = ImmutableList.Create(
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.Expression).GetTypeInfo().Assembly
.Location),
#if NET
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
#endif
MetadataReference.CreateFromFile(typeof(IFunctionEndpoint).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(EndpointConfiguration).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(AzureServiceBusTransport).GetTypeInfo().Assembly.Location));
}

static readonly ImmutableList<PortableExecutableReference> ProjectReferences;

static readonly Regex DocumentSplittingRegex = new Regex("^-{5,}.*", RegexOptions.Compiled | RegexOptions.Multiline);

protected static void WriteCompilerDiagnostics(IEnumerable<Diagnostic> diagnostics)
{
if (!VerboseLogging)
{
return;
}

Console.WriteLine("Compiler diagnostics:");

foreach (var diagnostic in diagnostics)
{
Console.WriteLine($" {diagnostic}");
}
}

protected static void WriteAnalyzerDiagnostics(IEnumerable<Diagnostic> diagnostics)
{
if (!VerboseLogging)
{
return;
}

Console.WriteLine("Analyzer diagnostics:");

foreach (var diagnostic in diagnostics)
{
Console.WriteLine($" {diagnostic}");
}
}

protected static string[] SplitMarkupCodeIntoFiles(string markupCode)
{
return DocumentSplittingRegex.Split(markupCode)
.Where(docCode => !string.IsNullOrWhiteSpace(docCode))
.ToArray();
}

static (string[] code, List<(string file, TextSpan span)>) Parse(string markupCode)
{
if (markupCode == null)
{
return (Array.Empty<string>(), new List<(string, TextSpan)>());
}

var documents = SplitMarkupCodeIntoFiles(markupCode);

var markupSpans = new List<(string, TextSpan)>();

for (var i = 0; i < documents.Length; i++)
{
var code = new StringBuilder();
var name = $"TestDocument{i}";

var remainingCode = documents[i];
var remainingCodeStart = 0;

while (remainingCode.Length > 0)
{
var beforeAndAfterOpening = remainingCode.Split(new[] { "[|" }, 2, StringSplitOptions.None);

if (beforeAndAfterOpening.Length == 1)
{
_ = code.Append(beforeAndAfterOpening[0]);
break;
}

var midAndAfterClosing = beforeAndAfterOpening[1].Split(new[] { "|]" }, 2, StringSplitOptions.None);

if (midAndAfterClosing.Length == 1)
{
throw new Exception("The markup code does not contain a closing '|]'");
}

var markupSpan = new TextSpan(remainingCodeStart + beforeAndAfterOpening[0].Length, midAndAfterClosing[0].Length);

_ = code.Append(beforeAndAfterOpening[0]).Append(midAndAfterClosing[0]);
markupSpans.Add((name, markupSpan));

remainingCode = midAndAfterClosing[1];
remainingCodeStart += beforeAndAfterOpening[0].Length + markupSpan.Length;
}

documents[i] = code.ToString();
}

return (documents, markupSpans);
}

protected static readonly bool VerboseLogging = Environment.GetEnvironmentVariable("CI") != "true"
|| Environment.GetEnvironmentVariable("VERBOSE_TEST_LOGGING")?.ToLower() == "true";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace NServiceBus.AzureFunctions.Analyzer.Tests
{
using System.Threading.Tasks;
using NUnit.Framework;
using static AzureFunctionsDiagnostics;

[TestFixture]
public class AzureFunctionsConfigurationAnalyzerTests : AnalyzerTestFixture<AzureFunctionsConfigurationAnalyzer>
jpalac marked this conversation as resolved.
Show resolved Hide resolved
{
[TestCase("DefineCriticalErrorAction((errorContext, cancellationToken) => Task.CompletedTask)", DefineCriticalErrorActionNotAllowedId)]
[TestCase("LimitMessageProcessingConcurrencyTo(5)", LimitMessageProcessingToNotAllowedId)]
[TestCase("MakeInstanceUniquelyAddressable(null)", MakeInstanceUniquelyAddressableNotAllowedId)]
[TestCase("OverrideLocalAddress(null)", OverrideLocalAddressNotAllowedId)]
[TestCase("PurgeOnStartup(true)", PurgeOnStartupNotAllowedId)]
jpalac marked this conversation as resolved.
Show resolved Hide resolved
[TestCase("SetDiagnosticsPath(null)", SetDiagnosticsPathNotAllowedId)]
[TestCase("UseTransport(new AzureServiceBusTransport(null))", UseTransportNotAllowedId)]
public Task DiagnosticIsReportedForEndpointConfiguration(string configuration, string diagnosticId)
{
var source =
$@"using NServiceBus;
using System;
using System.Threading.Tasks;
class Foo
{{
void Bar(ServiceBusTriggeredEndpointConfiguration endpointConfig)
{{
[|endpointConfig.AdvancedConfiguration.{configuration}|];

var advancedConfig = endpointConfig.AdvancedConfiguration;
[|advancedConfig.{configuration}|];
}}
}}";

return Assert(diagnosticId, source);
jpalac marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace NServiceBus.AzureFunctions.Analyzer.Tests
{
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using NUnit.Framework;
using static AzureFunctionsDiagnostics;

[TestFixture]
public class AzureFunctionsConfigurationAnalyzerTestsCSharp8 : AnalyzerTestFixture<AzureFunctionsConfigurationAnalyzer>
{
// HINT: In C# 7 this call is ambiguous with the LearningTransport version as the compiler cannot differentiate method calls via generic type constraints
[TestCase("UseTransport<AzureServiceBusTransport>()", UseTransportNotAllowedId)]
public Task DiagnosticIsReportedForEndpointConfiguration(string configuration, string diagnosticId)
{
var source =
$@"using NServiceBus;
using System;
using System.Threading.Tasks;
class Foo
{{
void Bar(ServiceBusTriggeredEndpointConfiguration endpointConfig)
{{
[|endpointConfig.AdvancedConfiguration.{configuration}|];

var advancedConfig = endpointConfig.AdvancedConfiguration;
[|advancedConfig.{configuration}|];
}}
}}";

return Assert(diagnosticId, source);
}
protected override LanguageVersion AnalyzerLanguageVersion => LanguageVersion.CSharp8;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace NServiceBus.AzureFunctions.Analyzer.Tests
{
using System.Threading.Tasks;
using NUnit.Framework;
using static AzureFunctionsDiagnostics;

[TestFixture]
public class AzureFunctionsSendReplyOptionsAnalyzerTests : AnalyzerTestFixture<AzureFunctionsConfigurationAnalyzer>
jpalac marked this conversation as resolved.
Show resolved Hide resolved
{
[TestCase("SendOptions", "RouteReplyToAnyInstance", RouteReplyToAnyInstanceNotAllowedId)]
[TestCase("SendOptions", "RouteReplyToThisInstance", RouteReplyToThisInstanceNotAllowedId)]
[TestCase("SendOptions", "RouteToThisInstance", RouteToThisInstanceNotAllowedId)]
[TestCase("ReplyOptions", "RouteReplyToAnyInstance", RouteReplyToAnyInstanceNotAllowedId)]
[TestCase("ReplyOptions", "RouteReplyToThisInstance", RouteReplyToThisInstanceNotAllowedId)]
public Task DiagnosticIsReportedForOptions(string optionsType, string method, string diagnosticId)
{
var source =
$@"using NServiceBus;
class Foo
{{
void Bar({optionsType} options)
{{
[|options.{method}()|];
}}
}}";

return Assert(diagnosticId, source);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace NServiceBus.AzureFunctions.Analyzer.Tests
{
using System.Threading.Tasks;
using NUnit.Framework;
using static AzureFunctionsDiagnostics;

[TestFixture]
public class AzureFunctionsTransportAnalyzerTests : AnalyzerTestFixture<AzureFunctionsConfigurationAnalyzer>
jpalac marked this conversation as resolved.
Show resolved Hide resolved
{
[TestCase("EntityMaximumSize", "5", EntityMaximumSizeNotAllowedId)]
jpalac marked this conversation as resolved.
Show resolved Hide resolved
[TestCase("MaxAutoLockRenewalDuration", "new System.TimeSpan(0, 0, 5, 0)", MaxAutoLockRenewalDurationNotAllowedId)]
[TestCase("PrefetchCount", "5", PrefetchCountNotAllowedId)]
[TestCase("PrefetchMultiplier", "5", PrefetchMultiplierNotAllowedId)]
[TestCase("TimeToWaitBeforeTriggeringCircuitBreaker", "new System.TimeSpan(0, 0, 5, 0)", TimeToWaitBeforeTriggeringCircuitBreakerNotAllowedId)]
public Task DiagnosticIsReportedTransportConfiguration(string configName, string configValue, string diagnosticId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the analyzer also detect usage scenarios involving the builder?

        builder.UseNServiceBus(configuration =>
        {
            configuration.Transport.SubscriptionNamingConvention = (name) => name.Replace("X", "Y");
        });

{
var source =
$@"using NServiceBus;
using System;
using System.Threading.Tasks;
class Foo
{{
void Direct(ServiceBusTriggeredEndpointConfiguration endpointConfig)
{{
[|endpointConfig.Transport.{configName}|] = {configValue};

var transportConfig = endpointConfig.Transport;
[|transportConfig.{configName}|] = {configValue};
}}

void Extension(TransportExtensions<AzureServiceBusTransport> transportExtension)
{{
[|transportExtension.{configName}({configValue})|];
}}
}}";

return Assert(diagnosticId, source);
jpalac marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Loading