Skip to content

Commit b93bedd

Browse files
Meir017meziantou
andauthored
feat: Migrate MsTest to IOperation (#269)
* feat: Migrate MsTest to IOperation * feat: Migrate MsTest to IOperation * Update src/FluentAssertions.Analyzers/Tips/AssertAnalyzer.cs Co-authored-by: Gérald Barré <meziantou@users.noreply.github.com> * Update FluentAssertions.Analyzers.csproj * feat: Migrate MsTest to IOperation * feat: Migrate MsTest to IOperation * feat: Migrate MsTest to IOperation --------- Co-authored-by: Gérald Barré <meziantou@users.noreply.github.com>
1 parent 7099505 commit b93bedd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+917
-1636
lines changed

src/FluentAssertions.Analyzers.Tests/DiagnosticVerifier.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private static void VerifyFix(CsProjectArguments arguments, DiagnosticAnalyzer a
9494
}
9595
}
9696

97-
codeFixProvider.FixableDiagnosticIds.Should().BeEquivalentTo(analyzer.SupportedDiagnostics.Select(d => d.Id));
97+
codeFixProvider.FixableDiagnosticIds.Should().BeSubsetOf(analyzer.SupportedDiagnostics.Select(d => d.Id));
9898

9999
//after applying all of the code fixes, compare the resulting string to the inputted one
100100
var actual = GetStringFromDocument(document);

src/FluentAssertions.Analyzers.Tests/Tips/MsTestTests.cs

Lines changed: 117 additions & 121 deletions
Large diffs are not rendered by default.

src/FluentAssertions.Analyzers.Tests/Tips/SanityTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,8 @@ public static void Main()
279279
}";
280280

281281
DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments()
282-
.WithCodeFixProvider<AssertAreEqualCodeFix>()
283-
.WithDiagnosticAnalyzer<AssertAreEqualAnalyzer>()
282+
.WithCodeFixProvider<MsTestCodeFixProvider>()
283+
.WithDiagnosticAnalyzer<AssertAnalyzer>()
284284
.WithSources(oldSource)
285285
.WithFixedSources(newSource)
286286
.WithPackageReferences(PackageReference.FluentAssertions_6_12_0, PackageReference.MSTestTestFramework_3_1_1)

src/FluentAssertions.Analyzers/FluentAssertions.Analyzers.csproj

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

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
5-
<LangVersion>latest</LangVersion>
5+
<LangVersion>preview</LangVersion>
66
<RootNamespace>FluentAssertions.Analyzers</RootNamespace>
77

88
<IncludeBuildOutput>false</IncludeBuildOutput>
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2023 Gérald Barré (https://www.meziantou.net)
5+
* Modifications copyright (c) Meir Blachman.
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in all
15+
* copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
* SOFTWARE.
24+
*/
25+
26+
using System;
27+
using System.Collections.Immutable;
28+
using FluentAssertions.Analyzers.Utilities;
29+
using Microsoft.CodeAnalysis;
30+
using Microsoft.CodeAnalysis.Diagnostics;
31+
using Microsoft.CodeAnalysis.Operations;
32+
33+
namespace FluentAssertions.Analyzers;
34+
35+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
36+
public class AssertAnalyzer : DiagnosticAnalyzer
37+
{
38+
public static readonly string Message = "Use FluentAssertions equivalent";
39+
public static readonly DiagnosticDescriptor XunitRule = new(
40+
"FAA0002",
41+
title: "Replace Xunit assertion with Fluent Assertions equivalent",
42+
messageFormat: Message,
43+
description: "",
44+
category: Constants.Tips.Category,
45+
defaultSeverity: DiagnosticSeverity.Info, // TODO: change to DiagnosticSeverity.Warning,
46+
isEnabledByDefault: true);
47+
48+
public static readonly DiagnosticDescriptor MSTestsRule = new(
49+
"FAA0003",
50+
title: "Replace MSTests assertion with Fluent Assertions equivalent",
51+
messageFormat: Message,
52+
description: "",
53+
category: Constants.Tips.Category,
54+
defaultSeverity: DiagnosticSeverity.Info, // TODO: change to DiagnosticSeverity.Warning,
55+
isEnabledByDefault: true);
56+
57+
public static readonly DiagnosticDescriptor NUnitRule = new(
58+
"FAA0004",
59+
title: "Replace NUnit assertion with Fluent Assertions equivalent",
60+
messageFormat: Message,
61+
description: "",
62+
category: Constants.Tips.Category,
63+
defaultSeverity: DiagnosticSeverity.Info, // TODO: change to DiagnosticSeverity.Warning,
64+
isEnabledByDefault: true);
65+
66+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(XunitRule, MSTestsRule, NUnitRule);
67+
68+
public override void Initialize(AnalysisContext context)
69+
{
70+
context.EnableConcurrentExecution();
71+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
72+
73+
context.RegisterCompilationStartAction(context =>
74+
{
75+
if (context.Compilation.GetTypeByMetadataName("FluentAssertions.AssertionExtensions") is null)
76+
return;
77+
78+
var analyzerContext = new AnalyzerContext(context.Compilation);
79+
80+
// TODO: enable xunit
81+
// if (analyzerContext.IsXUnitAvailable)
82+
// {
83+
// context.RegisterOperationAction(analyzerContext.AnalyzeXunitInvocation, OperationKind.Invocation);
84+
// }
85+
86+
if (analyzerContext.IsMSTestsAvailable)
87+
{
88+
context.RegisterOperationAction(analyzerContext.AnalyzeMsTestInvocation, OperationKind.Invocation);
89+
context.RegisterOperationAction(analyzerContext.AnalyzeMsTestThrow, OperationKind.Throw);
90+
}
91+
92+
// TODO: enable NUnit
93+
// if (analyzerContext.IsNUnitAvailable)
94+
// {
95+
// context.RegisterOperationAction(analyzerContext.AnalyzeNunitInvocation, OperationKind.Invocation);
96+
// context.RegisterOperationAction(analyzerContext.AnalyzeNunitDynamicInvocation, OperationKind.DynamicInvocation);
97+
// context.RegisterOperationAction(analyzerContext.AnalyzeNunitThrow, OperationKind.Throw);
98+
// }
99+
});
100+
}
101+
102+
private sealed class AnalyzerContext(Compilation compilation)
103+
{
104+
private readonly INamedTypeSymbol _xunitAssertSymbol = compilation.GetTypeByMetadataName("Xunit.Assert");
105+
106+
private readonly INamedTypeSymbol _msTestsAssertSymbol = compilation.GetTypeByMetadataName("Microsoft.VisualStudio.TestTools.UnitTesting.Assert");
107+
private readonly INamedTypeSymbol _msTestsStringAssertSymbol = compilation.GetTypeByMetadataName("Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert");
108+
private readonly INamedTypeSymbol _msTestsCollectionAssertSymbol = compilation.GetTypeByMetadataName("Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert");
109+
private readonly INamedTypeSymbol _msTestsUnitTestAssertExceptionSymbol = compilation.GetTypeByMetadataName("Microsoft.VisualStudio.TestTools.UnitTesting.UnitTestAssertException");
110+
111+
private readonly INamedTypeSymbol _nunitAssertionExceptionSymbol = compilation.GetTypeByMetadataName("NUnit.Framework.AssertionException");
112+
private readonly INamedTypeSymbol _nunitAssertSymbol = compilation.GetTypeByMetadataName("NUnit.Framework.Assert");
113+
private readonly INamedTypeSymbol _nunitCollectionAssertSymbol = compilation.GetTypeByMetadataName("NUnit.Framework.CollectionAssert");
114+
private readonly INamedTypeSymbol _nunitDirectoryAssertSymbol = compilation.GetTypeByMetadataName("NUnit.Framework.DirectoryAssert");
115+
private readonly INamedTypeSymbol _nunitFileAssertSymbol = compilation.GetTypeByMetadataName("NUnit.Framework.FileAssert");
116+
private readonly INamedTypeSymbol _nunitStringAssertSymbol = compilation.GetTypeByMetadataName("NUnit.Framework.StringAssert");
117+
private readonly INamedTypeSymbol _nunitClassicAssertSymbol = compilation.GetTypeByMetadataName("NUnit.Framework.Legacy.ClassicAssert");
118+
private readonly INamedTypeSymbol _nunitResultStateExceptionSymbol = compilation.GetTypeByMetadataName("NUnit.Framework.ResultStateException");
119+
120+
public bool IsMSTestsAvailable => _msTestsAssertSymbol is not null;
121+
public bool IsNUnitAvailable => _nunitAssertionExceptionSymbol is not null;
122+
public bool IsXUnitAvailable => _xunitAssertSymbol is not null;
123+
124+
private static readonly char[] SymbolsSeparators = [';'];
125+
126+
private bool IsMethodExcluded(AnalyzerOptions options, IInvocationOperation operation)
127+
{
128+
var location = operation.Syntax.GetLocation().SourceTree;
129+
if (location is null)
130+
return false;
131+
132+
var fileOptions = options.AnalyzerConfigOptionsProvider.GetOptions(location);
133+
if (fileOptions is null)
134+
return false;
135+
136+
if (!fileOptions.TryGetValue("mfa_excluded_methods", out var symbolDocumentationIds))
137+
return false;
138+
139+
var parts = symbolDocumentationIds.Split(SymbolsSeparators, StringSplitOptions.RemoveEmptyEntries);
140+
foreach (var part in parts)
141+
{
142+
var symbols = DocumentationCommentId.GetSymbolsForDeclarationId(part, compilation);
143+
foreach (var symbol in symbols)
144+
{
145+
if (operation.TargetMethod.EqualsSymbol(symbol))
146+
return true;
147+
}
148+
}
149+
150+
return false;
151+
}
152+
153+
public void AnalyzeXunitInvocation(OperationAnalysisContext context)
154+
{
155+
var op = (IInvocationOperation)context.Operation;
156+
if (op.TargetMethod.ContainingType.EqualsSymbol(_xunitAssertSymbol) && !IsMethodExcluded(context.Options, op))
157+
{
158+
context.ReportDiagnostic(Diagnostic.Create(XunitRule, op.Syntax.GetLocation()));
159+
}
160+
}
161+
162+
public void AnalyzeMsTestInvocation(OperationAnalysisContext context)
163+
{
164+
var op = (IInvocationOperation)context.Operation;
165+
if (IsMsTestAssertClass(op.TargetMethod.ContainingType) && !IsMethodExcluded(context.Options, op))
166+
{
167+
context.ReportDiagnostic(Diagnostic.Create(MSTestsRule, op.Syntax.GetLocation()));
168+
}
169+
}
170+
171+
public void AnalyzeMsTestThrow(OperationAnalysisContext context)
172+
{
173+
var op = (IThrowOperation)context.Operation;
174+
if (op.Exception is not null && op.Exception.UnwrapConversion().Type.IsOrInheritsFrom(_msTestsUnitTestAssertExceptionSymbol))
175+
{
176+
context.ReportDiagnostic(Diagnostic.Create(MSTestsRule, op.Syntax.GetLocation()));
177+
}
178+
}
179+
180+
public void AnalyzeNunitInvocation(OperationAnalysisContext context)
181+
{
182+
var op = (IInvocationOperation)context.Operation;
183+
if (IsNunitAssertClass(op.TargetMethod.ContainingType) && !IsMethodExcluded(context.Options, op))
184+
{
185+
if (op.TargetMethod.Name is "Inconclusive" or "Ignore" && op.TargetMethod.ContainingType.EqualsSymbol(_nunitAssertSymbol))
186+
return;
187+
188+
context.ReportDiagnostic(Diagnostic.Create(NUnitRule, op.Syntax.GetLocation()));
189+
}
190+
}
191+
192+
public void AnalyzeNunitDynamicInvocation(OperationAnalysisContext context)
193+
{
194+
var op = (IDynamicInvocationOperation)context.Operation;
195+
196+
if (op.Arguments.Length < 2)
197+
return;
198+
199+
var containingType = ((op.Arguments[1]
200+
.Parent as IDynamicInvocationOperation)?
201+
.Operation as IDynamicMemberReferenceOperation)?
202+
.ContainingType;
203+
if (IsNunitAssertClass(containingType))
204+
{
205+
context.ReportDiagnostic(Diagnostic.Create(NUnitRule, op.Syntax.GetLocation()));
206+
}
207+
}
208+
209+
public void AnalyzeNunitThrow(OperationAnalysisContext context)
210+
{
211+
var op = (IThrowOperation)context.Operation;
212+
if (op.Exception is not null && op.Exception.UnwrapConversion().Type.IsOrInheritsFrom(_nunitResultStateExceptionSymbol))
213+
{
214+
context.ReportDiagnostic(Diagnostic.Create(NUnitRule, op.Syntax.GetLocation()));
215+
}
216+
}
217+
218+
private bool IsMsTestAssertClass(ITypeSymbol typeSymbol)
219+
{
220+
if (typeSymbol is null)
221+
return false;
222+
223+
return typeSymbol.EqualsSymbol(_msTestsAssertSymbol)
224+
|| typeSymbol.EqualsSymbol(_msTestsStringAssertSymbol)
225+
|| typeSymbol.EqualsSymbol(_msTestsCollectionAssertSymbol);
226+
}
227+
228+
private bool IsNunitAssertClass(ITypeSymbol typeSymbol)
229+
{
230+
if (typeSymbol is null)
231+
return false;
232+
233+
return typeSymbol.EqualsSymbol(_nunitAssertSymbol)
234+
|| typeSymbol.EqualsSymbol(_nunitCollectionAssertSymbol)
235+
|| typeSymbol.EqualsSymbol(_nunitDirectoryAssertSymbol)
236+
|| typeSymbol.EqualsSymbol(_nunitFileAssertSymbol)
237+
|| typeSymbol.EqualsSymbol(_nunitStringAssertSymbol)
238+
|| typeSymbol.EqualsSymbol(_nunitClassicAssertSymbol);
239+
}
240+
}
241+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeFixes;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Editing;
9+
using Microsoft.CodeAnalysis.Operations;
10+
using CreateChangedDocument = System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.CodeAnalysis.Document>>;
11+
12+
namespace FluentAssertions.Analyzers;
13+
14+
public class DocumentEditorUtils
15+
{
16+
public static CreateChangedDocument RenameMethodToSubjectShouldAssertion(IInvocationOperation invocation, CodeFixContext context, string newName, int argumentIndex, int[] argumentsToRemove)
17+
=> async ctx =>
18+
{
19+
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;
20+
21+
return await RewriteExpression(invocationExpression, [
22+
..Array.ConvertAll(argumentsToRemove, arg => new RemoveNodeAction(invocationExpression.ArgumentList.Arguments[arg])),
23+
new SubjectShouldAssertionAction(argumentIndex, newName)
24+
], context, ctx);
25+
};
26+
27+
public static CreateChangedDocument RenameGenericMethodToSubjectShouldGenericAssertion(IInvocationOperation invocation, CodeFixContext context, string newName, int argumentIndex, int[] argumentsToRemove)
28+
=> RenameMethodToSubjectShouldGenericAssertion(invocation, invocation.TargetMethod.TypeArguments, context, newName, argumentIndex, argumentsToRemove);
29+
public static CreateChangedDocument RenameMethodToSubjectShouldGenericAssertion(IInvocationOperation invocation, ImmutableArray<ITypeSymbol> genericTypes, CodeFixContext context, string newName, int argumentIndex, int[] argumentsToRemove)
30+
=> async ctx =>
31+
{
32+
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;
33+
34+
return await RewriteExpression(invocationExpression, [
35+
..Array.ConvertAll(argumentsToRemove, arg => new RemoveNodeAction(invocationExpression.ArgumentList.Arguments[arg])),
36+
new SubjectShouldGenericAssertionAction(argumentIndex, newName, genericTypes)
37+
], context, ctx);
38+
};
39+
40+
41+
private static async Task<Document> RewriteExpression(InvocationExpressionSyntax invocationExpression, IEditAction[] actions, CodeFixContext context, CancellationToken cancellationToken)
42+
{
43+
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken);
44+
45+
foreach (var action in actions)
46+
{
47+
action.Apply(editor, invocationExpression);
48+
}
49+
50+
return editor.GetChangedDocument();
51+
}
52+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Microsoft.CodeAnalysis.CSharp;
2+
using Microsoft.CodeAnalysis.CSharp.Syntax;
3+
using Microsoft.CodeAnalysis.Editing;
4+
5+
namespace FluentAssertions.Analyzers;
6+
7+
public interface IEditAction
8+
{
9+
void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression);
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp.Syntax;
3+
using Microsoft.CodeAnalysis.Editing;
4+
5+
namespace FluentAssertions.Analyzers;
6+
7+
public class RemoveNodeAction(SyntaxNode node) : IEditAction
8+
{
9+
public void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression) => editor.RemoveNode(node);
10+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp.Syntax;
3+
using Microsoft.CodeAnalysis.Editing;
4+
5+
namespace FluentAssertions.Analyzers;
6+
7+
public class SubjectShouldAssertionAction : IEditAction
8+
{
9+
private readonly int _argumentIndex;
10+
protected readonly string _assertion;
11+
12+
public SubjectShouldAssertionAction(int argumentIndex, string assertion)
13+
{
14+
_argumentIndex = argumentIndex;
15+
_assertion = assertion;
16+
}
17+
18+
public void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression)
19+
{
20+
var generator = editor.Generator;
21+
var subject = invocationExpression.ArgumentList.Arguments[_argumentIndex];
22+
var should = generator.InvocationExpression(generator.MemberAccessExpression(subject.Expression, "Should"));
23+
editor.RemoveNode(subject);
24+
editor.ReplaceNode(invocationExpression.Expression, generator.MemberAccessExpression(should, GenerateAssertion(generator)).WithTriviaFrom(invocationExpression.Expression));
25+
}
26+
27+
protected virtual SyntaxNode GenerateAssertion(SyntaxGenerator generator) => generator.IdentifierName(_assertion);
28+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Editing;
4+
5+
namespace FluentAssertions.Analyzers;
6+
7+
public class SubjectShouldGenericAssertionAction : SubjectShouldAssertionAction
8+
{
9+
private readonly ImmutableArray<ITypeSymbol> _types;
10+
11+
public SubjectShouldGenericAssertionAction(int argumentIndex, string assertion, ImmutableArray<ITypeSymbol> types) : base(argumentIndex, assertion)
12+
{
13+
_types = types;
14+
}
15+
16+
protected override SyntaxNode GenerateAssertion(SyntaxGenerator generator) => generator.GenericName(_assertion, _types);
17+
}

0 commit comments

Comments
 (0)