diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index c0d10da6fc..da9bcafd5c 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -196,6 +196,9 @@
+
+
+
diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
index ea17c49f82..b7d7673a64 100644
--- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
@@ -333,6 +333,12 @@ public async Task CallIndirect()
await Run();
}
+ [Test]
+ public async Task CompoundAssignmentOperatorEdgeCases()
+ {
+ await Run();
+ }
+
[Test]
public async Task FSharpLoops_Debug()
{
diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
index 634aaa5cf1..09bc9eae21 100644
--- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
@@ -800,6 +800,12 @@ public async Task RefStructInterfaces([ValueSource(nameof(roslyn4OrNewerOptions)
await RunForLibrary(cscOptions: cscOptions);
}
+ [Test]
+ public async Task UserDefinedCompoundAssignment([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
+ {
+ await RunForLibrary(cscOptions: cscOptions);
+ }
+
[Test]
public async Task ExpandParamsArgumentsDisabled([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions)
{
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs
new file mode 100644
index 0000000000..99cf2afe5b
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs
@@ -0,0 +1,68 @@
+using System.Runtime.CompilerServices;
+
+public class CompoundTarget
+{
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ // Not "operator -=": C# 14 only recognizes void-returning instance methods as compound
+ // assignment operators, so a value-returning one stays a plain method.
+ [SpecialName]
+ public CompoundTarget op_SubtractionAssignment(int rhs)
+ {
+ return this;
+ }
+
+ [SpecialName]
+ public static CompoundTarget op_MultiplicationAssignment(CompoundTarget lhs, int rhs)
+ {
+ return lhs;
+ }
+}
+public class DerivedCompoundTarget : CompoundTarget
+{
+ public void CallBaseOperator(int n)
+ {
+ base.op_AdditionAssignment(n);
+ }
+}
+public class EdgeCases
+{
+ public static CompoundTarget GetTarget()
+ {
+ return new CompoundTarget();
+ }
+
+ public static void NonVariableReceiver(int n)
+ {
+ GetTarget().op_AdditionAssignment(n);
+ }
+
+ public static void UnconstrainedGenericReceiver(T x, int n) where T : IOther
+ {
+ ((ICompound)(object)x).op_AdditionAssignment(n);
+ }
+
+ public static void MismatchedReceiverType(object o, int n)
+ {
+ ((CompoundTarget)o).op_AdditionAssignment(n);
+ }
+
+ public static void CallNonCSharpOperators(CompoundTarget t, int n)
+ {
+ t.op_SubtractionAssignment(n);
+ CompoundTarget.op_MultiplicationAssignment(t, n);
+ }
+}
+public interface ICompound
+{
+ void operator +=(int rhs);
+}
+public interface IOther
+{
+ void M();
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il
new file mode 100644
index 0000000000..7305c3ed40
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il
@@ -0,0 +1,174 @@
+// Metadata version: v4.0.30319
+.assembly extern System.Runtime
+{
+ .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
+ .ver 11:0:0:0
+}
+.assembly CompoundAssignmentOperatorEdgeCases
+{
+ .ver 1:0:0:0
+}
+.module CompoundAssignmentOperatorEdgeCases.dll
+.imagebase 0x10000000
+.file alignment 0x00000200
+.stackreserve 0x00100000
+.subsystem 0x0003 // WINDOWS_CUI
+.corflags 0x00000001 // ILONLY
+
+.class public auto ansi beforefieldinit CompoundTarget
+ extends [System.Runtime]System.Object
+{
+ .field public int32 Value
+
+ // A C# 14 user-defined compound assignment operator.
+ .method public hidebysig specialname instance void
+ op_AdditionAssignment(int32 rhs) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.0
+ IL_0002: ldfld int32 CompoundTarget::Value
+ IL_0007: ldarg.1
+ IL_0008: add
+ IL_0009: stfld int32 CompoundTarget::Value
+ IL_000e: ret
+ }
+
+ // C++/CLI emits value-returning instance operators; C# has no syntax for those.
+ .method public hidebysig specialname instance class CompoundTarget
+ op_SubtractionAssignment(int32 rhs) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ret
+ }
+
+ // F# mangles "static member (*=)" to a static, value-returning op_MultiplicationAssignment.
+ .method public hidebysig specialname static class CompoundTarget
+ op_MultiplicationAssignment(class CompoundTarget lhs,
+ int32 rhs) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class interface public abstract auto ansi beforefieldinit ICompound
+{
+ .method public hidebysig newslot specialname abstract virtual
+ instance void op_AdditionAssignment(int32 rhs) cil managed
+ {
+ }
+}
+
+.class interface public abstract auto ansi beforefieldinit IOther
+{
+ .method public hidebysig newslot abstract virtual
+ instance void M() cil managed
+ {
+ }
+}
+
+.class public auto ansi beforefieldinit DerivedCompoundTarget
+ extends CompoundTarget
+{
+ .method public hidebysig instance void CallBaseOperator(int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.1
+ IL_0002: call instance void CompoundTarget::op_AdditionAssignment(int32)
+ IL_0007: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void CompoundTarget::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class public auto ansi beforefieldinit EdgeCases
+ extends [System.Runtime]System.Object
+{
+ .method public hidebysig static class CompoundTarget
+ GetTarget() cil managed
+ {
+ .maxstack 8
+ IL_0000: newobj instance void CompoundTarget::.ctor()
+ IL_0005: ret
+ }
+
+ // The receiver is not a variable, so it cannot become the target of "x += n".
+ .method public hidebysig static void NonVariableReceiver(int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: call class CompoundTarget EdgeCases::GetTarget()
+ IL_0005: ldarg.0
+ IL_0006: callvirt instance void CompoundTarget::op_AdditionAssignment(int32)
+ IL_000b: ret
+ }
+
+ // T is not constrained to ICompound, so the cast selects the operator and has to stay.
+ .method public hidebysig static void UnconstrainedGenericReceiver<(IOther) T>(!!T x,
+ int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: box !!T
+ IL_0006: castclass ICompound
+ IL_000b: ldarg.1
+ IL_000c: callvirt instance void ICompound::op_AdditionAssignment(int32)
+ IL_0011: ret
+ }
+
+ // The receiver's static type does not declare the operator, so overload resolution has no
+ // candidate to pick.
+ .method public hidebysig static void MismatchedReceiverType(object o,
+ int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.1
+ IL_0002: callvirt instance void CompoundTarget::op_AdditionAssignment(int32)
+ IL_0007: ret
+ }
+
+ .method public hidebysig static void CallNonCSharpOperators(class CompoundTarget t,
+ int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.1
+ IL_0002: callvirt instance class CompoundTarget CompoundTarget::op_SubtractionAssignment(int32)
+ IL_0007: pop
+ IL_0008: ldarg.0
+ IL_0009: ldarg.1
+ IL_000a: call class CompoundTarget CompoundTarget::op_MultiplicationAssignment(class CompoundTarget,
+ int32)
+ IL_000f: pop
+ IL_0010: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedCompoundAssignment.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedCompoundAssignment.cs
new file mode 100644
index 0000000000..67a125886b
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedCompoundAssignment.cs
@@ -0,0 +1,308 @@
+// Copyright (c) 2026 Siegfried Pammer
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+
+namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
+{
+ internal class UserDefinedCompoundAssignment
+ {
+ public class CompoundClass
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ public void operator checked +=(int rhs)
+ {
+ checked
+ {
+ Value += rhs;
+ }
+ }
+
+ public void operator -=(int rhs)
+ {
+ Value -= rhs;
+ }
+
+ public void operator checked -=(int rhs)
+ {
+ checked
+ {
+ Value -= rhs;
+ }
+ }
+
+ public void operator *=(int rhs)
+ {
+ Value *= rhs;
+ }
+
+ public void operator checked *=(int rhs)
+ {
+ checked
+ {
+ Value *= rhs;
+ }
+ }
+
+ public void operator /=(int rhs)
+ {
+ Value /= rhs;
+ }
+
+ public void operator checked /=(int rhs)
+ {
+ Value /= rhs;
+ }
+
+ public void operator %=(int rhs)
+ {
+ Value %= rhs;
+ }
+
+ public void operator &=(int rhs)
+ {
+ Value &= rhs;
+ }
+
+ public void operator |=(int rhs)
+ {
+ Value |= rhs;
+ }
+
+ public void operator ^=(int rhs)
+ {
+ Value ^= rhs;
+ }
+
+ public void operator <<=(int rhs)
+ {
+ Value <<= rhs;
+ }
+
+ public void operator >>=(int rhs)
+ {
+ Value >>= rhs;
+ }
+
+ public void operator >>>=(int rhs)
+ {
+ Value >>>= rhs;
+ }
+
+ public void operator ++()
+ {
+ Value++;
+ }
+
+ public void operator checked ++()
+ {
+ checked
+ {
+ Value++;
+ }
+ }
+
+ public void operator --()
+ {
+ Value--;
+ }
+
+ public void operator checked --()
+ {
+ checked
+ {
+ Value--;
+ }
+ }
+
+ public virtual void operator +=(long rhs)
+ {
+ Value += (int)rhs;
+ }
+ }
+
+ public struct CompoundStruct
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ public readonly void operator -=(int rhs)
+ {
+ Console.WriteLine(Value - rhs);
+ }
+
+ public void operator ++()
+ {
+ Value++;
+ }
+
+ public void AddViaThis()
+ {
+ this += 10;
+ }
+ }
+
+ public class DerivedCompoundClass : CompoundClass
+ {
+ }
+
+ public class BothOperators
+ {
+ public int Value;
+
+ public static BothOperators operator +(BothOperators lhs, int rhs)
+ {
+ return new BothOperators {
+ Value = lhs.Value + rhs
+ };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ public void operator ++()
+ {
+ Value++;
+ }
+ }
+
+ public interface ICompound
+ {
+ void operator +=(T rhs);
+ void operator ++();
+ }
+
+ public class ExplicitCompound : ICompound
+ {
+ public int Value;
+
+ void ICompound.operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ void ICompound.operator ++()
+ {
+ Value++;
+ }
+ }
+
+ private static CompoundClass staticField = new CompoundClass();
+
+ private CompoundClass instanceField = new CompoundClass();
+
+ private BothOperators bothField = new BothOperators();
+
+ public static void UseClass(CompoundClass c, int n)
+ {
+ c += n;
+ c += 1;
+ c -= n;
+ c *= n;
+ c /= n;
+ c %= n;
+ c &= n;
+ c |= n;
+ c ^= n;
+ c <<= n;
+ c >>= n;
+ c >>>= n;
+ c += 2L;
+ c++;
+ c--;
+ checked
+ {
+ c += n;
+ c -= n;
+ c *= n;
+ c /= n;
+ c++;
+ c--;
+ }
+ }
+
+ public static void UseStruct(CompoundStruct s, int n)
+ {
+ s += n;
+ s -= n;
+ s++;
+ }
+
+ public static void UseOtherTargets(CompoundClass[] arr, ref CompoundClass rc, ref CompoundStruct rs, UserDefinedCompoundAssignment inst, int n)
+ {
+ staticField += n;
+ inst.instanceField += n;
+ arr[0] += n;
+ rc += n;
+ rs += n;
+ rs++;
+ }
+
+ public static void UseGeneric(T x, int n) where T : ICompound
+ {
+ x += n;
+ x++;
+ }
+
+ public static void UseInheritedOperator(DerivedCompoundClass d, int n)
+ {
+ d += n;
+ d++;
+ }
+
+ public static BothOperators UseStaticOperator(BothOperators b, int n)
+ {
+ // The IL calls the static operator, so the decompiled code has to keep calling it:
+ // "b += n" and "b++" would bind to the instance operators instead.
+ b = b + n;
+ b = b + 1;
+ return b;
+ }
+
+ public static void UseStaticOperatorOnArrayElement(BothOperators[] arr, int n)
+ {
+ // Same shape as UseStaticOperator, but the target is an array element rather than a
+ // local: compound assignments to locals take a separate path through the decompiler.
+ arr[0] = arr[0] + n;
+ arr[0] = arr[0] + 1;
+ }
+
+ public static void UseStaticOperatorOnField(UserDefinedCompoundAssignment inst, int n)
+ {
+ inst.bothField = inst.bothField + n;
+ }
+
+ public static void UseInstanceOperator(BothOperators b, int n)
+ {
+ b += n;
+ b++;
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
index 0a2cd75f83..151e1ec2f8 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
@@ -794,6 +794,7 @@ static TypeSystemAstBuilder CreateAstBuilder(DecompilerSettings settings)
typeSystemAstBuilder.SupportOperatorChecked = settings.CheckedOperators;
typeSystemAstBuilder.AlwaysUseGlobal = settings.AlwaysUseGlobal;
typeSystemAstBuilder.SupportExtensionDeclarations = settings.ExtensionMembers;
+ typeSystemAstBuilder.SupportUserDefinedCompoundAssignmentOperators = settings.UserDefinedCompoundAssignmentOperators;
return typeSystemAstBuilder;
}
@@ -2219,6 +2220,10 @@ EntityDeclaration DoDecompile(IMethod method, DecompileRun decompileRun, ITypeRe
{
RemoveObsoleteAttribute(methodDecl, "Constructors of types with required members are not supported in this version of your compiler.");
}
+ if (methodDecl is OperatorDeclaration operatorDecl && OperatorDeclaration.IsCompoundAssignment(operatorDecl.OperatorType))
+ {
+ RemoveCompilerFeatureRequiredAttribute(methodDecl, "UserDefinedCompoundAssignmentOperators");
+ }
return methodDecl;
bool IsTypeHierarchyKnown(IType type)
diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
index 1efe31af69..c41e0908c2 100644
--- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
@@ -1586,7 +1586,7 @@ OverloadResolutionErrors IsUnambiguousCall(ExpectedTargetDetails expectedTargetD
}
}
}
- else if (method.IsOperator)
+ else if (method.IsOperator && method.IsStatic)
{
IEnumerable operatorCandidates;
if (arguments.Length == 1)
@@ -1622,6 +1622,15 @@ OverloadResolutionErrors IsUnambiguousCall(ExpectedTargetDetails expectedTargetD
or.AddCandidate(m);
}
}
+ else if (method.IsOperator && !method.IsStatic && target != null)
+ {
+ // C# 14 compound assignment operators are instance methods; member lookup skips
+ // operators, so collect the candidates from the receiver type instead.
+ foreach (var m in target.Type.GetMethods(m => m.IsOperator && !m.IsStatic && m.Name == method.Name))
+ {
+ or.AddCandidate(m);
+ }
+ }
else if (target == null)
{
var result = resolver.ResolveSimpleName(method.Name, typeArguments, isInvocationTarget: true)
@@ -1643,6 +1652,13 @@ OverloadResolutionErrors IsUnambiguousCall(ExpectedTargetDetails expectedTargetD
if (or.IsAmbiguous)
return OverloadResolutionErrors.AmbiguousMatch;
foundMember = or.GetBestCandidateWithSubstitutedTypeArguments();
+ if (foundMember == null)
+ {
+ // No candidate at all: overload resolution reports no error for an empty candidate
+ // set, so this is the only place the caller learns that the call cannot be spelled
+ // out with the arguments it has.
+ return OverloadResolutionErrors.AmbiguousMatch;
+ }
if (!IsAppropriateCallTarget(expectedTargetDetails, method, foundMember))
return OverloadResolutionErrors.AmbiguousMatch;
var map = or.GetArgumentToParameterMap();
diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
index 509c3ea7c4..12e4314f09 100644
--- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
+++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
@@ -286,6 +286,7 @@ TypeSystemAstBuilder CreateAstBuilder()
astBuilder.SupportUnsignedRightShift = (ConversionFlags & ConversionFlags.SupportUnsignedRightShift) != 0;
astBuilder.SupportOperatorChecked = (ConversionFlags & ConversionFlags.SupportOperatorChecked) != 0;
astBuilder.SupportExtensionDeclarations = (ConversionFlags & ConversionFlags.SupportExtensionDeclarations) != 0;
+ astBuilder.SupportUserDefinedCompoundAssignmentOperators = (ConversionFlags & ConversionFlags.SupportUserDefinedCompoundAssignmentOperators) != 0;
return astBuilder;
}
@@ -394,7 +395,8 @@ void WriteMemberDeclarationName(IMember member, TokenWriter writer, CSharpFormat
writer.WriteKeyword("operator");
writer.Space();
var operatorType = OperatorDeclaration.GetOperatorType(name);
- if (operatorType.HasValue && !((ConversionFlags & ConversionFlags.SupportOperatorChecked) == 0 && OperatorDeclaration.IsChecked(operatorType.Value)))
+ if (operatorType.HasValue && !((ConversionFlags & ConversionFlags.SupportOperatorChecked) == 0 && OperatorDeclaration.IsChecked(operatorType.Value))
+ && !((ConversionFlags & ConversionFlags.SupportUserDefinedCompoundAssignmentOperators) == 0 && OperatorDeclaration.IsCompoundAssignment(operatorType.Value)))
{
if (OperatorDeclaration.IsChecked(operatorType.Value))
{
diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs
index b54f309d6a..55a2f8cde0 100644
--- a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs
+++ b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs
@@ -1289,7 +1289,8 @@ public IEnumerable GetUserDefinedOperatorCandidates(IType
return EmptyList.Instance;
}
// C# spec (draft-v11): §12.4.6 Candidate user-defined operators
- var operators = type.GetMethods(m => m.IsOperator && m.Name == operatorName).ToList();
+ // C# 14 instance compound-assignment operators are not candidates for the static operator forms.
+ var operators = type.GetMethods(m => m.IsOperator && m.IsStatic && m.Name == operatorName).ToList();
LiftUserDefinedOperators(operators);
return operators;
}
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/OperatorDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/OperatorDeclaration.cs
index 86bb76c76b..50ea0bbeee 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/OperatorDeclaration.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/OperatorDeclaration.cs
@@ -75,7 +75,28 @@ public enum OperatorType
// Implicit and Explicit
Implicit,
Explicit,
- CheckedExplicit
+ CheckedExplicit,
+
+ // C# 14 user-defined compound assignment (void-returning instance operators)
+ AdditionAssignment,
+ CheckedAdditionAssignment,
+ SubtractionAssignment,
+ CheckedSubtractionAssignment,
+ MultiplicationAssignment,
+ CheckedMultiplicationAssignment,
+ DivisionAssignment,
+ CheckedDivisionAssignment,
+ ModulusAssignment,
+ BitwiseAndAssignment,
+ BitwiseOrAssignment,
+ ExclusiveOrAssignment,
+ LeftShiftAssignment,
+ RightShiftAssignment,
+ UnsignedRightShiftAssignment,
+ IncrementAssignment,
+ CheckedIncrementAssignment,
+ DecrementAssignment,
+ CheckedDecrementAssignment
}
///
@@ -100,7 +121,7 @@ public sealed partial class OperatorDeclaration : EntityDeclaration
static OperatorDeclaration()
{
- names = new string[(int)OperatorType.CheckedExplicit + 1][];
+ names = new string[(int)OperatorType.CheckedDecrementAssignment + 1][];
names[(int)OperatorType.LogicalNot] = new string[] { "!", "op_LogicalNot" };
names[(int)OperatorType.OnesComplement] = new string[] { "~", "op_OnesComplement" };
names[(int)OperatorType.Increment] = new string[] { "++", "op_Increment" };
@@ -136,6 +157,25 @@ static OperatorDeclaration()
names[(int)OperatorType.Implicit] = new string[] { "implicit", "op_Implicit" };
names[(int)OperatorType.Explicit] = new string[] { "explicit", "op_Explicit" };
names[(int)OperatorType.CheckedExplicit] = new string[] { "explicit", "op_CheckedExplicit" };
+ names[(int)OperatorType.AdditionAssignment] = new string[] { "+=", "op_AdditionAssignment" };
+ names[(int)OperatorType.CheckedAdditionAssignment] = new string[] { "+=", "op_CheckedAdditionAssignment" };
+ names[(int)OperatorType.SubtractionAssignment] = new string[] { "-=", "op_SubtractionAssignment" };
+ names[(int)OperatorType.CheckedSubtractionAssignment] = new string[] { "-=", "op_CheckedSubtractionAssignment" };
+ names[(int)OperatorType.MultiplicationAssignment] = new string[] { "*=", "op_MultiplicationAssignment" };
+ names[(int)OperatorType.CheckedMultiplicationAssignment] = new string[] { "*=", "op_CheckedMultiplicationAssignment" };
+ names[(int)OperatorType.DivisionAssignment] = new string[] { "/=", "op_DivisionAssignment" };
+ names[(int)OperatorType.CheckedDivisionAssignment] = new string[] { "/=", "op_CheckedDivisionAssignment" };
+ names[(int)OperatorType.ModulusAssignment] = new string[] { "%=", "op_ModulusAssignment" };
+ names[(int)OperatorType.BitwiseAndAssignment] = new string[] { "&=", "op_BitwiseAndAssignment" };
+ names[(int)OperatorType.BitwiseOrAssignment] = new string[] { "|=", "op_BitwiseOrAssignment" };
+ names[(int)OperatorType.ExclusiveOrAssignment] = new string[] { "^=", "op_ExclusiveOrAssignment" };
+ names[(int)OperatorType.LeftShiftAssignment] = new string[] { "<<=", "op_LeftShiftAssignment" };
+ names[(int)OperatorType.RightShiftAssignment] = new string[] { ">>=", "op_RightShiftAssignment" };
+ names[(int)OperatorType.UnsignedRightShiftAssignment] = new string[] { ">>>=", "op_UnsignedRightShiftAssignment" };
+ names[(int)OperatorType.IncrementAssignment] = new string[] { "++", "op_IncrementAssignment" };
+ names[(int)OperatorType.CheckedIncrementAssignment] = new string[] { "++", "op_CheckedIncrementAssignment" };
+ names[(int)OperatorType.DecrementAssignment] = new string[] { "--", "op_DecrementAssignment" };
+ names[(int)OperatorType.CheckedDecrementAssignment] = new string[] { "--", "op_CheckedDecrementAssignment" };
}
public override SymbolKind SymbolKind {
@@ -201,10 +241,25 @@ public static bool IsChecked(OperatorType type)
OperatorType.CheckedIncrement => true,
OperatorType.CheckedDecrement => true,
OperatorType.CheckedExplicit => true,
+ OperatorType.CheckedAdditionAssignment => true,
+ OperatorType.CheckedSubtractionAssignment => true,
+ OperatorType.CheckedMultiplicationAssignment => true,
+ OperatorType.CheckedDivisionAssignment => true,
+ OperatorType.CheckedIncrementAssignment => true,
+ OperatorType.CheckedDecrementAssignment => true,
_ => false,
};
}
+ ///
+ /// Gets whether the operator type is a C# 14 user-defined compound assignment operator
+ /// (a void-returning instance operator, including the increment/decrement forms).
+ ///
+ public static bool IsCompoundAssignment(OperatorType type)
+ {
+ return type >= OperatorType.AdditionAssignment;
+ }
+
///
/// Gets the token for the operator type ("+", "implicit", etc.).
/// Does not include the "checked" modifier.
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
index 7daaa3f6e5..9635df5228 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
@@ -261,6 +261,11 @@ void InitProperties()
/// Controls whether C# 14 "extension" declarations are supported.
///
public bool SupportExtensionDeclarations { get; set; }
+
+ ///
+ /// Controls whether C# 14 user-defined compound assignment operators ("operator +=") are supported.
+ ///
+ public bool SupportUserDefinedCompoundAssignmentOperators { get; set; }
#endregion
#region Convert Type
@@ -2426,7 +2431,9 @@ EntityDeclaration ConvertOperator(IMethod op)
OperatorType? opType = OperatorDeclaration.GetOperatorType(name);
if (opType == null)
return ConvertMethod(op);
- if (opType == OperatorType.UnsignedRightShift && !SupportUnsignedRightShift)
+ if (opType is OperatorType.UnsignedRightShift or OperatorType.UnsignedRightShiftAssignment && !SupportUnsignedRightShift)
+ return ConvertMethod(op);
+ if (!SupportUserDefinedCompoundAssignmentOperators && OperatorDeclaration.IsCompoundAssignment(opType.Value))
return ConvertMethod(op);
if (!SupportOperatorChecked && OperatorDeclaration.IsChecked(opType.Value))
return ConvertMethod(op);
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs
index 9c89119b0e..04c689af95 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs
@@ -61,7 +61,8 @@ public override void VisitAssignmentExpression(AssignmentExpression assignment)
if (rhs is BinaryOperatorExpression binary && assignment.Operator == AssignmentOperatorType.Assign)
{
if (CanConvertToCompoundAssignment(assignment.Left) && assignment.Left.IsMatch(binary.Left)
- && binary.Right != null && IsImplicitlyConvertible(binary.Right, expectedType))
+ && binary.Right != null && IsImplicitlyConvertible(binary.Right, expectedType)
+ && !IsShadowedByInstanceOperator(binary))
{
var newOperator = GetAssignmentOperatorForBinaryOperator(binary.Operator);
if (newOperator != AssignmentOperatorType.Assign)
@@ -102,6 +103,14 @@ public override void VisitAssignmentExpression(AssignmentExpression assignment)
}
}
+ bool IsShadowedByInstanceOperator(BinaryOperatorExpression binary)
+ {
+ // "x = x + y" must keep calling the static operator: if the type also declares the
+ // C# 14 instance "operator +=", "x += y" binds to that one instead.
+ return binary.GetSymbol() is IMethod method
+ && IL.UserDefinedCompoundAssign.IsShadowedByInstanceOperator(method, context.Settings);
+ }
+
bool IsImplicitlyConvertible(Expression rhs, IType? expectedType)
{
if (expectedType == null)
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
index 6136ec4963..339ab47875 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
@@ -192,6 +192,68 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression)
context.EndStep(binaryOperator);
return;
}
+ if (context.Settings.UserDefinedCompoundAssignmentOperators && method is { IsOperator: true, IsStatic: false }
+ && invocationExpression.Target is MemberReferenceExpression targetMemberRef)
+ {
+ Expression assignmentTarget = targetMemberRef.Target;
+ if (assignmentTarget is CastExpression castExpr
+ && castExpr.Expression.GetResolveResult().Type is ITypeParameter typeParameter
+ && typeParameter.DirectBaseTypes.Any(t => t.Equals(castExpr.Type.GetResolveResult().Type)))
+ {
+ // Calls through a generic type parameter constrained to an interface declaring the
+ // operator are built as "((I)x).op_...(y)"; the cast is not a valid assignment
+ // target, and "x op= y" binds to the same operator via the constraint.
+ // Without that constraint the cast selects the operator and cannot be dropped.
+ assignmentTarget = castExpr.Expression;
+ }
+ if (!IsValidAssignmentTarget(assignmentTarget))
+ return;
+ AssignmentOperatorType? aop = GetCompoundAssignmentOperatorTypeFromMetadataName(method.Name, out isChecked, context.Settings);
+ if (aop != null && arguments.Length == 1)
+ {
+ context.Step("Replace instance operator method with compound assignment", invocationExpression);
+ if (isChecked)
+ {
+ invocationExpression.AddAnnotation(AddCheckedBlocks.CheckedAnnotation);
+ }
+ else if (HasCheckedEquivalent(method))
+ {
+ invocationExpression.AddAnnotation(AddCheckedBlocks.UncheckedAnnotation);
+ }
+ var assignment = new AssignmentExpression(
+ assignmentTarget.Detach(),
+ aop.Value,
+ arguments[0].Detach().UnwrapInDirectionExpression()
+ ).CopyAnnotationsFrom(invocationExpression);
+ invocationExpression.ReplaceWith(assignment);
+ context.EndStep(assignment);
+ return;
+ }
+ UnaryOperatorType? incDecOp = method.Name switch {
+ "op_IncrementAssignment" => UnaryOperatorType.PostIncrement,
+ "op_DecrementAssignment" => UnaryOperatorType.PostDecrement,
+ "op_CheckedIncrementAssignment" when context.Settings.CheckedOperators => UnaryOperatorType.PostIncrement,
+ "op_CheckedDecrementAssignment" when context.Settings.CheckedOperators => UnaryOperatorType.PostDecrement,
+ _ => null,
+ };
+ if (incDecOp != null && arguments.Length == 0)
+ {
+ context.Step("Replace instance operator method with increment/decrement", invocationExpression);
+ if (method.Name is "op_CheckedIncrementAssignment" or "op_CheckedDecrementAssignment")
+ {
+ invocationExpression.AddAnnotation(AddCheckedBlocks.CheckedAnnotation);
+ }
+ else if (HasCheckedEquivalent(method))
+ {
+ invocationExpression.AddAnnotation(AddCheckedBlocks.UncheckedAnnotation);
+ }
+ var incDec = new UnaryOperatorExpression(incDecOp.Value, assignmentTarget.Detach())
+ .CopyAnnotationsFrom(invocationExpression);
+ invocationExpression.ReplaceWith(incDec);
+ context.EndStep(incDec);
+ return;
+ }
+ }
UnaryOperatorType? uop = GetUnaryOperatorTypeFromMetadataName(method.Name, out isChecked, context.Settings);
if (uop != null && arguments.Length == 1)
{
@@ -517,6 +579,71 @@ static bool ToStringIsKnownEffectFree(IType type)
}
}
+ static AssignmentOperatorType? GetCompoundAssignmentOperatorTypeFromMetadataName(string name, out bool isChecked, DecompilerSettings settings)
+ {
+ isChecked = false;
+ switch (name)
+ {
+ case "op_AdditionAssignment":
+ return AssignmentOperatorType.Add;
+ case "op_CheckedAdditionAssignment" when settings.CheckedOperators:
+ isChecked = true;
+ return AssignmentOperatorType.Add;
+ case "op_SubtractionAssignment":
+ return AssignmentOperatorType.Subtract;
+ case "op_CheckedSubtractionAssignment" when settings.CheckedOperators:
+ isChecked = true;
+ return AssignmentOperatorType.Subtract;
+ case "op_MultiplicationAssignment":
+ return AssignmentOperatorType.Multiply;
+ case "op_CheckedMultiplicationAssignment" when settings.CheckedOperators:
+ isChecked = true;
+ return AssignmentOperatorType.Multiply;
+ case "op_DivisionAssignment":
+ return AssignmentOperatorType.Divide;
+ case "op_CheckedDivisionAssignment" when settings.CheckedOperators:
+ isChecked = true;
+ return AssignmentOperatorType.Divide;
+ case "op_ModulusAssignment":
+ return AssignmentOperatorType.Modulus;
+ case "op_BitwiseAndAssignment":
+ return AssignmentOperatorType.BitwiseAnd;
+ case "op_BitwiseOrAssignment":
+ return AssignmentOperatorType.BitwiseOr;
+ case "op_ExclusiveOrAssignment":
+ return AssignmentOperatorType.ExclusiveOr;
+ case "op_LeftShiftAssignment":
+ return AssignmentOperatorType.ShiftLeft;
+ case "op_RightShiftAssignment":
+ return AssignmentOperatorType.ShiftRight;
+ case "op_UnsignedRightShiftAssignment" when settings.UnsignedRightShift:
+ return AssignmentOperatorType.UnsignedShiftRight;
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// Gets whether the expression is classified as a variable, the only thing a user-defined
+ /// compound assignment operator can be applied to. Instance operator calls carry their
+ /// receiver as the call target, which is under no such restriction: properties and indexers
+ /// route through the static operator instead, and hand-written IL can call the operator on
+ /// any value at all.
+ ///
+ static bool IsValidAssignmentTarget(Expression expression)
+ {
+ return expression switch {
+ BaseReferenceExpression => false,
+ IndexerExpression => expression.GetSymbol() is not IProperty,
+ _ => expression.GetResolveResult() switch {
+ ILVariableResolveResult => true,
+ ThisResolveResult => true,
+ MemberResolveResult mrr => mrr.Member is IField,
+ _ => false,
+ }
+ };
+ }
+
static readonly Expression getMethodOrConstructorFromHandlePattern =
new CastExpression(new Choice {
new TypePattern(typeof(MethodInfo)),
diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs
index be0f91eebf..8728663a0a 100644
--- a/ICSharpCode.Decompiler/DecompilerSettings.cs
+++ b/ICSharpCode.Decompiler/DecompilerSettings.cs
@@ -862,6 +862,13 @@ public bool LifetimeAnnotations {
[DecompilerSetting(CSharp.LanguageVersion.CSharp14_0)]
public partial bool FieldKeyword { get; set; }
+ ///
+ /// Gets/Sets whether C# 14.0 user-defined compound assignment operators should be used.
+ ///
+ [Description("DecompilerSettings.UserDefinedCompoundAssignmentOperators")]
+ [DecompilerSetting(CSharp.LanguageVersion.CSharp14_0)]
+ public partial bool UserDefinedCompoundAssignmentOperators { get; set; }
+
///
/// Gets/sets whether the decompiler should separate local variable declarations
/// from their initialization.
diff --git a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs
index 85f49aad6e..f0f82b7410 100644
--- a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs
+++ b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs
@@ -19,6 +19,7 @@
using System;
using System.Diagnostics;
+using System.Linq;
using System.Linq.Expressions;
using ICSharpCode.Decompiler.TypeSystem;
@@ -322,6 +323,8 @@ public static bool IsIncrementOrDecrement(IMethod method, DecompilerSettings? se
{
if (!(method.IsOperator && method.IsStatic))
return false;
+ if (IsShadowedByInstanceOperator(method, settings))
+ return false;
if (method.Name is "op_Increment" or "op_Decrement")
return true;
if (method.Name is "op_CheckedIncrement" or "op_CheckedDecrement")
@@ -329,6 +332,47 @@ public static bool IsIncrementOrDecrement(IMethod method, DecompilerSettings? se
return false;
}
+ ///
+ /// Gets whether the declaring type of the static operator also
+ /// declares the matching C# 14 instance compound assignment operator. C# 14 overload
+ /// resolution prefers the instance operator for "x op= y", so folding the static call into
+ /// a compound assignment would make the recompiled code call a different method.
+ ///
+ public static bool IsShadowedByInstanceOperator(IMethod method, DecompilerSettings? settings = null)
+ {
+ if (settings?.UserDefinedCompoundAssignmentOperators == false)
+ return false;
+ string? name = GetCompoundAssignmentOperatorName(method.Name);
+ if (name == null)
+ return false;
+ // Both the checked and the unchecked instance operator shadow the static one: which of
+ // them applies depends on the checked context the assignment ends up in.
+ string checkedName = "op_Checked" + name.Substring("op_".Length);
+ return method.DeclaringType
+ .GetMethods(m => !m.IsStatic && m.IsOperator && (m.Name == name || m.Name == checkedName))
+ .Any();
+ }
+
+ static string? GetCompoundAssignmentOperatorName(string staticOperatorName)
+ {
+ return staticOperatorName switch {
+ "op_Addition" or "op_CheckedAddition" => "op_AdditionAssignment",
+ "op_Subtraction" or "op_CheckedSubtraction" => "op_SubtractionAssignment",
+ "op_Multiply" or "op_CheckedMultiply" => "op_MultiplicationAssignment",
+ "op_Division" or "op_CheckedDivision" => "op_DivisionAssignment",
+ "op_Modulus" => "op_ModulusAssignment",
+ "op_BitwiseAnd" => "op_BitwiseAndAssignment",
+ "op_BitwiseOr" => "op_BitwiseOrAssignment",
+ "op_ExclusiveOr" => "op_ExclusiveOrAssignment",
+ "op_LeftShift" => "op_LeftShiftAssignment",
+ "op_RightShift" => "op_RightShiftAssignment",
+ "op_UnsignedRightShift" => "op_UnsignedRightShiftAssignment",
+ "op_Increment" or "op_CheckedIncrement" => "op_IncrementAssignment",
+ "op_Decrement" or "op_CheckedDecrement" => "op_DecrementAssignment",
+ _ => null,
+ };
+ }
+
public static bool IsStringConcat(IMethod method)
{
return method.Name == "Concat" && method.IsStatic && method.DeclaringType.IsKnownType(KnownTypeCode.String);
diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
index 6c0c98c880..cbe13c8d01 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
@@ -264,6 +264,13 @@ static bool DoInline(ILVariable v, ILInstruction inlinedExpression, ILInstructio
{
return false;
}
+ if (context.Settings.UserDefinedCompoundAssignmentOperators && IsCompoundAssignmentOperatorReceiver(loadInst))
+ {
+ // The receiver of a C# 14 instance compound assignment operator call becomes the
+ // target of "x op= y", which must remain a variable: inlining the expression that
+ // initializes it would produce "GetX() += y".
+ return false;
+ }
if (loadInst.OpCode == OpCode.LdLoca)
{
if (!IsGeneratedTemporaryForAddressOf((LdLoca)loadInst, v, inlinedExpression, options))
@@ -325,6 +332,16 @@ static bool IsStackAllocSpanConstructorArgument(ILInstruction loadInst)
|| newObj.Method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT));
}
+ ///
+ /// Returns true if is the receiver of a call to a C# 14
+ /// user-defined compound assignment operator (a void-returning instance operator).
+ ///
+ static bool IsCompoundAssignmentOperatorReceiver(ILInstruction loadInst)
+ {
+ return loadInst.Parent is CallInstruction { Method: { IsOperator: true, IsStatic: false } } call
+ && call.Arguments.Count > 0 && call.Arguments[0] == loadInst;
+ }
+
///
/// Is this a temporary variable generated by the C# compiler for instance method calls on value type values
///
diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
index 8edba814aa..a9615994af 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
@@ -386,10 +386,12 @@ internal static bool HandleCompoundAssign(ILInstruction compoundStore, Statement
binary, target, targetKind, binary.Right,
targetType, CompoundEvalMode.EvaluatesToNewValue);
}
- else if (setterValue is Call operatorCall && operatorCall.Method.IsOperator)
+ else if (setterValue is Call operatorCall && operatorCall.Method.IsOperator && operatorCall.Method.IsStatic)
{
if (operatorCall.Arguments.Count == 0)
return false;
+ if (UserDefinedCompoundAssign.IsShadowedByInstanceOperator(operatorCall.Method, context.Settings))
+ return false;
if (!IsMatchingCompoundLoad(operatorCall.Arguments[0], compoundStore, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: storeInSetter?.Variable))
return false;
ILInstruction rhs;
@@ -892,6 +894,8 @@ bool TransformPreIncDecOperatorWithInlineStore(Block block, int pos)
{
if (!(operatorCall.Method.Name == "op_Increment" || operatorCall.Method.Name == "op_Decrement"))
return false;
+ if (UserDefinedCompoundAssign.IsShadowedByInstanceOperator(operatorCall.Method, context.Settings))
+ return false;
if (operatorCall.IsLifted)
return false; // TODO: add tests and think about whether nullables need special considerations
ldloc = operatorCall.Arguments[0] as LdLoc;
@@ -977,6 +981,8 @@ bool TransformPostIncDecOperatorWithInlineStore(Block block, int pos)
{
if (!(operatorCall.Method.Name == "op_Increment" || operatorCall.Method.Name == "op_Decrement"))
return false;
+ if (UserDefinedCompoundAssign.IsShadowedByInstanceOperator(operatorCall.Method, context.Settings))
+ return false;
if (operatorCall.IsLifted)
return false; // TODO: add tests and think about whether nullables need special considerations
stloc = operatorCall.Arguments[0] as StLoc;
diff --git a/ICSharpCode.Decompiler/Output/IAmbience.cs b/ICSharpCode.Decompiler/Output/IAmbience.cs
index f88a8576e3..a60d6f2665 100644
--- a/ICSharpCode.Decompiler/Output/IAmbience.cs
+++ b/ICSharpCode.Decompiler/Output/IAmbience.cs
@@ -125,6 +125,10 @@ public enum ConversionFlags
/// Support C# 14 extension declarations.
///
SupportExtensionDeclarations = 0x400000,
+ ///
+ /// Support C# 14 user-defined compound assignment operators (operator +=).
+ ///
+ SupportUserDefinedCompoundAssignmentOperators = 0x800000,
StandardConversionFlags = ShowParameterNames |
ShowAccessibility |
diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs
index 8f6e8f06be..eaa046c402 100644
--- a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs
@@ -234,6 +234,7 @@ internal bool IgnoreAttribute(TopLevelTypeName attributeType, SymbolKind target)
return (options & TypeSystemOptions.ReadOnlyStructsAndParameters) != 0;
case SymbolKind.Method:
case SymbolKind.Accessor:
+ case SymbolKind.Operator:
return (options & TypeSystemOptions.ReadOnlyMethods) != 0;
case SymbolKind.ReturnType:
case SymbolKind.Property:
diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs
index 1f69799d07..70c171e1bb 100644
--- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs
@@ -83,7 +83,8 @@ internal MetadataMethod(MetadataModule module, MethodDefinitionHandle handle)
this.symbolKind = SymbolKind.Constructor;
}
else if (name.StartsWith("op_", StringComparison.Ordinal)
- && CSharp.Syntax.OperatorDeclaration.GetOperatorType(name) != null)
+ && CSharp.Syntax.OperatorDeclaration.GetOperatorType(name) is CSharp.Syntax.OperatorType operatorType
+ && (!CSharp.Syntax.OperatorDeclaration.IsCompoundAssignment(operatorType) || IsCompoundAssignmentOperatorSignature()))
{
this.symbolKind = SymbolKind.Operator;
}
@@ -96,7 +97,7 @@ internal MetadataMethod(MetadataModule module, MethodDefinitionHandle handle)
this.symbolKind = SymbolKind.Destructor;
}
}
- else if ((attributes & MethodAttributes.Static) != 0 && typeParameters.Length == 0)
+ else if (typeParameters.Length == 0)
{
// Operators that are explicit interface implementations are not marked
// with MethodAttributes.SpecialName or MethodAttributes.RTSpecialName
@@ -107,7 +108,10 @@ internal MetadataMethod(MetadataModule module, MethodDefinitionHandle handle)
name = name.Substring(index + 1);
if (name.StartsWith("op_", StringComparison.Ordinal)
- && CSharp.Syntax.OperatorDeclaration.GetOperatorType(name) != null)
+ && CSharp.Syntax.OperatorDeclaration.GetOperatorType(name) is CSharp.Syntax.OperatorType operatorType
+ && (CSharp.Syntax.OperatorDeclaration.IsCompoundAssignment(operatorType)
+ ? IsCompoundAssignmentOperatorSignature()
+ : (attributes & MethodAttributes.Static) != 0))
{
this.symbolKind = SymbolKind.Operator;
}
@@ -118,6 +122,18 @@ internal MetadataMethod(MetadataModule module, MethodDefinitionHandle handle)
&& def.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.Extension);
}
+ ///
+ /// Gets whether this method has the shape C# requires of a user-defined compound assignment
+ /// operator. Other languages give unrelated methods the same metadata names: F# mangles
+ /// "static member (+=)" to a static, value-returning op_AdditionAssignment, and C++/CLI emits
+ /// value-returning instance operators. Those are plain methods as far as C# is concerned.
+ ///
+ bool IsCompoundAssignmentOperatorSignature()
+ {
+ return (attributes & MethodAttributes.Static) == 0
+ && ReturnType.IsKnownType(KnownTypeCode.Void);
+ }
+
public EntityHandle MetadataToken => handle;
public override string ToString()
diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
index 9ecc669868..4c6d235507 100644
--- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
+++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
@@ -149,7 +149,7 @@ class ILSpyCmdProgram
[Option("-lv|--languageversion ", "C# Language version: CSharp1, CSharp2, CSharp3, " +
"CSharp4, CSharp5, CSharp6, CSharp7, CSharp7_1, CSharp7_2, CSharp7_3, CSharp8_0, CSharp9_0, " +
- "CSharp10_0, CSharp11_0, CSharp12_0, CSharp13_0, Preview or Latest", CommandOptionType.SingleValue)]
+ "CSharp10_0, CSharp11_0, CSharp12_0, CSharp13_0, CSharp14_0, CSharp15_0, Preview or Latest", CommandOptionType.SingleValue)]
public LanguageVersion LanguageVersion { get; } = LanguageVersion.Latest;
[FileExists]
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index f65ae29f58..9782fb6dd9 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -654,6 +654,9 @@ Are you sure you want to continue?
Use variable names from debug symbols, if available
+
+ User-defined compound assignment operators
+
UTF-8 string literals