Skip to content

Commit 8aedecb

Browse files
authored
Add support for nullable return types in ReactiveCommand (#345)
* 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. * Rename variables for argument type clarity Renamed 'methodReturnType' and 'methodParametersstring' to 'argumentType' and 'argumentTypeString' for improved clarity and consistency in naming related to method argument types. * Remove unused variable from TestViewModel constructor Eliminated the unused variable 'c' of type Class1 from the TestViewModel constructor to clean up the code. * Refactor parent class declaration generation Replaces LINQ ToArray with a range operator in GenerateParentClassDeclarations for improved clarity. Also removes an unused using directive from ReactiveCommandGenerator.cs.
1 parent 015657e commit 8aedecb

File tree

7 files changed

+112
-5
lines changed

7 files changed

+112
-5
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: 12 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

@@ -452,4 +453,15 @@ protected virtual void Dispose(bool disposing)
452453
[ReactiveCommand]
453454
private Task<System.Collections.IEnumerable> GetData(CancellationToken ct) =>
454455
Task.FromResult<System.Collections.IEnumerable>(Array.Empty<System.Collections.IEnumerable>());
456+
457+
[ReactiveCommand]
458+
private Execute.Nested2.Class1? SetProperty(Execute.Nested1.Class1? class1)
459+
{
460+
if (class1 == null)
461+
{
462+
return null;
463+
}
464+
465+
return new() { Property1 = class1.Property1 };
466+
}
455467
}

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

Lines changed: 8 additions & 4 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 argumentType = methodParameters.ToImmutable().SingleOrDefault()?.Type;
123+
var argumentTypeString = argumentType?.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+
argumentTypeString,
128132
isTask,
129133
isReturnTypeVoid,
130134
isObservable,
@@ -138,7 +142,7 @@ public partial class ReactiveCommandGenerator
138142
private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, CommandInfo[] commands)
139143
{
140144
// Get Parent class details from properties.ParentInfo
141-
var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(commands.Select(p => p.TargetInfo.ParentInfo).ToArray());
145+
var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations([.. commands.Select(p => p.TargetInfo.ParentInfo)]);
142146

143147
var classes = GenerateClassWithCommands(containingTypeName, containingNamespace, containingClassVisibility, containingType, commands);
144148

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Microsoft.CodeAnalysis;
1010
using Microsoft.CodeAnalysis.CSharp.Syntax;
1111
using Microsoft.CodeAnalysis.Text;
12-
using ReactiveUI.SourceGenerators.Extensions;
1312
using ReactiveUI.SourceGenerators.Helpers;
1413

1514
namespace ReactiveUI.SourceGenerators;

0 commit comments

Comments
 (0)