Skip to content

Commit d6eaf35

Browse files
committed
Add support for nullable return types in ReactiveCommand
Introduces new unit tests and verified source for ReactiveCommand methods with nullable parameter and return types. Updates generator logic to handle nullable types correctly. Adds usage of nested classes with nullable types in test and example view models, and updates project references accordingly.
1 parent 1f97e4a commit d6eaf35

File tree

6 files changed

+112
-3
lines changed

6 files changed

+112
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//HintName: ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.cs
2+
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
3+
// Licensed to the .NET Foundation under one or more agreements.
4+
// The .NET Foundation licenses this file to you under the MIT license.
5+
// See the LICENSE file in the project root for full license information.
6+
7+
// <auto-generated/>
8+
#pragma warning disable
9+
#nullable enable
10+
namespace ReactiveUI.SourceGenerators;
11+
12+
/// <summary>
13+
/// ReativeCommandAttribute.
14+
/// </summary>
15+
/// <seealso cref="Attribute" />
16+
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
17+
internal sealed class ReactiveCommandAttribute : global::System.Attribute
18+
{
19+
/// <summary>
20+
/// Gets the can execute method or property.
21+
/// </summary>
22+
/// <value>
23+
/// The name of the CanExecute Observable of bool.
24+
/// </value>
25+
public string? CanExecute { get; init; }
26+
27+
/// <summary>
28+
/// Gets the output scheduler.
29+
/// </summary>
30+
/// <value>
31+
/// The output scheduler.
32+
/// </value>
33+
public string? OutputScheduler { get; init; }
34+
35+
/// <summary>
36+
/// Gets the AccessModifier of the ReactiveCommand property.
37+
/// </summary>
38+
/// <value>
39+
/// The AccessModifier of the property.
40+
/// </value>
41+
public PropertyAccessModifier AccessModifier { get; init; }
42+
}
43+
#nullable restore
44+
#pragma warning restore

src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,31 @@ public partial class TestVM : ReactiveObject
238238
""";
239239
return TestHelper.TestPass(sourceCode);
240240
}
241+
242+
/// <summary>
243+
/// Froms the type of the reactive command with nullable type and nullable return.
244+
/// </summary>
245+
/// <returns>A task to monitor the async.</returns>
246+
[Test]
247+
public Task FromReactiveCommandWithNullableTypeAndNullableReturnType()
248+
{
249+
const string sourceCode = """
250+
using System;
251+
using ReactiveUI;
252+
using ReactiveUI.SourceGenerators;
253+
namespace TestNs;
254+
255+
public class NullableInput
256+
{
257+
public string? Name { get; set; }
258+
}
259+
260+
public partial class TestVM : ReactiveObject
261+
{
262+
[ReactiveCommand]
263+
private NullableInput? Test1(NullableInput? input) => input;
264+
}
265+
""";
266+
return TestHelper.TestPass(sourceCode);
267+
}
241268
}

src/ReactiveUI.SourceGenerators.Execute.Nested3/Class1.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics.CodeAnalysis;
77
using ReactiveUI;
88
using ReactiveUI.SourceGenerators;
9+
using SGReactiveUI.SourceGenerators.Execute.Nested2;
910

1011
namespace SGReactiveUI.SourceGenerators.Execute.Nested3;
1112

@@ -17,4 +18,23 @@ public partial class Class1 : ReactiveObject
1718
{
1819
[Reactive]
1920
private string? _property1;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="Class1"/> class.
24+
/// </summary>
25+
public Class1()
26+
{
27+
SetPropertyCommand.Execute(new Nested1.Class1 { Property1 = "Initial Value" }).Subscribe();
28+
}
29+
30+
[ReactiveCommand]
31+
private SGReactiveUI.SourceGenerators.Execute.Nested2.Class1? SetProperty(Nested1.Class1? class1)
32+
{
33+
if (class1 == null)
34+
{
35+
return null;
36+
}
37+
38+
return new() { Property1 = class1.Property1 };
39+
}
2040
}

src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
<ItemGroup>
2222
<ProjectReference Include="..\ReactiveUI.SourceGenerators.Analyzers.CodeFixes\ReactiveUI.SourceGenerators.Analyzers.CodeFixes.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
23+
<ProjectReference Include="..\ReactiveUI.SourceGenerators.Execute.Nested3\ReactiveUI.SourceGenerators.Execute.Nested3.csproj" />
2324
<ProjectReference Include="..\ReactiveUI.SourceGenerators.Roslyn4120\ReactiveUI.SourceGenerators.Roslyn4120.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
2425
</ItemGroup>
2526
</Project>

src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using DynamicData;
1717
using ReactiveUI;
1818
using ReactiveUI.SourceGenerators;
19+
using SGReactiveUI.SourceGenerators.Execute.Nested3;
1920

2021
namespace SGReactiveUI.SourceGenerators.Test;
2122

@@ -88,6 +89,7 @@ public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDis
8889
[SetsRequiredMembers]
8990
public TestViewModel()
9091
{
92+
var c = new Class1();
9193
var itv = new InternalTestViewModel { PublicRequiredPartialPropertyTest = true };
9294
MustBeSet = "Test";
9395
this.WhenActivated(disposables =>
@@ -452,4 +454,15 @@ protected virtual void Dispose(bool disposing)
452454
[ReactiveCommand]
453455
private Task<System.Collections.IEnumerable> GetData(CancellationToken ct) =>
454456
Task.FromResult<System.Collections.IEnumerable>(Array.Empty<System.Collections.IEnumerable>());
457+
458+
[ReactiveCommand]
459+
private Execute.Nested2.Class1? SetProperty(Execute.Nested1.Class1? class1)
460+
{
461+
if (class1 == null)
462+
{
463+
return null;
464+
}
465+
466+
return new() { Property1 = class1.Property1 };
467+
}
455468
}

src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.Execute.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// The ReactiveUI and contributors licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.
55

6-
using System.Collections.Generic;
6+
using System;
77
using System.Collections.Immutable;
88
using System.Diagnostics.CodeAnalysis;
99
using System.Globalization;
@@ -15,7 +15,6 @@
1515
using ReactiveUI.SourceGenerators.Helpers;
1616
using ReactiveUI.SourceGenerators.Input.Models;
1717
using ReactiveUI.SourceGenerators.Models;
18-
using static ReactiveUI.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1918

2019
namespace ReactiveUI.SourceGenerators;
2120

@@ -120,11 +119,16 @@ public partial class ReactiveCommandGenerator
120119

121120
token.ThrowIfCancellationRequested();
122121

122+
var methodReturnType = methodParameters.ToImmutable().SingleOrDefault()?.Type;
123+
var methodParametersstring = methodReturnType?.GetFullyQualifiedNameWithNullabilityAnnotations();
124+
125+
token.ThrowIfCancellationRequested();
126+
123127
return new(
124128
targetInfo,
125129
symbol.Name,
126130
realReturnType.GetFullyQualifiedNameWithNullabilityAnnotations(),
127-
methodParameters.ToImmutable().SingleOrDefault()?.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
131+
methodParametersstring,
128132
isTask,
129133
isReturnTypeVoid,
130134
isObservable,

0 commit comments

Comments
 (0)