Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Add Attribute Passthrough #12

Merged
merged 2 commits into from
Jul 31, 2024
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
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,58 @@ public partial class MyReactiveClass
}
}
```

### Usage ReactiveCommand with CanExecute
```csharp
using ReactiveUI.SourceGenerators;

public partial class MyReactiveClass
{
private IObservable<bool> _canExecute;

[Reactive]
private string _myProperty1;

[Reactive]
private string _myProperty2;

public MyReactiveClass()
{
InitializeCommands();
_canExecute = this.WhenAnyValue(x => x.MyProperty1, x => x.MyProperty2, (x, y) => !string.IsNullOrEmpty(x) && !string.IsNullOrEmpty(y));
}

[ReactiveCommand(CanExecute = nameof(_canExecute))]
private void Search() { }
}
```

### Usage ReactiveCommand with parameterless Attribute pass through
```csharp
using ReactiveUI.SourceGenerators;

public partial class MyReactiveClass
{
private IObservable<bool> _canExecute;

[Reactive]
private string _myProperty1;

[Reactive]
private string _myProperty2;

public MyReactiveClass()
{
InitializeCommands();
_canExecute = this.WhenAnyValue(x => x.MyProperty1, x => x.MyProperty2, (x, y) => !string.IsNullOrEmpty(x) && !string.IsNullOrEmpty(y));
}

[ReactiveCommand(CanExecute = nameof(_canExecute))]
[property: JsonIgnore]
private void Search() { }
}
```

### TODO:
- Add ReactiveCommand with parameterised Attribute pass through
- Add ObservableAsProperty to generate from a IObservable creating a property and the property helper wired to the Observable.
1 change: 1 addition & 0 deletions src/ReactiveUI.SourceGenerators.Execute/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public TestClass()
/// Test1s this instance.
/// </summary>
[ReactiveCommand(CanExecute = nameof(CanExecuteTest1))]
[property: JsonInclude]
private void Test1() => Console.Out.WriteLine("Test1");

/// <summary>
Expand Down
16 changes: 6 additions & 10 deletions src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,10 @@ RXUISG0005 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See
RXUISG0006 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0006
RXUISG0007 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0007
RXUISG0008 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0008
RXUISG0009 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0009
RXUISG0010 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0010
RXUISG0011 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0011
RXUISG0012 | ReactiveUI.SourceGenerators.ReactiveCommandGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0012
RXUISG0013 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0013
RXUISG0014 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0014
RXUISG0009 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0009
RXUISG0010 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0010
RXUISG0011 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0011
RXUISG0012 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0012
RXUISG0013 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0013
RXUISG0014 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0014
RXUISG0015 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0015
RXUISG0016 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0016
RXUISG0017 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0017
RXUISG0018 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0018
RXUISG0019 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0019
104 changes: 20 additions & 84 deletions src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2024 .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 Microsoft.CodeAnalysis;

namespace ReactiveUI.SourceGenerators.Diagnostics;

internal static class SuppressionDescriptors
{
/// <summary>
/// Gets a <see cref="SuppressionDescriptor"/> for a method using [ReactiveCommand] with an attribute list targeting a field or property.
/// </summary>
public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForReactiveCommandMethod = new(
id: "RXUISPR0001",
suppressedDiagnosticId: "CS0657",
justification: "Methods using [ReactiveCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties");

public static readonly SuppressionDescriptor FieldIsUsedToGenerateAObservableAsPropertyHelper = new(
id: "RXUISPR0002",
suppressedDiagnosticId: "IDE0052",
justification: "Fields using [ObservableAsProperty] are never read");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2024 .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.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using ReactiveUI.SourceGenerators.Extensions;
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;

namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
{
/// <summary>
/// ObservableAsProperty Attribute With Field Never Read Diagnostic Suppressor.
/// </summary>
/// <seealso cref="Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor" />
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor : DiagnosticSuppressor
{
/// <inheritdoc/>
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper);

/// <inheritdoc/>
public override void ReportSuppressions(SuppressionAnalysisContext context)
{
foreach (var diagnostic in context.ReportedDiagnostics)
{
var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan);

// Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for
if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) })
{
var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);

// Get the method symbol from the first variable declaration
ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);

// Check if the method is using [RelayCommand], in which case we should suppress the warning
if (declaredSymbol is IMethodSymbol methodSymbol &&
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute") is INamedTypeSymbol reactiveCommandSymbol &&
methodSymbol.HasAttributeWithType(reactiveCommandSymbol))
{
context.ReportSuppression(Suppression.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper, diagnostic));
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2024 .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.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using ReactiveUI.SourceGenerators.Extensions;
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;

namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
{
/// <summary>
/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor.
/// </summary>
/// <seealso cref="DiagnosticSuppressor" />
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor
{
/// <inheritdoc/>
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForReactiveCommandMethod);

/// <inheritdoc/>
public override void ReportSuppressions(SuppressionAnalysisContext context)
{
foreach (var diagnostic in context.ReportedDiagnostics)
{
var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan);

// Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for
if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) })
{
var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);

// Get the method symbol from the first variable declaration
ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);

// Check if the method is using [RelayCommand], in which case we should suppress the warning
if (declaredSymbol is IMethodSymbol methodSymbol &&
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ReactiveCommandAttribute") is INamedTypeSymbol reactiveCommandSymbol &&
methodSymbol.HasAttributeWithType(reactiveCommandSymbol))
{
context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForReactiveCommandMethod, diagnostic));
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ internal record CommandExtensionInfo(
bool IsObservable,
string? CanExecuteObservableName,
CanExecuteTypeInfo? CanExecuteTypeInfo,
EquatableArray<AttributeInfo> ForwardedFieldAttributes,
EquatableArray<AttributeInfo> ForwardedPropertyAttributes)
{
private const string UnitTypeName = "global::System.Reactive.Unit";
Expand Down
Loading
Loading