-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathAnalyzerTestFixture.cs
More file actions
215 lines (170 loc) · 7.84 KB
/
AnalyzerTestFixture.cs
File metadata and controls
215 lines (170 loc) · 7.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
namespace NServiceBus.AzureFunctions.InProcess.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 Azure.Core;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using NUnit.Framework;
public class AnalyzerTestFixture<TAnalyzer> where TAnalyzer : DiagnosticAnalyzer, new()
{
protected virtual LanguageVersion AnalyzerLanguageVersion => LanguageVersion.CSharp7;
protected Task Assert(string markupCode, CancellationToken cancellationToken = default) =>
Assert([], markupCode, [], cancellationToken);
protected Task Assert(string expectedDiagnosticId, string markupCode, CancellationToken cancellationToken = default) =>
Assert([expectedDiagnosticId], markupCode, [], 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, cancellationToken);
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.Assert.That(actualSpansAndIds, Is.EqualTo(expectedSpansAndIds).AsCollection);
}
protected static async Task WriteCode(Project project, CancellationToken cancellationToken = default)
{
if (!VerboseLogging)
{
return;
}
foreach (var document in project.Documents)
{
Console.WriteLine(document.Name);
var code = await document.GetCode(cancellationToken);
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 =
[
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.Expression).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
MetadataReference.CreateFromFile(typeof(IFunctionEndpoint).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(TokenCredential).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 ([], []);
}
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(["[|"], 2, StringSplitOptions.None);
if (beforeAndAfterOpening.Length == 1)
{
_ = code.Append(beforeAndAfterOpening[0]);
break;
}
var midAndAfterClosing = beforeAndAfterOpening[1].Split(["|]"], 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";
}