diff --git a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs index a00fc4425..0e898a8f5 100644 --- a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs @@ -145,6 +145,12 @@ namespace ICSharpCode.Decompiler.Tests CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest, }, executesCompiledOutput: true); + static readonly CompilerOptions[] roslyn5OrNewerOptions = Tester.SupportedOnCurrentPlatform(new[] + { + CompilerOptions.UseRoslynLatest, + CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest, + }); + static readonly CompilerOptions[] roslyn2OrNewerOptions = Tester.SupportedOnCurrentPlatform(new[] { CompilerOptions.UseRoslyn2_10_0 | CompilerOptions.TargetNet40, @@ -211,6 +217,18 @@ namespace ICSharpCode.Decompiler.Tests await RunCS(options: options); } + [Test] + public async Task UserDefinedCompoundAssignment([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions options) + { + await RunCS(options: options); + } + + [Test] + public async Task UserDefinedCompoundAssignmentInheritance([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions options) + { + await RunCS(options: options); + } + [Test] public async Task PropertiesAndEvents([ValueSource(nameof(defaultOptions))] CompilerOptions options) { diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index af25b8827..ca125f4bb 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -209,6 +209,9 @@ + + + diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index 7e9f8df94..44e1d8e8a 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -345,6 +345,12 @@ namespace ICSharpCode.Decompiler.Tests await Run(); } + [Test] + public async Task CompoundAssignmentOperatorEdgeCases() + { + await Run(); + } + [Test] public async Task FSharpLoops_Debug() { diff --git a/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs b/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs index 1c7db3227..f39402e68 100644 --- a/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs +++ b/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs @@ -59,7 +59,8 @@ namespace ICSharpCode.Decompiler.Tests.Output return foundType; } - const ConversionFlags ILSpyMainTreeViewTypeFlags = ShowTypeParameterList | PlaceReturnTypeAfterParameterList; + const ConversionFlags ILSpyMainTreeViewTypeFlags = ShowTypeParameterList | PlaceReturnTypeAfterParameterList + | SupportOperatorChecked | SupportUnsignedRightShift | SupportUserDefinedCompoundAssignmentOperators; const ConversionFlags ILSpyMainTreeViewMemberFlags = ILSpyMainTreeViewTypeFlags | ShowParameterList | ShowReturnType | ShowParameterModifiers; #region ITypeDefinition tests @@ -311,6 +312,41 @@ namespace ICSharpCode.Decompiler.Tests.Output Assert.That(ambience.ConvertSymbol(prop), Is.EqualTo(expectedOutput)); } + const ConversionFlags TooltipFlags = All & ~(ShowBody | PlaceReturnTypeAfterParameterList); + + [TestCase(StandardConversionFlags, "public void operator op_AdditionAssignment(int rhs);")] + [TestCase(StandardConversionFlags | SupportUserDefinedCompoundAssignmentOperators, "public void operator +=(int rhs);")] + [TestCase(ILSpyMainTreeViewMemberFlags, "operator +=(int) : void")] + [TestCase(TooltipFlags, "public void ICSharpCode.Decompiler.Tests.Output.CSharpAmbienceTests.Program.operator +=(int rhs)")] + public void CompoundAssignmentOperatorTests(ConversionFlags flags, string expectedOutput) + { + var op = compilation.FindType(typeof(CSharpAmbienceTests.Program)) + .GetMethods(m => m.Name == "op_AdditionAssignment").Single(); + ambience.ConversionFlags = flags; + + Assert.That(ambience.ConvertSymbol(op), Is.EqualTo(expectedOutput)); + } + + [TestCase(ILSpyMainTreeViewMemberFlags, "operator checked +=(int) : void")] + public void CheckedCompoundAssignmentOperatorTests(ConversionFlags flags, string expectedOutput) + { + var op = compilation.FindType(typeof(CSharpAmbienceTests.Program)) + .GetMethods(m => m.Name == "op_CheckedAdditionAssignment").Single(); + ambience.ConversionFlags = flags; + + Assert.That(ambience.ConvertSymbol(op), Is.EqualTo(expectedOutput)); + } + + [TestCase(ILSpyMainTreeViewMemberFlags, "operator ++() : void")] + public void InstanceIncrementOperatorTests(ConversionFlags flags, string expectedOutput) + { + var op = compilation.FindType(typeof(CSharpAmbienceTests.Program)) + .GetMethods(m => m.Name == "op_IncrementAssignment").Single(); + ambience.ConversionFlags = flags; + + Assert.That(ambience.ConvertSymbol(op), Is.EqualTo(expectedOutput)); + } + [TestCase(StandardConversionFlags, "~Program();")] [TestCase(ILSpyMainTreeViewMemberFlags, "~Program()")] public void DestructorTests(ConversionFlags flags, string expectedOutput) @@ -441,6 +477,21 @@ namespace ICSharpCode.Decompiler.Tests.Output throw new NotImplementedException(); } + public void operator +=(int rhs) + { + throw new NotImplementedException(); + } + + public void operator checked +=(int rhs) + { + throw new NotImplementedException(); + } + + public void operator ++() + { + throw new NotImplementedException(); + } + public static implicit operator Test(Program lhs) { throw new NotImplementedException(); diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 01b6228c9..45afed5cb 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -836,6 +836,12 @@ namespace ICSharpCode.Decompiler.Tests 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/Correctness/UserDefinedCompoundAssignment.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignment.cs new file mode 100644 index 000000000..f906572e2 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignment.cs @@ -0,0 +1,329 @@ +// 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; +using System.Collections.Generic; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness +{ + class UserDefinedCompoundAssignment + { + static void Main() + { + InstanceOperator(); + InheritedOperator(); + ShadowedOperator(); + StaticOperatorStaysStatic(); + StaticOperatorOnRefLocalStaysStatic(); + StaticTypeSelectsTheOperator(); + IncrementDependsOnWhetherTheResultIsUsed(); + StructOperator(); + GenericOperator(); + ForeachTarget(); + UsingTarget(); + InParameterTarget(new Base { Value = 40 }); + ReadonlyFieldTarget(); + } + + class Base + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine("Base.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + + public void operator ++() + { + Console.WriteLine("Base.op_IncrementAssignment()"); + Value++; + } + } + + class Derived : Base + { + } + + // Declares the same signature as Base, so "s += n" must bind here and not to Base. + class Shadowing : Base + { + public new void operator +=(int rhs) + { + Console.WriteLine("Shadowing.op_AdditionAssignment({0})", rhs); + Value += rhs * 100; + } + } + + // Declaring both forms means "b = b + n" and "b += n" do different things: the static + // operator produces a new instance, the instance one mutates the receiver. + class Both + { + public int Value; + + public static Both operator +(Both lhs, int rhs) + { + Console.WriteLine("Both.op_Addition({0})", rhs); + return new Both { Value = lhs.Value + rhs }; + } + + public void operator +=(int rhs) + { + Console.WriteLine("Both.op_AdditionAssignment({0})", rhs); + Value += rhs * 1000; + } + + public static Both operator ++(Both x) + { + Console.WriteLine("Both.op_Increment()"); + return new Both { Value = x.Value + 1 }; + } + + public void operator ++() + { + Console.WriteLine("Both.op_IncrementAssignment()"); + Value += 1000; + } + } + + // Only the derived type declares an instance operator. Which operator "x += y" binds to + // therefore depends on the declared type of x, not on the runtime type of the object. + class StaticBase + { + public int Value; + + public static StaticBase operator +(StaticBase lhs, int rhs) + { + Console.WriteLine("StaticBase.op_Addition({0})", rhs); + return new StaticBase { Value = lhs.Value + rhs }; + } + } + + class InstanceDerived : StaticBase + { + public void operator +=(int rhs) + { + Console.WriteLine("InstanceDerived.op_AdditionAssignment({0})", rhs); + Value += rhs * 10; + } + } + + struct Counter + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine("Counter.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + interface ICompound + { + void operator +=(int rhs); + } + + class ViaInterface : ICompound + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine("ViaInterface.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + static void InstanceOperator() + { + Base b = new Base { Value = 1 }; + b += 2; + b++; + Console.WriteLine("InstanceOperator: " + b.Value); + } + + static void InheritedOperator() + { + Derived d = new Derived { Value = 1 }; + d += 2; + d++; + Console.WriteLine("InheritedOperator: " + d.Value); + } + + static void StaticOperatorOnRefLocalStaysStatic() + { + Both[] arr = new Both[1] { new Both { Value = 1 } }; + ref Both r = ref arr[0]; + r = r + 5; + Console.WriteLine("StaticOperatorOnRefLocalStaysStatic: " + arr[0].Value); + } + + static void ShadowedOperator() + { + Shadowing s = new Shadowing { Value = 1 }; + s += 2; + Console.WriteLine("ShadowedOperator: " + s.Value); + } + + static void StaticOperatorStaysStatic() + { + Both b = new Both { Value = 1 }; + Both original = b; + b = b + 2; + Console.WriteLine("StaticOperatorStaysStatic: {0} {1} {2}", b.Value, original.Value, ReferenceEquals(b, original)); + + b += 2; + Console.WriteLine("InstanceOperatorMutatesInPlace: " + b.Value); + } + + static void StaticTypeSelectsTheOperator() + { + StaticBase asBase = new InstanceDerived { Value = 1 }; + asBase += 2; + Console.WriteLine("AsBase: " + asBase.Value); + + InstanceDerived asDerived = new InstanceDerived { Value = 1 }; + asDerived += 4; + Console.WriteLine("AsDerived: " + asDerived.Value); + } + + // A postfix increment whose result is used cannot call an operator that mutates in place, + // so it takes the static one even though an instance operator is declared. Discarding the + // result, or writing the prefix form, reaches the instance operator instead. + static void IncrementDependsOnWhetherTheResultIsUsed() + { + Both a = new Both { Value = 1 }; + Both old = a++; + Console.WriteLine("PostfixResultUsed: {0} {1} {2}", a.Value, old.Value, ReferenceEquals(a, old)); + + Both b = new Both { Value = 1 }; + b++; + Console.WriteLine("PostfixDiscarded: " + b.Value); + + Both c = new Both { Value = 1 }; + Both n = ++c; + Console.WriteLine("PrefixResultUsed: {0} {1} {2}", c.Value, n.Value, ReferenceEquals(c, n)); + } + + static void StructOperator() + { + Counter c = default(Counter); + c += 3; + c += 4; + Console.WriteLine("StructOperator: " + c.Value); + } + + static void UseGeneric(T x, int n) where T : ICompound + { + x += n; + } + + static void GenericOperator() + { + ViaInterface v = new ViaInterface { Value = 1 }; + UseGeneric(v, 5); + Console.WriteLine("GenericOperator: " + v.Value); + } + + sealed class DisposableHolder : IDisposable + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine("DisposableHolder.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + + public void Dispose() + { + Console.WriteLine("Dispose({0})", Value); + } + } + + static readonly Base readonlyField = new Base { Value = 10 }; + readonly Base readonlyInstanceField; + + static UserDefinedCompoundAssignment() + { + // A readonly field is a variable inside the matching constructor, so the + // operator form is legal here and has to stay. + readonlyField += 1; + } + + UserDefinedCompoundAssignment() + { + readonlyInstanceField = new Base { Value = 20 }; + readonlyInstanceField += 2; + } + + static void ForeachTarget() + { + // The iteration variable is read-only, so the operator can only be applied + // to a copy; the copy has to survive decompilation. + List list = new List { new Base(), new Base() }; + foreach (Base b in list) + { + Base copy = b; + copy += 3; + } + foreach (Base b in list) + { + Console.WriteLine("ForeachTarget: {0}", b.Value); + } + Base[] array = { new Base { Value = 30 } }; + foreach (Base b in array) + { + Base copy = b; + copy += 4; + } + Console.WriteLine("ForeachTargetArray: {0}", array[0].Value); + } + + static void UsingTarget() + { + // Same for a using variable. + using (DisposableHolder d = new DisposableHolder()) + { + DisposableHolder copy = d; + copy += 5; + } + } + + static void InParameterTarget(in Base b) + { + // And for an "in" parameter. + Base copy = b; + copy += 6; + Console.WriteLine("InParameterTarget: {0}", b.Value); + } + + static void ReadonlyFieldTarget() + { + // Outside the constructor the readonly field is not a variable, so the + // copy has to survive here as well. + Base copy = readonlyField; + copy += 7; + Console.WriteLine("ReadonlyFieldTarget: {0}", readonlyField.Value); + Console.WriteLine("ReadonlyInstanceField: {0}", new UserDefinedCompoundAssignment().readonlyInstanceField.Value); + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignmentInheritance.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignmentInheritance.cs new file mode 100644 index 000000000..ae7239c0e --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignmentInheritance.cs @@ -0,0 +1,681 @@ +// 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.Correctness +{ + // Every combination of where a static "operator +" and an instance "operator +=" are + // declared across a two-level hierarchy, used through both a base-typed and a + // derived-typed variable. The class name suffix says what each level declares: + // N nothing, S static only, I instance only, X both - so Base_IS declares an instance + // operator and its derived class declares a static one. + // + // Which operator runs depends on the declared type of the left operand, not on the + // runtime type, and an instance operator reachable from that type wins over any static + // one. "x = x + n" therefore has to survive decompilation as-is wherever both forms + // exist: folding it to "x += n" would call a different operator. + class UserDefinedCompoundAssignmentInheritance + { + static void Main() + { + Combo_NS(); + Combo_NI(); + Combo_NX(); + Combo_SN(); + Combo_SS(); + Combo_SI(); + Combo_SX(); + Combo_IN(); + Combo_IS(); + Combo_II(); + Combo_IX(); + Combo_XN(); + Combo_XS(); + Combo_XI(); + Combo_XX(); + Combo_BaseStaticReturnsDerived(); + Combo_InstanceSignatureDoesNotApply(); + Combo_DerivedOverloadHidesBase(); + } + + static void Combo_NS() + { + Console.WriteLine("NS derived-typed +=:"); + Derived_NS asDerived = new Derived_NS { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + static void Combo_NI() + { + Console.WriteLine("NI derived-typed +=:"); + Derived_NI asDerived = new Derived_NI { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + static void Combo_NX() + { + Console.WriteLine("NX derived-typed +=:"); + Derived_NX asDerived = new Derived_NX { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + Console.WriteLine("NX derived-typed x = x + n:"); + Derived_NX sDerived = new Derived_NX { Value = 1 }; + sDerived = sDerived + 4; + Console.WriteLine(" -> " + sDerived.Value); + } + + static void Combo_SN() + { + Console.WriteLine("SN base-typed +=:"); + Base_SN asBase = new Derived_SN { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + } + + static void Combo_SS() + { + Console.WriteLine("SS base-typed +=:"); + Base_SS asBase = new Derived_SS { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("SS derived-typed +=:"); + Derived_SS asDerived = new Derived_SS { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + static void Combo_SI() + { + Console.WriteLine("SI base-typed +=:"); + Base_SI asBase = new Derived_SI { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("SI derived-typed +=:"); + Derived_SI asDerived = new Derived_SI { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + static void Combo_SX() + { + Console.WriteLine("SX base-typed +=:"); + Base_SX asBase = new Derived_SX { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("SX derived-typed +=:"); + Derived_SX asDerived = new Derived_SX { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + Console.WriteLine("SX derived-typed x = x + n:"); + Derived_SX sDerived = new Derived_SX { Value = 1 }; + sDerived = sDerived + 4; + Console.WriteLine(" -> " + sDerived.Value); + } + + // The static operator is declared on the base but returns the derived type, so + // "x = x + n" is expressible on a derived-typed variable. Binary "+" only ever + // considers static operators, but "x += n" would find the derived class's instance + // operator first, so the assignment has to stay spelled out. + static void Combo_BaseStaticReturnsDerived() + { + Console.WriteLine("base static returning derived, derived-typed x = x + n:"); + Derived_SR asDerived = new Derived_SR { Value = 1 }; + asDerived = asDerived + 4; + Console.WriteLine(" -> " + asDerived.Value); + Console.WriteLine("base static returning derived, array element x = x + n:"); + Derived_SR[] arr = new Derived_SR[1] { new Derived_SR { Value = 1 } }; + arr[0] = arr[0] + 4; + Console.WriteLine(" -> " + arr[0].Value); + } + + // Overload resolution removes base-type candidates when an applicable candidate exists + // in a more derived type: on a derived-typed target even an int argument binds the + // derived operator taking long. The base-typed variable is what makes the int overload + // reachable, so it has to survive decompilation. + static void Combo_DerivedOverloadHidesBase() + { + Console.WriteLine("overload base-typed +=:"); + Derived_Overload target = new Derived_Overload { Value = 1 }; + Base_Overload asBase = target; + asBase += 2; + Console.WriteLine(" -> " + target.Value); + Console.WriteLine("overload derived-typed +=:"); + Derived_Overload asDerived = new Derived_Overload { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + // The instance operator takes an int, so it is not a candidate for a long argument + // and "x += n" binds the static operator: the assignment can be folded. + static void Combo_InstanceSignatureDoesNotApply() + { + Console.WriteLine("instance operator not applicable, long argument:"); + MixedSignature asLocal = new MixedSignature { Value = 1 }; + asLocal += 4L; + Console.WriteLine(" -> " + asLocal.Value); + Console.WriteLine("instance operator applicable, int argument:"); + MixedSignature asInt = new MixedSignature { Value = 1 }; + asInt += 4; + Console.WriteLine(" -> " + asInt.Value); + } + + static void Combo_IN() + { + Console.WriteLine("IN base-typed +=:"); + Base_IN asBase = new Derived_IN { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("IN derived-typed +=:"); + Derived_IN asDerived = new Derived_IN { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + static void Combo_IS() + { + Console.WriteLine("IS base-typed +=:"); + Base_IS asBase = new Derived_IS { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("IS derived-typed +=:"); + Derived_IS asDerived = new Derived_IS { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + Console.WriteLine("IS derived-typed x = x + n:"); + Derived_IS sDerived = new Derived_IS { Value = 1 }; + sDerived = sDerived + 4; + Console.WriteLine(" -> " + sDerived.Value); + } + + static void Combo_II() + { + Console.WriteLine("II base-typed +=:"); + Base_II asBase = new Derived_II { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("II derived-typed +=:"); + Derived_II asDerived = new Derived_II { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + static void Combo_IX() + { + Console.WriteLine("IX base-typed +=:"); + Base_IX asBase = new Derived_IX { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("IX derived-typed +=:"); + Derived_IX asDerived = new Derived_IX { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + Console.WriteLine("IX derived-typed x = x + n:"); + Derived_IX sDerived = new Derived_IX { Value = 1 }; + sDerived = sDerived + 4; + Console.WriteLine(" -> " + sDerived.Value); + } + + static void Combo_XN() + { + Console.WriteLine("XN base-typed +=:"); + Base_XN asBase = new Derived_XN { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("XN base-typed x = x + n:"); + Base_XN sBase = new Derived_XN { Value = 1 }; + sBase = sBase + 2; + Console.WriteLine(" -> " + sBase.Value); + Console.WriteLine("XN derived-typed +=:"); + Derived_XN asDerived = new Derived_XN { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + static void Combo_XS() + { + Console.WriteLine("XS base-typed +=:"); + Base_XS asBase = new Derived_XS { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("XS base-typed x = x + n:"); + Base_XS sBase = new Derived_XS { Value = 1 }; + sBase = sBase + 2; + Console.WriteLine(" -> " + sBase.Value); + Console.WriteLine("XS derived-typed +=:"); + Derived_XS asDerived = new Derived_XS { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + Console.WriteLine("XS derived-typed x = x + n:"); + Derived_XS sDerived = new Derived_XS { Value = 1 }; + sDerived = sDerived + 4; + Console.WriteLine(" -> " + sDerived.Value); + } + + static void Combo_XI() + { + Console.WriteLine("XI base-typed +=:"); + Base_XI asBase = new Derived_XI { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("XI base-typed x = x + n:"); + Base_XI sBase = new Derived_XI { Value = 1 }; + sBase = sBase + 2; + Console.WriteLine(" -> " + sBase.Value); + Console.WriteLine("XI derived-typed +=:"); + Derived_XI asDerived = new Derived_XI { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + } + + static void Combo_XX() + { + Console.WriteLine("XX base-typed +=:"); + Base_XX asBase = new Derived_XX { Value = 1 }; + asBase += 2; + Console.WriteLine(" -> " + asBase.Value); + Console.WriteLine("XX base-typed x = x + n:"); + Base_XX sBase = new Derived_XX { Value = 1 }; + sBase = sBase + 2; + Console.WriteLine(" -> " + sBase.Value); + Console.WriteLine("XX derived-typed +=:"); + Derived_XX asDerived = new Derived_XX { Value = 1 }; + asDerived += 4; + Console.WriteLine(" -> " + asDerived.Value); + Console.WriteLine("XX derived-typed x = x + n:"); + Derived_XX sDerived = new Derived_XX { Value = 1 }; + sDerived = sDerived + 4; + Console.WriteLine(" -> " + sDerived.Value); + } + + class Base_NS + { + public int Value; + } + + class Derived_NS : Base_NS + { + public static Derived_NS operator +(Derived_NS lhs, int rhs) + { + Console.WriteLine(" Derived_NS.op_Addition({0})", rhs); + return new Derived_NS { Value = lhs.Value + rhs }; + } + } + + class Base_NI + { + public int Value; + } + + class Derived_NI : Base_NI + { + public void operator +=(int rhs) + { + Console.WriteLine(" Derived_NI.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Base_NX + { + public int Value; + } + + class Derived_NX : Base_NX + { + public static Derived_NX operator +(Derived_NX lhs, int rhs) + { + Console.WriteLine(" Derived_NX.op_Addition({0})", rhs); + return new Derived_NX { Value = lhs.Value + rhs }; + } + + public void operator +=(int rhs) + { + Console.WriteLine(" Derived_NX.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Base_SN + { + public int Value; + + public static Base_SN operator +(Base_SN lhs, int rhs) + { + Console.WriteLine(" Base_SN.op_Addition({0})", rhs); + return new Base_SN { Value = lhs.Value + rhs }; + } + } + + class Derived_SN : Base_SN + { + } + + class Base_SS + { + public int Value; + + public static Base_SS operator +(Base_SS lhs, int rhs) + { + Console.WriteLine(" Base_SS.op_Addition({0})", rhs); + return new Base_SS { Value = lhs.Value + rhs }; + } + } + + class Derived_SS : Base_SS + { + public static Derived_SS operator +(Derived_SS lhs, int rhs) + { + Console.WriteLine(" Derived_SS.op_Addition({0})", rhs); + return new Derived_SS { Value = lhs.Value + rhs }; + } + } + + class Base_SI + { + public int Value; + + public static Base_SI operator +(Base_SI lhs, int rhs) + { + Console.WriteLine(" Base_SI.op_Addition({0})", rhs); + return new Base_SI { Value = lhs.Value + rhs }; + } + } + + class Derived_SI : Base_SI + { + public void operator +=(int rhs) + { + Console.WriteLine(" Derived_SI.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Base_SX + { + public int Value; + + public static Base_SX operator +(Base_SX lhs, int rhs) + { + Console.WriteLine(" Base_SX.op_Addition({0})", rhs); + return new Base_SX { Value = lhs.Value + rhs }; + } + } + + class Derived_SX : Base_SX + { + public static Derived_SX operator +(Derived_SX lhs, int rhs) + { + Console.WriteLine(" Derived_SX.op_Addition({0})", rhs); + return new Derived_SX { Value = lhs.Value + rhs }; + } + + public void operator +=(int rhs) + { + Console.WriteLine(" Derived_SX.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Base_IN + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_IN.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_IN : Base_IN + { + } + + class Base_IS + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_IS.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_IS : Base_IS + { + public static Derived_IS operator +(Derived_IS lhs, int rhs) + { + Console.WriteLine(" Derived_IS.op_Addition({0})", rhs); + return new Derived_IS { Value = lhs.Value + rhs }; + } + } + + class Base_II + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_II.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_II : Base_II + { + public new void operator +=(int rhs) + { + Console.WriteLine(" Derived_II.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Base_IX + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_IX.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_IX : Base_IX + { + public static Derived_IX operator +(Derived_IX lhs, int rhs) + { + Console.WriteLine(" Derived_IX.op_Addition({0})", rhs); + return new Derived_IX { Value = lhs.Value + rhs }; + } + + public new void operator +=(int rhs) + { + Console.WriteLine(" Derived_IX.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Base_XN + { + public int Value; + + public static Base_XN operator +(Base_XN lhs, int rhs) + { + Console.WriteLine(" Base_XN.op_Addition({0})", rhs); + return new Base_XN { Value = lhs.Value + rhs }; + } + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_XN.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_XN : Base_XN + { + } + + class Base_XS + { + public int Value; + + public static Base_XS operator +(Base_XS lhs, int rhs) + { + Console.WriteLine(" Base_XS.op_Addition({0})", rhs); + return new Base_XS { Value = lhs.Value + rhs }; + } + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_XS.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_XS : Base_XS + { + public static Derived_XS operator +(Derived_XS lhs, int rhs) + { + Console.WriteLine(" Derived_XS.op_Addition({0})", rhs); + return new Derived_XS { Value = lhs.Value + rhs }; + } + } + + class Base_XI + { + public int Value; + + public static Base_XI operator +(Base_XI lhs, int rhs) + { + Console.WriteLine(" Base_XI.op_Addition({0})", rhs); + return new Base_XI { Value = lhs.Value + rhs }; + } + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_XI.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_XI : Base_XI + { + public new void operator +=(int rhs) + { + Console.WriteLine(" Derived_XI.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Base_XX + { + public int Value; + + public static Base_XX operator +(Base_XX lhs, int rhs) + { + Console.WriteLine(" Base_XX.op_Addition({0})", rhs); + return new Base_XX { Value = lhs.Value + rhs }; + } + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_XX.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_XX : Base_XX + { + public static Derived_XX operator +(Derived_XX lhs, int rhs) + { + Console.WriteLine(" Derived_XX.op_Addition({0})", rhs); + return new Derived_XX { Value = lhs.Value + rhs }; + } + + public new void operator +=(int rhs) + { + Console.WriteLine(" Derived_XX.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Base_SR + { + public int Value; + + public static Derived_SR operator +(Base_SR lhs, int rhs) + { + Console.WriteLine(" Base_SR.op_Addition({0})", rhs); + return new Derived_SR { Value = lhs.Value + rhs }; + } + } + + class Derived_SR : Base_SR + { + public void operator +=(int rhs) + { + Console.WriteLine(" Derived_SR.op_AdditionAssignment({0})", rhs); + Value += rhs * 100; + } + } + + class MixedSignature + { + public int Value; + + public static MixedSignature operator +(MixedSignature lhs, long rhs) + { + Console.WriteLine(" MixedSignature.op_Addition({0})", rhs); + return new MixedSignature { Value = lhs.Value + (int)rhs }; + } + + public void operator +=(int rhs) + { + Console.WriteLine(" MixedSignature.op_AdditionAssignment({0})", rhs); + Value += rhs * 100; + } + } + + class Base_Overload + { + public int Value; + + public void operator +=(int rhs) + { + Console.WriteLine(" Base_Overload.op_AdditionAssignment({0})", rhs); + Value += rhs; + } + } + + class Derived_Overload : Base_Overload + { + public void operator +=(long rhs) + { + Console.WriteLine(" Derived_Overload.op_AdditionAssignment({0}L)", rhs); + Value += (int)rhs * 100; + } + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs new file mode 100644 index 000000000..7540202bc --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs @@ -0,0 +1,243 @@ +using System.Runtime.CompilerServices; + +public class BadArityTarget +{ + public int Value; + + // C# 14 compound assignment operators take exactly one parameter and the instance + // increment operators none; any other arity stays a plain method. + [SpecialName] + public void op_AdditionAssignment(int a, int b) + { + } + + [SpecialName] + public void op_SubtractionAssignment() + { + } + + [SpecialName] + public void op_IncrementAssignment(int a) + { + } +} +public class BadParameterShapes +{ + // C# allows only value, "in" and "ref readonly" parameters on a compound assignment + // operator; ref and params stay plain methods. + [SpecialName] + public void op_SubtractionAssignment(ref int rhs) + { + } + + [SpecialName] + public void op_MultiplicationAssignment(params int[] rhs) + { + } +} +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 CovariantBase +{ + public int Value; + + public static CovariantDerived operator ++(CovariantBase x) + { + return (CovariantDerived)x; + } +} +public class CovariantDerived : CovariantBase +{ + public void operator ++() + { + } +} +public class DerivedCompoundTarget : CompoundTarget +{ + public void CallBaseOperator(int n) + { + // "this" cannot be the target of "x op= y", so the receiver keeps a copy of it; + // the copy binds the same inherited operator the IL calls. + DerivedCompoundTarget derivedCompoundTarget = this; + derivedCompoundTarget += n; + } +} +public class EdgeCases +{ + public static int Sink; + + public static ShadowTarget Shared; + + // A statement-level "x++" considers the instance operator first; the only form that always + // binds the static operator is a postfix increment whose result is used, so the result goes + // to a discard. + public static void StatementLevelPostIncrement() + { + _ = Shared++; + } + + // A pre-increment whose result is used prefers the instance operator too, so the increment + // becomes a statement of its own before the uses of the new value. + public static void ValueUsedPreIncrement() + { + ShadowTarget shared = Shared; + _ = shared++; + UseValue(Shared = shared); + } + + public static void UseValue(ShadowTarget t) + { + } + + // The same holds for the shape that stores the old value inside the operator call. + public static void InlineStorePostIncrement() + { + _ = Shared++; + } + + // An array element is a variable, so a statement-level "++arr[0]" would bind the instance + // operator too. + public static void ArrayElementIncrement(ShadowTarget[] arr) + { + ShadowTarget shadowTarget = arr[0]; + _ = shadowTarget++; + arr[0] = shadowTarget; + } + + public static CompoundTarget GetTarget() + { + return new CompoundTarget(); + } + + public static void NonVariableReceiver(int n) + { + CompoundTarget target = GetTarget(); + target += n; + } + + public static void UnconstrainedGenericReceiver(T x, int n) where T : IOther + { + ICompound compound = (ICompound)(object)x; + compound += n; + } + + // Not "x += n": C# requires a user-defined operator to be public, so an operator that + // is not cannot be bound by the operator form and the call has to stay a call. + public static void InaccessibleOperator(InaccessibleOperatorTarget x, int n) + { + x.op_AdditionAssignment(n); + } + + public static void MismatchedReceiverType(object o, int n) + { + ((CompoundTarget)o).op_AdditionAssignment(n); + } + + // A prefix increment prefers the instance "operator ++()" even where its result is used, so + // the increment is a statement with a discarded postfix result; the copy keeps the + // parameter unchanged, as in the IL. + public static ShadowTarget ShadowedIncrement(ShadowTarget x) + { + ShadowTarget result = x; + _ = result++; + return result; + } + + public static void CallNonCSharpOperators(CompoundTarget t, int n) + { + t.op_SubtractionAssignment(n); + CompoundTarget.op_MultiplicationAssignment(t, n); + } + + // The static operator's covariant return lets the result be stored in a derived-typed + // local, and on that type "++x" would bind the instance operator; a postfix increment with + // its result discarded binds the static one. + public static CovariantDerived CovariantIncrement(CovariantDerived d) + { + CovariantDerived covariantDerived = d; + _ = covariantDerived++; + Sink = covariantDerived.Value; + return covariantDerived; + } + + // Not "x += n": the "in" overload is what the IL calls, but "x += n" binds the by-value + // overload, so the call has to stay a call. + public static void CallInOverload(InOverloads x, int n) + { + int rhs = n; + x.op_AdditionAssignment(in rhs); + } +} +public interface ICompound +{ + void operator +=(int rhs); +} +public class InaccessibleOperatorTarget +{ + public int Value; + + // Not "operator +=": a non-public operator is not declarable as one in C#. + private void op_AdditionAssignment(int rhs) + { + Value += rhs; + } +} +public class InOverloads +{ + public int Value; + + public void operator +=(int rhs) + { + } + + public void operator +=(in int rhs) + { + } +} +public interface IOther +{ + void M(); +} +public class ShadowTarget +{ + public int Value; + + public static ShadowTarget operator ++(ShadowTarget x) + { + return x; + } + + public void operator ++() + { + Value++; + } +} +public static class StaticClassOps +{ + // a static class cannot contain operators + [SpecialName] + public void op_AdditionAssignment(int rhs) + { + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il new file mode 100644 index 000000000..6e2ec1b15 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il @@ -0,0 +1,522 @@ +// 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 public auto ansi beforefieldinit InaccessibleOperatorTarget + extends [System.Runtime]System.Object +{ + .field public int32 Value + + // C# requires a user-defined operator to be public, so no C# expression can bind to + // this one and it cannot be declared as an operator either. + .method private hidebysig specialname instance void + op_AdditionAssignment(int32 rhs) cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.0 + IL_0002: ldfld int32 InaccessibleOperatorTarget::Value + IL_0007: ldarg.1 + IL_0008: add + IL_0009: stfld int32 InaccessibleOperatorTarget::Value + IL_000e: 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 ShadowTarget + extends [System.Runtime]System.Object +{ + .field public int32 Value + + // A static increment operator on a type that also declares the C# 14 instance one. + .method public hidebysig specialname static class ShadowTarget + op_Increment(class ShadowTarget x) cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ret + } + + .method public hidebysig specialname instance void + op_IncrementAssignment() cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.0 + IL_0002: ldfld int32 ShadowTarget::Value + IL_0007: ldc.i4.1 + IL_0008: add + IL_0009: stfld int32 ShadowTarget::Value + IL_000e: 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 public auto ansi beforefieldinit EdgeCases + extends [System.Runtime]System.Object +{ + .field public static int32 Sink + .field public static class ShadowTarget Shared + + // The post-increment shape legacy csc emits: the old value goes to a temp that nothing reads, + // so the increment is written as a statement, where C# considers the instance operator first. + .method public hidebysig static void StatementLevelPostIncrement() cil managed + { + .maxstack 2 + .locals init (class ShadowTarget V_0) + IL_0000: ldsfld class ShadowTarget EdgeCases::Shared + IL_0005: stloc.0 + IL_0006: ldloc.0 + IL_0007: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget) + IL_000c: stsfld class ShadowTarget EdgeCases::Shared + IL_0011: ret + } + + // A pre-increment whose result is used: a prefix increment would bind the instance operator, + // so the increment has to become a statement of its own before the uses of the new value. + .method public hidebysig static void ValueUsedPreIncrement() cil managed + { + .maxstack 2 + IL_0000: ldsfld class ShadowTarget EdgeCases::Shared + IL_0005: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget) + IL_000a: dup + IL_000b: stsfld class ShadowTarget EdgeCases::Shared + IL_0010: call void EdgeCases::UseValue(class ShadowTarget) + IL_0015: ret + } + + .method public hidebysig static void UseValue(class ShadowTarget t) cil managed + { + .maxstack 8 + IL_0000: ret + } + + // The other legacy post-increment shape, storing the old value inside the operator call: it is + // written as a statement too where nothing reads that value. + .method public hidebysig static void InlineStorePostIncrement() cil managed + { + .maxstack 3 + .locals init (class ShadowTarget V_0) + IL_0000: ldsfld class ShadowTarget EdgeCases::Shared + IL_0005: dup + IL_0006: stloc.0 + IL_0007: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget) + IL_000c: stsfld class ShadowTarget EdgeCases::Shared + IL_0011: ret + } + + // An array element is a variable, and "ldelem.ref" leaves the reader with the stack type + // rather than with the element type. + .method public hidebysig static void ArrayElementIncrement(class ShadowTarget[] arr) cil managed + { + .maxstack 3 + .locals init (class ShadowTarget V_0) + IL_0000: ldarg.0 + IL_0001: dup + IL_0002: ldc.i4.0 + IL_0003: ldelem.ref + IL_0004: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget) + IL_0009: stloc.0 + IL_000a: ldc.i4.0 + IL_000b: ldloc.0 + IL_000c: stelem.ref + IL_000d: ret + } + + .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 has to be given one before it can 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; it + // is the variable holding the cast value that becomes the assignment target. + .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 operator is not public, so the call cannot become "x += n". + .method public hidebysig static void InaccessibleOperator(class InaccessibleOperatorTarget x, + int32 n) cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: callvirt instance void InaccessibleOperatorTarget::op_AdditionAssignment(int32) + IL_0007: 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 + } + + // "x++" would bind the instance operator, so the call to the static one has to stay a call. + .method public hidebysig static class ShadowTarget + ShadowedIncrement(class ShadowTarget x) cil managed + { + .maxstack 8 + .locals init (class ShadowTarget V_0) + IL_0000: ldarg.0 + IL_0001: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget) + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: 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 + } + + // Not "++x": the static operator's covariant return lets the result be stored in a + // derived-typed local, and on that type "++x" would bind the instance operator. + .method public hidebysig static class CovariantDerived + CovariantIncrement(class CovariantDerived d) cil managed + { + .maxstack 8 + .locals init (class CovariantDerived V_0) + IL_0000: ldarg.0 + IL_0001: call class CovariantDerived CovariantBase::op_Increment(class CovariantBase) + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: ldfld int32 CovariantBase::Value + IL_000d: stsfld int32 EdgeCases::Sink + IL_0012: ldloc.0 + IL_0013: ret + } + + // Not "x += n": the "in" overload is what the IL calls, but "x += n" binds the by-value + // overload, so the call has to stay a call. + .method public hidebysig static void CallInOverload(class InOverloads x, int32 n) cil managed + { + .maxstack 8 + .locals init (int32 V_0) + IL_0000: ldarg.1 + IL_0001: stloc.0 + IL_0002: ldarg.0 + IL_0003: ldloca.s V_0 + IL_0005: callvirt instance void InOverloads::op_AdditionAssignment(int32&) + IL_000a: 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 public auto ansi beforefieldinit BadArityTarget + extends [System.Runtime]System.Object +{ + .field public int32 Value + + // C# 14 compound assignment operators take exactly one parameter and the instance + // increment operators none; any other arity stays a plain method. + .method public hidebysig specialname instance void + op_AdditionAssignment(int32 a, + int32 b) cil managed + { + .maxstack 8 + IL_0000: ret + } + + .method public hidebysig specialname instance void + op_SubtractionAssignment() cil managed + { + .maxstack 8 + IL_0000: ret + } + + .method public hidebysig specialname instance void + op_IncrementAssignment(int32 a) cil managed + { + .maxstack 8 + IL_0000: 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 public auto ansi beforefieldinit CovariantBase + extends [System.Runtime]System.Object +{ + .field public int32 Value + + .method public hidebysig specialname static class CovariantDerived + op_Increment(class CovariantBase x) cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: castclass CovariantDerived + IL_0006: 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 public auto ansi beforefieldinit CovariantDerived + extends CovariantBase +{ + .method public hidebysig specialname instance void + op_IncrementAssignment() cil managed + { + .maxstack 8 + IL_0000: ret + } + + .method public hidebysig specialname rtspecialname + instance void .ctor() cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: call instance void CovariantBase::.ctor() + IL_0006: ret + } +} + +.class public auto ansi beforefieldinit BadParameterShapes + extends [System.Runtime]System.Object +{ + // C# allows only value, "in" and "ref readonly" parameters on a compound assignment + // operator; ref and params stay plain methods. + .method public hidebysig specialname instance void + op_SubtractionAssignment(int32& rhs) cil managed + { + .maxstack 8 + ret + } + + .method public hidebysig specialname instance void + op_MultiplicationAssignment(int32[] rhs) cil managed + { + .param [1] + .custom instance void [System.Runtime]System.ParamArrayAttribute::.ctor() = ( 01 00 00 00 ) + .maxstack 8 + ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void [System.Runtime]System.Object::.ctor() + ret + } +} + +// a static class cannot contain operators +.class public abstract auto ansi sealed beforefieldinit StaticClassOps + extends [System.Runtime]System.Object +{ + .method public hidebysig specialname instance void + op_AdditionAssignment(int32 rhs) cil managed + { + .maxstack 8 + ret + } +} + +.class public auto ansi beforefieldinit InOverloads + extends [System.Runtime]System.Object +{ + .field public int32 Value + + .method public hidebysig specialname instance void + op_AdditionAssignment(int32 rhs) cil managed + { + .maxstack 8 + IL_0000: ret + } + + .method public hidebysig specialname instance void + op_AdditionAssignment([in] int32& rhs) cil managed + { + .param [1] + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) + .maxstack 8 + IL_0000: 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 000000000..52d9f1713 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedCompoundAssignment.cs @@ -0,0 +1,628 @@ +// 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; +using System.Collections.Generic; + +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; + } + + // "this" is not an assignable variable in a class, so the copy is what makes the + // operator form legal; it has to survive decompilation. + public void AddViaThisCopy(int n) + { + CompoundClass compoundClass = this; + compoundClass += n; + } + + public void IncrementViaThisCopy() + { + CompoundClass compoundClass = this; + compoundClass++; + } + } + + 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 OverridingCompoundClass : CompoundClass + { + public override void operator +=(long rhs) + { + Value += (int)(rhs * 2); + } + } + + public class ShadowingCompoundClass : CompoundClass + { + public new void operator +=(int rhs) + { + Value += rhs * 2; + } + } + + public class InParameterOperators + { + public int Value; + + public static InParameterOperators operator +(InParameterOperators lhs, in InParameterOperators rhs) + { + return new InParameterOperators { + Value = lhs.Value + rhs.Value + }; + } + + public void operator +=(in InParameterOperators rhs) + { + Value += rhs.Value; + } + } + + public class ShadowingInParameterOperators : InParameterOperators + { + public new void operator +=(in InParameterOperators rhs) + { + Value += rhs.Value * 2; + } + } + + public sealed class DisposableCompound : IDisposable + { + public int Value; + + public void operator +=(int rhs) + { + Value += rhs; + } + + public void Dispose() + { + } + } + + public class ReadonlyFieldHolder + { + public static readonly CompoundClass Shared; + + private readonly CompoundClass instance = new CompoundClass(); + + static ReadonlyFieldHolder() + { + Shared = new CompoundClass(); + // A readonly field is a variable inside the matching constructor, so the + // operator form stays legal here. + Shared += 1; + } + + public ReadonlyFieldHolder() + { + instance += 2; + } + } + + // A value-type element makes csc address the storage once with ldelema, which is a + // different shape again from the reference-type case. + public struct StaticOnlyStruct + { + public int Value; + + public static StaticOnlyStruct operator +(StaticOnlyStruct lhs, int rhs) + { + return new StaticOnlyStruct { + Value = lhs.Value + rhs + }; + } + } + + // No instance operator, so "x += n" reaches the static one and csc emits the compound + // assignment shape rather than a load and a store. + public class StaticOnlyOperators + { + public int Value; + + public static StaticOnlyOperators operator +(StaticOnlyOperators lhs, int rhs) + { + return new StaticOnlyOperators { + Value = lhs.Value + rhs + }; + } + + public static StaticOnlyOperators operator ++(StaticOnlyOperators x) + { + return new StaticOnlyOperators { + Value = x.Value + 1 + }; + } + } + + 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 static BothOperators operator ++(BothOperators x) + { + return new BothOperators { + Value = x.Value + 1 + }; + } + + public void operator ++() + { + Value++; + } + } + + public class MixedSignatureOperators + { + public int Value; + + public static MixedSignatureOperators operator +(MixedSignatureOperators lhs, long rhs) + { + return new MixedSignatureOperators { + Value = lhs.Value + (int)rhs + }; + } + + public void operator +=(int rhs) + { + Value += rhs; + } + } + + 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(); + + private BothOperators[] bothArray = new BothOperators[4]; + + private static CompoundClass refReturnTarget = new CompoundClass(); + + private static CompoundStruct refReturnStructTarget; + + public BothOperators BothProperty { get; set; } + + public BothOperators this[int index] { + get { + return bothArray[index]; + } + set { + bothArray[index] = value; + } + } + + 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 void UseShadowedOperator(ShadowingCompoundClass s, int n) + { + s += n; + } + + public static void UseOverriddenOperator(OverridingCompoundClass o, long n) + { + o += n; + } + + public static BothOperators UsePostfixIncrementResultUsed(BothOperators b) + { + return b++; + } + + 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 UseCompoundAssignmentOnArrayElement(StaticOnlyOperators[] arr, int n) + { + arr[0] += n; + arr[0]++; + } + + public static void UseCompoundAssignmentOnStructArrayElement(StaticOnlyStruct[] arr, int n) + { + arr[0] += n; + } + + public static void UseStaticOperatorOnArrayElement(BothOperators[] arr, int n) + { + // The element type declares an instance operator too, so the static call has to stay + // spelled out; array elements take a separate path through the decompiler to locals. + 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 UseStaticOperatorOnRefLocal(BothOperators[] arr, int n) + { + // A ref local is a variable, so "r += n" would bind the instance operator here too. + ref BothOperators reference = ref arr[0]; + reference = reference + n; + reference = reference + 1; + } + + public static void UseBaseTypedLocalOfShadowingClass(ShadowingCompoundClass s, int n) + { + // The local is what makes "c += n" bind the base operator rather than the shadowing + // one, so it has to survive even where the compiler optimized it away. + CompoundClass compoundClass = s; + compoundClass += n; + } + + public static void UseCheckedOperatorOfBaseTypedLocal(ShadowingCompoundClass s, int n) + { + // The derived type hides only the regular flavor, but a checked context does not + // exclude regular operators - it prefers a checked one within the type that wins the + // lookup. On a derived-typed receiver the hiding operator would take this call, so + // the base-typed local has to survive here too. + CompoundClass compoundClass = s; + checked + { + compoundClass += n; + } + } + + public static void UseStaticOperatorOnPropertyAndIndexer(UserDefinedCompoundAssignment inst, int n) + { + // A property and an indexer are not variables, so the instance operator is never a + // candidate for them: "x += n" binds the static operator, which is the one the IL calls. + inst.BothProperty += n; + inst[0] += n; + } + + public static void UseStaticOperatorWhenInstanceDoesNotApply(MixedSignatureOperators m, MixedSignatureOperators[] arr, long n) + { + // The instance operator takes an int, so it is not a candidate for a long argument: + // "x += n" binds the static operator here, which is the one the IL calls. + m += n; + arr[0] += n; + } + + public static void UseInstanceOperator(BothOperators b, int n) + { + b += n; + b++; + } + + public static ref CompoundClass GetRefTarget() + { + return ref refReturnTarget; + } + + public static ref CompoundStruct GetRefStructTarget() + { + return ref refReturnStructTarget; + } + + public static void UseRefReturnTarget(int n) + { + // A ref-returning invocation is a variable, so the operator form binds here. + GetRefTarget() += n; + GetRefTarget()++; + GetRefStructTarget() += n; + GetRefStructTarget()++; + } + + public unsafe static void UsePointerReceiver(CompoundStruct* p, int n) + { + // A pointer indirection is a variable, so the operator form binds here. + *p += n; + (*p)++; + } + + public static void UseForeachVariableCopy(List list, int n) + { + // The iteration variable is read-only, so the operator is applied to a copy; the + // copy has to survive even where the compiler optimized it away. + foreach (CompoundClass item in list) + { + CompoundClass compoundClass = item; + compoundClass += n; + } + } + + public static void UseUsingVariableCopy(int n) + { + // The using variable is read-only, so the operator is applied to a copy; the copy has + // to survive even where the compiler optimized it away. + using DisposableCompound disposableCompound = new DisposableCompound(); + DisposableCompound disposableCompound2 = disposableCompound; + disposableCompound2 += n; + } + + public static void UseExplicitInterfaceOperator(ExplicitCompound e, int n) + { + // The interface-typed variable is the only receiver that can bind an explicitly + // implemented operator, so the copy has to survive decompilation. + ICompound compound = e; + compound += n; + ICompound compound2 = e; + compound2++; + } + + public static void KeepStaticOperatorWithInParameter(InParameterOperators x, InParameterOperators y) + { + // "x += y" binds the instance operator, so folding the static call the IL makes into + // the operator form would change which operator runs. + x = x + y; + } + + public static void UseCopyForShadowedInParameterOperator(ShadowingInParameterOperators s, InParameterOperators y) + { + // The receiver has to keep the base type: the derived type hides the operator with a + // "new" one, which the operator form would bind instead. + InParameterOperators inParameterOperators = s; + inParameterOperators += y; + } + + public static void UseForeachDeconstructionCopy(List<(CompoundClass, int)> list, int n) + { + // The variables a foreach deconstruction designation introduces are read-only, so the + // deconstruction stays in the body, where the operator has an assignable target. + foreach (var item in list) + { + var (compoundClass, value) = item; + compoundClass += n; + Console.WriteLine(value); + } + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index ed0668734..0c872d55e 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -827,6 +827,7 @@ namespace ICSharpCode.Decompiler.CSharp typeSystemAstBuilder.SupportOperatorChecked = settings.CheckedOperators; typeSystemAstBuilder.AlwaysUseGlobal = settings.AlwaysUseGlobal; typeSystemAstBuilder.SupportExtensionDeclarations = settings.ExtensionMembers; + typeSystemAstBuilder.SupportUserDefinedCompoundAssignmentOperators = settings.UserDefinedCompoundAssignmentOperators; return typeSystemAstBuilder; } @@ -1730,15 +1731,16 @@ namespace ICSharpCode.Decompiler.CSharp return true; } } - else if (entity.SymbolKind == SymbolKind.Method) + else if (entity.SymbolKind is SymbolKind.Method or SymbolKind.Operator) { // A method introduced in a class or struct hides all non-method base class members with the same name, and all // base class methods with the same signature (method name and parameter count, modifiers, and types). + // Instance operators hide by those same rules. if (baseType.GetMembers(m => m.SymbolKind != SymbolKind.Indexer && m.SymbolKind != SymbolKind.Constructor && m.SymbolKind != SymbolKind.Destructor && m.Name == entity.Name && lookup.IsAccessible(m, true)) - .Any(m => m.SymbolKind != SymbolKind.Method || + .Any(m => m.SymbolKind is not (SymbolKind.Method or SymbolKind.Operator) || (((IMethod)entity).TypeParameters.Count == ((IMethod)m).TypeParameters.Count && parameterListComparer.Equals(((IMethod)entity).Parameters, ((IMethod)m).Parameters)))) { @@ -2245,8 +2247,11 @@ namespace ICSharpCode.Decompiler.CSharp methodDecl.Modifiers |= Modifiers.Extern; } // Accessors qualify only when they are emitted as ordinary methods (parameterized - // properties); an Accessor node cannot carry the 'new' modifier. - if ((method.SymbolKind == SymbolKind.Method || (method.SymbolKind == SymbolKind.Accessor && methodDecl is MethodDeclaration)) + // properties); an Accessor node cannot carry the 'new' modifier. Instance operators + // hide by signature like ordinary methods do; static ones cannot carry 'new' at all. + if ((method.SymbolKind == SymbolKind.Method + || (method.SymbolKind == SymbolKind.Operator && !method.IsStatic) + || (method.SymbolKind == SymbolKind.Accessor && methodDecl is MethodDeclaration)) && !method.IsExplicitInterfaceImplementation && methodDefinition.HasFlag(System.Reflection.MethodAttributes.Virtual) == methodDefinition.HasFlag(System.Reflection.MethodAttributes.NewSlot)) { @@ -2272,6 +2277,10 @@ namespace ICSharpCode.Decompiler.CSharp { 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/OutputVisitor/CSharpAmbience.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs index 509c3ea7c..12e4314f0 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs @@ -286,6 +286,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor 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 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor 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/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index e3c66d95e..94e7028de 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -260,6 +260,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax /// 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 @@ -2556,7 +2561,15 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax 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); + // C# requires a user-defined operator to be public, so a compound assignment operator + // that is not can only be written as a plain method. An explicit interface + // implementation is private in metadata but is still written in operator form. + if (OperatorDeclaration.IsCompoundAssignment(opType.Value) + && op.Accessibility != Accessibility.Public && !op.IsExplicitInterfaceImplementation) return ConvertMethod(op); if (!SupportOperatorChecked && OperatorDeclaration.IsChecked(opType.Value)) return ConvertMethod(op); diff --git a/ICSharpCode.Decompiler/Output/IAmbience.cs b/ICSharpCode.Decompiler/Output/IAmbience.cs index f88a8576e..aec5ff0e7 100644 --- a/ICSharpCode.Decompiler/Output/IAmbience.cs +++ b/ICSharpCode.Decompiler/Output/IAmbience.cs @@ -125,6 +125,10 @@ namespace ICSharpCode.Decompiler.Output /// Support C# 14 extension declarations. /// SupportExtensionDeclarations = 0x400000, + /// + /// Support C# 14 user-defined compound assignment operators (operator +=). + /// + SupportUserDefinedCompoundAssignmentOperators = 0x800000, StandardConversionFlags = ShowParameterNames | ShowAccessibility | @@ -140,7 +144,7 @@ namespace ICSharpCode.Decompiler.Output ShowDefinitionKeyword | ShowBody, - All = 0x1fffff, + All = 0xffffff, } /// diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs index 15e74dab4..bbe3dc2e0 100644 --- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs +++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs @@ -160,7 +160,7 @@ Examples: [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/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 10a46c120..ff5a373e2 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -86,7 +86,9 @@ namespace ICSharpCode.ILSpy.Languages }; static CSharpAmbience CreateAmbience() => new() { - ConversionFlags = ConversionFlags.ShowTypeParameterList | ConversionFlags.PlaceReturnTypeAfterParameterList, + ConversionFlags = ConversionFlags.ShowTypeParameterList | ConversionFlags.PlaceReturnTypeAfterParameterList + | ConversionFlags.SupportOperatorChecked | ConversionFlags.SupportUnsignedRightShift + | ConversionFlags.SupportUserDefinedCompoundAssignmentOperators, }; public override string TypeToString(IType type, ConversionFlags conversionFlags = ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.UseFullyQualifiedTypeNames)