Skip to content

Commit 0942a8c

Browse files
authored
Feature Add Attribute Passthrough (#12)
* Feature Add Attribute Passthrough * update Readme
1 parent 95c0b23 commit 0942a8c

10 files changed

+252
-133
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,58 @@ public partial class MyReactiveClass
199199
}
200200
}
201201
```
202+
203+
### Usage ReactiveCommand with CanExecute
204+
```csharp
205+
using ReactiveUI.SourceGenerators;
206+
207+
public partial class MyReactiveClass
208+
{
209+
private IObservable<bool> _canExecute;
210+
211+
[Reactive]
212+
private string _myProperty1;
213+
214+
[Reactive]
215+
private string _myProperty2;
216+
217+
public MyReactiveClass()
218+
{
219+
InitializeCommands();
220+
_canExecute = this.WhenAnyValue(x => x.MyProperty1, x => x.MyProperty2, (x, y) => !string.IsNullOrEmpty(x) && !string.IsNullOrEmpty(y));
221+
}
222+
223+
[ReactiveCommand(CanExecute = nameof(_canExecute))]
224+
private void Search() { }
225+
}
226+
```
227+
228+
### Usage ReactiveCommand with parameterless Attribute pass through
229+
```csharp
230+
using ReactiveUI.SourceGenerators;
231+
232+
public partial class MyReactiveClass
233+
{
234+
private IObservable<bool> _canExecute;
235+
236+
[Reactive]
237+
private string _myProperty1;
238+
239+
[Reactive]
240+
private string _myProperty2;
241+
242+
public MyReactiveClass()
243+
{
244+
InitializeCommands();
245+
_canExecute = this.WhenAnyValue(x => x.MyProperty1, x => x.MyProperty2, (x, y) => !string.IsNullOrEmpty(x) && !string.IsNullOrEmpty(y));
246+
}
247+
248+
[ReactiveCommand(CanExecute = nameof(_canExecute))]
249+
[property: JsonIgnore]
250+
private void Search() { }
251+
}
252+
```
253+
254+
### TODO:
255+
- Add ReactiveCommand with parameterised Attribute pass through
256+
- Add ObservableAsProperty to generate from a IObservable creating a property and the property helper wired to the Observable.

src/ReactiveUI.SourceGenerators.Execute/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public TestClass()
9797
/// Test1s this instance.
9898
/// </summary>
9999
[ReactiveCommand(CanExecute = nameof(CanExecuteTest1))]
100+
[property: JsonInclude]
100101
private void Test1() => Console.Out.WriteLine("Test1");
101102

102103
/// <summary>

src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,10 @@ RXUISG0005 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See
1515
RXUISG0006 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0006
1616
RXUISG0007 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0007
1717
RXUISG0008 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0008
18-
RXUISG0009 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0009
19-
RXUISG0010 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0010
20-
RXUISG0011 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0011
21-
RXUISG0012 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0012
22-
RXUISG0013 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0013
23-
RXUISG0014 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0014
18+
RXUISG0009 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0009
19+
RXUISG0010 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0010
20+
RXUISG0011 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0011
21+
RXUISG0012 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0012
22+
RXUISG0013 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0013
23+
RXUISG0014 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0014
2424
RXUISG0015 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0015
25-
RXUISG0016 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0016
26-
RXUISG0017 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0017
27-
RXUISG0018 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0018
28-
RXUISG0019 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0019

src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 20 additions & 84 deletions
Large diffs are not rendered by default.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation 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 Microsoft.CodeAnalysis;
7+
8+
namespace ReactiveUI.SourceGenerators.Diagnostics;
9+
10+
internal static class SuppressionDescriptors
11+
{
12+
/// <summary>
13+
/// Gets a <see cref="SuppressionDescriptor"/> for a method using [ReactiveCommand] with an attribute list targeting a field or property.
14+
/// </summary>
15+
public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForReactiveCommandMethod = new(
16+
id: "RXUISPR0001",
17+
suppressedDiagnosticId: "CS0657",
18+
justification: "Methods using [ReactiveCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties");
19+
20+
public static readonly SuppressionDescriptor FieldIsUsedToGenerateAObservableAsPropertyHelper = new(
21+
id: "RXUISPR0002",
22+
suppressedDiagnosticId: "IDE0052",
23+
justification: "Fields using [ObservableAsProperty] are never read");
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation 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 System.Collections.Immutable;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using ReactiveUI.SourceGenerators.Extensions;
12+
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;
13+
14+
namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
15+
{
16+
/// <summary>
17+
/// ObservableAsProperty Attribute With Field Never Read Diagnostic Suppressor.
18+
/// </summary>
19+
/// <seealso cref="Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor" />
20+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
21+
public sealed class ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor : DiagnosticSuppressor
22+
{
23+
/// <inheritdoc/>
24+
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper);
25+
26+
/// <inheritdoc/>
27+
public override void ReportSuppressions(SuppressionAnalysisContext context)
28+
{
29+
foreach (var diagnostic in context.ReportedDiagnostics)
30+
{
31+
var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan);
32+
33+
// Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for
34+
if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) })
35+
{
36+
var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);
37+
38+
// Get the method symbol from the first variable declaration
39+
ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);
40+
41+
// Check if the method is using [RelayCommand], in which case we should suppress the warning
42+
if (declaredSymbol is IMethodSymbol methodSymbol &&
43+
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute") is INamedTypeSymbol reactiveCommandSymbol &&
44+
methodSymbol.HasAttributeWithType(reactiveCommandSymbol))
45+
{
46+
context.ReportSuppression(Suppression.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper, diagnostic));
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation 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 System.Collections.Immutable;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using ReactiveUI.SourceGenerators.Extensions;
12+
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;
13+
14+
namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
15+
{
16+
/// <summary>
17+
/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor.
18+
/// </summary>
19+
/// <seealso cref="DiagnosticSuppressor" />
20+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
21+
public sealed class ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor
22+
{
23+
/// <inheritdoc/>
24+
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForReactiveCommandMethod);
25+
26+
/// <inheritdoc/>
27+
public override void ReportSuppressions(SuppressionAnalysisContext context)
28+
{
29+
foreach (var diagnostic in context.ReportedDiagnostics)
30+
{
31+
var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan);
32+
33+
// Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for
34+
if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) })
35+
{
36+
var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);
37+
38+
// Get the method symbol from the first variable declaration
39+
ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);
40+
41+
// Check if the method is using [RelayCommand], in which case we should suppress the warning
42+
if (declaredSymbol is IMethodSymbol methodSymbol &&
43+
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ReactiveCommandAttribute") is INamedTypeSymbol reactiveCommandSymbol &&
44+
methodSymbol.HasAttributeWithType(reactiveCommandSymbol))
45+
{
46+
context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForReactiveCommandMethod, diagnostic));
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}

src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ internal record CommandExtensionInfo(
1818
bool IsObservable,
1919
string? CanExecuteObservableName,
2020
CanExecuteTypeInfo? CanExecuteTypeInfo,
21-
EquatableArray<AttributeInfo> ForwardedFieldAttributes,
2221
EquatableArray<AttributeInfo> ForwardedPropertyAttributes)
2322
{
2423
private const string UnitTypeName = "global::System.Reactive.Unit";

0 commit comments

Comments
 (0)