Welcome to mirror list, hosted at ThFree Co, Russian Federation.

TestChecker.cs « ILLink.RoslynAnalyzer.Tests « test - github.com/mono/linker.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: ecf91c376aba435507081e2e4459361decd77347 (plain)
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Xunit;

namespace ILLink.RoslynAnalyzer.Tests
{
	internal class TestChecker
	{
		private readonly CompilationWithAnalyzers Compilation;

		private readonly SemanticModel SemanticModel;

		private readonly List<Diagnostic> DiagnosticMessages;

		private readonly SyntaxNode MemberSyntax;

		public TestChecker (MemberDeclarationSyntax memberSyntax, (CompilationWithAnalyzers Compilation, SemanticModel SemanticModel) compilationResult)
		{
			Compilation = compilationResult.Compilation;
			SemanticModel = compilationResult.SemanticModel;
			DiagnosticMessages = Compilation.GetAnalyzerDiagnosticsAsync ().Result
				.Where (d => {
					// Filter down to diagnostics which originate from this member.

					// Test data may include diagnostics originating from a testcase or testcase dependencies.
					if (memberSyntax.SyntaxTree != d.Location.SourceTree)
						return false;

					// Filter down to diagnostics which originate from this member
					if (memberSyntax is ClassDeclarationSyntax classSyntax) {
						if (SemanticModel.GetDeclaredSymbol (classSyntax) is not ITypeSymbol typeSymbol)
							throw new NotImplementedException ("Unable to get type symbol for class declaration syntax.");

						if (typeSymbol.Locations.Length != 1)
							throw new NotImplementedException ("Type defined in multiple source locations.");

						// For classes, only consider diagnostics which originate from the type (not its members).
						// Approximate this by getting the location from the start of the type's syntax (which includes
						// attributes declared on the type) to the opening brace.
						var classSpan = TextSpan.FromBounds (
							classSyntax.GetLocation ().SourceSpan.Start,
							classSyntax.OpenBraceToken.GetLocation ().SourceSpan.Start
						);

						return d.Location.SourceSpan.IntersectsWith (classSpan);
					}

					return d.Location.SourceSpan.IntersectsWith (memberSyntax.Span);
				})
				.ToList ();
			MemberSyntax = memberSyntax;
		}

		bool IsExpectedDiagnostic (AttributeSyntax attribute)
		{
			switch (attribute.Name.ToString ()) {
			case "ExpectedWarning":
				var args = TestCaseUtils.GetAttributeArguments (attribute);
				if (args.TryGetValue ("ProducedBy", out var producedBy) &&
					producedBy is MemberAccessExpressionSyntax memberAccessExpression &&
					memberAccessExpression.Name is IdentifierNameSyntax identifierNameSyntax &&
					identifierNameSyntax.Identifier.ValueText == "Trimmer")
					return false;
				return true;
			case "LogContains":
			case "UnrecognizedReflectionAccessPattern":
				return true;
			default:
				return false;
			}
		}

		bool TryValidateExpectedDiagnostic (AttributeSyntax attribute, List<Diagnostic> diagnostics, [NotNullWhen (true)] out int? matchIndex, [NotNullWhen (false)] out string? missingDiagnosticMessage)
		{
			switch (attribute.Name.ToString ()) {
			case "ExpectedWarning":
				return TryValidateExpectedWarningAttribute (attribute!, diagnostics, out matchIndex, out missingDiagnosticMessage);
			case "LogContains":
				return TryValidateLogContainsAttribute (attribute!, diagnostics, out matchIndex, out missingDiagnosticMessage);
			case "UnrecognizedReflectionAccessPattern":
				return TryValidateUnrecognizedReflectionAccessPatternAttribute (attribute!, diagnostics, out matchIndex, out missingDiagnosticMessage);
			default:
				throw new InvalidOperationException ($"Unsupported attribute type {attribute.Name}");
			}
		}

		public void ValidateAttributes (List<AttributeSyntax> attributes)
		{
			var unmatchedDiagnostics = DiagnosticMessages.ToList ();

			var missingDiagnostics = new List<(AttributeSyntax Attribute, string Message)> ();
			foreach (var attribute in attributes) {
				if (attribute.Name.ToString () == "LogDoesNotContain")
					ValidateLogDoesNotContainAttribute (attribute, DiagnosticMessages);

				if (!IsExpectedDiagnostic (attribute))
					continue;

				if (!TryValidateExpectedDiagnostic (attribute, unmatchedDiagnostics, out int? matchIndex, out string? missingDiagnosticMessage)) {
					missingDiagnostics.Add ((attribute, missingDiagnosticMessage));
					continue;
				}

				unmatchedDiagnostics.RemoveAt (matchIndex.Value);
			}

			var missingDiagnosticsMessage = missingDiagnostics.Any ()
				? $"Missing diagnostics:{Environment.NewLine}{string.Join (Environment.NewLine, missingDiagnostics.Select (md => md.Message))}"
				: String.Empty;

			var unmatchedDiagnosticsMessage = unmatchedDiagnostics.Any ()
				? $"Found unmatched diagnostics:{Environment.NewLine}{string.Join (Environment.NewLine, unmatchedDiagnostics)}"
				: String.Empty;

			Assert.True (!missingDiagnostics.Any (), $"{missingDiagnosticsMessage}{Environment.NewLine}{unmatchedDiagnosticsMessage}");
			Assert.True (!unmatchedDiagnostics.Any (), unmatchedDiagnosticsMessage);
		}

		private bool TryValidateExpectedWarningAttribute (AttributeSyntax attribute, List<Diagnostic> diagnostics, out int? matchIndex, out string? missingDiagnosticMessage)
		{
			missingDiagnosticMessage = null;
			matchIndex = null;
			var args = TestCaseUtils.GetAttributeArguments (attribute);
			string expectedWarningCode = TestCaseUtils.GetStringFromExpression (args["#0"]);

			if (!expectedWarningCode.StartsWith ("IL"))
				throw new InvalidOperationException ($"Expected warning code should start with \"IL\" prefix.");

			List<string> expectedMessages = args
				.Where (arg => arg.Key.StartsWith ("#") && arg.Key != "#0")
				.Select (arg => TestCaseUtils.GetStringFromExpression (arg.Value, SemanticModel))
				.ToList ();

			for (int i = 0; i < diagnostics.Count; i++) {
				if (Matches (diagnostics[i])) {
					matchIndex = i;
					return true;
				}
			}

			missingDiagnosticMessage = $"Expected to find warning containing:{string.Join (" ", expectedMessages.Select (m => "'" + m + "'"))}" +
					$", but no such message was found.{ Environment.NewLine}";
			return false;

			bool Matches (Diagnostic diagnostic)
			{
				if (diagnostic.Id != expectedWarningCode)
					return false;

				foreach (var expectedMessage in expectedMessages)
					if (!diagnostic.GetMessage ().Contains (expectedMessage))
						return false;

				return true;
			}
		}

		private bool TryValidateLogContainsAttribute (AttributeSyntax attribute, List<Diagnostic> diagnostics, out int? matchIndex, out string? missingDiagnosticMessage)
		{
			missingDiagnosticMessage = null;
			matchIndex = null;
			var arg = Assert.Single (TestCaseUtils.GetAttributeArguments (attribute));
			var text = TestCaseUtils.GetStringFromExpression (arg.Value);

			// If the text starts with `warning IL...` then it probably follows the pattern
			//	'warning <diagId>: <location>:'
			// We don't want to repeat the location in the error message for the analyzer, so
			// it's better to just trim here. We've already filtered by diagnostic location so
			// the text location shouldn't matter
			if (text.StartsWith ("warning IL")) {
				var firstColon = text.IndexOf (": ");
				if (firstColon > 0) {
					var secondColon = text.IndexOf (": ", firstColon + 1);
					if (secondColon > 0) {
						text = text.Substring (secondColon + 2);
					}
				}
			}

			for (int i = 0; i < diagnostics.Count; i++) {
				if (diagnostics[i].GetMessage ().Contains (text)) {
					matchIndex = i;
					return true;
				}
			}

			missingDiagnosticMessage = $"Could not find text:\n{text}\nIn diagnostics:\n{(string.Join (Environment.NewLine, DiagnosticMessages))}";
			return false;
		}

		private void ValidateLogDoesNotContainAttribute (AttributeSyntax attribute, List<Diagnostic> diagnosticMessages)
		{
			var arg = Assert.Single (TestCaseUtils.GetAttributeArguments (attribute));
			var text = TestCaseUtils.GetStringFromExpression (arg.Value);
			foreach (var diagnostic in DiagnosticMessages)
				Assert.DoesNotContain (text, diagnostic.GetMessage ());
		}

		private bool TryValidateUnrecognizedReflectionAccessPatternAttribute (AttributeSyntax attribute, List<Diagnostic> diagnostics, out int? matchIndex, out string? missingDiagnosticMessage)
		{
			missingDiagnosticMessage = null;
			matchIndex = null;
			var args = TestCaseUtils.GetAttributeArguments (attribute);

			MemberDeclarationSyntax sourceMember = attribute.Ancestors ().OfType<MemberDeclarationSyntax> ().First ();
			if (SemanticModel.GetDeclaredSymbol (sourceMember) is not ISymbol memberSymbol)
				return false;

			string sourceMemberName = memberSymbol!.GetDisplayName ();
			string expectedReflectionMemberMethodType = TestCaseUtils.GetStringFromExpression (args["#0"], SemanticModel);
			string expectedReflectionMemberMethodName = TestCaseUtils.GetStringFromExpression (args["#1"], SemanticModel);

			var reflectionMethodParameters = new List<string> ();
			if (args.TryGetValue ("#2", out var reflectionMethodParametersExpr) || args.TryGetValue ("reflectionMethodParameters", out reflectionMethodParametersExpr)) {
				if (reflectionMethodParametersExpr is ArrayCreationExpressionSyntax arrayReflectionMethodParametersExpr) {
					foreach (var rmp in arrayReflectionMethodParametersExpr.Initializer!.Expressions)
						reflectionMethodParameters.Add (TestCaseUtils.GetStringFromExpression (rmp, SemanticModel));
				}
			}

			var expectedStringsInMessage = new List<string> ();
			if (args.TryGetValue ("#3", out var messageExpr) || args.TryGetValue ("message", out messageExpr)) {
				if (messageExpr is ArrayCreationExpressionSyntax arrayMessageExpr) {
					foreach (var m in arrayMessageExpr.Initializer!.Expressions)
						expectedStringsInMessage.Add (TestCaseUtils.GetStringFromExpression (m, SemanticModel));
				}
			}

			string expectedWarningCode = string.Empty;
			if (args.TryGetValue ("#4", out var messageCodeExpr) || args.TryGetValue ("messageCode", out messageCodeExpr)) {
				expectedWarningCode = TestCaseUtils.GetStringFromExpression (messageCodeExpr);
				Assert.True (expectedWarningCode.StartsWith ("IL"),
					$"The warning code specified in {messageCodeExpr.ToString ()} must start with the 'IL' prefix. Specified value: '{expectedWarningCode}'");
			}

			// Don't validate the return type becasue this is not included in the diagnostic messages.

			var sb = new StringBuilder ();

			// Format the member signature the same way Roslyn would since this is what will be included in the warning message.
			sb.Append (expectedReflectionMemberMethodType).Append (".").Append (expectedReflectionMemberMethodName);
			if (!expectedReflectionMemberMethodName.EndsWith (".get") &&
				!expectedReflectionMemberMethodName.EndsWith (".set") &&
				reflectionMethodParameters is not null)
				sb.Append ("(").Append (string.Join (", ", reflectionMethodParameters)).Append (")");

			var reflectionAccessPattern = sb.ToString ();

			for (int i = 0; i < diagnostics.Count; i++) {
				if (Matches (diagnostics[i])) {
					matchIndex = i;
					return true;
				}
			}

			missingDiagnosticMessage = $"Expected to find unrecognized reflection access pattern '{(expectedWarningCode == string.Empty ? "" : expectedWarningCode + " ")}" +
					$"{sourceMemberName}: Usage of {reflectionAccessPattern} unrecognized.";
			return false;

			bool Matches (Diagnostic diagnostic)
			{
				if (!string.IsNullOrEmpty (expectedWarningCode) && diagnostic.Id != expectedWarningCode)
					return false;

				// Don't check whether the message contains the source member name. Roslyn's diagnostics don't include the source
				// member as part of the message.

				foreach (var expectedString in expectedStringsInMessage)
					if (!diagnostic.GetMessage ().Contains (expectedString))
						return false;

				return diagnostic.GetMessage ().Contains (reflectionAccessPattern);
			}
		}
	}
}