diff --git a/README.md b/README.md index ca92992..a111b3e 100644 --- a/README.md +++ b/README.md @@ -199,3 +199,58 @@ public partial class MyReactiveClass } } ``` + +### Usage ReactiveCommand with CanExecute +```csharp +using ReactiveUI.SourceGenerators; + +public partial class MyReactiveClass +{ + private IObservable _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 _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. diff --git a/src/ReactiveUI.SourceGenerators.Execute/Program.cs b/src/ReactiveUI.SourceGenerators.Execute/Program.cs index 67bd51b..a5129f9 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/Program.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/Program.cs @@ -97,6 +97,7 @@ public TestClass() /// Test1s this instance. /// [ReactiveCommand(CanExecute = nameof(CanExecuteTest1))] + [property: JsonInclude] private void Test1() => Console.Out.WriteLine("Test1"); /// diff --git a/src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md b/src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md index b3cb335..8a202df 100644 --- a/src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md @@ -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 diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 6238bb2..3da2d5d 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -91,70 +91,6 @@ internal static class DiagnosticDescriptors description: "The CanExecute name in [ReactiveCommand] must refer to a compatible member (either a property or a method) to be used in a generated command.", helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0005"); - /// - /// Gets a indicating when ReactiveCommandAttribute.AllowConcurrentExecutions is being set for a non-asynchronous method. - /// - /// Format: "The method {0}.{1} cannot be annotated with the [ReactiveCommand] attribute specifying a concurrency control option, as it maps to a non-asynchronous command type". - /// - /// - public static readonly DiagnosticDescriptor InvalidConcurrentExecutionsParameterError = new DiagnosticDescriptor( - id: "RXUISG0006", - title: "Invalid concurrency control option usage", - messageFormat: "The method {0}.{1} cannot be annotated with the [ReactiveCommand] attribute specifying a concurrency control option, as it maps to a non-asynchronous command type", - category: typeof(ReactiveCommandGenerator).FullName, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Cannot apply the [ReactiveCommand] attribute specifying a concurrency control option to methods mapping to non-asynchronous command types.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0006"); - - /// - /// Gets a indicating when ReactiveCommandAttribute.IncludeCancelCommandParameter is being set for an invalid method. - /// - /// Format: "The method {0}.{1} cannot be annotated with the [ReactiveCommand] attribute specifying to include a cancel command, as it does not map to an asynchronous command type taking a cancellation token". - /// - /// - public static readonly DiagnosticDescriptor InvalidIncludeCancelCommandParameterError = new DiagnosticDescriptor( - id: "RXUISG0007", - title: "Invalid include cancel command setting usage", - messageFormat: "The method {0}.{1} cannot be annotated with the [ReactiveCommand] attribute specifying to include a cancel command, as it does not map to an asynchronous command type taking a cancellation token", - category: typeof(ReactiveCommandGenerator).FullName, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Cannot apply the [ReactiveCommand] attribute specifying to include a cancel command to methods not mapping to an asynchronous command type accepting a cancellation token.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0007"); - - /// - /// Gets a indicating when a specified [ReactiveCommand] method has any overloads. - /// - /// Format: "The CanExecute name must refer to a single member, but "{0}" has multiple matches in type {1}". - /// - /// - public static readonly DiagnosticDescriptor MultipleReactiveCommandMethodOverloadsError = new DiagnosticDescriptor( - id: "RXUISG0008", - title: "Multiple overloads for method annotated with ReactiveCommand", - messageFormat: "The method {0}.{1} cannot be annotated with [ReactiveCommand], has it has multiple overloads (command methods must be unique within their containing type)", - category: typeof(ReactiveCommandGenerator).FullName, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Methods with multiple overloads cannot be annotated with [ReactiveCommand], as command methods must be unique within their containing type.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0008"); - - /// - /// Gets a indicating when ReactiveCommandAttribute.FlowExceptionsToTaskScheduler is being set for a non-asynchronous method. - /// - /// Format: "The method {0}.{1} cannot be annotated with the [ReactiveCommand] attribute specifying an exception flow option, as it maps to a non-asynchronous command type". - /// - /// - public static readonly DiagnosticDescriptor InvalidFlowExceptionsToTaskSchedulerParameterError = new DiagnosticDescriptor( - id: "RXUISG0009", - title: "Invalid task scheduler exception flow option usage", - messageFormat: "The method {0}.{1} cannot be annotated with the [ReactiveCommand] attribute specifying a task scheduler exception flow option, as it maps to a non-asynchronous command type", - category: typeof(ReactiveCommandGenerator).FullName, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Cannot apply the [ReactiveCommand] attribute specifying a task scheduler exception flow option to methods mapping to non-asynchronous command types.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0009"); - /// /// Gets a indicating when a method with [ReactiveCommand] is using an invalid attribute targeting the field or property. /// @@ -162,14 +98,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor InvalidFieldOrPropertyTargetedAttributeOnReactiveCommandMethod = new DiagnosticDescriptor( - id: "RXUISG0010", + id: "RXUISG0006", title: "Invalid field or property targeted attribute type", messageFormat: "The method {0} annotated with [ReactiveCommand] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", category: typeof(ReactiveCommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated field or property for a method annotated with [ReactiveCommand] must correctly be resolved to valid types.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0010"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0006"); /// /// Gets a indicating when a method with [ReactiveCommand] is using an invalid attribute targeting the field or property. @@ -178,14 +114,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor InvalidFieldOrPropertyTargetedAttributeExpressionOnReactiveCommandMethod = new DiagnosticDescriptor( - id: "RXUISG0011", + id: "RXUISG0007", title: "Invalid field or property targeted attribute expression", messageFormat: "The method {0} annotated with [ReactiveCommand] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", category: typeof(ReactiveCommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated field or property for a method annotated with [ReactiveCommand] must be using valid expressions.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0011"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0007"); /// /// Gets a indicating when a method with [ReactiveCommand] is async void. @@ -194,14 +130,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor AsyncVoidReturningReactiveCommandMethod = new DiagnosticDescriptor( - id: "RXUISG0012", + id: "RXUISG0008", title: "Async void returning method annotated with ReactiveCommand", messageFormat: "The method {0} annotated with [ReactiveCommand] is async void (make sure to return a Task type instead)", category: typeof(ReactiveCommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All asynchronous methods annotated with [ReactiveCommand] should return a Task type, to benefit from the additional support provided by ReactiveCommand and ReactiveCommand.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0012"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0008"); /// /// Gets a indicating when a generated property created with [Reactive] would collide with the source field. @@ -210,14 +146,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor ReactivePropertyNameCollisionError = new DiagnosticDescriptor( - id: "RXUISG0013", + id: "RXUISG0009", title: "Name collision for generated property", messageFormat: "The field {0}.{1} cannot be used to generate an reactive property, as its name would collide with the field name (instance fields should use the \"lowerCamel\", \"_lowerCamel\" or \"m_lowerCamel\" pattern)", category: typeof(ReactiveGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The name of fields annotated with [Reactive] should use \"lowerCamel\", \"_lowerCamel\" or \"m_lowerCamel\" pattern to avoid collisions with the generated properties.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0013"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0009"); /// /// Gets a indicating when a field with [Reactive] is using an invalid attribute targeting the property. @@ -226,14 +162,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeOnReactiveField = new DiagnosticDescriptor( - id: "RXUISG0014", + id: "RXUISG0010", title: "Invalid property targeted attribute type", messageFormat: "The field {0} annotated with [Reactive] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", category: typeof(ReactiveGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [Reactive] must correctly be resolved to valid types.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0014"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0010"); /// /// Gets a indicating when a field with [Reactive] is using an invalid attribute expression targeting the property. @@ -242,14 +178,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeExpressionOnReactiveField = new DiagnosticDescriptor( - id: "RXUISG0015", + id: "RXUISG0011", title: "Invalid property targeted attribute expression", messageFormat: "The field {0} annotated with [Reactive] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", category: typeof(ReactiveGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [Reactive] must be using valid expressions.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0015"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0011"); /// /// Gets a indicating when a field with [ObservableAsProperty] is using an invalid attribute targeting the property. @@ -258,14 +194,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeOnObservableAsPropertyField = new DiagnosticDescriptor( - id: "RXUISG0016", + id: "RXUISG0012", title: "Invalid property targeted attribute type", messageFormat: "The field {0} annotated with [ObservableAsProperty] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", category: typeof(ObservableAsPropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [ObservableAsProperty] must correctly be resolved to valid types.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0016"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0012"); /// /// Gets a indicating when a field with [ObservableAsProperty] is using an invalid attribute expression targeting the property. @@ -274,14 +210,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeExpressionOnObservableAsPropertyField = new DiagnosticDescriptor( - id: "RXUISG0017", + id: "RXUISG0013", title: "Invalid property targeted attribute expression", messageFormat: "The field {0} annotated with [ObservableAsProperty] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", category: typeof(ObservableAsPropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [ObservableAsProperty] must be using valid expressions.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0017"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0013"); /// /// Gets a indicating when a generated property created with [ObservableAsProperty] would cause conflicts with other generated members. @@ -290,14 +226,14 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor InvalidObservableAsPropertyError = new DiagnosticDescriptor( - id: "RXUISG0018", + id: "RXUISG0014", title: "Invalid generated property declaration", messageFormat: "The field {0}.{1} cannot be used to generate an observable As property, as its name or type would cause conflicts with other generated members", category: typeof(ObservableAsPropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The fields annotated with [ObservableAsProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0018"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0014"); /// /// Gets a indicating when a generated property created with [Reactive] would cause conflicts with other generated members. @@ -306,12 +242,12 @@ internal static class DiagnosticDescriptors /// /// public static readonly DiagnosticDescriptor InvalidReactiveError = new DiagnosticDescriptor( - id: "RXUISG0019", + id: "RXUISG0015", title: "Invalid generated property declaration", messageFormat: "The field {0}.{1} cannot be used to generate an reactive property, as its name or type would cause conflicts with other generated members", category: typeof(ReactiveGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "The fields annotated with [Reactive] cannot result in a property name or have a type that would cause conflicts with other generated members.", - helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0019"); + helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0015"); } diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs new file mode 100644 index 0000000..b59c1aa --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -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 +{ + /// + /// Gets a for a method using [ReactiveCommand] with an attribute list targeting a field or property. + /// + 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"); +} diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs new file mode 100644 index 0000000..641ab2f --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs @@ -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 +{ + /// + /// ObservableAsProperty Attribute With Field Never Read Diagnostic Suppressor. + /// + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor : DiagnosticSuppressor + { + /// + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper); + + /// + 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)); + } + } + } + } + } +} diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs new file mode 100644 index 0000000..bd64964 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs @@ -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 +{ + /// + /// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. + /// + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor + { + /// + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForReactiveCommandMethod); + + /// + 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)); + } + } + } + } + } +} diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs index 38b5e43..04140c5 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs @@ -18,7 +18,6 @@ internal record CommandExtensionInfo( bool IsObservable, string? CanExecuteObservableName, CanExecuteTypeInfo? CanExecuteTypeInfo, - EquatableArray ForwardedFieldAttributes, EquatableArray ForwardedPropertyAttributes) { private const string UnitTypeName = "global::System.Reactive.Unit"; diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs index 2ce0c72..5bbe283 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs @@ -31,6 +31,11 @@ public partial class ReactiveCommandGenerator private const string RxCmd = "ReactiveUI.ReactiveCommand"; private const string RxCmdAttribute = "ReactiveUI.SourceGenerators.ReactiveCommandAttribute"; private const string RxCmdProp = " { get; private set; }"; + private const string Attribute = "Attribute"; + private const string Create = ".Create"; + private const string CreateO = ".CreateFromObservable"; + private const string CreateT = ".CreateFromTask"; + private const string GeneratedCode = "global::System.CodeDom.Compiler.GeneratedCode"; /// /// A container for all the logic for . @@ -83,13 +88,24 @@ internal static CompilationUnitSyntax GetSyntax(CommandInfo commandInfo) var inputType = commandExtensionInfo.GetInputTypeText(); var commandName = GetGeneratedCommandName(commandExtensionInfo.MethodName); + // Prepare any forwarded property attributes + var forwardedPropertyAttributes = + commandExtensionInfo.ForwardedPropertyAttributes + .Select(static a => $"[{a.TypeName.Substring(0, a.TypeName.IndexOf(Attribute))}]") + .ToImmutableArray(); + writer.WriteLine(AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) + Attribute(IdentifierName(GeneratedCode)) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ReactiveCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ReactiveCommandGenerator).Assembly.GetName().Version.ToString()))))))); writer.WriteLine(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))); + foreach (var attribute in forwardedPropertyAttributes) + { + writer.WriteLine(attribute); + } + writer.WriteLine($"{Token(SyntaxKind.PublicKeyword)} {RxCmd}<{inputType}, {outputType}>? {commandName}{RxCmdProp}"); } @@ -112,31 +128,31 @@ internal static CompilationUnitSyntax GetSyntax(CommandInfo commandInfo) { if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromObservable({commandExtensionInfo.MethodName});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateO}({commandExtensionInfo.MethodName});"); } else { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromObservable({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateO}({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } else if (commandExtensionInfo.IsTask) { if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromTask({commandExtensionInfo.MethodName});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateT}({commandExtensionInfo.MethodName});"); } else { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromTask({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateT}({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } else if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - writer.WriteLine($"{commandName} = {RxCmd}.Create({commandExtensionInfo.MethodName});"); + writer.WriteLine($"{commandName} = {RxCmd}{Create}({commandExtensionInfo.MethodName});"); } else { - writer.WriteLine($"{commandName} = {RxCmd}.Create({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); + writer.WriteLine($"{commandName} = {RxCmd}{Create}({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } else if (commandExtensionInfo.ArgumentType != null && !commandExtensionInfo.IsReturnTypeVoid) @@ -145,31 +161,31 @@ internal static CompilationUnitSyntax GetSyntax(CommandInfo commandInfo) { if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromObservable<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateO}<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"); } else { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromObservable<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateO}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } else if (commandExtensionInfo.IsTask) { if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromTask<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateT}<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"); } else { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromTask<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateT}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } else if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - writer.WriteLine($"{commandName} = {RxCmd}.Create<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"); + writer.WriteLine($"{commandName} = {RxCmd}{Create}<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"); } else { - writer.WriteLine($"{commandName} = {RxCmd}.Create<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); + writer.WriteLine($"{commandName} = {RxCmd}{Create}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } else if (commandExtensionInfo.ArgumentType != null && commandExtensionInfo.IsReturnTypeVoid) @@ -178,20 +194,20 @@ internal static CompilationUnitSyntax GetSyntax(CommandInfo commandInfo) { if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - writer.WriteLine($"{commandName} = {RxCmd}.Create<{inputType}>({commandExtensionInfo.MethodName});"); + writer.WriteLine($"{commandName} = {RxCmd}{Create}<{inputType}>({commandExtensionInfo.MethodName});"); } else { - writer.WriteLine($"{commandName} = {RxCmd}.Create<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); + writer.WriteLine($"{commandName} = {RxCmd}{Create}<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } else if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromTask<{inputType}>({commandExtensionInfo.MethodName});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateT}<{inputType}>({commandExtensionInfo.MethodName});"); } else { - writer.WriteLine($"{commandName} = {RxCmd}.CreateFromTask<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); + writer.WriteLine($"{commandName} = {RxCmd}{CreateT}<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } } @@ -238,13 +254,13 @@ internal static void GetCommandInfoFromClass(ImmutableArrayBuilder x.Type.ToDisplayString() == "System.Threading.CancellationToken"); + var hasCancellationToken = isTask && methodSymbol.Parameters.Any(x => x.Type.ToDisplayString() == "System.Threading.CancellationToken"); var methodParameters = new List(); - if (hasCancllationToken && methodSymbol.Parameters.Length == 2) + if (hasCancellationToken && methodSymbol.Parameters.Length == 2) { methodParameters.Add(methodSymbol.Parameters[0]); } - else if (!hasCancllationToken) + else if (!hasCancellationToken) { methodParameters.AddRange(methodSymbol.Parameters); } @@ -273,7 +289,6 @@ internal static void GetCommandInfoFromClass(ImmutableArrayBuilderThe instance for the current run. /// The method declaration. /// The cancellation token for the current operation. - /// The resulting field attributes to forward. /// The resulting property attributes to forward. private static void GatherForwardedAttributes( IMethodSymbol methodSymbol, SemanticModel semanticModel, MethodDeclarationSyntax methodDeclaration, CancellationToken token, - out ImmutableArray fieldAttributes, out ImmutableArray propertyAttributes) { - using var fieldAttributesInfo = ImmutableArrayBuilder.Rent(); using var propertyAttributesInfo = ImmutableArrayBuilder.Rent(); static void GatherForwardedAttributes( @@ -539,7 +550,6 @@ static void GatherForwardedAttributes( SemanticModel semanticModel, MethodDeclarationSyntax methodDeclaration, CancellationToken token, - ImmutableArrayBuilder fieldAttributesInfo, ImmutableArrayBuilder propertyAttributesInfo) { // Get the single syntax reference for the input method symbol (there should be only one) @@ -551,14 +561,13 @@ static void GatherForwardedAttributes( // Gather explicit forwarded attributes info foreach (var attributeList in methodDeclaration.AttributeLists) { - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword)) + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) { continue; } foreach (var attribute in attributeList.Attributes) { - // Get the symbol info for the attribute (once again just like in the [ObservableProperty] generator) if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) { continue; @@ -573,11 +582,7 @@ static void GatherForwardedAttributes( } // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword)) - { - fieldAttributesInfo.Add(attributeInfo); - } - else + if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.PropertyKeyword)) { propertyAttributesInfo.Add(attributeInfo); } @@ -592,16 +597,15 @@ static void GatherForwardedAttributes( var partialImplementation = methodSymbol.PartialImplementationPart ?? methodSymbol; // We always give priority to the partial definition, to ensure a predictable and testable ordering - GatherForwardedAttributes(partialDefinition, semanticModel, methodDeclaration, token, fieldAttributesInfo, propertyAttributesInfo); - GatherForwardedAttributes(partialImplementation, semanticModel, methodDeclaration, token, fieldAttributesInfo, propertyAttributesInfo); + GatherForwardedAttributes(partialDefinition, semanticModel, methodDeclaration, token, propertyAttributesInfo); + GatherForwardedAttributes(partialImplementation, semanticModel, methodDeclaration, token, propertyAttributesInfo); } else { // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributes(methodSymbol, semanticModel, methodDeclaration, token, fieldAttributesInfo, propertyAttributesInfo); + GatherForwardedAttributes(methodSymbol, semanticModel, methodDeclaration, token, propertyAttributesInfo); } - fieldAttributes = fieldAttributesInfo.ToImmutable(); propertyAttributes = propertyAttributesInfo.ToImmutable(); } diff --git a/version.json b/version.json index 2ec07f1..2ae4ec2 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.0", + "version": "1.1", "publicReleaseRefSpec": [ "^refs/heads/master$", // we release out of master "^refs/heads/main$",