Skip to content

Commit 9715fc0

Browse files
committed
Fix SA1119 handling of 'with' expressions
Fixes #3239
1 parent 363a36c commit 9715fc0

File tree

3 files changed

+153
-6
lines changed

3 files changed

+153
-6
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp9/MaintainabilityRules/SA1119CSharp9UnitTests.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,153 @@
33

44
namespace StyleCop.Analyzers.Test.CSharp9.MaintainabilityRules
55
{
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.Testing;
610
using StyleCop.Analyzers.Test.CSharp8.MaintainabilityRules;
11+
using Xunit;
12+
using static StyleCop.Analyzers.Test.Verifiers.StyleCopCodeFixVerifier<
13+
StyleCop.Analyzers.MaintainabilityRules.SA1119StatementMustNotUseUnnecessaryParenthesis,
14+
StyleCop.Analyzers.MaintainabilityRules.SA1119CodeFixProvider>;
715

816
public class SA1119CSharp9UnitTests : SA1119CSharp8UnitTests
917
{
18+
/// <summary>
19+
/// Verifies that a type cast followed by a <c>with</c> expression is handled correctly.
20+
/// </summary>
21+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
22+
[Fact]
23+
[WorkItem(3239, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3239")]
24+
public async Task TestTypeCastFollowedByWithExpressionIsHandledCorrectlyAsync()
25+
{
26+
const string testCode = @"
27+
record Foo(int Value)
28+
{
29+
public object TestMethod(Foo n, int a)
30+
{
31+
return (object)(n with { Value = a });
32+
}
33+
}
34+
";
35+
36+
await new CSharpTest(LanguageVersion.CSharp9)
37+
{
38+
ReferenceAssemblies = ReferenceAssemblies.Net.Net50,
39+
TestCode = testCode,
40+
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
41+
}
42+
43+
/// <summary>
44+
/// Verifies that a type cast followed by a <c>with</c> expression with unnecessary parentheses is handled
45+
/// correctly.
46+
/// </summary>
47+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
48+
[Fact]
49+
[WorkItem(3239, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3239")]
50+
public async Task TestTypeCastFollowedByWithExpressionWithUnnecessaryParenthesesIsHandledCorrectlyAsync()
51+
{
52+
const string testCode = @"
53+
record Foo(int Value)
54+
{
55+
public object TestMethod(Foo n, int a)
56+
{
57+
return (object){|#0:{|#1:(|}(n with { Value = a }){|#2:)|}|};
58+
}
59+
}
60+
";
61+
62+
const string fixedCode = @"
63+
record Foo(int Value)
64+
{
65+
public object TestMethod(Foo n, int a)
66+
{
67+
return (object)(n with { Value = a });
68+
}
69+
}
70+
";
71+
72+
await new CSharpTest(LanguageVersion.CSharp9)
73+
{
74+
ReferenceAssemblies = ReferenceAssemblies.Net.Net50,
75+
TestCode = testCode,
76+
ExpectedDiagnostics =
77+
{
78+
Diagnostic(DiagnosticId).WithLocation(0),
79+
Diagnostic(ParenthesesDiagnosticId).WithLocation(1),
80+
Diagnostic(ParenthesesDiagnosticId).WithLocation(2),
81+
},
82+
FixedCode = fixedCode,
83+
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
84+
}
85+
86+
/// <summary>
87+
/// Verifies that a <c>with</c> expression with unnecessary parentheses is handled correcly.
88+
/// </summary>
89+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
90+
[Fact]
91+
[WorkItem(3239, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3239")]
92+
public async Task TestWithExpressionWithUnnecessaryParenthesesAsync()
93+
{
94+
const string testCode = @"
95+
record Foo(int Value)
96+
{
97+
public void TestMethod(Foo n, int a)
98+
{
99+
var test = {|#0:{|#1:(|}n with { Value = a }{|#2:)|}|};
100+
}
101+
}
102+
";
103+
104+
const string fixedCode = @"
105+
record Foo(int Value)
106+
{
107+
public void TestMethod(Foo n, int a)
108+
{
109+
var test = n with { Value = a };
110+
}
111+
}
112+
";
113+
114+
await new CSharpTest(LanguageVersion.CSharp9)
115+
{
116+
ReferenceAssemblies = ReferenceAssemblies.Net.Net50,
117+
TestCode = testCode,
118+
ExpectedDiagnostics =
119+
{
120+
Diagnostic(DiagnosticId).WithLocation(0),
121+
Diagnostic(ParenthesesDiagnosticId).WithLocation(1),
122+
Diagnostic(ParenthesesDiagnosticId).WithLocation(2),
123+
},
124+
FixedCode = fixedCode,
125+
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
126+
}
127+
128+
[Theory]
129+
[InlineData(".ToString()")]
130+
[InlineData("?.ToString()")]
131+
[InlineData("[0]")]
132+
[InlineData("?[0]")]
133+
[WorkItem(3239, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3239")]
134+
public async Task TestWithExpressionFollowedByDereferenceAsync(string operation)
135+
{
136+
string testCode = $@"
137+
record Foo(int Value)
138+
{{
139+
public object this[int index] => null;
140+
141+
public object TestMethod(Foo n, int a)
142+
{{
143+
return (n with {{ Value = a }}){operation};
144+
}}
145+
}}
146+
";
147+
148+
await new CSharpTest(LanguageVersion.CSharp9)
149+
{
150+
ReferenceAssemblies = ReferenceAssemblies.Net.Net50,
151+
TestCode = testCode,
152+
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
153+
}
10154
}
11155
}

StyleCop.Analyzers/StyleCop.Analyzers/Lightup/SyntaxKindEx.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ internal static class SyntaxKindEx
5353
public const SyntaxKind NullableDirectiveTrivia = (SyntaxKind)9055;
5454
public const SyntaxKind FunctionPointerType = (SyntaxKind)9056;
5555
public const SyntaxKind FunctionPointerParameter = (SyntaxKind)9057;
56+
public const SyntaxKind WithExpression = (SyntaxKind)9061;
5657
public const SyntaxKind WithInitializerExpression = (SyntaxKind)9062;
5758
public const SyntaxKind RecordDeclaration = (SyntaxKind)9063;
5859
public const SyntaxKind FunctionPointerUnmanagedCallingConventionList = (SyntaxKind)9066;

StyleCop.Analyzers/StyleCop.Analyzers/MaintainabilityRules/SA1119StatementMustNotUseUnnecessaryParenthesis.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,12 @@ private static void HandleParenthesizedExpression(SyntaxNodeAnalysisContext cont
127127
return;
128128
}
129129

130-
if (IsSwitchExpressionPrecededByTypeCast(node))
130+
if (IsSwitchOrWithExpressionPrecededByTypeCast(node))
131131
{
132132
return;
133133
}
134134

135-
if (IsSwitchExpressionExpressionOfMemberAccess(node))
135+
if (IsSwitchOrWithExpressionExpressionOfMemberAccess(node))
136136
{
137137
return;
138138
}
@@ -216,9 +216,10 @@ private static bool IsConditionalAccessInInterpolation(ExpressionSyntax node)
216216
return false;
217217
}
218218

219-
private static bool IsSwitchExpressionPrecededByTypeCast(ParenthesizedExpressionSyntax node)
219+
private static bool IsSwitchOrWithExpressionPrecededByTypeCast(ParenthesizedExpressionSyntax node)
220220
{
221-
if (!node.Expression.IsKind(SyntaxKindEx.SwitchExpression))
221+
if (!node.Expression.IsKind(SyntaxKindEx.SwitchExpression)
222+
&& !node.Expression.IsKind(SyntaxKindEx.WithExpression))
222223
{
223224
return false;
224225
}
@@ -233,9 +234,10 @@ private static bool IsSwitchExpressionPrecededByTypeCast(ParenthesizedExpression
233234
return previousToken.IsKind(SyntaxKind.CloseParenToken) && previousToken.Parent.IsKind(SyntaxKind.CastExpression);
234235
}
235236

236-
private static bool IsSwitchExpressionExpressionOfMemberAccess(ParenthesizedExpressionSyntax node)
237+
private static bool IsSwitchOrWithExpressionExpressionOfMemberAccess(ParenthesizedExpressionSyntax node)
237238
{
238-
if (!node.Expression.IsKind(SyntaxKindEx.SwitchExpression))
239+
if (!node.Expression.IsKind(SyntaxKindEx.SwitchExpression)
240+
&& !node.Expression.IsKind(SyntaxKindEx.WithExpression))
239241
{
240242
return false;
241243
}

0 commit comments

Comments
 (0)