Skip to content

Commit e094440

Browse files
authored
Feature OAPH From Observable Property (#32)
* Feature OAPH From Observable Property A ReadOnly Property will be generated, a OAPH Helper will be generated, the Read Only Property will be attached to the Observable upon calling InitializeOAPH() If PropertyName is not set the Method Name is added to "Property" to make MethodNameProperty, otherwise the PropertyName property of the Attribute will be used as the base for generated elements. * Update Documentation
1 parent 6314d3b commit e094440

17 files changed

+292
-103
lines changed

README.md

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# ReactiveUI.SourceGenerators
2-
Use source generators to generate ReactiveUI objects.
3-
4-
Not taking public contributions at this time
2+
Use source generators to generate ReactiveUI objects.
53

64
These Source Generators were designed to work in full with ReactiveUI V19.5.31 and newer supporting all features, currently:
75
- [Reactive]
86
- [ObservableAsProperty]
7+
- [ObservableAsProperty(PropertyName = "ReadOnlyPropertyName")]
98
- [ReactiveCommand]
109
- [ReactiveCommand(CanExecute = nameof(IObservableBoolName))] with CanExecute
1110
- [ReactiveCommand][property: AttribueToAddToCommand] with Attribute passthrough
@@ -75,6 +74,8 @@ public partial class MyReactiveClass : ReactiveObject
7574
```
7675

7776
## Usage ObservableAsPropertyHelper `[ObservableAsProperty]`
77+
78+
### Usage ObservableAsPropertyHelper with Field
7879
```csharp
7980
using ReactiveUI.SourceGenerators;
8081

@@ -93,6 +94,80 @@ public partial class MyReactiveClass : ReactiveObject
9394
}
9495
```
9596

97+
### Usage ObservableAsPropertyHelper with Observable Property
98+
```csharp
99+
using ReactiveUI.SourceGenerators;
100+
101+
public partial class MyReactiveClass : ReactiveObject
102+
{
103+
public MyReactiveClass()
104+
{
105+
// Initialize generated _myObservablePropertyHelper
106+
// for the generated MyObservableProperty
107+
InitializeOAPH();
108+
}
109+
110+
[ObservableAsProperty]
111+
IObservable<string> MyObservable => Observable.Return("Test Value");
112+
}
113+
```
114+
115+
### Usage ObservableAsPropertyHelper with Observable Property and specific PropertyName
116+
```csharp
117+
using ReactiveUI.SourceGenerators;
118+
119+
public partial class MyReactiveClass : ReactiveObject
120+
{
121+
public MyReactiveClass()
122+
{
123+
// Initialize generated _testValuePropertyHelper
124+
// for the generated TestValueProperty
125+
InitializeOAPH();
126+
}
127+
128+
[ObservableAsProperty(PropertyName = TestValueProperty)]
129+
IObservable<string> MyObservable => Observable.Return("Test Value");
130+
}
131+
```
132+
133+
### Usage ObservableAsPropertyHelper with Observable Method
134+
135+
NOTE: This does not support methods with parameters
136+
```csharp
137+
using ReactiveUI.SourceGenerators;
138+
139+
public partial class MyReactiveClass : ReactiveObject
140+
{
141+
public MyReactiveClass()
142+
{
143+
// Initialize generated _myObservablePropertyHelper
144+
// for the generated MyObservableProperty
145+
InitializeOAPH();
146+
}
147+
148+
[ObservableAsProperty]
149+
IObservable<string> MyObservable() => Observable.Return("Test Value");
150+
}
151+
```
152+
153+
### Usage ObservableAsPropertyHelper with Observable Method and specific PropertyName
154+
```csharp
155+
using ReactiveUI.SourceGenerators;
156+
157+
public partial class MyReactiveClass : ReactiveObject
158+
{
159+
public MyReactiveClass()
160+
{
161+
// Initialize generated _testValuePropertyHelper
162+
// for the generated TestValueProperty
163+
InitializeOAPH();
164+
}
165+
166+
[ObservableAsProperty(PropertyName = TestValueProperty)]
167+
IObservable<string> MyObservable() => Observable.Return("Test Value");
168+
}
169+
```
170+
96171
## Usage ReactiveCommand `[ReactiveCommand]`
97172

98173
### Usage ReactiveCommand without parameter
@@ -279,7 +354,7 @@ The class must inherit from a UI Control from any of the following platforms and
279354
```csharp
280355
using ReactiveUI.SourceGenerators;
281356

282-
[IViewFor(MyReactiveClass)]
357+
[IViewFor(nameof(MyReactiveClass))]
283358
public partial class MyReactiveControl : UserControl
284359
{
285360
public MyReactiveControl()
@@ -291,4 +366,4 @@ public partial class MyReactiveControl : UserControl
291366
```
292367

293368
### TODO:
294-
- Add ObservableAsProperty to generate from a IObservable creating a property and the property helper wired to the Observable.
369+
- Add ObservableAsProperty to generate from a IObservable method with parameters.

src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public TestViewModel()
6060
Test8ObservableCommand?.Execute(100).Subscribe(Console.Out.WriteLine);
6161
Console.Out.WriteLine($"Test2Property Value: {Test2Property}");
6262
Console.Out.WriteLine($"Test2Property underlying Value: {_test2Property}");
63-
63+
Console.Out.WriteLine(ObservableAsPropertyTest2Property);
64+
Console.Out.WriteLine(MyReadOnlyProperty);
6465
Test9AsyncCommand?.ThrownExceptions.Subscribe(Console.Out.WriteLine);
6566
var cancel = Test9AsyncCommand?.Execute().Subscribe();
6667
Task.Delay(1000).Wait();
@@ -94,9 +95,16 @@ public TestViewModel()
9495
/// <value>
9596
/// The can execute test1.
9697
/// </value>
97-
#pragma warning disable CA1822 // Mark members as static
98-
public IObservable<bool> CanExecuteTest1 => Observable.Return(true);
99-
#pragma warning restore CA1822 // Mark members as static
98+
public IObservable<bool> CanExecuteTest1 => ObservableAsPropertyTest2.Select(x => x > 0);
99+
100+
/// <summary>
101+
/// Gets the observable as property test2.
102+
/// </summary>
103+
/// <value>
104+
/// The observable as property test2.
105+
/// </value>
106+
[ObservableAsProperty]
107+
public IObservable<int> ObservableAsPropertyTest2 => Observable.Return(9);
100108

101109
/// <summary>
102110
/// Gets observables as property test.
@@ -113,7 +121,7 @@ public TestViewModel()
113121
[ReactiveCommand(CanExecute = nameof(CanExecuteTest1))]
114122
[property: JsonInclude]
115123
[property: Test(AParameter = "Test Input")]
116-
private void Test1() => Console.Out.WriteLine("Test1");
124+
private void Test1() => Console.Out.WriteLine("Test1 Command Executed");
117125

118126
/// <summary>
119127
/// Test3s the asynchronous.

src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ internal static class SuppressionDescriptors
2222
public static readonly SuppressionDescriptor ReactiveCommandDoesNotAccessInstanceData = new(
2323
id: "RXUISPR0003",
2424
suppressedDiagnosticId: "CA1822",
25-
justification: "Methods using [ReactiveCommand] do not need to be static");
25+
justification: "Methods using [ReactiveCommand] or [ObservableAsProperty] do not need to be static");
2626

2727
public static readonly SuppressionDescriptor ReactiveFieldsShouldNotBeReadOnly = new(
2828
id: "RXUISPR0004",

src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/OAPHMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using Microsoft.CodeAnalysis.Diagnostics;
1111
using ReactiveUI.SourceGenerators.Extensions;
12+
using ReactiveUI.SourceGenerators.Helpers;
1213
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;
1314

1415
namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions;
@@ -40,12 +41,29 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
4041

4142
// Check if the method is using [ObservableAsProperty], in which case we should suppress the warning
4243
if (declaredSymbol is IMethodSymbol methodSymbol &&
43-
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute") is INamedTypeSymbol oaphSymbol &&
44+
semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) is INamedTypeSymbol oaphSymbol &&
4445
methodSymbol.HasAttributeWithType(oaphSymbol))
4546
{
4647
context.ReportSuppression(Suppression.Create(ReactiveCommandDoesNotAccessInstanceData, diagnostic));
4748
}
4849
}
50+
51+
// Check that the target is a property declaration, which is the case we're looking for
52+
if (syntaxNode is PropertyDeclarationSyntax propertyDeclaration)
53+
{
54+
var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);
55+
56+
// Get the method symbol from the first variable declaration
57+
ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, context.CancellationToken);
58+
59+
// Check if the method is using [ObservableAsProperty], in which case we should suppress the warning
60+
if (declaredSymbol is IPropertySymbol propertySymbol &&
61+
semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) is INamedTypeSymbol oaphSymbol &&
62+
propertySymbol.HasAttributeWithType(oaphSymbol))
63+
{
64+
context.ReportSuppression(Suppression.Create(ReactiveCommandDoesNotAccessInstanceData, diagnostic));
65+
}
66+
}
4967
}
5068
}
5169
}

src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using Microsoft.CodeAnalysis.Diagnostics;
1111
using ReactiveUI.SourceGenerators.Extensions;
12+
using ReactiveUI.SourceGenerators.Helpers;
1213
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;
1314

1415
namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
@@ -40,7 +41,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
4041

4142
// Check if the method is using [ObservableAsProperty], in which case we should suppress the warning
4243
if (declaredSymbol is IMethodSymbol methodSymbol &&
43-
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute") is INamedTypeSymbol reactiveCommandSymbol &&
44+
semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) is INamedTypeSymbol reactiveCommandSymbol &&
4445
methodSymbol.HasAttributeWithType(reactiveCommandSymbol))
4546
{
4647
context.ReportSuppression(Suppression.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper, diagnostic));

src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using Microsoft.CodeAnalysis.Diagnostics;
1111
using ReactiveUI.SourceGenerators.Extensions;
12+
using ReactiveUI.SourceGenerators.Helpers;
1213
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;
1314

1415
namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
@@ -40,7 +41,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
4041

4142
// Check if the method is using [ReactiveCommand], in which case we should suppress the warning
4243
if (declaredSymbol is IMethodSymbol methodSymbol &&
43-
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ReactiveCommandAttribute") is INamedTypeSymbol reactiveCommandSymbol &&
44+
semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveCommandAttributeType) is INamedTypeSymbol reactiveCommandSymbol &&
4445
methodSymbol.HasAttributeWithType(reactiveCommandSymbol))
4546
{
4647
context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForReactiveCommandMethod, diagnostic));

src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveCommandMethodDoesNotNeedToBeStaticDiagnosticSuppressor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using Microsoft.CodeAnalysis.Diagnostics;
1111
using ReactiveUI.SourceGenerators.Extensions;
12+
using ReactiveUI.SourceGenerators.Helpers;
1213
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;
1314

1415
namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions;
@@ -40,7 +41,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
4041

4142
// Check if the method is using [ReactiveCommand], in which case we should suppress the warning
4243
if (declaredSymbol is IMethodSymbol methodSymbol &&
43-
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ReactiveCommandAttribute") is INamedTypeSymbol reactiveCommandSymbol &&
44+
semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveCommandAttributeType) is INamedTypeSymbol reactiveCommandSymbol &&
4445
methodSymbol.HasAttributeWithType(reactiveCommandSymbol))
4546
{
4647
context.ReportSuppression(Suppression.Create(ReactiveCommandDoesNotAccessInstanceData, diagnostic));

src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.CodeAnalysis.CSharp.Syntax;
1010
using Microsoft.CodeAnalysis.Diagnostics;
1111
using ReactiveUI.SourceGenerators.Extensions;
12+
using ReactiveUI.SourceGenerators.Helpers;
1213
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;
1314

1415
namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
@@ -40,7 +41,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
4041

4142
// Check if the method is using [Reactive], in which case we should suppress the warning
4243
if (declaredSymbol is IFieldSymbol fieldSymbol &&
43-
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ReactiveAttribute") is INamedTypeSymbol reactiveSymbol &&
44+
semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveAttributeType) is INamedTypeSymbol reactiveSymbol &&
4445
fieldSymbol.HasAttributeWithType(reactiveSymbol))
4546
{
4647
context.ReportSuppression(Suppression.Create(ReactiveFieldsShouldNotBeReadOnly, diagnostic));

src/ReactiveUI.SourceGenerators/Helpers/AttributeDefinitions.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace ReactiveUI.SourceGenerators.Helpers;
99

1010
internal static class AttributeDefinitions
1111
{
12+
public const string GeneratedCode = "global::System.CodeDom.Compiler.GeneratedCode";
13+
public const string ExcludeFromCodeCoverage = "global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage";
1214
public const string ReactiveObjectAttribute = """
1315
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
1416
// Licensed to the .NET Foundation under one or more agreements.
@@ -33,6 +35,7 @@ public sealed class ReactiveObjectAttribute : Attribute;
3335
#pragma warning restore
3436
""";
3537

38+
public const string ReactiveCommandAttributeType = "ReactiveUI.SourceGenerators.ReactiveCommandAttribute";
3639
public const string ReactiveCommandAttribute = """
3740
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
3841
// Licensed to the .NET Foundation under one or more agreements.
@@ -66,6 +69,7 @@ public sealed class ReactiveCommandAttribute : Attribute
6669
#pragma warning restore
6770
""";
6871

72+
public const string ReactiveAttributeType = "ReactiveUI.SourceGenerators.ReactiveAttribute";
6973
public const string ReactiveAttribute = """
7074
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
7175
// Licensed to the .NET Foundation under one or more agreements.
@@ -90,6 +94,7 @@ public sealed class ReactiveAttribute : Attribute;
9094
#pragma warning restore
9195
""";
9296

97+
public const string ObservableAsPropertyAttributeType = "ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute";
9398
public const string ObservableAsPropertyAttribute = """
9499
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
95100
// Licensed to the .NET Foundation under one or more agreements.
@@ -104,11 +109,11 @@ public sealed class ReactiveAttribute : Attribute;
104109
namespace ReactiveUI.SourceGenerators;
105110
106111
/// <summary>
107-
/// ReactivePropertyAttribute.
112+
/// ObservableAsPropertyAttribute.
108113
/// </summary>
109114
/// <seealso cref="Attribute" />
110115
[global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator", "1.1.0.0")]
111-
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
116+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
112117
public sealed class ObservableAsPropertyAttribute : Attribute
113118
{
114119
/// <summary>
@@ -123,6 +128,7 @@ public sealed class ObservableAsPropertyAttribute : Attribute
123128
#pragma warning restore
124129
""";
125130

131+
public const string IViewForAttributeType = "ReactiveUI.SourceGenerators.IViewForAttribute";
126132
public const string IViewForAttribute = """
127133
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
128134
// Licensed to the .NET Foundation under one or more agreements.
@@ -137,7 +143,7 @@ public sealed class ObservableAsPropertyAttribute : Attribute
137143
namespace ReactiveUI.SourceGenerators;
138144
139145
/// <summary>
140-
/// ReactiveObjectAttribute.
146+
/// IViewForAttribute.
141147
/// </summary>
142148
/// <seealso cref="System.Attribute" />
143149
/// <remarks>

src/ReactiveUI.SourceGenerators/IViewFor/IViewForGenerator.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
using ReactiveUI.SourceGenerators.Helpers;
1818
using ReactiveUI.SourceGenerators.Input.Models;
1919
using ReactiveUI.SourceGenerators.Models;
20-
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
2120

2221
namespace ReactiveUI.SourceGenerators;
2322

@@ -28,19 +27,18 @@ namespace ReactiveUI.SourceGenerators;
2827
public sealed partial class IViewForGenerator : IIncrementalGenerator
2928
{
3029
private const string GeneratedCode = "global::System.CodeDom.Compiler.GeneratedCode";
31-
private const string IViewForAttribute = "ReactiveUI.SourceGenerators.IViewForAttribute";
3230

3331
/// <inheritdoc/>
3432
public void Initialize(IncrementalGeneratorInitializationContext context)
3533
{
3634
context.RegisterPostInitializationOutput(ctx =>
37-
ctx.AddSource($"{IViewForAttribute}.g.cs", SourceText.From(AttributeDefinitions.IViewForAttribute, Encoding.UTF8)));
35+
ctx.AddSource($"{AttributeDefinitions.IViewForAttributeType}.g.cs", SourceText.From(AttributeDefinitions.IViewForAttribute, Encoding.UTF8)));
3836

3937
// Gather info for all annotated IViewFor Classes
4038
IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result<IViewForInfo> Info)> iViewForInfoWithErrors =
4139
context.SyntaxProvider
4240
.ForAttributeWithMetadataName(
43-
IViewForAttribute,
41+
AttributeDefinitions.IViewForAttributeType,
4442
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
4543
static (context, token) =>
4644
{
@@ -55,7 +53,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5553
var compilation = context.SemanticModel.Compilation;
5654
var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree);
5755
var symbol = ModelExtensions.GetDeclaredSymbol(semanticModel, declaredClass, token)!;
58-
if (symbol.TryGetAttributeWithFullyQualifiedMetadataName(IViewForAttribute, out var attributeData))
56+
if (symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.IViewForAttributeType, out var attributeData))
5957
{
6058
token.ThrowIfCancellationRequested();
6159
var classSymbol = symbol as INamedTypeSymbol;

0 commit comments

Comments
 (0)