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