Browse Source

Fix #3382: Support compiler-generated throw-helper invocations in switch-expression implicit default-case.

pull/3404/head
Siegfried Pammer 3 months ago
parent
commit
8c6e642c9e
  1. 54
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/SwitchExpressions.cs
  2. 11
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  3. 112
      ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs
  4. 8
      ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs
  5. 7
      ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs

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

@ -159,5 +159,59 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
_ => "default", _ => "default",
}; };
} }
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
public static int Issue3382(StringComparison c)
{
return c switch {
StringComparison.Ordinal => 0,
StringComparison.OrdinalIgnoreCase => 1,
};
}
public static void Issue3382b(ref StringComparison? c)
{
StringComparison? stringComparison = c;
c = stringComparison switch {
null => StringComparison.Ordinal,
StringComparison.Ordinal => StringComparison.OrdinalIgnoreCase,
StringComparison.OrdinalIgnoreCase => StringComparison.InvariantCulture,
};
}
public static void Issue3382c(StringComparison? c)
{
c = c switch {
null => StringComparison.Ordinal,
StringComparison.Ordinal => StringComparison.OrdinalIgnoreCase,
StringComparison.OrdinalIgnoreCase => StringComparison.InvariantCulture,
};
}
public static void Issue3382d(ref StringComparison c)
{
StringComparison stringComparison = c;
c = stringComparison switch {
StringComparison.Ordinal => StringComparison.OrdinalIgnoreCase,
StringComparison.OrdinalIgnoreCase => StringComparison.InvariantCulture,
};
}
public static int SwitchOnStringImplicitDefault(string s)
{
return s switch {
"Hello" => 42,
"World" => 4711,
"!" => 7,
"Foo" => 13,
"Bar" => 21,
"Baz" => 84,
"Qux" => 168,
"Quux" => 336,
"Corge" => 672,
"Grault" => 1344,
"Garply" => 2688,
};
}
#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
} }
} }

11
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -4135,10 +4135,13 @@ namespace ICSharpCode.Decompiler.CSharp
switchExpr.SwitchSections.Add(ses); switchExpr.SwitchSections.Add(ses);
} }
var defaultSES = new SwitchExpressionSection(); if (!defaultSection.IsCompilerGeneratedDefaultSection)
defaultSES.Pattern = new IdentifierExpression("_"); {
defaultSES.Body = TranslateSectionBody(defaultSection); var defaultSES = new SwitchExpressionSection();
switchExpr.SwitchSections.Add(defaultSES); defaultSES.Pattern = new IdentifierExpression("_");
defaultSES.Body = TranslateSectionBody(defaultSection);
switchExpr.SwitchSections.Add(defaultSES);
}
return switchExpr.WithILInstruction(inst).WithRR(new ResolveResult(resultType)); return switchExpr.WithILInstruction(inst).WithRR(new ResolveResult(resultType));

112
ICSharpCode.Decompiler/IL/ControlFlow/SwitchDetection.cs

@ -16,8 +16,10 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using ICSharpCode.Decompiler.FlowAnalysis; using ICSharpCode.Decompiler.FlowAnalysis;
@ -216,12 +218,118 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
// (1st pass was in ControlFlowSimplification) // (1st pass was in ControlFlowSimplification)
SimplifySwitchInstruction(block, context); SimplifySwitchInstruction(block, context);
} }
InlineSwitchExpressionDefaultCaseThrowHelper(block, context);
}
// call ThrowInvalidOperationException(...)
// leave IL_0000 (ldloc temp)
//
// -or-
//
// call ThrowSwitchExpressionException(...)
// leave IL_0000 (ldloc temp)
//
// -to-
//
// throw(newobj SwitchExpressionException(...))
internal static void InlineSwitchExpressionDefaultCaseThrowHelper(Block block, ILTransformContext context)
{
#nullable enable
// due to our of of basic blocks at this point,
// switch instructions can only appear as last instruction
var sw = block.Instructions.LastOrDefault() as SwitchInstruction;
if (sw == null)
return;
IMethod[] exceptionCtorTable = new IMethod[2];
exceptionCtorTable[0] = FindConstructor("System.InvalidOperationException")!;
exceptionCtorTable[1] = FindConstructor("System.Runtime.CompilerServices.SwitchExpressionException", typeof(object))!;
if (exceptionCtorTable[0] == null && exceptionCtorTable[1] == null)
return;
if (sw.GetDefaultSection() is not { Body: Branch { TargetBlock: Block defaultBlock } } defaultSection)
return;
if (defaultBlock is { Instructions: [var call, Branch or Leave] })
{
if (!MatchThrowHelperCall(call, out IMethod? exceptionCtor, out ILInstruction? value))
return;
context.Step("SwitchExpressionDefaultCaseTransform", block.Instructions[0]);
var newObj = new NewObj(exceptionCtor);
if (value != null)
newObj.Arguments.Add(value);
defaultBlock.Instructions[0] = new Throw(newObj).WithILRange(defaultBlock.Instructions[0]).WithILRange(defaultBlock.Instructions[1]);
defaultBlock.Instructions.RemoveAt(1);
defaultSection.IsCompilerGeneratedDefaultSection = true;
}
else if (defaultBlock is { Instructions: [Throw { Argument: NewObj { Method: var ctor, Arguments: [var arg] } newObj }] }
&& ctor.Equals(exceptionCtorTable[1]))
{
defaultSection.IsCompilerGeneratedDefaultSection = true;
}
else
{
return;
}
bool MatchThrowHelperCall(ILInstruction inst, [NotNullWhen(true)] out IMethod? exceptionCtor, out ILInstruction? value)
{
exceptionCtor = null;
value = null;
if (inst is not Call call)
return false;
if (call.Method.DeclaringType.FullName != "<PrivateImplementationDetails>")
return false;
switch (call.Arguments.Count)
{
case 0:
if (call.Method.Name != "ThrowInvalidOperationException")
return false;
exceptionCtor = exceptionCtorTable[0];
break;
case 1:
if (call.Method.Name != "ThrowSwitchExpressionException")
return false;
exceptionCtor = exceptionCtorTable[1];
value = call.Arguments[0];
break;
default:
return false;
}
return exceptionCtor != null;
}
IMethod? FindConstructor(string fullTypeName, params Type[] argumentTypes)
{
IType exceptionType = context.TypeSystem.FindType(new FullTypeName(fullTypeName));
var types = argumentTypes.SelectArray(context.TypeSystem.FindType);
foreach (var ctor in exceptionType.GetConstructors(m => !m.IsStatic && m.Parameters.Count == argumentTypes.Length))
{
bool found = true;
foreach (var pair in ctor.Parameters.Select(p => p.Type).Zip(types))
{
if (!NormalizeTypeVisitor.IgnoreNullability.EquivalentTypes(pair.Item1, pair.Item2))
{
found = false;
break;
}
}
if (found)
return ctor;
}
return null;
}
#nullable restore
} }
internal static void SimplifySwitchInstruction(Block block, ILTransformContext context) internal static void SimplifySwitchInstruction(Block block, ILTransformContext context)
{ {
// due to our of of basic blocks at this point, // due to our of of basic blocks at this point,
// switch instructions can only appear as last insturction // switch instructions can only appear as last instruction
var sw = block.Instructions.LastOrDefault() as SwitchInstruction; var sw = block.Instructions.LastOrDefault() as SwitchInstruction;
if (sw == null) if (sw == null)
return; return;
@ -445,7 +553,9 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
!nullableBlock.Instructions.SecondToLastOrDefault().MatchIfInstruction(out var cond, out var trueInst) || !nullableBlock.Instructions.SecondToLastOrDefault().MatchIfInstruction(out var cond, out var trueInst) ||
!cond.MatchLogicNot(out var getHasValue) || !cond.MatchLogicNot(out var getHasValue) ||
!NullableLiftingTransform.MatchHasValueCall(getHasValue, out ILInstruction nullableInst)) !NullableLiftingTransform.MatchHasValueCall(getHasValue, out ILInstruction nullableInst))
{
return; return;
}
// could check that nullableInst is ldloc or ldloca and that the switch variable matches a GetValueOrDefault // could check that nullableInst is ldloc or ldloca and that the switch variable matches a GetValueOrDefault
// but the effect of adding an incorrect block to the flowBlock list would only be disasterous if it branched directly // but the effect of adding an incorrect block to the flowBlock list would only be disasterous if it branched directly

8
ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs

@ -202,6 +202,12 @@ namespace ICSharpCode.Decompiler.IL
/// </summary> /// </summary>
public bool HasNullLabel { get; set; } public bool HasNullLabel { get; set; }
/// <summary>
/// If true, this section only contains a compiler-generated throw helper
/// used in a switch expression and will not be visible in the decompiled source code.
/// </summary>
public bool IsCompilerGeneratedDefaultSection { get; set; }
/// <summary> /// <summary>
/// The set of labels that cause execution to jump to this switch section. /// The set of labels that cause execution to jump to this switch section.
/// </summary> /// </summary>
@ -221,6 +227,8 @@ namespace ICSharpCode.Decompiler.IL
public override void WriteTo(ITextOutput output, ILAstWritingOptions options) public override void WriteTo(ITextOutput output, ILAstWritingOptions options)
{ {
WriteILRange(output, options); WriteILRange(output, options);
if (IsCompilerGeneratedDefaultSection)
output.Write("generated.");
output.WriteLocalReference("case", this, isDefinition: true); output.WriteLocalReference("case", this, isDefinition: true);
output.Write(' '); output.Write(' ');
if (HasNullLabel) if (HasNullLabel)

7
ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs

@ -1169,7 +1169,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
var newSwitch = new SwitchInstruction(new StringToInt(switchValueInst, values, switchValueLoad.Variable.Type)); var newSwitch = new SwitchInstruction(new StringToInt(switchValueInst, values, switchValueLoad.Variable.Type));
newSwitch.Sections.AddRange(sections); newSwitch.Sections.AddRange(sections);
newSwitch.Sections.Add(new SwitchSection { Labels = defaultLabel, Body = defaultSection.Body }); newSwitch.Sections.Add(new SwitchSection {
Labels = defaultLabel,
Body = defaultSection.Body,
IsCompilerGeneratedDefaultSection = defaultSection.IsCompilerGeneratedDefaultSection
});
instructions[offset].ReplaceWith(newSwitch); instructions[offset].ReplaceWith(newSwitch);
return newSwitch; return newSwitch;
} }
@ -1310,6 +1314,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
instructions[i] = newSwitch; instructions[i] = newSwitch;
instructions.RemoveRange(i + 1, instructions.Count - (i + 1)); instructions.RemoveRange(i + 1, instructions.Count - (i + 1));
SwitchDetection.InlineSwitchExpressionDefaultCaseThrowHelper(block, context);
return true; return true;
bool MatchGetChars(ILInstruction instruction, ILVariable switchValueVar, out int index) bool MatchGetChars(ILInstruction instruction, ILVariable switchValueVar, out int index)

Loading…
Cancel
Save