Skip to content

Commit 3df1093

Browse files
authored
Merge pull request #717 from Particular/api-usage-analyzer-spike
Add Rosylyn analyzer for irrelevant APIs
2 parents 5ebfc83 + 420d65b commit 3df1093

17 files changed

Lines changed: 1029 additions & 1 deletion

src/Custom.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<ParticularAnalyzersVersion>0.9.0</ParticularAnalyzersVersion>
5+
<AnalyzerTargetFramework>netstandard2.0</AnalyzerTargetFramework>
56
</PropertyGroup>
67

78
<PropertyGroup>
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
namespace NServiceBus.AzureFunctions.Analyzer.Tests
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Collections.Immutable;
6+
using System.Linq;
7+
using System.Reflection;
8+
using System.Text;
9+
using System.Text.RegularExpressions;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using Microsoft.CodeAnalysis;
13+
using Microsoft.CodeAnalysis.CSharp;
14+
using Microsoft.CodeAnalysis.Diagnostics;
15+
using Microsoft.CodeAnalysis.Text;
16+
17+
public class AnalyzerTestFixture<TAnalyzer> where TAnalyzer : DiagnosticAnalyzer, new()
18+
{
19+
protected virtual LanguageVersion AnalyzerLanguageVersion => LanguageVersion.CSharp7;
20+
21+
protected Task Assert(string markupCode, CancellationToken cancellationToken = default) =>
22+
Assert(Array.Empty<string>(), markupCode, Array.Empty<string>(), cancellationToken);
23+
24+
protected Task Assert(string expectedDiagnosticId, string markupCode, CancellationToken cancellationToken = default) =>
25+
Assert(new[] { expectedDiagnosticId }, markupCode, Array.Empty<string>(), cancellationToken);
26+
27+
protected async Task Assert(string[] expectedDiagnosticIds, string markupCode, string[] ignoreDiagnosticIds, CancellationToken cancellationToken = default)
28+
{
29+
var (code, markupSpans) = Parse(markupCode);
30+
31+
var project = CreateProject(code);
32+
await WriteCode(project);
33+
34+
var compilerDiagnostics = (await Task.WhenAll(project.Documents
35+
.Select(doc => doc.GetCompilerDiagnostics(cancellationToken))))
36+
.SelectMany(diagnostics => diagnostics);
37+
38+
WriteCompilerDiagnostics(compilerDiagnostics);
39+
40+
var compilation = await project.GetCompilationAsync(cancellationToken);
41+
compilation.Compile();
42+
43+
var analyzerDiagnostics = (await compilation.GetAnalyzerDiagnostics(new TAnalyzer(), cancellationToken))
44+
.Where(d => !ignoreDiagnosticIds.Contains(d.Id))
45+
.ToList();
46+
WriteAnalyzerDiagnostics(analyzerDiagnostics);
47+
48+
var expectedSpansAndIds = expectedDiagnosticIds
49+
.SelectMany(id => markupSpans.Select(span => (span.file, span.span, id)))
50+
.OrderBy(item => item.span)
51+
.ThenBy(item => item.id)
52+
.ToList();
53+
54+
var actualSpansAndIds = analyzerDiagnostics
55+
.Select(diagnostic => (diagnostic.Location.SourceTree.FilePath, diagnostic.Location.SourceSpan, diagnostic.Id))
56+
.ToList();
57+
58+
NUnit.Framework.CollectionAssert.AreEqual(expectedSpansAndIds, actualSpansAndIds);
59+
}
60+
61+
protected static async Task WriteCode(Project project)
62+
{
63+
if (!VerboseLogging)
64+
{
65+
return;
66+
}
67+
68+
foreach (var document in project.Documents)
69+
{
70+
Console.WriteLine(document.Name);
71+
var code = await document.GetCode();
72+
foreach (var (line, index) in code.Replace("\r\n", "\n").Split('\n')
73+
.Select((line, index) => (line, index)))
74+
{
75+
Console.WriteLine($" {index + 1,3}: {line}");
76+
}
77+
}
78+
79+
}
80+
81+
static readonly ImmutableDictionary<string, ReportDiagnostic> DiagnosticOptions = new Dictionary<string, ReportDiagnostic>
82+
{
83+
{ "CS1701", ReportDiagnostic.Hidden }
84+
}
85+
.ToImmutableDictionary();
86+
87+
protected Project CreateProject(string[] code)
88+
{
89+
var workspace = new AdhocWorkspace();
90+
var project = workspace.AddProject("TestProject", LanguageNames.CSharp)
91+
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
92+
.WithSpecificDiagnosticOptions(DiagnosticOptions))
93+
.WithParseOptions(new CSharpParseOptions(AnalyzerLanguageVersion))
94+
.AddMetadataReferences(ProjectReferences);
95+
96+
for (int i = 0; i < code.Length; i++)
97+
{
98+
project = project.AddDocument($"TestDocument{i}", code[i]).Project;
99+
}
100+
101+
return project;
102+
}
103+
104+
static AnalyzerTestFixture()
105+
{
106+
ProjectReferences = ImmutableList.Create(
107+
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
108+
MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location),
109+
MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.Expression).GetTypeInfo().Assembly
110+
.Location),
111+
#if NET
112+
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
113+
#endif
114+
MetadataReference.CreateFromFile(typeof(IFunctionEndpoint).GetTypeInfo().Assembly.Location),
115+
MetadataReference.CreateFromFile(typeof(EndpointConfiguration).GetTypeInfo().Assembly.Location),
116+
MetadataReference.CreateFromFile(typeof(AzureServiceBusTransport).GetTypeInfo().Assembly.Location));
117+
}
118+
119+
static readonly ImmutableList<PortableExecutableReference> ProjectReferences;
120+
121+
static readonly Regex DocumentSplittingRegex = new Regex("^-{5,}.*", RegexOptions.Compiled | RegexOptions.Multiline);
122+
123+
protected static void WriteCompilerDiagnostics(IEnumerable<Diagnostic> diagnostics)
124+
{
125+
if (!VerboseLogging)
126+
{
127+
return;
128+
}
129+
130+
Console.WriteLine("Compiler diagnostics:");
131+
132+
foreach (var diagnostic in diagnostics)
133+
{
134+
Console.WriteLine($" {diagnostic}");
135+
}
136+
}
137+
138+
protected static void WriteAnalyzerDiagnostics(IEnumerable<Diagnostic> diagnostics)
139+
{
140+
if (!VerboseLogging)
141+
{
142+
return;
143+
}
144+
145+
Console.WriteLine("Analyzer diagnostics:");
146+
147+
foreach (var diagnostic in diagnostics)
148+
{
149+
Console.WriteLine($" {diagnostic}");
150+
}
151+
}
152+
153+
protected static string[] SplitMarkupCodeIntoFiles(string markupCode)
154+
{
155+
return DocumentSplittingRegex.Split(markupCode)
156+
.Where(docCode => !string.IsNullOrWhiteSpace(docCode))
157+
.ToArray();
158+
}
159+
160+
static (string[] code, List<(string file, TextSpan span)>) Parse(string markupCode)
161+
{
162+
if (markupCode == null)
163+
{
164+
return (Array.Empty<string>(), new List<(string, TextSpan)>());
165+
}
166+
167+
var documents = SplitMarkupCodeIntoFiles(markupCode);
168+
169+
var markupSpans = new List<(string, TextSpan)>();
170+
171+
for (var i = 0; i < documents.Length; i++)
172+
{
173+
var code = new StringBuilder();
174+
var name = $"TestDocument{i}";
175+
176+
var remainingCode = documents[i];
177+
var remainingCodeStart = 0;
178+
179+
while (remainingCode.Length > 0)
180+
{
181+
var beforeAndAfterOpening = remainingCode.Split(new[] { "[|" }, 2, StringSplitOptions.None);
182+
183+
if (beforeAndAfterOpening.Length == 1)
184+
{
185+
_ = code.Append(beforeAndAfterOpening[0]);
186+
break;
187+
}
188+
189+
var midAndAfterClosing = beforeAndAfterOpening[1].Split(new[] { "|]" }, 2, StringSplitOptions.None);
190+
191+
if (midAndAfterClosing.Length == 1)
192+
{
193+
throw new Exception("The markup code does not contain a closing '|]'");
194+
}
195+
196+
var markupSpan = new TextSpan(remainingCodeStart + beforeAndAfterOpening[0].Length, midAndAfterClosing[0].Length);
197+
198+
_ = code.Append(beforeAndAfterOpening[0]).Append(midAndAfterClosing[0]);
199+
markupSpans.Add((name, markupSpan));
200+
201+
remainingCode = midAndAfterClosing[1];
202+
remainingCodeStart += beforeAndAfterOpening[0].Length + markupSpan.Length;
203+
}
204+
205+
documents[i] = code.ToString();
206+
}
207+
208+
return (documents, markupSpans);
209+
}
210+
211+
protected static readonly bool VerboseLogging = Environment.GetEnvironmentVariable("CI") != "true"
212+
|| Environment.GetEnvironmentVariable("VERBOSE_TEST_LOGGING")?.ToLower() == "true";
213+
}
214+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
namespace NServiceBus.AzureFunctions.Analyzer.Tests
2+
{
3+
using System.Threading.Tasks;
4+
using NUnit.Framework;
5+
using static AzureFunctionsDiagnostics;
6+
7+
[TestFixture]
8+
public class ConfigurationAnalyzerTests : AnalyzerTestFixture<ConfigurationAnalyzer>
9+
{
10+
[TestCase("DefineCriticalErrorAction((errorContext, cancellationToken) => Task.CompletedTask)", DefineCriticalErrorActionNotAllowedId)]
11+
[TestCase("LimitMessageProcessingConcurrencyTo(5)", LimitMessageProcessingToNotAllowedId)]
12+
[TestCase("MakeInstanceUniquelyAddressable(null)", MakeInstanceUniquelyAddressableNotAllowedId)]
13+
[TestCase("OverrideLocalAddress(null)", OverrideLocalAddressNotAllowedId)]
14+
[TestCase("PurgeOnStartup(true)", PurgeOnStartupNotAllowedId)]
15+
[TestCase("SetDiagnosticsPath(null)", SetDiagnosticsPathNotAllowedId)]
16+
[TestCase("UseTransport(new AzureServiceBusTransport(null))", UseTransportNotAllowedId)]
17+
public Task DiagnosticIsReportedForEndpointConfiguration(string configuration, string diagnosticId)
18+
{
19+
var source =
20+
$@"using NServiceBus;
21+
using System;
22+
using System.Threading.Tasks;
23+
class Foo
24+
{{
25+
void Bar(ServiceBusTriggeredEndpointConfiguration endpointConfig)
26+
{{
27+
[|endpointConfig.AdvancedConfiguration.{configuration}|];
28+
29+
var advancedConfig = endpointConfig.AdvancedConfiguration;
30+
[|advancedConfig.{configuration}|];
31+
}}
32+
}}";
33+
34+
return Assert(diagnosticId, source);
35+
}
36+
37+
[TestCase("DefineCriticalErrorAction((errorContext, cancellationToken) => Task.CompletedTask)", DefineCriticalErrorActionNotAllowedId)]
38+
[TestCase("LimitMessageProcessingConcurrencyTo(5)", LimitMessageProcessingToNotAllowedId)]
39+
[TestCase("MakeInstanceUniquelyAddressable(null)", MakeInstanceUniquelyAddressableNotAllowedId)]
40+
[TestCase("OverrideLocalAddress(null)", OverrideLocalAddressNotAllowedId)]
41+
[TestCase("PurgeOnStartup(true)", PurgeOnStartupNotAllowedId)]
42+
[TestCase("SetDiagnosticsPath(null)", SetDiagnosticsPathNotAllowedId)]
43+
[TestCase("UseTransport(new AzureServiceBusTransport(null))", UseTransportNotAllowedId)]
44+
public Task DiagnosticIsNotReportedForOtherEndpointConfiguration(string configuration, string diagnosticId)
45+
{
46+
var source =
47+
$@"using NServiceBus;
48+
using System;
49+
using System.Threading;
50+
using System.Threading.Tasks;
51+
52+
class SomeOtherClass
53+
{{
54+
internal void DefineCriticalErrorAction(Func<ICriticalErrorContext, CancellationToken, Task> onCriticalError) {{ }}
55+
internal void LimitMessageProcessingConcurrencyTo(int Number) {{ }}
56+
internal void MakeInstanceUniquelyAddressable(string someProperty) {{ }}
57+
internal void OverrideLocalAddress(string someProperty) {{ }}
58+
internal void PurgeOnStartup(bool purge) {{ }}
59+
internal void SetDiagnosticsPath(string someProperty) {{ }}
60+
internal void UseTransport(AzureServiceBusTransport transport) {{ }}
61+
}}
62+
63+
class Foo
64+
{{
65+
void Bar(SomeOtherClass endpointConfig)
66+
{{
67+
endpointConfig.{configuration};
68+
}}
69+
}}";
70+
71+
return Assert(diagnosticId, source);
72+
}
73+
}
74+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace NServiceBus.AzureFunctions.Analyzer.Tests
2+
{
3+
using System.Threading.Tasks;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using NUnit.Framework;
6+
using static AzureFunctionsDiagnostics;
7+
8+
[TestFixture]
9+
public class ConfigurationAnalyzerTestsCSharp8 : AnalyzerTestFixture<ConfigurationAnalyzer>
10+
{
11+
// HINT: In C# 7 this call is ambiguous with the LearningTransport version as the compiler cannot differentiate method calls via generic type constraints
12+
[TestCase("UseTransport<AzureServiceBusTransport>()", UseTransportNotAllowedId)]
13+
public Task DiagnosticIsReportedForEndpointConfiguration(string configuration, string diagnosticId)
14+
{
15+
var source =
16+
$@"using NServiceBus;
17+
using System;
18+
using System.Threading.Tasks;
19+
class Foo
20+
{{
21+
void Bar(ServiceBusTriggeredEndpointConfiguration endpointConfig)
22+
{{
23+
[|endpointConfig.AdvancedConfiguration.{configuration}|];
24+
25+
var advancedConfig = endpointConfig.AdvancedConfiguration;
26+
[|advancedConfig.{configuration}|];
27+
}}
28+
}}";
29+
30+
return Assert(diagnosticId, source);
31+
}
32+
protected override LanguageVersion AnalyzerLanguageVersion => LanguageVersion.CSharp8;
33+
}
34+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace NServiceBus.AzureFunctions.Analyzer.Tests
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Collections.Immutable;
6+
using System.Diagnostics;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.Diagnostics;
13+
14+
static class CompilationExtensions
15+
{
16+
public static void Compile(this Compilation compilation, bool throwOnFailure = true)
17+
{
18+
using (var peStream = new MemoryStream())
19+
{
20+
var emitResult = compilation.Emit(peStream);
21+
22+
if (!emitResult.Success)
23+
{
24+
if (throwOnFailure)
25+
{
26+
throw new Exception("Compilation failed.");
27+
}
28+
else
29+
{
30+
Debug.WriteLine("Compilation failed.");
31+
}
32+
}
33+
}
34+
}
35+
36+
public static async Task<IEnumerable<Diagnostic>> GetAnalyzerDiagnostics(this Compilation compilation, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken = default)
37+
{
38+
var exceptions = new List<Exception>();
39+
40+
var analysisOptions = new CompilationWithAnalyzersOptions(
41+
new AnalyzerOptions(ImmutableArray<AdditionalText>.Empty),
42+
(exception, _, __) => exceptions.Add(exception),
43+
concurrentAnalysis: false,
44+
logAnalyzerExecutionTime: false);
45+
46+
var diagnostics = await compilation
47+
.WithAnalyzers(ImmutableArray.Create(analyzer), analysisOptions)
48+
.GetAnalyzerDiagnosticsAsync(cancellationToken);
49+
50+
if (exceptions.Any())
51+
{
52+
throw new AggregateException(exceptions);
53+
}
54+
55+
return diagnostics
56+
.OrderBy(diagnostic => diagnostic.Location.SourceSpan)
57+
.ThenBy(diagnostic => diagnostic.Id);
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)