Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal enum AccessModifier
Private,
InternalProtected,
PrivateProtected,
Init,
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ internal sealed class ReactiveAttribute : Attribute
/// Gets the InheritanceModifier of the property.
/// </sumary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Use Required attribute to indicate that the property is required.
/// </summary>
public bool UseRequired { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal enum AccessModifier
Private,
InternalProtected,
PrivateProtected,
Init,
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ internal sealed class ReactiveAttribute : Attribute
/// Gets the InheritanceModifier of the property.
/// </sumary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Use Required attribute to indicate that the property is required.
/// </summary>
public bool UseRequired { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal enum AccessModifier
Private,
InternalProtected,
PrivateProtected,
Init,
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ internal sealed class ReactiveAttribute : Attribute
/// Gets the InheritanceModifier of the property.
/// </sumary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Use Required attribute to indicate that the property is required.
/// </summary>
public bool UseRequired { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal enum AccessModifier
Private,
InternalProtected,
PrivateProtected,
Init,
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ internal sealed class ReactiveAttribute : Attribute
/// Gets the InheritanceModifier of the property.
/// </sumary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Use Required attribute to indicate that the property is required.
/// </summary>
public bool UseRequired { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal enum AccessModifier
Private,
InternalProtected,
PrivateProtected,
Init,
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ internal sealed class ReactiveAttribute : Attribute
/// Gets the InheritanceModifier of the property.
/// </sumary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Use Required attribute to indicate that the property is required.
/// </summary>
public bool UseRequired { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//HintName: ReactiveUI.SourceGenerators.AccessModifier.g.cs
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace ReactiveUI.SourceGenerators;

/// <summary>
/// AccessModifier.
/// </summary>
internal enum AccessModifier
{
Public,
Protected,
Internal,
Private,
InternalProtected,
PrivateProtected,
Init,
}

/// <summary>
/// InheritanceModifier.
/// </summary>
internal enum InheritanceModifier
{
None,
Virtual,
Override,
New,
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//HintName: ReactiveUI.SourceGenerators.ReactiveAttribute.g.cs
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace ReactiveUI.SourceGenerators;

/// <summary>
/// ReactiveAttribute.
/// </summary>
/// <seealso cref="Attribute" />
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
internal sealed class ReactiveAttribute : Attribute
{
/// <summary>
/// Gets the AccessModifier of the set property.
/// </summary>
/// <value>
/// The AccessModifier of the set property.
/// </value>
public AccessModifier SetModifier { get; init; }

/// <summary>
/// Gets the InheritanceModifier of the property.
/// </sumary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Use Required attribute to indicate that the property is required.
/// </summary>
public bool UseRequired { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//HintName: TestNs.TestVM.Properties.g.cs
// <auto-generated/>
using ReactiveUI;

#pragma warning disable
#nullable enable

namespace TestNs
{
/// <summary>
/// Partial class for the TestVM which contains ReactiveUI Reactive property initialization.
/// </summary>
public partial class TestVM
{
/// <inheritdoc cref="_mustBeSet"/>
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public string MustBeSet
{
get => _mustBeSet;
[global::System.Diagnostics.CodeAnalysis.MemberNotNull("_mustBeSet")]
set => this.RaiseAndSetIfChanged(ref _mustBeSet, value);
}
}
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal enum AccessModifier
Private,
InternalProtected,
PrivateProtected,
Init,
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ internal sealed class ReactiveAttribute : Attribute
/// Gets the InheritanceModifier of the property.
/// </sumary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Use Required attribute to indicate that the property is required.
/// </summary>
public bool UseRequired { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<LangVersion>12.0</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<NoWarn>$(NoWarn);CA1812;CA1001</NoWarn>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,31 @@ public partial class TestInnerClass3 : ReactiveObject
// Act: Initialize the helper and run the generator. Assert: Verify the generated code.
return TestHelper.TestPass(sourceCode);
}

/// <summary>
/// Tests that the source generator correctly generates reactive properties.
/// </summary>
/// <returns>A task to monitor the async.</returns>
[Fact]
public Task FromReactivePropertiesWithInit()
{
// Arrange: Setup the source code that matches the generator input expectations.
const string sourceCode = """
using System;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System.Reactive.Linq;

namespace TestNs;

public partial class TestVM : ReactiveObject
{
[Reactive(SetModifier = AccessModifier.Init, UseRequired = true)]
private string _mustBeSet;
}
""";

// Act: Initialize the helper and run the generator. Assert: Verify the generated code.
return TestHelper.TestPass(sourceCode);
}
}
6 changes: 6 additions & 0 deletions src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
Expand Down Expand Up @@ -56,11 +57,16 @@ public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDis
[Reactive(Inheritance = InheritanceModifier.Virtual, SetModifier = AccessModifier.Protected)]
private string? _name;

[Reactive(SetModifier = AccessModifier.Init, UseRequired = true)]
private string _mustBeSet;

/// <summary>
/// Initializes a new instance of the <see cref="TestViewModel"/> class.
/// </summary>
[SetsRequiredMembers]
public TestViewModel()
{
MustBeSet = "Test";
this.WhenActivated(disposables =>
{
Console.Out.WriteLine("Activated");
Expand Down
6 changes: 6 additions & 0 deletions src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal enum AccessModifier
Private,
InternalProtected,
PrivateProtected,
Init,
}

/// <summary>
Expand Down Expand Up @@ -155,6 +156,11 @@ internal sealed class ReactiveAttribute : Attribute
/// Gets the InheritanceModifier of the property.
/// </sumary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Use Required attribute to indicate that the property is required.
/// </summary>
public bool UseRequired { get; init; }
}
#nullable restore
#pragma warning restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ internal sealed record PropertyInfo(
bool IncludeMemberNotNullOnSetAccessor,
EquatableArray<string> ForwardedAttributes,
string AccessModifier,
string Inheritance);
string Inheritance,
string UseRequired);
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,17 @@ public sealed partial class ReactiveGenerator
attributeData.TryGetNamedArgument("SetModifier", out int accessModifierArgument);
var accessModifier = accessModifierArgument switch
{
1 => "protected ",
2 => "internal ",
3 => "private ",
4 => "internal protected ",
5 => "private protected ",
_ => string.Empty,
1 => "protected set",
2 => "internal set",
3 => "private set",
4 => "internal protected set",
5 => "private protected set",
6 => "init",
_ => "set",
};

token.ThrowIfCancellationRequested();

// Get Inheritance value from the attribute
attributeData.TryGetNamedArgument("Inheritance", out int inheritanceArgument);
var inheritance = inheritanceArgument switch
Expand All @@ -87,6 +90,12 @@ public sealed partial class ReactiveGenerator

token.ThrowIfCancellationRequested();

// Get Inheritance value from the attribute
attributeData.TryGetNamedArgument("UseRequired", out bool useRequiredArgument);
var useRequired = useRequiredArgument ? "required " : string.Empty;

token.ThrowIfCancellationRequested();

// Get the property type and name
var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations();
var fieldName = fieldSymbol.Name;
Expand Down Expand Up @@ -219,7 +228,8 @@ public sealed partial class ReactiveGenerator
includeMemberNotNullOnSetAccessor,
forwardedAttributesString,
accessModifier,
inheritance),
inheritance,
useRequired),
builder.ToImmutable());
}

Expand Down Expand Up @@ -303,11 +313,11 @@ private static string GetPropertySyntax(PropertyInfo propertyInfo)
$$"""
/// <inheritdoc cref="{{propertyInfo.FieldName}}"/>
{{propertyAttributes}}
{{propertyInfo.TargetInfo.TargetVisibility}}{{propertyInfo.Inheritance}} {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}}
{{propertyInfo.TargetInfo.TargetVisibility}}{{propertyInfo.Inheritance}} {{propertyInfo.UseRequired}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}}
{
get => {{propertyInfo.FieldName}};
[global::System.Diagnostics.CodeAnalysis.MemberNotNull("{{propertyInfo.FieldName}}")]
{{propertyInfo.AccessModifier}}set => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value);
{{propertyInfo.AccessModifier}} => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value);
}
""";
}
Expand All @@ -316,7 +326,7 @@ private static string GetPropertySyntax(PropertyInfo propertyInfo)
$$"""
/// <inheritdoc cref="{{propertyInfo.FieldName}}"/>
{{propertyAttributes}}
{{propertyInfo.TargetInfo.TargetVisibility}}{{propertyInfo.Inheritance}} {{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} { get => {{propertyInfo.FieldName}}; {{propertyInfo.AccessModifier}}set => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value); }
{{propertyInfo.TargetInfo.TargetVisibility}}{{propertyInfo.Inheritance}} {{propertyInfo.UseRequired}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} { get => {{propertyInfo.FieldName}}; {{propertyInfo.AccessModifier}} => this.RaiseAndSetIfChanged(ref {{propertyInfo.FieldName}}, value); }
""";
}

Expand Down
Loading