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