From 123da4deeb1511d9822be5f38d2c43f9e9f5b886 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 30 Aug 2026 12:53:30 +0200 Subject: [PATCH 1/3] #nullable enable for TransformAssignment. --- .../IL/Instructions/InstructionCollection.cs | 2 +- .../IL/Transforms/TransformAssignment.cs | 47 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Instructions/InstructionCollection.cs b/ICSharpCode.Decompiler/IL/Instructions/InstructionCollection.cs index 13fad3f78..d3bce292d 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/InstructionCollection.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/InstructionCollection.cs @@ -292,7 +292,7 @@ namespace ICSharpCode.Decompiler.IL parentInstruction.InstructionCollectionUpdateComplete(); } - public bool Remove(T item) + public bool Remove(T? item) { int index = IndexOf(item); if (index >= 0) diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs index 8edba814a..9fb4a948b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs @@ -16,8 +16,11 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +#nullable enable + using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; @@ -37,7 +40,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// public class TransformAssignment : IStatementTransform { - StatementTransformContext context; + StatementTransformContext context = null!; void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) { @@ -109,8 +112,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms } ILVariable local; int nextPos; - if (block.Instructions[pos + 1] is StLoc localStore) + StLoc? localStore; + if (block.Instructions[pos + 1] is StLoc localStoreInst) { // with extra local + localStore = localStoreInst; if (localStore.Variable.Kind != VariableKind.Local || !localStore.Value.MatchLdLoc(inst.Variable)) return false; // if we're using an extra local, we'll delete "s", so check that that doesn't have any additional uses @@ -179,7 +184,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return false; if (call.ResultType != StackType.Void || call.Arguments.Count == 0) return false; - IProperty property = call.Method.AccessorOwner as IProperty; + IProperty? property = call.Method.AccessorOwner as IProperty; if (property == null) return false; if (!call.Method.Equals(property.Setter)) @@ -246,21 +251,19 @@ namespace ICSharpCode.Decompiler.IL.Transforms }; } - static ILInstruction UnwrapSmallIntegerConv(ILInstruction inst, out Conv conv) + static ILInstruction UnwrapSmallIntegerConv(ILInstruction inst, [NotNullWhen(true)] out Conv? conv) { - conv = inst as Conv; - if (conv != null && conv.Kind == ConversionKind.Truncate && conv.TargetType.IsSmallIntegerType()) + if (inst is Conv { Kind: ConversionKind.Truncate } convInst && convInst.TargetType.IsSmallIntegerType()) { // for compound assignments to small integers, the compiler emits a "conv" instruction - return conv.Argument; - } - else - { - return inst; + conv = convInst; + return convInst.Argument; } + conv = null; + return inst; } - static bool ValidateCompoundAssign(BinaryNumericInstruction binary, Conv conv, IType targetType, DecompilerSettings settings) + static bool ValidateCompoundAssign(BinaryNumericInstruction binary, Conv? conv, IType targetType, DecompilerSettings settings) { if (!NumericCompoundAssign.IsBinaryCompatibleWithType(binary, targetType, settings)) return false; @@ -269,7 +272,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } - static bool MatchingGetterAndSetterCalls(CallInstruction getterCall, CallInstruction setterCall, out Action finalizeMatch) + static bool MatchingGetterAndSetterCalls(CallInstruction? getterCall, CallInstruction? setterCall, out Action? finalizeMatch) { finalizeMatch = null; if (getterCall == null || setterCall == null || !IsSameMember(getterCall.Method.AccessorOwner, setterCall.Method.AccessorOwner)) @@ -684,8 +687,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// /// Every IsCompoundStore() call should be followed by an IsMatchingCompoundLoad() call. /// - static bool IsCompoundStore(ILInstruction inst, out IType storeType, - out ILInstruction value, ICompilation compilation) + static bool IsCompoundStore(ILInstruction inst, [NotNullWhen(true)] out IType? storeType, + [NotNullWhen(true)] out ILInstruction? value, ICompilation compilation) { value = null; storeType = null; @@ -773,10 +776,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// Instruction preceding the load. /// static bool IsMatchingCompoundLoad(ILInstruction load, ILInstruction store, - out ILInstruction target, out CompoundTargetKind targetKind, - out Action finalizeMatch, - ILVariable forbiddenVariable = null, - ILInstruction previousInstruction = null) + [NotNullWhen(true)] out ILInstruction? target, out CompoundTargetKind targetKind, + out Action? finalizeMatch, + ILVariable? forbiddenVariable = null, + ILInstruction? previousInstruction = null) { target = null; targetKind = 0; @@ -870,7 +873,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms var targetType = targetType1; var stloc_outer = store as StLoc; var stloc_inner = value1 as StLoc; - LdLoc ldloc; + LdLoc? ldloc; var binary = UnwrapSmallIntegerConv(value2, out var conv) as BinaryNumericInstruction; if (binary != null && (binary.Right.MatchLdcI(1) || binary.Right.MatchLdcF4(1) || binary.Right.MatchLdcF8(1))) { @@ -955,7 +958,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms { return false; } - StLoc stloc; + StLoc? stloc; var binary = UnwrapSmallIntegerConv(value, out var conv) as BinaryNumericInstruction; if (binary != null && (binary.Right.MatchLdcI(1) || binary.Right.MatchLdcF4(1) || binary.Right.MatchLdcF8(1))) { @@ -1111,7 +1114,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } - static bool IsSameMember(IMember a, IMember b) + static bool IsSameMember(IMember? a, IMember? b) { if (a == null || b == null) return false; From 6ac9b813e64ad398a9ba5ba948796d4f387e1add Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 30 Aug 2026 13:13:15 +0200 Subject: [PATCH 2/3] Fix nondeterministic VBPretty test failure. --- .../TestCases/VBPretty/VBAnonymousTypes.cs | 5 ----- ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs index dde61d8b2..6ae40a6f3 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/VBAnonymousTypes.cs @@ -12,13 +12,8 @@ using Microsoft.VisualBasic.CompilerServices; // A VB anonymous type. Its properties are settable and only those declared 'Key' // take part in Equals and GetHashCode, so it cannot be written as a C# anonymous // type and is declared here instead. -#if LEGACY_VBC && OPT [DebuggerDisplay("Value={Value}, Name={Name}")] [CompilerGenerated] -#else -[CompilerGenerated] -[DebuggerDisplay("Value={Value}, Name={Name}")] -#endif internal sealed class VB_AnonymousType_0 { #if !OPT && !LEGACY_VBC diff --git a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs index 3cd9f3b09..a700d4e7c 100644 --- a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs @@ -193,7 +193,8 @@ namespace ICSharpCode.Decompiler.Tests } var executable = await Tester.CompileVB(vbFile, options | CompilerOptions.ReferenceVisualBasic, exeFile).ConfigureAwait(false); - var decompiled = await Tester.DecompileCSharp(executable.PathToAssembly, settings ?? new DecompilerSettings { FileScopedNamespaces = false }).ConfigureAwait(false); + settings ??= new DecompilerSettings { FileScopedNamespaces = false, SortCustomAttributes = true }; + var decompiled = await Tester.DecompileCSharp(executable.PathToAssembly, settings).ConfigureAwait(false); CodeAssert.FilesAreEqual(csFile, decompiled, Tester.GetPreprocessorSymbols(options).ToArray()); Tester.RepeatOnIOError(() => File.Delete(decompiled)); From cf4dba7015d15e149fd90d351dd462843063edc3 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 30 Aug 2026 13:24:14 +0200 Subject: [PATCH 3/3] Always pass the type system into InferType(). --- ICSharpCode.Decompiler/IL/ILTypeExtensions.cs | 11 ++++------ .../CompoundAssignmentInstruction.cs | 15 ++++++------- .../IL/Transforms/TransformAssignment.cs | 21 ++++++++++--------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs b/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs index ca8da349a..1c0a436cb 100644 --- a/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs +++ b/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs @@ -17,6 +17,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System.Diagnostics; using System.Linq; using ICSharpCode.Decompiler.TypeSystem; @@ -215,17 +216,15 @@ namespace ICSharpCode.Decompiler.IL /// If not returning UnknownType, must return a type that can store /// the result of the instruction without loss of information. /// - public static IType InferType(this ILInstruction inst, ICompilation? compilation) + public static IType InferType(this ILInstruction inst, ICompilation compilation) { + Debug.Assert(compilation != null); switch (inst) { case NewObj newObj: return newObj.Method.DeclaringType ?? SpecialType.UnknownType; case NewArr newArr: - if (compilation != null) - return new ArrayType(compilation, newArr.Type, newArr.Indices.Count); - else - return SpecialType.UnknownType; + return new ArrayType(compilation, newArr.Type, newArr.Indices.Count); case Call call: return call.Method.ReturnType; case CallVirt callVirt: @@ -258,8 +257,6 @@ namespace ICSharpCode.Decompiler.IL } return new ByReferenceType(ldelema.Type); case Comp comp: - if (compilation == null) - return SpecialType.UnknownType; switch (comp.LiftingKind) { case ComparisonLiftingKind.None: diff --git a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs index b456173ed..634ecf832 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs @@ -153,10 +153,11 @@ namespace ICSharpCode.Decompiler.IL public bool IsLifted { get; } public NumericCompoundAssign(BinaryNumericInstruction binary, ILInstruction target, - CompoundTargetKind targetKind, ILInstruction value, IType type, CompoundEvalMode evalMode) + CompoundTargetKind targetKind, ILInstruction value, IType type, CompoundEvalMode evalMode, + Transforms.ILTransformContext context) : base(OpCode.NumericCompoundAssign, evalMode, target, targetKind, value) { - Debug.Assert(IsBinaryCompatibleWithType(binary, type, null)); + Debug.Assert(IsBinaryCompatibleWithType(binary, type, context)); this.CheckForOverflow = binary.CheckForOverflow; this.Sign = binary.Sign; this.LeftInputType = binary.LeftInputType; @@ -166,14 +167,14 @@ namespace ICSharpCode.Decompiler.IL this.IsLifted = binary.IsLifted; this.type = type; this.AddILRange(binary); - Debug.Assert(evalMode == CompoundEvalMode.EvaluatesToNewValue || (Operator == BinaryNumericOperator.Add || Operator == BinaryNumericOperator.Sub)); + Debug.Assert(evalMode == CompoundEvalMode.EvaluatesToNewValue || Operator == BinaryNumericOperator.Add || Operator == BinaryNumericOperator.Sub); Debug.Assert(this.ResultType == (IsLifted ? StackType.O : UnderlyingResultType)); } /// /// Gets whether the specific binary instruction is compatible with a compound operation on the specified type. /// - internal static bool IsBinaryCompatibleWithType(BinaryNumericInstruction binary, IType type, DecompilerSettings? settings) + internal static bool IsBinaryCompatibleWithType(BinaryNumericInstruction binary, IType type, Transforms.ILTransformContext context) { if (binary.IsLifted) { @@ -220,7 +221,7 @@ namespace ICSharpCode.Decompiler.IL // If the LHS is C# 9 IntPtr (but not nint or C# 11 IntPtr): // "target.intptr *= 2;" is compiler error, but // "target.intptr *= (nint)2;" works - if (settings != null && !settings.NativeIntegers) + if (!context.Settings.NativeIntegers) { // But if native integers are not available, we cannot use compound assignment. return false; @@ -235,7 +236,7 @@ namespace ICSharpCode.Decompiler.IL } if (binary.Sign != Sign.None) { - bool signMismatchAllowed = (binary.Sign == Sign.Unsigned && binary.Operator == BinaryNumericOperator.ShiftRight && (settings == null || settings.UnsignedRightShift)); + bool signMismatchAllowed = binary.Sign == Sign.Unsigned && binary.Operator == BinaryNumericOperator.ShiftRight && context.Settings.UnsignedRightShift; if (type.IsCSharpSmallIntegerType()) { // C# will use numeric promotion to int, binary op must be signed @@ -250,7 +251,7 @@ namespace ICSharpCode.Decompiler.IL } } // Can't transform if the RHS value would be need to be truncated for the LHS type. - if (Transforms.TransformAssignment.IsImplicitTruncation(binary.Right, type, null, binary.IsLifted)) + if (Transforms.TransformAssignment.IsImplicitTruncation(binary.Right, type, context.TypeSystem, binary.IsLifted)) return false; return true; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs index 9fb4a948b..bf22be536 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs @@ -25,6 +25,7 @@ using System.Linq; using System.Linq.Expressions; using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; @@ -263,9 +264,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms return inst; } - static bool ValidateCompoundAssign(BinaryNumericInstruction binary, Conv? conv, IType targetType, DecompilerSettings settings) + static bool ValidateCompoundAssign(BinaryNumericInstruction binary, Conv? conv, IType targetType, ILTransformContext context) { - if (!NumericCompoundAssign.IsBinaryCompatibleWithType(binary, targetType, settings)) + if (!NumericCompoundAssign.IsBinaryCompatibleWithType(binary, targetType, context)) return false; if (conv != null && !(conv.TargetType == targetType.ToPrimitiveType() && conv.CheckForOverflow == binary.CheckForOverflow)) return false; // conv does not match binary operation @@ -381,13 +382,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms } if (!IsMatchingCompoundLoad(binary.Left, compoundStore, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: storeInSetter?.Variable)) return false; - if (!ValidateCompoundAssign(binary, smallIntConv, targetType, context.Settings)) + if (!ValidateCompoundAssign(binary, smallIntConv, targetType, context)) return false; context.Step($"Compound assignment (binary.numeric)", compoundStore); finalizeMatch?.Invoke(context); newInst = new NumericCompoundAssign( binary, target, targetKind, binary.Right, - targetType, CompoundEvalMode.EvaluatesToNewValue); + targetType, CompoundEvalMode.EvaluatesToNewValue, context); } else if (setterValue is Call operatorCall && operatorCall.Method.IsOperator) { @@ -887,7 +888,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms targetType = SwapSign(targetType, context.TypeSystem); } - if (!ValidateCompoundAssign(binary, conv, targetType, context.Settings)) + if (!ValidateCompoundAssign(binary, conv, targetType, context)) return false; ldloc = binary.Left as LdLoc; } @@ -920,7 +921,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (binary != null) { block.Instructions[pos] = new StLoc(stloc_outer.Variable, new NumericCompoundAssign( - binary, target, targetKind, binary.Right, targetType, CompoundEvalMode.EvaluatesToNewValue)); + binary, target, targetKind, binary.Right, targetType, CompoundEvalMode.EvaluatesToNewValue, context)); } else { @@ -972,7 +973,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms targetType = SwapSign(targetType, context.TypeSystem); } - if (!ValidateCompoundAssign(binary, conv, targetType, context.Settings)) + if (!ValidateCompoundAssign(binary, conv, targetType, context)) return false; stloc = binary.Left as StLoc; } @@ -1001,7 +1002,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (binary != null) { block.Instructions[pos] = new StLoc(stloc.Variable, new NumericCompoundAssign( - binary, target, targetKind, binary.Right, targetType, CompoundEvalMode.EvaluatesToOldValue)); + binary, target, targetKind, binary.Right, targetType, CompoundEvalMode.EvaluatesToOldValue, context)); } else { @@ -1080,12 +1081,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms // Change the sign of the type to skip implicit truncation stObj.Type = targetType = SwapSign(targetType, context.TypeSystem); } - if (!ValidateCompoundAssign(binary, conv, targetType, context.Settings)) + if (!ValidateCompoundAssign(binary, conv, targetType, context)) return false; context.Step("TransformPostIncDecOperator (builtin)", inst); finalizeMatch?.Invoke(context); inst.Value = new NumericCompoundAssign(binary, target, targetKind, binary.Right, - targetType, CompoundEvalMode.EvaluatesToOldValue); + targetType, CompoundEvalMode.EvaluatesToOldValue, context); } else if (value is Call operatorCall && operatorCall.Method.IsOperator && operatorCall.Arguments.Count == 1) {