Skip to content

Commit e21cc35

Browse files
C#: Auto-parenthesize IsPattern with or/and combinators in binary expressions (#7196)
When CSharpTemplate substitutes an IsPattern containing an `or` or `and` pattern combinator into a Binary(&&) expression, the result needs parentheses to preserve operator precedence. Without them, C# parses `x is A or B && y` as `x is (A or (B && y))` instead of `(x is A or B) && y`. - Make GetPrecedence context-aware for IsPattern: `or` combinator lowers effective precedence to 1 (same as ||), `and` to 2 (same as &&) - Fix NeedsParenthesesInContext to not wrap CsBinary Or/And pattern combinators inside IsPattern — they are legitimate patterns, not boolean expressions - Add ParenthesizeDeep public entry point for recursive parenthesization - Clarify CSharpTemplate.Apply comment: inner precedence is handled by ApplySubstitutions, MaybeParenthesize only checks the graft site
1 parent c27eebc commit e21cc35

5 files changed

Lines changed: 101 additions & 2 deletions

File tree

rewrite-csharp/csharp/OpenRewrite/CSharp/CSharpParenthesizeVisitor.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ or TypeCast or CsBinary or CsUnary or IsPattern or RangeExpression
5353

5454
return newTree;
5555
}
56+
57+
/// <summary>
58+
/// Recursively walks <paramref name="tree"/> and adds parentheses wherever operator
59+
/// precedence requires them within the tree itself (not considering the graft site).
60+
/// Call this on template results before <see cref="MaybeParenthesize"/>.
61+
/// </summary>
62+
public static J ParenthesizeDeep(J tree)
63+
{
64+
return new CSharpParenthesizeVisitor<int>(true).Visit(tree, 0)!;
65+
}
5666
}
5767

5868
/// <summary>
@@ -239,6 +249,11 @@ private bool NeedsParenthesesInContext(Expression expr)
239249

240250
if (parent is IsPattern)
241251
{
252+
// CsBinary with Or/And inside IsPattern is a pattern combinator, not a
253+
// boolean expression — it must NOT be wrapped in parentheses.
254+
if (expr is CsBinary csb && csb.Operator.Element is CsBinary.OperatorType.Or or CsBinary.OperatorType.And)
255+
return false;
256+
242257
return expr is Binary or CsBinary or Ternary or Assignment or AssignmentOperation or Lambda;
243258
}
244259

rewrite-csharp/csharp/OpenRewrite/CSharp/CSharpPrecedences.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal static class CSharpPrecedences
4848
Unary => 13,
4949
CsUnary => 13,
5050
TypeCast => 13,
51-
IsPattern => 7,
51+
IsPattern ip => GetIsPatternPrecedence(ip),
5252
RangeExpression => 12,
5353
SwitchExpression => 11,
5454
WithExpression => 11,
@@ -80,6 +80,25 @@ public static Parentheses<Expression> Parenthesize(Expression expr)
8080
new JRightPadded<Expression>(J.SetPrefix(expr, Space.Empty), Space.Empty, Markers.Empty));
8181
}
8282

83+
/// <summary>
84+
/// IsPattern with pattern combinators (or/and) has lower effective precedence
85+
/// than a plain is-type check, because the combinator keywords extend the pattern
86+
/// in a way that can be ambiguous when nested inside binary expressions like &amp;&amp;.
87+
/// </summary>
88+
private static int GetIsPatternPrecedence(IsPattern ip)
89+
{
90+
if (ip.Pattern.Element is CsBinary csb)
91+
{
92+
return csb.Operator.Element switch
93+
{
94+
CsBinary.OperatorType.Or => 1, // same as ||
95+
CsBinary.OperatorType.And => 2, // same as &&
96+
_ => 7
97+
};
98+
}
99+
return 7;
100+
}
101+
83102
public static bool IsAssociative(Expression expr) => expr switch
84103
{
85104
Binary b => b.Operator.Element is

rewrite-csharp/csharp/OpenRewrite/CSharp/Template/CSharpTemplate.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@ public J GetTree()
204204
tree = TemplateEngine.ApplySubstitutions(tree, values);
205205
}
206206

207-
// Phase 1.5: auto-parenthesization after substitution
207+
// Phase 1.5: check if the result needs wrapping when grafted into the cursor position.
208+
// Inner precedence is already handled by ApplySubstitutions (which runs a recursive
209+
// CSharpParenthesizeVisitor after substitution); this only checks the root vs. cursor parent.
208210
if (tree is Expression expr && cursorJ != null)
209211
{
210212
tree = CSharpParenthesizeVisitor.MaybeParenthesize(expr, cursor);

rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/CSharpParenthesizeVisitorTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,60 @@ public void MaybeParenthesize_TernaryInsideTernary_AddsParens()
156156

157157
Assert.IsType<Parentheses<Expression>>(result);
158158
}
159+
160+
[Fact]
161+
public void MaybeParenthesize_IsPatternWithOrCombinatorInsideLogicalAnd_AddsParens()
162+
{
163+
// placeholder && flag => (m is A or B) && flag — or combinator needs parens inside &&
164+
var placeholder = MakeId("placeholder");
165+
var parent = MakeBinary(Binary.OperatorType.And, placeholder, MakeId("flag"));
166+
var cursor = new Cursor(new Cursor(null, "root"), parent);
167+
cursor = new Cursor(cursor, placeholder);
168+
169+
var orPattern = MakeCsBinary(CsBinary.OperatorType.Or, MakeId("A"), MakeId("B"));
170+
var newExpr = MakeIsPattern(MakeId("m"), orPattern);
171+
var result = CSharpParenthesizeVisitor.MaybeParenthesize(newExpr, cursor);
172+
173+
Assert.IsType<Parentheses<Expression>>(result);
174+
}
175+
176+
[Fact]
177+
public void MaybeParenthesize_IsPatternWithoutCombinatorInsideLogicalAnd_NoParens()
178+
{
179+
// placeholder && flag => m is A && flag — simple is pattern doesn't need parens inside &&
180+
var placeholder = MakeId("placeholder");
181+
var parent = MakeBinary(Binary.OperatorType.And, placeholder, MakeId("flag"));
182+
var cursor = new Cursor(new Cursor(null, "root"), parent);
183+
cursor = new Cursor(cursor, placeholder);
184+
185+
var newExpr = MakeIsPattern(MakeId("m"), MakeConstantPattern(MakeId("A")));
186+
var result = CSharpParenthesizeVisitor.MaybeParenthesize(newExpr, cursor);
187+
188+
Assert.IsNotType<Parentheses<Expression>>(result);
189+
}
190+
191+
[Fact]
192+
public void ParenthesizeDeep_IsPatternWithOrInsideBinaryAnd_AddsParens()
193+
{
194+
// (m is A or B) && flag — recursive walk should parenthesize inner IsPattern
195+
var orPattern = MakeCsBinary(CsBinary.OperatorType.Or, MakeId("A"), MakeId("B"));
196+
var isExpr = MakeIsPattern(MakeId("m"), orPattern);
197+
var tree = MakeBinary(Binary.OperatorType.And, isExpr, MakeId("flag"));
198+
199+
var result = (Binary)CSharpParenthesizeVisitor.ParenthesizeDeep(tree);
200+
201+
Assert.IsType<Parentheses<Expression>>(result.Left);
202+
}
203+
204+
[Fact]
205+
public void ParenthesizeDeep_SimpleIsPatternInsideBinaryAnd_NoParens()
206+
{
207+
// m is A && flag — no inner parens needed
208+
var isExpr = MakeIsPattern(MakeId("m"), MakeConstantPattern(MakeId("A")));
209+
var tree = MakeBinary(Binary.OperatorType.And, isExpr, MakeId("flag"));
210+
211+
var result = (Binary)CSharpParenthesizeVisitor.ParenthesizeDeep(tree);
212+
213+
Assert.IsType<IsPattern>(result.Left);
214+
}
159215
}

rewrite-csharp/csharp/OpenRewrite/Tests/CSharp/TestHelpers.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,11 @@ public static Assignment MakeAssignment(Expression variable, Expression value) =
7474
public static Parentheses<Expression> MakeParens(Expression expr) =>
7575
new(Guid.NewGuid(), Space.Empty, Markers.Empty,
7676
new JRightPadded<Expression>(expr, Space.Empty, Markers.Empty));
77+
78+
public static IsPattern MakeIsPattern(Expression expression, Pattern pattern) =>
79+
new(Guid.NewGuid(), Space.Empty, Markers.Empty, expression,
80+
new JLeftPadded<Pattern>(Space.Empty, pattern));
81+
82+
public static ConstantPattern MakeConstantPattern(Expression value) =>
83+
new(Guid.NewGuid(), Space.Empty, Markers.Empty, value);
7784
}

0 commit comments

Comments
 (0)