Skip to content

Commit 71d3120

Browse files
committed
#2552: Initial support for null coalescing assignment (??=).
1 parent bd9ee28 commit 71d3120

File tree

17 files changed

+635
-8
lines changed

17 files changed

+635
-8
lines changed

.vscode/settings.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"dotnet-test-explorer.testProjectPath": "*.Tests/*Tests.csproj",
32
"files.exclude": {
43
"ILSpy-tests/**": true
54
},

ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
<Compile Include="TestCases\ILPretty\Issue3524.cs" />
153153
<Compile Include="TestCases\Pretty\ExpandParamsArgumentsDisabled.cs" />
154154
<Compile Include="TestCases\Pretty\ExtensionProperties.cs" />
155+
<Compile Include="TestCases\Pretty\NullCoalescingAssign.cs" />
155156
<None Include="TestCases\ILPretty\Issue3504.cs" />
156157
<Compile Include="TestCases\ILPretty\MonoFixed.cs" />
157158
<Compile Include="TestCases\Pretty\Comparisons.cs" />

ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,12 @@ public async Task NullPropagation([ValueSource(nameof(roslynOnlyOptions))] Compi
539539
await RunForLibrary(cscOptions: cscOptions);
540540
}
541541

542+
[Test]
543+
public async Task NullCoalescingAssign([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
544+
{
545+
await RunForLibrary(cscOptions: cscOptions);
546+
}
547+
542548
[Test]
543549
public async Task StringInterpolation([ValueSource(nameof(roslynOnlyWithNet40Options))] CompilerOptions cscOptions)
544550
{
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (c) 2018 Daniel Grunwald
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4+
// software and associated documentation files (the "Software"), to deal in the Software
5+
// without restriction, including without limitation the rights to use, copy, modify, merge,
6+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7+
// to whom the Software is furnished to do so, subject to the following conditions:
8+
//
9+
// The above copyright notice and this permission notice shall be included in all copies or
10+
// substantial portions of the Software.
11+
//
12+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14+
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15+
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17+
// DEALINGS IN THE SOFTWARE.
18+
19+
using System;
20+
21+
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
22+
{
23+
// Note: for null coalescing without assignment (??), see
24+
// LiftedOperators.cs, NullPropagation.cs and ThrowExpressions.cs.
25+
internal class NullCoalescingAssign
26+
{
27+
private class MyClass
28+
{
29+
public static string? StaticField1;
30+
public static int? StaticField2;
31+
public string? InstanceField1;
32+
public int? InstanceField2;
33+
34+
public static string? StaticProperty1 { get; set; }
35+
public static int? StaticProperty2 { get; set; }
36+
37+
public string? InstanceProperty1 { get; set; }
38+
public int? InstanceProperty2 { get; set; }
39+
40+
public string? this[string name] {
41+
get {
42+
return null;
43+
}
44+
set {
45+
}
46+
}
47+
48+
public int? this[int index] {
49+
get {
50+
return null;
51+
}
52+
set {
53+
}
54+
}
55+
}
56+
57+
private struct MyStruct
58+
{
59+
public string? InstanceField1;
60+
public int? InstanceField2;
61+
public string? InstanceProperty1 { get; set; }
62+
public int? InstanceProperty2 { get; set; }
63+
64+
public string? this[string name] {
65+
get {
66+
return null;
67+
}
68+
set {
69+
}
70+
}
71+
72+
public int? this[int index] {
73+
get {
74+
return null;
75+
}
76+
set {
77+
}
78+
}
79+
}
80+
81+
private static T Get<T>()
82+
{
83+
return default(T);
84+
}
85+
86+
private static ref T GetRef<T>()
87+
{
88+
throw null;
89+
}
90+
91+
private static void Use<T>(T t)
92+
{
93+
}
94+
95+
public static void Locals()
96+
{
97+
string? local1 = null;
98+
int? local2 = null;
99+
local1 ??= "Hello";
100+
local2 ??= 42;
101+
Use(local1 ??= "World");
102+
Use(local2 ??= 42);
103+
}
104+
105+
public static void StaticFields()
106+
{
107+
MyClass.StaticField1 ??= "Hello";
108+
MyClass.StaticField2 ??= 42;
109+
Use(MyClass.StaticField1 ??= "World");
110+
Use(MyClass.StaticField2 ??= 42);
111+
}
112+
113+
public static void InstanceFields()
114+
{
115+
Get<MyClass>().InstanceField1 ??= "Hello";
116+
Get<MyClass>().InstanceField2 ??= 42;
117+
Use(Get<MyClass>().InstanceField1 ??= "World");
118+
Use(Get<MyClass>().InstanceField2 ??= 42);
119+
}
120+
121+
public static void ArrayElements()
122+
{
123+
Get<string[]>()[Get<int>()] ??= "Hello";
124+
Get<int?[]>()[Get<int>()] ??= 42;
125+
Use(Get<string[]>()[Get<int>()] ??= "World");
126+
Use(Get<int?[]>()[Get<int>()] ??= 42);
127+
}
128+
129+
public static void InstanceProperties()
130+
{
131+
Get<MyClass>().InstanceProperty1 ??= "Hello";
132+
Get<MyClass>().InstanceProperty2 ??= 42;
133+
Use(Get<MyClass>().InstanceProperty1 ??= "World");
134+
Use(Get<MyClass>().InstanceProperty2 ??= 42);
135+
}
136+
137+
public static void StaticProperties()
138+
{
139+
MyClass.StaticProperty1 ??= "Hello";
140+
MyClass.StaticProperty2 ??= 42;
141+
Use(MyClass.StaticProperty1 ??= "World");
142+
Use(MyClass.StaticProperty2 ??= 42);
143+
}
144+
145+
public static void RefReturn()
146+
{
147+
GetRef<string>() ??= "Hello";
148+
GetRef<int?>() ??= 42;
149+
Use(GetRef<string>() ??= "World");
150+
Use(GetRef<int?>() ??= 42);
151+
}
152+
153+
public static void Dynamic()
154+
{
155+
Get<dynamic>().X ??= "Hello";
156+
Get<dynamic>().Y ??= 42;
157+
Use(Get<dynamic>().X ??= "Hello");
158+
Use(Get<dynamic>().Y ??= 42);
159+
}
160+
}
161+
}

ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public static List<IILTransform> GetILTransforms()
142142
new DynamicIsEventAssignmentTransform(),
143143
new TransformAssignment(), // inline and compound assignments
144144
new NullCoalescingTransform(),
145+
new NullCoalescingAssignTransform(),
145146
new NullableLiftingStatementTransform(),
146147
new NullPropagationStatementTransform(),
147148
new TransformArrayInitializers(),

ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3877,6 +3877,29 @@ protected internal override TranslatedExpression VisitNullCoalescingInstruction(
38773877
.WithRR(rr);
38783878
}
38793879

3880+
protected internal override TranslatedExpression VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst, TranslationContext context)
3881+
{
3882+
ExpressionWithResolveResult target;
3883+
if (inst.TargetKind == CompoundTargetKind.Address)
3884+
{
3885+
target = LdObj(inst.Target, inst.Type);
3886+
}
3887+
else
3888+
{
3889+
target = Translate(inst.Target, inst.Type);
3890+
}
3891+
var fallback = Translate(inst.Value);
3892+
fallback = AdjustConstantExpressionToType(fallback, inst.Type);
3893+
var resultType = target.Type;
3894+
if (inst.CoalescingKind == NullCoalescingKind.NullableWithValueFallback)
3895+
{
3896+
resultType = NullableType.GetUnderlyingType(resultType);
3897+
}
3898+
return new AssignmentExpression(target, AssignmentOperatorType.NullCoalescing, fallback)
3899+
.WithILInstruction(inst)
3900+
.WithRR(new ResolveResult(resultType));
3901+
}
3902+
38803903
protected internal override TranslatedExpression VisitIfInstruction(IfInstruction inst, TranslationContext context)
38813904
{
38823905
var condition = TranslateCondition(inst.Condition);

ICSharpCode.Decompiler/CSharp/Syntax/Expressions/AssignmentExpression.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class AssignmentExpression : Expression
5151
public readonly static TokenRole BitwiseAndRole = new TokenRole("&=");
5252
public readonly static TokenRole BitwiseOrRole = new TokenRole("|=");
5353
public readonly static TokenRole ExclusiveOrRole = new TokenRole("^=");
54+
public readonly static TokenRole NullCoalescingRole = new TokenRole("??=");
5455

5556
public AssignmentExpression()
5657
{
@@ -138,6 +139,8 @@ public static TokenRole GetOperatorRole(AssignmentOperatorType op)
138139
return BitwiseOrRole;
139140
case AssignmentOperatorType.ExclusiveOr:
140141
return ExclusiveOrRole;
142+
case AssignmentOperatorType.NullCoalescing:
143+
return NullCoalescingRole;
141144
default:
142145
throw new NotSupportedException("Invalid value for AssignmentOperatorType");
143146
}
@@ -175,6 +178,8 @@ public static TokenRole GetOperatorRole(AssignmentOperatorType op)
175178
return BinaryOperatorType.BitwiseOr;
176179
case AssignmentOperatorType.ExclusiveOr:
177180
return BinaryOperatorType.ExclusiveOr;
181+
case AssignmentOperatorType.NullCoalescing:
182+
return BinaryOperatorType.NullCoalescing;
178183
default:
179184
throw new NotSupportedException("Invalid value for AssignmentOperatorType");
180185
}
@@ -275,6 +280,8 @@ public enum AssignmentOperatorType
275280
BitwiseOr,
276281
/// <summary>left ^= right</summary>
277282
ExclusiveOr,
283+
/// <summary>left ??= right</summary>
284+
NullCoalescing,
278285

279286
/// <summary>Any operator (for pattern matching)</summary>
280287
Any

ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,11 @@ protected internal override void VisitNullCoalescingInstruction(NullCoalescingIn
754754
HandleBinaryWithOptionalEvaluation(inst, inst.ValueInst, inst.FallbackInst);
755755
}
756756

757+
protected internal override void VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst)
758+
{
759+
HandleBinaryWithOptionalEvaluation(inst, inst.Target, inst.Value);
760+
}
761+
757762
protected internal override void VisitDynamicLogicOperatorInstruction(DynamicLogicOperatorInstruction inst)
758763
{
759764
HandleBinaryWithOptionalEvaluation(inst, inst.Left, inst.Right);

ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
<Compile Include="Disassembler\SortByNameProcessor.cs" />
111111
<Compile Include="Humanizer\StringHumanizeExtensions.cs" />
112112
<Compile Include="IL\Transforms\InlineArrayTransform.cs" />
113+
<Compile Include="IL\Transforms\NullCoalescingAssignTransform.cs" />
113114
<Compile Include="IL\Transforms\RemoveUnconstrainedGenericReferenceTypeCheck.cs" />
114115
<Compile Include="Metadata\MetadataFile.cs" />
115116
<Compile Include="Metadata\ModuleReferenceMetadata.cs" />

ICSharpCode.Decompiler/IL/ILVariable.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ internal void WriteTo(ITextOutput output)
574574

575575
/// <summary>
576576
/// Gets whether this variable occurs within the specified instruction.
577+
/// Only detects direct usages, may incorrectly return false for variables that have aliases via ref-locals/pointers.
577578
/// </summary>
578579
internal bool IsUsedWithin(ILInstruction inst)
579580
{
@@ -588,6 +589,24 @@ internal bool IsUsedWithin(ILInstruction inst)
588589
}
589590
return false;
590591
}
592+
593+
/// <summary>
594+
/// Gets whether this variable is used for a store within the specified instruction.
595+
/// Only detects direct stores, may incorrect return false for variables that have their addresses taken.
596+
/// </summary>
597+
internal bool IsWrittenWithin(ILInstruction inst)
598+
{
599+
if (inst is IStoreInstruction iwvo && iwvo.Variable == this)
600+
{
601+
return true;
602+
}
603+
foreach (var child in inst.Children)
604+
{
605+
if (IsWrittenWithin(child))
606+
return true;
607+
}
608+
return false;
609+
}
591610
}
592611

593612
public interface IInstructionWithVariableOperand

0 commit comments

Comments
 (0)