Skip to content

Commit a31d58e

Browse files
authored
Feature Enable Partial Reactive Properties to have initializer (#231)
1 parent b016b77 commit a31d58e

File tree

5 files changed

+130
-24
lines changed

5 files changed

+130
-24
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2025 ReactiveUI and contributors. All rights reserved.
2+
// Licensed to the ReactiveUI and contributors under one or more agreements.
3+
// The ReactiveUI and contributors licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using ReactiveUI;
7+
using ReactiveUI.SourceGenerators;
8+
9+
namespace SGReactiveUI.SourceGenerators.Test;
10+
11+
internal partial class InternalTestViewModel : ReactiveObject
12+
{
13+
[Reactive]
14+
public partial int PublicPartialPropertyTest { get; set; }
15+
16+
[Reactive]
17+
public partial int PublicPartialPropertyWithInternalProtectedTest { get; protected internal set; }
18+
19+
[Reactive]
20+
public partial int PublicPartialPropertyWithPrivateProtectedTest { get; private protected set; }
21+
22+
[Reactive]
23+
public partial int PublicPartialPropertyWithProtectedTest { get; protected set; }
24+
25+
[Reactive]
26+
public partial int PublicPartialPropertyWithInternalTest { get; internal set; }
27+
28+
[Reactive]
29+
public partial int PublicPartialPropertyWithPrivateTest { get; private set; }
30+
31+
[Reactive]
32+
internal partial int InternalPartialPropertyTest { get; set; }
33+
34+
[Reactive]
35+
protected internal partial int InternalProtectedPartialPropertyTest { get; set; }
36+
37+
[Reactive]
38+
protected partial int ProtectedPartialPropertyTest { get; set; }
39+
40+
[Reactive]
41+
private partial int PrivatePartialPropertyTest { get; set; }
42+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<Nullable>enable</Nullable>
1010
<IsPackable>false</IsPackable>
1111
<LangVersion>13.0</LangVersion>
12+
<NoWarn>$(NoWarn);CA1812</NoWarn>
1213
<PackageDescription>A MVVM framework that integrates with the Reactive Extensions for .NET to create elegant, testable User Interfaces that run on any mobile or desktop platform. This is the Source Generators package for ReactiveUI</PackageDescription>
1314
</PropertyGroup>
1415

src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,9 @@ public TestViewModel()
331331
[ObservableAsProperty(InitialValue = "10")]
332332
public partial int ObservableAsPropertyFromProperty { get; }
333333

334+
[Reactive]
335+
internal partial int InternalPartialPropertyTest { get; set; }
336+
334337
[ObservableAsProperty]
335338
private IObservable<object> ReferenceTypeObservable { get; }
336339

src/ReactiveUI.SourceGenerators.Roslyn/Reactive/Models/PropertyInfo.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ internal sealed record PropertyInfo(
1919
bool IsReferenceTypeOrUnconstrainedTypeParameter,
2020
bool IncludeMemberNotNullOnSetAccessor,
2121
EquatableArray<string> ForwardedAttributes,
22-
string AccessModifier,
22+
string SetAccessModifier,
2323
string Inheritance,
2424
string UseRequired,
25-
bool IsProperty);
25+
bool IsProperty,
26+
string PropertyAccessModifier);

src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.Execute.cs

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Linq;
99
using System.Threading;
1010
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CSharp;
1112
using Microsoft.CodeAnalysis.CSharp.Syntax;
1213
using ReactiveUI.SourceGenerators.Extensions;
1314
using ReactiveUI.SourceGenerators.Helpers;
@@ -59,14 +60,60 @@ public sealed partial class ReactiveGenerator
5960

6061
token.ThrowIfCancellationRequested();
6162

62-
var accessModifier = $"{propertySymbol.SetMethod?.DeclaredAccessibility} set".ToLower();
63-
if (accessModifier.StartsWith("public", StringComparison.Ordinal))
63+
// Get Property AccessModifier.
64+
var propertyAccessModifier = propertySymbol.DeclaredAccessibility.ToString().ToLower();
65+
if (propertyAccessModifier?.Contains("and") == true)
6466
{
65-
accessModifier = "set";
67+
propertyAccessModifier = propertyAccessModifier.Replace("and", " ");
6668
}
67-
else if (accessModifier.Contains("and"))
69+
else if (propertyAccessModifier?.Contains("or") == true)
6870
{
69-
accessModifier = accessModifier.Replace("and", " ");
71+
propertyAccessModifier = propertyAccessModifier.Replace("or", " ");
72+
}
73+
74+
token.ThrowIfCancellationRequested();
75+
76+
// Get Set AccessModifier.
77+
var setAccessModifier = $"{propertySymbol.SetMethod?.DeclaredAccessibility} set".ToLower();
78+
if (setAccessModifier.StartsWith("public", StringComparison.Ordinal))
79+
{
80+
setAccessModifier = "set";
81+
}
82+
else if (setAccessModifier?.Contains("and") == true)
83+
{
84+
if (setAccessModifier.Contains("protectedandinternal"))
85+
{
86+
setAccessModifier = setAccessModifier.Replace("protectedandinternal", "private protected");
87+
}
88+
else
89+
{
90+
setAccessModifier = setAccessModifier.Replace("and", " ");
91+
}
92+
}
93+
else if (setAccessModifier?.Contains("or") == true)
94+
{
95+
setAccessModifier = setAccessModifier.Replace("or", " ");
96+
}
97+
98+
if (propertyAccessModifier == "private" && setAccessModifier == "private set")
99+
{
100+
setAccessModifier = "set";
101+
}
102+
else if (propertyAccessModifier == "internal" && setAccessModifier == "internal set")
103+
{
104+
setAccessModifier = "set";
105+
}
106+
else if (propertyAccessModifier == "protected" && setAccessModifier == "protected set")
107+
{
108+
setAccessModifier = "set";
109+
}
110+
else if (propertyAccessModifier == "protected internal" && setAccessModifier == "protected internal set")
111+
{
112+
setAccessModifier = "set";
113+
}
114+
else if (propertyAccessModifier == "private protected" && setAccessModifier == "private protected set")
115+
{
116+
setAccessModifier = "set";
70117
}
71118

72119
token.ThrowIfCancellationRequested();
@@ -78,6 +125,12 @@ public sealed partial class ReactiveGenerator
78125

79126
var typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations();
80127
var fieldName = propertySymbol.GetGeneratedFieldName();
128+
129+
if (context.SemanticModel.Compilation is CSharpCompilation compilation && compilation.LanguageVersion == LanguageVersion.Preview)
130+
{
131+
fieldName = "field";
132+
}
133+
81134
var propertyName = propertySymbol.Name;
82135

83136
// Get the nullability info for the property
@@ -111,10 +164,11 @@ public sealed partial class ReactiveGenerator
111164
isReferenceTypeOrUnconstraindTypeParameter,
112165
includeMemberNotNullOnSetAccessor,
113166
forwardedAttributesString,
114-
accessModifier,
167+
setAccessModifier!,
115168
inheritance,
116169
useRequired,
117-
true),
170+
true,
171+
propertyAccessModifier!),
118172
builder.ToImmutable());
119173
}
120174
#endif
@@ -156,12 +210,12 @@ public sealed partial class ReactiveGenerator
156210

157211
// Get AccessModifier enum value from the attribute
158212
attributeData.TryGetNamedArgument("SetModifier", out int accessModifierArgument);
159-
var accessModifier = accessModifierArgument switch
213+
var setAccessModifier = accessModifierArgument switch
160214
{
161215
1 => "protected set",
162216
2 => "internal set",
163217
3 => "private set",
164-
4 => "internal protected set",
218+
4 => "protected internal set",
165219
5 => "private protected set",
166220
6 => "init",
167221
_ => "set",
@@ -236,10 +290,11 @@ public sealed partial class ReactiveGenerator
236290
isReferenceTypeOrUnconstraindTypeParameter,
237291
includeMemberNotNullOnSetAccessor,
238292
forwardedAttributesString,
239-
accessModifier,
293+
setAccessModifier,
240294
inheritance,
241295
useRequired,
242-
false),
296+
false,
297+
"public"),
243298
builder.ToImmutable());
244299
}
245300

@@ -255,7 +310,7 @@ public sealed partial class ReactiveGenerator
255310
private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, PropertyInfo[] properties)
256311
{
257312
// Get Parent class details from properties.ParentInfo
258-
var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations(properties.Select(p => p.TargetInfo.ParentInfo).ToArray());
313+
var (parentClassDeclarationsString, closingBrackets) = TargetInfo.GenerateParentClassDeclarations([.. properties.Select(p => p.TargetInfo.ParentInfo)]);
259314

260315
var classes = GenerateClassWithProperties(containingTypeName, containingNamespace, containingClassVisibility, containingType, properties);
261316

@@ -315,43 +370,47 @@ private static string GetPropertySyntax(PropertyInfo propertyInfo)
315370
return string.Empty;
316371
}
317372

318-
var fieldName = propertyInfo.FieldName;
373+
var setFieldName = propertyInfo.FieldName;
374+
var getFieldName = propertyInfo.FieldName;
319375
if (propertyInfo.FieldName == "value")
320376
{
321-
fieldName = "this.value";
377+
setFieldName = "this.value";
322378
}
323379

324380
var fieldSyntax = string.Empty;
325381
var partialModifier = propertyInfo.IsProperty ? "partial " : string.Empty;
326-
if (propertyInfo.IsProperty)
382+
if (propertyInfo.IsProperty && propertyInfo.FieldName != "field")
327383
{
328384
fieldSyntax = $"private {propertyInfo.TypeNameWithNullabilityAnnotations} {propertyInfo.FieldName};";
329385
}
330386

387+
var accessModifier = propertyInfo.PropertyAccessModifier;
388+
var setAccessModifier = propertyInfo.SetAccessModifier;
389+
331390
var propertyAttributes = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage.Concat(propertyInfo.ForwardedAttributes));
332391

333392
if (propertyInfo.IncludeMemberNotNullOnSetAccessor || propertyInfo.IsReferenceTypeOrUnconstrainedTypeParameter)
334393
{
335394
return
336395
$$"""
337396
{{fieldSyntax}}
338-
/// <inheritdoc cref="{{fieldName}}"/>
397+
/// <inheritdoc cref="{{setFieldName}}"/>
339398
{{propertyAttributes}}
340-
{{propertyInfo.TargetInfo.TargetVisibility}}{{propertyInfo.Inheritance}} {{propertyInfo.UseRequired}}{{partialModifier}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}}
399+
{{accessModifier}}{{propertyInfo.Inheritance}} {{propertyInfo.UseRequired}}{{partialModifier}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}}
341400
{
342-
get => {{propertyInfo.FieldName}};
343-
[global::System.Diagnostics.CodeAnalysis.MemberNotNull("{{fieldName}}")]
344-
{{propertyInfo.AccessModifier}} => this.RaiseAndSetIfChanged(ref {{fieldName}}, value);
401+
get => {{getFieldName}};
402+
[global::System.Diagnostics.CodeAnalysis.MemberNotNull("{{setFieldName}}")]
403+
{{setAccessModifier}} => this.RaiseAndSetIfChanged(ref {{setFieldName}}, value);
345404
}
346405
""";
347406
}
348407

349408
return
350409
$$"""
351410
{{fieldSyntax}}
352-
/// <inheritdoc cref="{{fieldName}}"/>
411+
/// <inheritdoc cref="{{setFieldName}}"/>
353412
{{propertyAttributes}}
354-
{{propertyInfo.TargetInfo.TargetVisibility}}{{propertyInfo.Inheritance}} {{partialModifier}}{{propertyInfo.UseRequired}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} { get => {{propertyInfo.FieldName}}; {{propertyInfo.AccessModifier}} => this.RaiseAndSetIfChanged(ref {{fieldName}}, value); }
413+
{{accessModifier}}{{propertyInfo.Inheritance}} {{partialModifier}}{{propertyInfo.UseRequired}}{{propertyInfo.TypeNameWithNullabilityAnnotations}} {{propertyInfo.PropertyName}} { get => {{getFieldName}}; {{setAccessModifier}} => this.RaiseAndSetIfChanged(ref {{setFieldName}}, value); }
355414
""";
356415
}
357416
}

0 commit comments

Comments
 (0)