Skip to content

Commit 198ed02

Browse files
authored
Features/exceptions inner (#61)
* cleanup * more cleanup * t * wip * support for innerException
1 parent f65050b commit 198ed02

File tree

7 files changed

+196
-14
lines changed

7 files changed

+196
-14
lines changed

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

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using Microsoft.CodeAnalysis;
22
using Microsoft.VisualStudio.TestTools.UnitTesting;
33

4-
namespace FluentAssertions.Analyzers.Tests.Tips
4+
namespace FluentAssertions.Analyzers.Tests
55
{
66
[TestClass]
77
public class ExceptionsTests
88
{
9-
[AssertionDataTestMethod]
9+
[AssertionDataTestMethod]
1010
[AssertionDiagnostic("action.Should().Throw<Exception>().Which.Message.Should().Contain(expectedMessage{0});")]
1111
[AssertionDiagnostic("action.Should().Throw<Exception>().And.Message.Should().Contain(expectedMessage{0});")]
1212
[AssertionDiagnostic("action.Should().Throw<Exception>().Which.Message.Should().Contain(\"a constant string\"{0});")]
@@ -38,7 +38,7 @@ public class ExceptionsTests
3838
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"*a constant string*\"{0});")]
3939
[AssertionCodeFix(
4040
oldAssertion: "action.Should().Throw<Exception>().And.Message.Should().Contain(\"a constant string\"{0});",
41-
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"*a constant string*\"{0});")]
41+
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"*a constant string*\"{0});")]
4242
[AssertionCodeFix(
4343
oldAssertion: "action.Should().Throw<Exception>().Which.Message.Should().Be(expectedMessage{0});",
4444
newAssertion: "action.Should().Throw<Exception>().WithMessage(expectedMessage{0});")]
@@ -62,7 +62,7 @@ public class ExceptionsTests
6262
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"a constant string*\"{0});")]
6363
[AssertionCodeFix(
6464
oldAssertion: "action.Should().Throw<Exception>().And.Message.Should().StartWith(\"a constant string\"{0});",
65-
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"a constant string*\"{0});")]
65+
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"a constant string*\"{0});")]
6666
[AssertionCodeFix(
6767
oldAssertion: "action.Should().Throw<Exception>().Which.Message.Should().EndWith(expectedMessage{0});",
6868
newAssertion: "action.Should().Throw<Exception>().WithMessage($\"*{{expectedMessage}}\"{0});")]
@@ -74,7 +74,7 @@ public class ExceptionsTests
7474
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"*a constant string\"{0});")]
7575
[AssertionCodeFix(
7676
oldAssertion: "action.Should().Throw<Exception>().And.Message.Should().EndWith(\"a constant string\"{0});",
77-
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"*a constant string\"{0});")]
77+
newAssertion: "action.Should().Throw<Exception>().WithMessage(\"*a constant string\"{0});")]
7878
[Implemented]
7979
public void ExceptionShouldThrowWithMessage_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix<ExceptionShouldThrowWithMessageCodeFix, ExceptionShouldThrowWithMessageAnalyzer>(oldAssertion, newAssertion);
8080

@@ -134,7 +134,7 @@ public class ExceptionsTests
134134
newAssertion: "action.Should().ThrowExactly<Exception>().WithMessage(\"a constant string*\"{0});")]
135135
[AssertionCodeFix(
136136
oldAssertion: "action.Should().ThrowExactly<Exception>().And.Message.Should().StartWith(\"a constant string\"{0});",
137-
newAssertion: "action.Should().ThrowExactly<Exception>().WithMessage(\"a constant string*\"{0});")]
137+
newAssertion: "action.Should().ThrowExactly<Exception>().WithMessage(\"a constant string*\"{0});")]
138138
[AssertionCodeFix(
139139
oldAssertion: "action.Should().ThrowExactly<Exception>().Which.Message.Should().EndWith(expectedMessage{0});",
140140
newAssertion: "action.Should().ThrowExactly<Exception>().WithMessage($\"*{{expectedMessage}}\"{0});")]
@@ -146,14 +146,62 @@ public class ExceptionsTests
146146
newAssertion: "action.Should().ThrowExactly<Exception>().WithMessage(\"*a constant string\"{0});")]
147147
[AssertionCodeFix(
148148
oldAssertion: "action.Should().ThrowExactly<Exception>().And.Message.Should().EndWith(\"a constant string\"{0});",
149-
newAssertion: "action.Should().ThrowExactly<Exception>().WithMessage(\"*a constant string\"{0});")]
149+
newAssertion: "action.Should().ThrowExactly<Exception>().WithMessage(\"*a constant string\"{0});")]
150150
[Implemented]
151151
public void ExceptionShouldThrowExactlyWithMessage_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix<ExceptionShouldThrowWithMessageCodeFix, ExceptionShouldThrowWithMessageAnalyzer>(oldAssertion, newAssertion);
152152

153-
private void VerifyCSharpDiagnostic<TDiagnosticAnalyzer>(string sourceAssersion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
153+
[AssertionDataTestMethod]
154+
[AssertionDiagnostic("action.Should().Throw<Exception>().And.InnerException.Should().BeOfType<ArgumentException>({0});")]
155+
[AssertionDiagnostic("action.Should().Throw<Exception>().Which.InnerException.Should().BeOfType<ArgumentException>({0});")]
156+
[AssertionDiagnostic("action.Should().ThrowExactly<Exception>().And.InnerException.Should().BeOfType<ArgumentException>({0});")]
157+
[AssertionDiagnostic("action.Should().ThrowExactly<Exception>().Which.InnerException.Should().BeOfType<ArgumentException>({0});")]
158+
[Implemented]
159+
public void ExceptionShouldThrowWithInnerExceptionExactly_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic<ExceptionShouldThrowWithInnerExceptionAnalyzer>(assertion);
160+
161+
[AssertionDataTestMethod]
162+
[AssertionCodeFix(
163+
oldAssertion: "action.Should().Throw<Exception>().And.InnerException.Should().BeOfType<ArgumentException>({0});",
164+
newAssertion: "action.Should().Throw<Exception>().WithInnerExceptionExactly<ArgumentException>({0});")]
165+
[AssertionCodeFix(
166+
oldAssertion: "action.Should().Throw<Exception>().Which.InnerException.Should().BeOfType<ArgumentException>({0});",
167+
newAssertion: "action.Should().Throw<Exception>().WithInnerExceptionExactly<ArgumentException>({0});")]
168+
[AssertionCodeFix(
169+
oldAssertion: "action.Should().ThrowExactly<Exception>().And.InnerException.Should().BeOfType<ArgumentException>({0});",
170+
newAssertion: "action.Should().ThrowExactly<Exception>().WithInnerExceptionExactly<ArgumentException>({0});")]
171+
[AssertionCodeFix(
172+
oldAssertion: "action.Should().ThrowExactly<Exception>().Which.InnerException.Should().BeOfType<ArgumentException>({0});",
173+
newAssertion: "action.Should().ThrowExactly<Exception>().WithInnerExceptionExactly<ArgumentException>({0});")]
174+
[Implemented]
175+
public void ExceptionShouldThrowWithInnerExceptionExactly_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix<ExceptionShouldThrowWithInnerExceptionCodeFix, ExceptionShouldThrowWithInnerExceptionAnalyzer>(oldAssertion, newAssertion);
176+
177+
[AssertionDataTestMethod]
178+
[AssertionDiagnostic("action.Should().ThrowExactly<Exception>().And.InnerException.Should().BeAssignableTo<ArgumentException>({0});")]
179+
[AssertionDiagnostic("action.Should().ThrowExactly<Exception>().Which.InnerException.Should().BeAssignableTo<ArgumentException>({0});")]
180+
[AssertionDiagnostic("action.Should().Throw<Exception>().And.InnerException.Should().BeAssignableTo<ArgumentException>({0});")]
181+
[AssertionDiagnostic("action.Should().Throw<Exception>().Which.InnerException.Should().BeAssignableTo<ArgumentException>({0});")]
182+
[Implemented]
183+
public void ExceptionShouldThrowWithInnerException_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic<ExceptionShouldThrowWithInnerExceptionAnalyzer>(assertion);
184+
185+
[AssertionDataTestMethod]
186+
[AssertionCodeFix(
187+
oldAssertion: "action.Should().Throw<Exception>().And.InnerException.Should().BeAssignableTo<ArgumentException>({0});",
188+
newAssertion: "action.Should().Throw<Exception>().WithInnerException<ArgumentException>({0});")]
189+
[AssertionCodeFix(
190+
oldAssertion: "action.Should().Throw<Exception>().Which.InnerException.Should().BeAssignableTo<ArgumentException>({0});",
191+
newAssertion: "action.Should().Throw<Exception>().WithInnerException<ArgumentException>({0});")]
192+
[AssertionCodeFix(
193+
oldAssertion: "action.Should().ThrowExactly<Exception>().And.InnerException.Should().BeAssignableTo<ArgumentException>({0});",
194+
newAssertion: "action.Should().ThrowExactly<Exception>().WithInnerException<ArgumentException>({0});")]
195+
[AssertionCodeFix(
196+
oldAssertion: "action.Should().ThrowExactly<Exception>().Which.InnerException.Should().BeAssignableTo<ArgumentException>({0});",
197+
newAssertion: "action.Should().ThrowExactly<Exception>().WithInnerException<ArgumentException>({0});")]
198+
[Implemented]
199+
public void ExceptionShouldThrowWithInnerException_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix<ExceptionShouldThrowWithInnerExceptionCodeFix, ExceptionShouldThrowWithInnerExceptionAnalyzer>(oldAssertion, newAssertion);
200+
201+
private void VerifyCSharpDiagnostic<TDiagnosticAnalyzer>(string sourceAssertion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
154202
{
155-
System.Console.WriteLine(sourceAssersion);
156-
var source = GenerateCode.ExceptionAssertion(sourceAssersion);
203+
System.Console.WriteLine(sourceAssertion);
204+
var source = GenerateCode.ExceptionAssertion(sourceAssertion);
157205

158206
var type = typeof(TDiagnosticAnalyzer);
159207
var diagnosticId = (string)type.GetField("DiagnosticId").GetValue(null);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using Microsoft.VisualStudio.TestTools.UnitTesting;
22
using System.Text;
33

4-
namespace FluentAssertions.Analyzers.Tests.Tips
4+
namespace FluentAssertions.Analyzers.Tests
55
{
66
[TestClass]
77
public class NullConditionalAssertionTests

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using Microsoft.CodeAnalysis;
22
using Microsoft.VisualStudio.TestTools.UnitTesting;
33

4-
namespace FluentAssertions.Analyzers.Tests.Tips
4+
namespace FluentAssertions.Analyzers.Tests
55
{
66
[TestClass]
77
public class NumericTests

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Microsoft.VisualStudio.TestTools.UnitTesting;
22

3-
namespace FluentAssertions.Analyzers.Tests.Tips
3+
namespace FluentAssertions.Analyzers.Tests
44
{
55
[TestClass]
66
public class SanityTests

src/FluentAssertions.Analyzers/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public static class Numeric
8080
public static class Exceptions
8181
{
8282
public const string ExceptionShouldThrowWithMessage = nameof(ExceptionShouldThrowWithMessage);
83+
public const string ExceptionShouldThrowWithInnerException = nameof(ExceptionShouldThrowWithInnerException);
8384
}
8485
}
8586

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System.Collections.Generic;
2+
using System.Collections.Immutable;
3+
using System.Composition;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
9+
10+
namespace FluentAssertions.Analyzers
11+
{
12+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
13+
public class ExceptionShouldThrowWithInnerExceptionAnalyzer : ExceptionAnalyzer
14+
{
15+
public const string DiagnosticId = Constants.Tips.Exceptions.ExceptionShouldThrowWithInnerException;
16+
public const string Category = Constants.Tips.Category;
17+
18+
public const string Message = "Use .WithInnerException() instead.";
19+
20+
protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
21+
22+
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
23+
{
24+
get
25+
{
26+
yield return new ShouldThrowWhichInnerExceptionShouldBeOfType();
27+
yield return new ShouldThrowAndInnerExceptionShouldBeOfType();
28+
yield return new ShouldThrowExactlyAndInnerExceptionShouldBeOfType();
29+
yield return new ShouldThrowExactlyWhichInnerExceptionShouldBeOfType();
30+
yield return new ShouldThrowAndInnerExceptionShouldBeAssignableTo();
31+
yield return new ShouldThrowWhichInnerExceptionShouldBeAssignableTo();
32+
yield return new ShouldThrowExactlyAndInnerExceptionShouldBeAssignableTo();
33+
yield return new ShouldThrowExactlyWhichInnerExceptionShouldBeAssignableTo();
34+
}
35+
}
36+
37+
public class ShouldThrowExactlyWhichInnerExceptionShouldBeOfType : FluentAssertionsCSharpSyntaxVisitor
38+
{
39+
public ShouldThrowExactlyWhichInnerExceptionShouldBeOfType() : base(MemberValidator.Should, new MemberValidator("ThrowExactly"), MemberValidator.Which, new MemberValidator("InnerException"), MemberValidator.Should, new MemberValidator("BeOfType"))
40+
{
41+
}
42+
}
43+
public class ShouldThrowExactlyAndInnerExceptionShouldBeOfType : FluentAssertionsCSharpSyntaxVisitor
44+
{
45+
public ShouldThrowExactlyAndInnerExceptionShouldBeOfType() : base(MemberValidator.Should, new MemberValidator("ThrowExactly"), MemberValidator.And, new MemberValidator("InnerException"), MemberValidator.Should, new MemberValidator("BeOfType"))
46+
{
47+
}
48+
}
49+
public class ShouldThrowWhichInnerExceptionShouldBeOfType : FluentAssertionsCSharpSyntaxVisitor
50+
{
51+
public ShouldThrowWhichInnerExceptionShouldBeOfType() : base(MemberValidator.Should, new MemberValidator("Throw"), MemberValidator.Which, new MemberValidator("InnerException"), MemberValidator.Should, new MemberValidator("BeOfType"))
52+
{
53+
}
54+
}
55+
public class ShouldThrowAndInnerExceptionShouldBeOfType : FluentAssertionsCSharpSyntaxVisitor
56+
{
57+
public ShouldThrowAndInnerExceptionShouldBeOfType() : base(MemberValidator.Should, new MemberValidator("Throw"), MemberValidator.And, new MemberValidator("InnerException"), MemberValidator.Should, new MemberValidator("BeOfType"))
58+
{
59+
}
60+
}
61+
62+
public class ShouldThrowExactlyAndInnerExceptionShouldBeAssignableTo : FluentAssertionsCSharpSyntaxVisitor
63+
{
64+
public ShouldThrowExactlyAndInnerExceptionShouldBeAssignableTo() : base(MemberValidator.Should, new MemberValidator("ThrowExactly"), MemberValidator.And, new MemberValidator("InnerException"), MemberValidator.Should, new MemberValidator("BeAssignableTo"))
65+
{
66+
}
67+
}
68+
public class ShouldThrowExactlyWhichInnerExceptionShouldBeAssignableTo : FluentAssertionsCSharpSyntaxVisitor
69+
{
70+
public ShouldThrowExactlyWhichInnerExceptionShouldBeAssignableTo() : base(MemberValidator.Should, new MemberValidator("ThrowExactly"), MemberValidator.Which, new MemberValidator("InnerException"), MemberValidator.Should, new MemberValidator("BeAssignableTo"))
71+
{
72+
}
73+
}
74+
public class ShouldThrowAndInnerExceptionShouldBeAssignableTo : FluentAssertionsCSharpSyntaxVisitor
75+
{
76+
public ShouldThrowAndInnerExceptionShouldBeAssignableTo() : base(MemberValidator.Should, new MemberValidator("Throw"), MemberValidator.And, new MemberValidator("InnerException"), MemberValidator.Should, new MemberValidator("BeAssignableTo"))
77+
{
78+
}
79+
}
80+
public class ShouldThrowWhichInnerExceptionShouldBeAssignableTo : FluentAssertionsCSharpSyntaxVisitor
81+
{
82+
public ShouldThrowWhichInnerExceptionShouldBeAssignableTo() : base(MemberValidator.Should, new MemberValidator("Throw"), MemberValidator.Which, new MemberValidator("InnerException"), MemberValidator.Should, new MemberValidator("BeAssignableTo"))
83+
{
84+
}
85+
}
86+
}
87+
88+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ExceptionShouldThrowWithInnerExceptionCodeFix)), Shared]
89+
public class ExceptionShouldThrowWithInnerExceptionCodeFix : FluentAssertionsCodeFixProvider
90+
{
91+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(ExceptionShouldThrowWithInnerExceptionAnalyzer.DiagnosticId);
92+
93+
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
94+
{
95+
switch (properties.VisitorName)
96+
{
97+
case nameof(ExceptionShouldThrowWithInnerExceptionAnalyzer.ShouldThrowWhichInnerExceptionShouldBeOfType):
98+
case nameof(ExceptionShouldThrowWithInnerExceptionAnalyzer.ShouldThrowExactlyWhichInnerExceptionShouldBeOfType):
99+
return ReplaceBeOfTypeInnerException(expression, "Which");
100+
case nameof(ExceptionShouldThrowWithInnerExceptionAnalyzer.ShouldThrowAndInnerExceptionShouldBeOfType):
101+
case nameof(ExceptionShouldThrowWithInnerExceptionAnalyzer.ShouldThrowExactlyAndInnerExceptionShouldBeOfType):
102+
return ReplaceBeOfTypeInnerException(expression, "And");
103+
case nameof(ExceptionShouldThrowWithInnerExceptionAnalyzer.ShouldThrowWhichInnerExceptionShouldBeAssignableTo):
104+
case nameof(ExceptionShouldThrowWithInnerExceptionAnalyzer.ShouldThrowExactlyWhichInnerExceptionShouldBeAssignableTo):
105+
return ReplaceBeAssignableToInnerException(expression, "Which");
106+
case nameof(ExceptionShouldThrowWithInnerExceptionAnalyzer.ShouldThrowAndInnerExceptionShouldBeAssignableTo):
107+
case nameof(ExceptionShouldThrowWithInnerExceptionAnalyzer.ShouldThrowExactlyAndInnerExceptionShouldBeAssignableTo):
108+
return ReplaceBeAssignableToInnerException(expression, "And");
109+
default:
110+
throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}");
111+
}
112+
}
113+
114+
private ExpressionSyntax ReplaceBeOfTypeInnerException(ExpressionSyntax expression, string whichOrAnd)
115+
=> ReplaceWithInnerException(expression, whichOrAnd, renameFrom: "BeOfType", renameTo: "WithInnerExceptionExactly");
116+
117+
private ExpressionSyntax ReplaceBeAssignableToInnerException(ExpressionSyntax expression, string whichOrAnd)
118+
=> ReplaceWithInnerException(expression, whichOrAnd, renameFrom: "BeAssignableTo", renameTo: "WithInnerException");
119+
120+
private ExpressionSyntax ReplaceWithInnerException(ExpressionSyntax expression, string whichOrAnd, string renameFrom, string renameTo)
121+
{
122+
var replacements = new[]
123+
{
124+
NodeReplacement.Remove(whichOrAnd),
125+
NodeReplacement.Remove("InnerException"),
126+
NodeReplacement.RemoveOccurrence("Should", occurrence: 2)
127+
};
128+
var newExpression = GetNewExpression(expression, replacements);
129+
var rename = NodeReplacement.RenameAndExtractArguments(renameFrom, renameTo);
130+
return GetNewExpression(newExpression, rename);
131+
}
132+
}
133+
}

src/FluentAssertions.Analyzers/Utilities/NodeReplacements/RenameNodeReplacement.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public sealed override SyntaxNode ComputeNew(LinkedListNode<MemberAccessExpressi
2727
var current = listNode.Value;
2828
var parent = (InvocationExpressionSyntax)current.Parent;
2929

30-
var newNode = parent.WithExpression(current.WithName(SyntaxFactory.IdentifierName(_newName)));
30+
var newNode = parent.WithExpression(current.WithName(current.Name.WithIdentifier(SyntaxFactory.ParseToken(_newName))));
3131
return ComputeNew(newNode);
3232
}
3333
}

0 commit comments

Comments
 (0)