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
+ }
0 commit comments