Files
ILSpy/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedCompoundAssignment.cs
T
Siegfried Pammer e4fa167afc Render user-defined compound assignment operator declarations
TypeSystemAstBuilder writes the C# 14 "operator +=" declaration form, behind a
support flag like the other version-gated operator syntax; a non-public
operator has no legal operator declaration (CS9308) and falls back to a plain
method, except an explicit interface implementation, which is private in
metadata but still written in operator form. An instance operator hides by
signature like an ordinary method, so it can carry "new"; the
[CompilerFeatureRequired] marker the compiler emits is removed like the other
feature markers. Tooltips get the same rendering via a ConversionFlags bit;
widening ConversionFlags.All also turns on the existing checked-operator and
unsigned-right-shift flags, which the tooltip ambience now sets explicitly.

The test fixtures land here, where the pipeline is complete end to end: pretty
and IL round-trips, correctness runs against Roslyn's C# 14 binding (including
the operator-inheritance matrix and a ref-local target), and the ugly
configuration pins the output with the setting off.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-29 09:39:10 +02:00

629 lines
14 KiB
C#

// 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<T>
{
void operator +=(T rhs);
void operator ++();
}
public class ExplicitCompound : ICompound<int>
{
public int Value;
void ICompound<int>.operator +=(int rhs)
{
Value += rhs;
}
void ICompound<int>.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>(T x, int n) where T : ICompound<int>
{
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<CompoundClass> 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<int> compound = e;
compound += n;
ICompound<int> 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);
}
}
}
}