forked from microsoft/testfx
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFlowTestContextCancellationTokenFixer.cs
More file actions
263 lines (227 loc) · 14 KB
/
FlowTestContextCancellationTokenFixer.cs
File metadata and controls
263 lines (227 loc) · 14 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
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
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Immutable;
using System.Composition;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Text;
using MSTest.Analyzers.Helpers;
namespace MSTest.Analyzers;
/// <summary>
/// Code fixer for <see cref="FlowTestContextCancellationTokenAnalyzer"/>.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FlowTestContextCancellationTokenFixer))]
[Shared]
public sealed class FlowTestContextCancellationTokenFixer : CodeFixProvider
{
/// <inheritdoc />
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; }
= ImmutableArray.Create(DiagnosticIds.FlowTestContextCancellationTokenRuleId);
/// <inheritdoc />
public override FixAllProvider GetFixAllProvider()
// Use custom FixAllProvider to handle adding TestContext property when needed
=> FlowTestContextCancellationTokenFixAllProvider.Instance;
/// <inheritdoc />
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
Diagnostic diagnostic = context.Diagnostics[0];
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
// Find the invocation expression identified by the diagnostic
SyntaxNode node = root.FindNode(diagnosticSpan, getInnermostNodeForTie: true);
if (node is not InvocationExpressionSyntax invocationExpression)
{
return;
}
diagnostic.Properties.TryGetValue(FlowTestContextCancellationTokenAnalyzer.TestContextMemberNamePropertyKey, out string? testContextMemberName);
diagnostic.Properties.TryGetValue(FlowTestContextCancellationTokenAnalyzer.CancellationTokenParameterNamePropertyKey, out string? cancellationTokenParameterName);
diagnostic.Properties.TryGetValue(nameof(FlowTestContextCancellationTokenAnalyzer.TestContextState), out string? testContextState);
// Register a code action that will invoke the fix
context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixResources.PassCancellationTokenFix,
createChangedDocument: async c =>
{
DocumentEditor editor = await DocumentEditor.CreateAsync(context.Document, context.CancellationToken).ConfigureAwait(false);
return ApplyFix(editor, invocationExpression, testContextMemberName, testContextState, cancellationTokenParameterName, adjustedSymbols: null, c);
},
equivalenceKey: nameof(FlowTestContextCancellationTokenFixer)),
diagnostic);
}
internal static Document ApplyFix(
DocumentEditor editor,
InvocationExpressionSyntax invocationExpression,
string? testContextMemberName,
string? testContextState,
string? cancellationTokenParameterName,
HashSet<ISymbol>? adjustedSymbols,
CancellationToken cancellationToken)
{
if (testContextState == nameof(FlowTestContextCancellationTokenAnalyzer.TestContextState.CouldBeInScopeAsProperty))
{
Debug.Assert(testContextMemberName is null, "TestContext member name should be null when state is CouldBeInScopeAsProperty");
AddCancellationTokenArgument(editor, invocationExpression, "TestContext", cancellationTokenParameterName);
TypeDeclarationSyntax? containingTypeDeclaration = invocationExpression.FirstAncestorOrSelf<TypeDeclarationSyntax>();
if (containingTypeDeclaration is not null)
{
// adjustedSymbols is null meaning we are only applying a single fix (in that case we add the property).
// If we are in fix all, we then verify if a previous fix has already added the property.
// We only add the property if it wasn't added by a previous fix.
// NOTE: We don't expect GetDeclaredSymbol to return null, but if it did (e.g, error scenario), we add the property.
if (adjustedSymbols is null ||
editor.SemanticModel.GetDeclaredSymbol(containingTypeDeclaration, cancellationToken) is not { } symbol ||
adjustedSymbols.Add(symbol))
{
editor.ReplaceNode(containingTypeDeclaration, (containingTypeDeclaration, _) => AddTestContextProperty((TypeDeclarationSyntax)containingTypeDeclaration));
}
}
}
else if (testContextState == nameof(FlowTestContextCancellationTokenAnalyzer.TestContextState.CouldBeInScopeAsParameter))
{
Debug.Assert(testContextMemberName is null, "TestContext member name should be null when state is CouldBeInScopeAsParameter");
AddCancellationTokenArgument(editor, invocationExpression, "testContext", cancellationTokenParameterName);
MethodDeclarationSyntax? containingMethodDeclaration = invocationExpression.FirstAncestorOrSelf<MethodDeclarationSyntax>();
if (containingMethodDeclaration is not null)
{
// adjustedSymbols is null meaning we are only applying a single fix (in that case we add the parameter).
// If we are in fix all, we then verify if a previous fix has already added the parameter.
// We only add the parameter if it wasn't added by a previous fix.
// NOTE: We don't expect GetDeclaredSymbol to return null, but if it did (e.g, error scenario), we add the property.
if (adjustedSymbols is null ||
editor.SemanticModel.GetDeclaredSymbol(containingMethodDeclaration, cancellationToken) is not { } symbol ||
adjustedSymbols.Add(symbol))
{
editor.ReplaceNode(containingMethodDeclaration, (containingMethodDeclaration, _) => AddTestContextParameterToMethod((MethodDeclarationSyntax)containingMethodDeclaration));
}
}
}
else
{
Ensure.NotNull(testContextMemberName);
AddCancellationTokenArgument(editor, invocationExpression, testContextMemberName, cancellationTokenParameterName);
}
return editor.GetChangedDocument();
}
internal static void AddCancellationTokenArgument(
DocumentEditor editor,
InvocationExpressionSyntax invocationExpression,
string testContextMemberName,
string? cancellationTokenParameterName)
{
// Find the containing method to determine the context
MethodDeclarationSyntax? containingMethod = invocationExpression.FirstAncestorOrSelf<MethodDeclarationSyntax>();
// Create the TestContext.CancellationToken expression
MemberAccessExpressionSyntax testContextExpression = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName(testContextMemberName),
SyntaxFactory.IdentifierName("CancellationToken"));
editor.ReplaceNode(invocationExpression, (node, _) =>
{
var invocationExpression = (InvocationExpressionSyntax)node;
ArgumentListSyntax currentArguments = invocationExpression.ArgumentList;
NameColonSyntax? nameColon = cancellationTokenParameterName is null ? null : SyntaxFactory.NameColon(cancellationTokenParameterName);
SeparatedSyntaxList<ArgumentSyntax> newArguments = currentArguments.Arguments.Add(SyntaxFactory.Argument(nameColon, default, testContextExpression));
return invocationExpression.WithArgumentList(currentArguments.WithArguments(newArguments));
});
}
internal static MethodDeclarationSyntax AddTestContextParameterToMethod(MethodDeclarationSyntax method)
{
// Create TestContext parameter
ParameterSyntax testContextParameter = SyntaxFactory.Parameter(SyntaxFactory.Identifier("testContext"))
.WithType(SyntaxFactory.IdentifierName("TestContext"));
// Add the parameter to the method
SeparatedSyntaxList<ParameterSyntax> updatedParameterList = method.ParameterList.Parameters.Count == 0
? SyntaxFactory.SingletonSeparatedList(testContextParameter)
: method.ParameterList.Parameters.Add(testContextParameter);
return method.WithParameterList(method.ParameterList.WithParameters(updatedParameterList));
}
internal static TypeDeclarationSyntax AddTestContextProperty(TypeDeclarationSyntax typeDeclaration)
{
PropertyDeclarationSyntax testContextProperty = SyntaxFactory.PropertyDeclaration(
SyntaxFactory.IdentifierName("TestContext"),
"TestContext")
.WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)))
.WithAccessorList(SyntaxFactory.AccessorList(
SyntaxFactory.List(new[]
{
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
})));
return typeDeclaration.AddMembers(testContextProperty);
}
}
/// <summary>
/// Custom FixAllProvider for <see cref="FlowTestContextCancellationTokenFixer"/> that can add TestContext property when needed.
/// This ensures that when multiple fixes are applied to the same class, the TestContext property is added only once.
/// </summary>
internal sealed class FlowTestContextCancellationTokenFixAllProvider : FixAllProvider
{
public static readonly FlowTestContextCancellationTokenFixAllProvider Instance = new();
private FlowTestContextCancellationTokenFixAllProvider()
{
}
public override Task<CodeAction?> GetFixAsync(FixAllContext fixAllContext)
=> Task.FromResult<CodeAction?>(new FixAllCodeAction(fixAllContext));
private sealed class FixAllCodeAction : CodeAction
{
private readonly FixAllContext _fixAllContext;
public FixAllCodeAction(FixAllContext fixAllContext)
=> _fixAllContext = fixAllContext;
public override string Title => CodeFixResources.PassCancellationTokenFix;
public override string? EquivalenceKey => nameof(FlowTestContextCancellationTokenFixer);
protected override async Task<Solution?> GetChangedSolutionAsync(CancellationToken cancellationToken)
{
FixAllContext fixAllContext = _fixAllContext;
var editor = new SolutionEditor(fixAllContext.Solution);
var fixedSymbols = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
if (fixAllContext.Scope == FixAllScope.Document)
{
DocumentEditor documentEditor = await editor.GetDocumentEditorAsync(fixAllContext.Document!.Id, cancellationToken).ConfigureAwait(false);
foreach (Diagnostic diagnostic in await fixAllContext.GetDocumentDiagnosticsAsync(fixAllContext.Document!).ConfigureAwait(false))
{
FixOneDiagnostic(documentEditor, diagnostic, fixedSymbols, cancellationToken);
}
}
else if (fixAllContext.Scope == FixAllScope.Project)
{
await FixAllInProjectAsync(fixAllContext, fixAllContext.Project, editor, fixedSymbols, cancellationToken).ConfigureAwait(false);
}
else if (fixAllContext.Scope == FixAllScope.Solution)
{
foreach (Project project in fixAllContext.Solution.Projects)
{
await FixAllInProjectAsync(fixAllContext, project, editor, fixedSymbols, cancellationToken).ConfigureAwait(false);
}
}
return editor.GetChangedSolution();
}
private static async Task FixAllInProjectAsync(FixAllContext fixAllContext, Project project, SolutionEditor editor, HashSet<ISymbol> fixedSymbols, CancellationToken cancellationToken)
{
foreach (Diagnostic diagnostic in await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false))
{
DocumentId documentId = editor.OriginalSolution.GetDocumentId(diagnostic.Location.SourceTree)!;
DocumentEditor documentEditor = await editor.GetDocumentEditorAsync(documentId, cancellationToken).ConfigureAwait(false);
FixOneDiagnostic(documentEditor, diagnostic, fixedSymbols, cancellationToken);
}
}
private static void FixOneDiagnostic(DocumentEditor documentEditor, Diagnostic diagnostic, HashSet<ISymbol> fixedSymbols, CancellationToken cancellationToken)
{
SyntaxNode node = documentEditor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
if (node is not InvocationExpressionSyntax invocationExpression)
{
return;
}
diagnostic.Properties.TryGetValue(FlowTestContextCancellationTokenAnalyzer.TestContextMemberNamePropertyKey, out string? testContextMemberName);
diagnostic.Properties.TryGetValue(FlowTestContextCancellationTokenAnalyzer.CancellationTokenParameterNamePropertyKey, out string? cancellationTokenParameterName);
diagnostic.Properties.TryGetValue(nameof(FlowTestContextCancellationTokenAnalyzer.TestContextState), out string? testContextState);
FlowTestContextCancellationTokenFixer.ApplyFix(documentEditor, invocationExpression, testContextMemberName, testContextState, cancellationTokenParameterName, fixedSymbols, cancellationToken);
}
}
}