Skip to content

Commit 8c6e642

Browse files
Fix #3382: Support compiler-generated throw-helper invocations in switch-expression implicit default-case.
1 parent 4a29de5 commit 8c6e642

File tree

5 files changed

+186
-6
lines changed

5 files changed

+186
-6
lines changed

ICSharpCode.Decompiler.Tests/TestCases/Pretty/SwitchExpressions.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,5 +159,59 @@ public static string Issue2222()
159159
_ => "default",
160160
};
161161
}
162+
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
163+
public static int Issue3382(StringComparison c)
164+
{
165+
return c switch {
166+
StringComparison.Ordinal => 0,
167+
StringComparison.OrdinalIgnoreCase => 1,
168+
};
169+
}
170+
171+
public static void Issue3382b(ref StringComparison? c)
172+
{
173+
StringComparison? stringComparison = c;
174+
c = stringComparison switch {
175+
null => StringComparison.Ordinal,
176+
StringComparison.Ordinal => StringComparison.OrdinalIgnoreCase,
177+
StringComparison.OrdinalIgnoreCase => StringComparison.InvariantCulture,
178+
};
179+
}
180+
181+
public static void Issue3382c(StringComparison? c)
182+
{
183+
c = c switch {
184+
null => StringComparison.Ordinal,
185+
StringComparison.Ordinal => StringComparison.OrdinalIgnoreCase,
186+
StringComparison.OrdinalIgnoreCase => StringComparison.InvariantCulture,
187+
};
188+
}
189+
190+
public static void Issue3382d(ref StringComparison c)
191+
{
192+
StringComparison stringComparison = c;
193+
c = stringComparison switch {
194+
StringComparison.Ordinal => StringComparison.OrdinalIgnoreCase,
195+
StringComparison.OrdinalIgnoreCase => StringComparison.InvariantCulture,
196+
};
197+
}
198+
199+
public static int SwitchOnStringImplicitDefault(string s)
200+
{
201+
return s switch {
202+
"Hello" => 42,
203+
"World" => 4711,
204+
"!" => 7,
205+
"Foo" => 13,
206+
"Bar" => 21,
207+
"Baz" => 84,
208+
"Qux" => 168,
209+
"Quux" => 336,
210+
"Corge" => 672,
211+
"Grault" => 1344,
212+
"Garply" => 2688,
213+
};
214+
}
215+
#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
162216
}
163217
}

ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4135,10 +4135,13 @@ protected internal override TranslatedExpression VisitSwitchInstruction(SwitchIn
41354135
switchExpr.SwitchSections.Add(ses);
41364136
}
41374137

4138-
var defaultSES = new SwitchExpressionSection();
4139-
defaultSES.Pattern = new IdentifierExpression("_");
4140-
defaultSES.Body = TranslateSectionBody(defaultSection);
4141-
switchExpr.SwitchSections.Add(defaultSES);
4138+
if (!defaultSection.IsCompilerGeneratedDefaultSection)
4139+
{
4140+
var defaultSES = new SwitchExpressionSection();
4141+
defaultSES.Pattern = new IdentifierExpression("_");
4142+
defaultSES.Body = TranslateSectionBody(defaultSection);
4143+
switchExpr.SwitchSections.Add(defaultSES);
4144+
}
41424145

41434146
return switchExpr.WithILInstruction(inst).WithRR(new ResolveResult(resultType));
41444147

ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1717
// DEALINGS IN THE SOFTWARE.
1818

19+
using System;
1920
using System.Collections.Generic;
2021
using System.Diagnostics;
22+
using System.Diagnostics.CodeAnalysis;
2123
using System.Linq;
2224

2325
using ICSharpCode.Decompiler.FlowAnalysis;
@@ -216,12 +218,118 @@ void ProcessBlock(Block block, ref bool blockContainerNeedsCleanup)
216218
// (1st pass was in ControlFlowSimplification)
217219
SimplifySwitchInstruction(block, context);
218220
}
221+
222+
InlineSwitchExpressionDefaultCaseThrowHelper(block, context);
223+
}
224+
225+
// call ThrowInvalidOperationException(...)
226+
// leave IL_0000 (ldloc temp)
227+
//
228+
// -or-
229+
//
230+
// call ThrowSwitchExpressionException(...)
231+
// leave IL_0000 (ldloc temp)
232+
//
233+
// -to-
234+
//
235+
// throw(newobj SwitchExpressionException(...))
236+
internal static void InlineSwitchExpressionDefaultCaseThrowHelper(Block block, ILTransformContext context)
237+
{
238+
#nullable enable
239+
// due to our of of basic blocks at this point,
240+
// switch instructions can only appear as last instruction
241+
var sw = block.Instructions.LastOrDefault() as SwitchInstruction;
242+
if (sw == null)
243+
return;
244+
245+
IMethod[] exceptionCtorTable = new IMethod[2];
246+
247+
exceptionCtorTable[0] = FindConstructor("System.InvalidOperationException")!;
248+
exceptionCtorTable[1] = FindConstructor("System.Runtime.CompilerServices.SwitchExpressionException", typeof(object))!;
249+
250+
if (exceptionCtorTable[0] == null && exceptionCtorTable[1] == null)
251+
return;
252+
253+
if (sw.GetDefaultSection() is not { Body: Branch { TargetBlock: Block defaultBlock } } defaultSection)
254+
return;
255+
if (defaultBlock is { Instructions: [var call, Branch or Leave] })
256+
{
257+
if (!MatchThrowHelperCall(call, out IMethod? exceptionCtor, out ILInstruction? value))
258+
return;
259+
context.Step("SwitchExpressionDefaultCaseTransform", block.Instructions[0]);
260+
var newObj = new NewObj(exceptionCtor);
261+
if (value != null)
262+
newObj.Arguments.Add(value);
263+
defaultBlock.Instructions[0] = new Throw(newObj).WithILRange(defaultBlock.Instructions[0]).WithILRange(defaultBlock.Instructions[1]);
264+
defaultBlock.Instructions.RemoveAt(1);
265+
defaultSection.IsCompilerGeneratedDefaultSection = true;
266+
}
267+
else if (defaultBlock is { Instructions: [Throw { Argument: NewObj { Method: var ctor, Arguments: [var arg] } newObj }] }
268+
&& ctor.Equals(exceptionCtorTable[1]))
269+
{
270+
defaultSection.IsCompilerGeneratedDefaultSection = true;
271+
}
272+
else
273+
{
274+
return;
275+
}
276+
277+
bool MatchThrowHelperCall(ILInstruction inst, [NotNullWhen(true)] out IMethod? exceptionCtor, out ILInstruction? value)
278+
{
279+
exceptionCtor = null;
280+
value = null;
281+
if (inst is not Call call)
282+
return false;
283+
if (call.Method.DeclaringType.FullName != "<PrivateImplementationDetails>")
284+
return false;
285+
switch (call.Arguments.Count)
286+
{
287+
case 0:
288+
if (call.Method.Name != "ThrowInvalidOperationException")
289+
return false;
290+
exceptionCtor = exceptionCtorTable[0];
291+
break;
292+
case 1:
293+
if (call.Method.Name != "ThrowSwitchExpressionException")
294+
return false;
295+
exceptionCtor = exceptionCtorTable[1];
296+
value = call.Arguments[0];
297+
break;
298+
default:
299+
return false;
300+
}
301+
return exceptionCtor != null;
302+
}
303+
304+
IMethod? FindConstructor(string fullTypeName, params Type[] argumentTypes)
305+
{
306+
IType exceptionType = context.TypeSystem.FindType(new FullTypeName(fullTypeName));
307+
var types = argumentTypes.SelectArray(context.TypeSystem.FindType);
308+
309+
foreach (var ctor in exceptionType.GetConstructors(m => !m.IsStatic && m.Parameters.Count == argumentTypes.Length))
310+
{
311+
bool found = true;
312+
foreach (var pair in ctor.Parameters.Select(p => p.Type).Zip(types))
313+
{
314+
if (!NormalizeTypeVisitor.IgnoreNullability.EquivalentTypes(pair.Item1, pair.Item2))
315+
{
316+
found = false;
317+
break;
318+
}
319+
}
320+
if (found)
321+
return ctor;
322+
}
323+
324+
return null;
325+
}
326+
#nullable restore
219327
}
220328

221329
internal static void SimplifySwitchInstruction(Block block, ILTransformContext context)
222330
{
223331
// due to our of of basic blocks at this point,
224-
// switch instructions can only appear as last insturction
332+
// switch instructions can only appear as last instruction
225333
var sw = block.Instructions.LastOrDefault() as SwitchInstruction;
226334
if (sw == null)
227335
return;
@@ -445,7 +553,9 @@ private void AddNullCase(List<ControlFlowNode> flowNodes, List<ControlFlowNode>
445553
!nullableBlock.Instructions.SecondToLastOrDefault().MatchIfInstruction(out var cond, out var trueInst) ||
446554
!cond.MatchLogicNot(out var getHasValue) ||
447555
!NullableLiftingTransform.MatchHasValueCall(getHasValue, out ILInstruction nullableInst))
556+
{
448557
return;
558+
}
449559

450560
// could check that nullableInst is ldloc or ldloca and that the switch variable matches a GetValueOrDefault
451561
// but the effect of adding an incorrect block to the flowBlock list would only be disasterous if it branched directly

ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,12 @@ public SwitchSection()
202202
/// </summary>
203203
public bool HasNullLabel { get; set; }
204204

205+
/// <summary>
206+
/// If true, this section only contains a compiler-generated throw helper
207+
/// used in a switch expression and will not be visible in the decompiled source code.
208+
/// </summary>
209+
public bool IsCompilerGeneratedDefaultSection { get; set; }
210+
205211
/// <summary>
206212
/// The set of labels that cause execution to jump to this switch section.
207213
/// </summary>
@@ -221,6 +227,8 @@ public override InstructionFlags DirectFlags {
221227
public override void WriteTo(ITextOutput output, ILAstWritingOptions options)
222228
{
223229
WriteILRange(output, options);
230+
if (IsCompilerGeneratedDefaultSection)
231+
output.Write("generated.");
224232
output.WriteLocalReference("case", this, isDefinition: true);
225233
output.Write(' ');
226234
if (HasNullLabel)

ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1169,7 +1169,11 @@ SwitchInstruction ReplaceWithSwitchInstruction(int offset)
11691169
}
11701170
var newSwitch = new SwitchInstruction(new StringToInt(switchValueInst, values, switchValueLoad.Variable.Type));
11711171
newSwitch.Sections.AddRange(sections);
1172-
newSwitch.Sections.Add(new SwitchSection { Labels = defaultLabel, Body = defaultSection.Body });
1172+
newSwitch.Sections.Add(new SwitchSection {
1173+
Labels = defaultLabel,
1174+
Body = defaultSection.Body,
1175+
IsCompilerGeneratedDefaultSection = defaultSection.IsCompilerGeneratedDefaultSection
1176+
});
11731177
instructions[offset].ReplaceWith(newSwitch);
11741178
return newSwitch;
11751179
}
@@ -1310,6 +1314,7 @@ private bool MatchRoslynSwitchOnStringUsingLengthAndChar(Block block, int i)
13101314
}
13111315
instructions[i] = newSwitch;
13121316
instructions.RemoveRange(i + 1, instructions.Count - (i + 1));
1317+
SwitchDetection.InlineSwitchExpressionDefaultCaseThrowHelper(block, context);
13131318
return true;
13141319

13151320
bool MatchGetChars(ILInstruction instruction, ILVariable switchValueVar, out int index)

0 commit comments

Comments
 (0)