diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsProperty.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsProperty.g.verified.cs new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsProperty.g.verified.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsPropertyFromObservable.g.received.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsPropertyFromObservable.g.received.cs new file mode 100644 index 0000000..9f077b6 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsPropertyFromObservable.g.received.cs @@ -0,0 +1,40 @@ +//HintName: TestVM.ObservableAsPropertyFromObservable.g.cs +// +using ReactiveUI; + +#nullable enable + +namespace TestNs +{ + /// + /// Partial class for the TestVM which contains ReactiveUI ObservableAsPropertyHelper initialization. + /// + public partial class TestVM + { + /// + /// The observable property for . + /// + + private global::System.IObservable _test2; + + /// + /// The observable property helper for . + /// + private ReactiveUI.ObservableAsPropertyHelper>? _test2Helper; + + /// + /// Gets the property. + /// + + public global::System.IObservable => + _test2Helper?.Value ?? _test2; + + /// + /// Initializes all observable properties. + /// + protected void InitializeOAPH() + { + _test2Helper = Test2()!.ToProperty(this, nameof()); + } + } +} \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsPropertyFromObservable.g.verified.cs new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromObservableMethods#TestVM.ObservableAsPropertyFromObservable.g.verified.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj index 5e66316..22dee48 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj @@ -9,6 +9,7 @@ enable false 12.0 + true 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 diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs index c4fabcf..6cbd6d7 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs @@ -11,57 +11,56 @@ using Microsoft.CodeAnalysis.Diagnostics; using ReactiveUI.SourceGenerators.Diagnostics; -namespace ReactiveUI.SourceGenerators.CodeAnalyzers +namespace ReactiveUI.SourceGenerators.CodeAnalyzers; + +/// +/// PropertyToFieldAnalyzer. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class PropertyToReactiveFieldAnalyzer : DiagnosticAnalyzer { /// - /// PropertyToFieldAnalyzer. + /// Gets the supported diagnostics. /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class PropertyToReactiveFieldAnalyzer : DiagnosticAnalyzer - { - /// - /// Gets the supported diagnostics. - /// - /// - /// The supported diagnostics. - /// - public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule); + /// + /// The supported diagnostics. + /// + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule); - /// - /// Initializes the specified context. - /// - /// The context. - public override void Initialize(AnalysisContext context) + /// + /// Initializes the specified context. + /// + /// The context. + public override void Initialize(AnalysisContext context) + { + if (context is null) { - if (context is null) - { - throw new System.ArgumentNullException(nameof(context)); - } - - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.PropertyDeclaration); + throw new System.ArgumentNullException(nameof(context)); } - private void AnalyzeNode(SyntaxNodeAnalysisContext context) + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.PropertyDeclaration); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + if (context.Node is not PropertyDeclarationSyntax propertyDeclaration) { - if (context.Node is not PropertyDeclarationSyntax propertyDeclaration) - { - return; - } + return; + } - var isAutoProperty = propertyDeclaration.ExpressionBody == null && (propertyDeclaration.AccessorList?.Accessors.All(a => a.Body == null && a.ExpressionBody == null) != false); - var hasCorrectModifiers = propertyDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword) && !propertyDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword); - var doesNotHavePrivateSetOrInternalSet = propertyDeclaration.AccessorList?.Accessors.Any(a => a.Modifiers.Any(SyntaxKind.PrivateKeyword) || a.Modifiers.Any(SyntaxKind.InternalKeyword)) == false; - var isNotReactiveCommand = !propertyDeclaration.Type.ToString().Contains("ReactiveCommand"); - var isNotReactiveProperty = !propertyDeclaration.Type.ToString().Contains("ReactiveProperty"); + var isAutoProperty = propertyDeclaration.ExpressionBody == null && (propertyDeclaration.AccessorList?.Accessors.All(a => a.Body == null && a.ExpressionBody == null) != false); + var hasCorrectModifiers = propertyDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword) && !propertyDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword); + var doesNotHavePrivateSetOrInternalSet = propertyDeclaration.AccessorList?.Accessors.Any(a => a.Modifiers.Any(SyntaxKind.PrivateKeyword) || a.Modifiers.Any(SyntaxKind.InternalKeyword)) == false; + var isNotReactiveCommand = !propertyDeclaration.Type.ToString().Contains("ReactiveCommand"); + var isNotReactiveProperty = !propertyDeclaration.Type.ToString().Contains("ReactiveProperty"); - if (isAutoProperty && hasCorrectModifiers && doesNotHavePrivateSetOrInternalSet && isNotReactiveCommand && isNotReactiveProperty) - { - var diagnostic = Diagnostic.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule, propertyDeclaration.GetLocation()); - context.ReportDiagnostic(diagnostic); - } + if (isAutoProperty && hasCorrectModifiers && doesNotHavePrivateSetOrInternalSet && isNotReactiveCommand && isNotReactiveProperty) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule, propertyDeclaration.GetLocation()); + context.ReportDiagnostic(diagnostic); } } } diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs index 313e9cf..fac19c4 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs @@ -15,74 +15,73 @@ using ReactiveUI.SourceGenerators.Diagnostics; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace ReactiveUI.SourceGenerators.CodeAnalyzers +namespace ReactiveUI.SourceGenerators.CodeAnalyzers; + +/// +/// PropertyToFieldCodeFixProvider. +/// +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PropertyToReactiveFieldCodeFixProvider))] +[Shared] +public class PropertyToReactiveFieldCodeFixProvider : CodeFixProvider { /// - /// PropertyToFieldCodeFixProvider. + /// Gets a list of diagnostic IDs that this provider can provide fixes for. /// - /// - [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PropertyToReactiveFieldCodeFixProvider))] - [Shared] - public class PropertyToReactiveFieldCodeFixProvider : CodeFixProvider - { - /// - /// Gets a list of diagnostic IDs that this provider can provide fixes for. - /// - public sealed override ImmutableArray FixableDiagnosticIds => - ImmutableArray.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule.Id); + public sealed override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule.Id); - /// - /// Gets an optional that can fix all/multiple occurrences of diagnostics fixed by this code fix provider. - /// Return null if the provider doesn't support fix all/multiple occurrences. - /// Otherwise, you can return any of the well known fix all providers from or implement your own fix all provider. - /// - /// FixAllProvider. - public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// + /// Gets an optional that can fix all/multiple occurrences of diagnostics fixed by this code fix provider. + /// Return null if the provider doesn't support fix all/multiple occurrences. + /// Otherwise, you can return any of the well known fix all providers from or implement your own fix all provider. + /// + /// FixAllProvider. + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - /// - /// Computes one or more fixes for the specified . - /// - /// A containing context information about the diagnostics to fix. - /// The context must only contain diagnostics with a included in the for the current provider. - /// A representing the asynchronous operation. - public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var diagnostic = context.Diagnostics[0]; - var diagnosticSpan = diagnostic.Location.SourceSpan; + /// + /// Computes one or more fixes for the specified . + /// + /// A containing context information about the diagnostics to fix. + /// The context must only contain diagnostics with a included in the for the current provider. + /// A representing the asynchronous operation. + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics[0]; + var diagnosticSpan = diagnostic.Location.SourceSpan; - // Find the property declaration syntax node - var propertyDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + // Find the property declaration syntax node + var propertyDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); - var fieldName = propertyDeclaration?.Identifier.Text; - fieldName = "_" + fieldName?.Substring(0, 1).ToLower() + fieldName?.Substring(1); + var fieldName = propertyDeclaration?.Identifier.Text; + fieldName = "_" + fieldName?.Substring(0, 1).ToLower() + fieldName?.Substring(1); - var attributeSyntaxes = - propertyDeclaration!.AttributeLists - .Select(static a => AttributeList(a.Attributes)).ToList(); - attributeSyntaxes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ReactiveUI.SourceGenerators.Reactive"))))); + var attributeSyntaxes = + propertyDeclaration!.AttributeLists + .Select(static a => AttributeList(a.Attributes)).ToList(); + attributeSyntaxes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ReactiveUI.SourceGenerators.Reactive"))))); - SyntaxList al = new(attributeSyntaxes); + SyntaxList al = new(attributeSyntaxes); - // Create a new field declaration syntax node - var fieldDeclaration = FieldDeclaration( - VariableDeclaration(propertyDeclaration!.Type) - .WithVariables(SingletonSeparatedList( - VariableDeclarator(fieldName).WithInitializer(propertyDeclaration.Initializer)))) - .WithAttributeLists(al) - .WithLeadingTrivia(propertyDeclaration.GetLeadingTrivia()) - .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword))); + // Create a new field declaration syntax node + var fieldDeclaration = FieldDeclaration( + VariableDeclaration(propertyDeclaration!.Type) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(fieldName).WithInitializer(propertyDeclaration.Initializer)))) + .WithAttributeLists(al) + .WithLeadingTrivia(propertyDeclaration.GetLeadingTrivia()) + .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword))); - // Replace the property with the field - var newRoot = root?.ReplaceNode(propertyDeclaration, fieldDeclaration); + // Replace the property with the field + var newRoot = root?.ReplaceNode(propertyDeclaration, fieldDeclaration); - // Apply the code fix - context.RegisterCodeFix( - CodeAction.Create( - "Convert to Reactive field", - c => Task.FromResult(context.Document.WithSyntaxRoot(newRoot!)), - "Convert to Reactive field"), - diagnostic); - } + // Apply the code fix + context.RegisterCodeFix( + CodeAction.Create( + "Convert to Reactive field", + c => Task.FromResult(context.Document.WithSyntaxRoot(newRoot!)), + "Convert to Reactive field"), + diagnostic); } } diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs index 89b8af2..7366db9 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor.cs @@ -12,40 +12,39 @@ using ReactiveUI.SourceGenerators.Helpers; using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; -namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +/// +/// ObservableAsProperty Attribute With Field Never Read Diagnostic Suppressor. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ObservableAsPropertyAttributeWithFieldNeverReadDiagnosticSuppressor : DiagnosticSuppressor { - /// - /// 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 ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper); - /// - public override void ReportSuppressions(SuppressionAnalysisContext context) + /// + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) { - foreach (var diagnostic in context.ReportedDiagnostics) - { - var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + 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); + // 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); + // Get the method symbol from the first variable declaration + ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken); - // Check if the method is using [ObservableAsProperty], in which case we should suppress the warning - if (declaredSymbol is IMethodSymbol methodSymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) is INamedTypeSymbol reactiveCommandSymbol && - methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) - { - context.ReportSuppression(Suppression.Create(FieldIsUsedToGenerateAObservableAsPropertyHelper, diagnostic)); - } + // Check if the method is using [ObservableAsProperty], in which case we should suppress the warning + if (declaredSymbol is IMethodSymbol methodSymbol && + semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType) 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 index 1862e9c..1a9bf8a 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs @@ -12,40 +12,39 @@ using ReactiveUI.SourceGenerators.Helpers; using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; -namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +/// +/// ReactiveCommand Attribute With Field Or Property Target Diagnostic Suppressor. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ReactiveCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor { - /// - /// 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 ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForReactiveCommandMethod); - /// - public override void ReportSuppressions(SuppressionAnalysisContext context) + /// + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) { - foreach (var diagnostic in context.ReportedDiagnostics) - { - var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + 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); + // 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); + // Get the method symbol from the first variable declaration + ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken); - // Check if the method is using [ReactiveCommand], in which case we should suppress the warning - if (declaredSymbol is IMethodSymbol methodSymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveCommandAttributeType) is INamedTypeSymbol reactiveCommandSymbol && - methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) - { - context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForReactiveCommandMethod, diagnostic)); - } + // Check if the method is using [ReactiveCommand], in which case we should suppress the warning + if (declaredSymbol is IMethodSymbol methodSymbol && + semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveCommandAttributeType) is INamedTypeSymbol reactiveCommandSymbol && + methodSymbol.HasAttributeWithType(reactiveCommandSymbol)) + { + context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForReactiveCommandMethod, diagnostic)); } } } diff --git a/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs index 19ae053..3305f5e 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/Suppressions/ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor.cs @@ -12,40 +12,39 @@ using ReactiveUI.SourceGenerators.Helpers; using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors; -namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions +namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions; + +/// +/// Reactive Attribute ReadOnly Field Target Diagnostic Suppressor. +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor : DiagnosticSuppressor { - /// - /// Reactive Attribute ReadOnly Field Target Diagnostic Suppressor. - /// - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor : DiagnosticSuppressor - { - /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(ReactiveFieldsShouldNotBeReadOnly); + /// + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(ReactiveFieldsShouldNotBeReadOnly); - /// - public override void ReportSuppressions(SuppressionAnalysisContext context) + /// + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) { - foreach (var diagnostic in context.ReportedDiagnostics) - { - var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); - // Check that the target is a method declaration, which is the case we're looking for - if (syntaxNode is FieldDeclarationSyntax fieldDeclaration) - { - var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); + // Check that the target is a method declaration, which is the case we're looking for + if (syntaxNode is FieldDeclarationSyntax fieldDeclaration) + { + var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); - // Get the method symbol from the first variable declaration - var declaredSymbol = semanticModel.GetDeclaredSymbol(fieldDeclaration, context.CancellationToken); + // Get the method symbol from the first variable declaration + var declaredSymbol = semanticModel.GetDeclaredSymbol(fieldDeclaration, context.CancellationToken); - // Check if the method is using [Reactive], in which case we should suppress the warning - if (declaredSymbol is IFieldSymbol fieldSymbol && - semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveAttributeType) is INamedTypeSymbol reactiveSymbol && - fieldSymbol.HasAttributeWithType(reactiveSymbol)) - { - context.ReportSuppression(Suppression.Create(ReactiveFieldsShouldNotBeReadOnly, diagnostic)); - } + // Check if the method is using [Reactive], in which case we should suppress the warning + if (declaredSymbol is IFieldSymbol fieldSymbol && + semanticModel.Compilation.GetTypeByMetadataName(AttributeDefinitions.ReactiveAttributeType) is INamedTypeSymbol reactiveSymbol && + fieldSymbol.HasAttributeWithType(reactiveSymbol)) + { + context.ReportSuppression(Suppression.Create(ReactiveFieldsShouldNotBeReadOnly, diagnostic)); } } } diff --git a/src/ReactiveUI.SourceGenerators/Extensions/SymbolExtensions.cs b/src/ReactiveUI.SourceGenerators/Extensions/SymbolExtensions.cs new file mode 100644 index 0000000..84afd80 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/Extensions/SymbolExtensions.cs @@ -0,0 +1,59 @@ +// 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; +using System.Collections.Generic; +using System.Text; + +using Microsoft.CodeAnalysis; + +namespace ReactiveUI.SourceGenerators.Extensions; + +/// +/// Provides extension methods for working with symbols. +/// +internal static class SymbolExtensions +{ + /// + /// Returns a string representation of the type, such as "class", "struct", or "interface". + /// + /// The type symbol to analyze. + /// A string representing the type kind. + public static string GetTypeString(this INamedTypeSymbol namedTypeSymbol) + { + if (namedTypeSymbol.TypeKind == TypeKind.Interface) + { + return "interface"; + } + + if (namedTypeSymbol.TypeKind == TypeKind.Struct) + { + return namedTypeSymbol.IsRecord ? "record struct" : "struct"; + } + + if (namedTypeSymbol.TypeKind == TypeKind.Class) + { + return namedTypeSymbol.IsRecord ? "record" : "class"; + } + + throw new InvalidOperationException("Unknown type kind."); + } + + /// + /// Gets the string representation of the accessibility level of the given symbol. + /// + /// The symbol to analyze. + /// A string representing the accessibility level, such as "public" or "private". + public static string GetAccessibilityString(this ISymbol symbol) => symbol.DeclaredAccessibility switch + { + Accessibility.Public => "public", + Accessibility.Private => "private", + Accessibility.Internal => "internal", + Accessibility.Protected => "protected", + Accessibility.ProtectedAndInternal => "protected internal", + Accessibility.ProtectedOrInternal => "private protected", + _ => throw new InvalidOperationException("unknown accessibility") + }; +} diff --git a/src/ReactiveUI.SourceGenerators/Helpers/SymbolHelpers.cs b/src/ReactiveUI.SourceGenerators/Helpers/SymbolHelpers.cs new file mode 100644 index 0000000..f813655 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/Helpers/SymbolHelpers.cs @@ -0,0 +1,26 @@ +// 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; +using System.Collections.Generic; +using System.Text; + +using Microsoft.CodeAnalysis; + +namespace ReactiveUI.SourceGenerators.Helpers; + +/// +/// Helper methods for working with symbols. +/// +internal static class SymbolHelpers +{ + /// + /// Default display format for symbols, omitting the global namespace and including nullable reference type modifiers. + /// + public static readonly SymbolDisplayFormat DefaultDisplay = + SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) + .WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); +} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableAttributeData.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableAttributeData.cs new file mode 100644 index 0000000..cfdea65 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableAttributeData.cs @@ -0,0 +1,8 @@ +// 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. + +namespace ReactiveUI.SourceGenerators.ObservableAsProperty.Models; + +internal record ObservableAttributeData(string? AttributeNamespace, string AttributeSyntax); diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableForwardAttributes.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableForwardAttributes.cs new file mode 100644 index 0000000..061cb3b --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableForwardAttributes.cs @@ -0,0 +1,10 @@ +// 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.Generic; + +namespace ReactiveUI.SourceGenerators.ObservableAsProperty.Models; + +internal record ObservableForwardAttributes(ObservableAttributeData[] FieldAttributes, ObservableAttributeData[] PropertyAttributes); diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableMethodInfo.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableMethodInfo.cs index 243b45f..2729078 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableMethodInfo.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/Models/ObservableMethodInfo.cs @@ -3,21 +3,26 @@ // 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; -using ReactiveUI.SourceGenerators.Helpers; +using System.Collections.Generic; -namespace ReactiveUI.SourceGenerators.ObservableAsProperty.Models -{ - internal record ObservableMethodInfo( - string MethodName, - ITypeSymbol MethodReturnType, - ITypeSymbol? ArgumentType, - string PropertyName, +namespace ReactiveUI.SourceGenerators.ObservableAsProperty.Models; + +internal record ObservableMethodInfo( + string FileHintName, + string TargetName, + string TargetNamespace, + string TargetNamespaceWithNamespace, + string TargetVisibility, + string TargetType, + string? MethodName, + string? MethodReturnTypeName, + string? MethodReturnTypeNameWithNamespace, + string? MethodReturnTypeNamespace, + bool IsMethodReturnObservableReturnType, + string? ArgumentTypeName, + string? ArgumentTypeNameWithNamespace, + string? ArgumentTypeNamespace, + bool IsArgumentTypeObservableReturnType, + string? PropertyName, bool IsProperty, - EquatableArray ForwardedPropertyAttributes) - { - public string GetObservableTypeText() => MethodReturnType is not INamedTypeSymbol typeSymbol - ? string.Empty - : typeSymbol.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - } -} + ObservableForwardAttributes ForwardedPropertyAttributes); diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs deleted file mode 100644 index 1e1bb04..0000000 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Execute.cs +++ /dev/null @@ -1,289 +0,0 @@ -// 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 System.Globalization; -using System.Linq; -using System.Threading; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using ReactiveUI.SourceGenerators.Extensions; -using ReactiveUI.SourceGenerators.Helpers; -using ReactiveUI.SourceGenerators.ObservableAsProperty.Models; - -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace ReactiveUI.SourceGenerators; - -/// -/// Observable As Property From Observable Generator. -/// -/// -public sealed partial class ObservableAsPropertyGenerator -{ - internal static partial class Execute - { - internal static ImmutableArray GetPropertySyntax(ObservableMethodInfo propertyInfo) - { - var getterFieldIdentifierName = GetGeneratedFieldName(propertyInfo); - - // Get the property type syntax - TypeSyntax propertyType = IdentifierName(propertyInfo.GetObservableTypeText()); - - ArrowExpressionClauseSyntax getterArrowExpression; - if (propertyType.ToFullString().EndsWith("?")) - { - getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)")); - } - else - { - getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}")); - } - - // Prepare the forwarded attributes, if any - var forwardedAttributes = - propertyInfo.ForwardedPropertyAttributes - .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); - - return ImmutableArray.Create( - FieldDeclaration(VariableDeclaration(propertyType)) - .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName)) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddModifiers( - Token(SyntaxKind.PrivateKeyword)), - FieldDeclaration(VariableDeclaration(ParseTypeName($"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?"))) - .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper")) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddModifiers( - Token(SyntaxKind.PrivateKeyword)), - PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName)) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) - .AddAttributeLists([.. forwardedAttributes]) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithExpressionBody(getterArrowExpression) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)))); - } - - internal static MethodDeclarationSyntax GetPropertyInitiliser(ObservableMethodInfo[] propertyInfos) - { - using var propertyInitilisers = ImmutableArrayBuilder.Rent(); - - foreach (var propertyInfo in propertyInfos) - { - var fieldIdentifierName = GetGeneratedFieldName(propertyInfo); - if (propertyInfo.IsProperty) - { - propertyInitilisers.Add(ParseStatement($"{fieldIdentifierName}Helper = {propertyInfo.MethodName}!.ToProperty(this, nameof({propertyInfo.PropertyName}));")); - } - else - { - propertyInitilisers.Add(ParseStatement($"{fieldIdentifierName}Helper = {propertyInfo.MethodName}()!.ToProperty(this, nameof({propertyInfo.PropertyName}));")); - } - } - - return MethodDeclaration( - PredefinedType(Token(SyntaxKind.VoidKeyword)), - Identifier("InitializeOAPH")) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) - .WithModifiers(TokenList(Token(SyntaxKind.ProtectedKeyword))) - .WithBody(Block(propertyInitilisers.ToImmutable())); - } - - internal static bool IsObservableReturnType(ITypeSymbol? typeSymbol) - { - var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; - do - { - var typeName = typeSymbol?.ToDisplayString(nameFormat); - if (typeName?.Contains("global::System.IObservable") == true) - { - return true; - } - - typeSymbol = typeSymbol?.BaseType; - } - while (typeSymbol != null); - - return false; - } - - /// - /// Gathers all forwarded attributes for the generated field and property. - /// - /// The input instance to process. - /// The instance for the current run. - /// The method declaration. - /// The cancellation token for the current operation. - /// The resulting property attributes to forward. - internal static void GatherForwardedAttributesFromMethod( - IMethodSymbol methodSymbol, - SemanticModel semanticModel, - MethodDeclarationSyntax methodDeclaration, - CancellationToken token, - out ImmutableArray propertyAttributes) - { - using var propertyAttributesInfo = ImmutableArrayBuilder.Rent(); - - static void GatherForwardedAttributesFromMethod( - IMethodSymbol methodSymbol, - SemanticModel semanticModel, - MethodDeclarationSyntax methodDeclaration, - CancellationToken token, - ImmutableArrayBuilder propertyAttributesInfo) - { - // Get the single syntax reference for the input method symbol (there should be only one) - if (methodSymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference]) - { - return; - } - - // Gather explicit forwarded attributes info - foreach (var attributeList in methodDeclaration.AttributeLists) - { - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) - { - continue; - } - - foreach (var attribute in attributeList.Attributes) - { - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - continue; - } - - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) - { - continue; - } - - // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.PropertyKeyword)) - { - propertyAttributesInfo.Add(attributeInfo); - } - } - } - } - - // If the method is a partial definition, also gather attributes from the implementation part - if (methodSymbol is { IsPartialDefinition: true } or { PartialDefinitionPart: not null }) - { - var partialDefinition = methodSymbol.PartialDefinitionPart ?? methodSymbol; - var partialImplementation = methodSymbol.PartialImplementationPart ?? methodSymbol; - - // We always give priority to the partial definition, to ensure a predictable and testable ordering - GatherForwardedAttributesFromMethod(partialDefinition, semanticModel, methodDeclaration, token, propertyAttributesInfo); - GatherForwardedAttributesFromMethod(partialImplementation, semanticModel, methodDeclaration, token, propertyAttributesInfo); - } - else - { - // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributesFromMethod(methodSymbol, semanticModel, methodDeclaration, token, propertyAttributesInfo); - } - - propertyAttributes = propertyAttributesInfo.ToImmutable(); - } - - internal static void GatherForwardedAttributesFromProperty( - IPropertySymbol methodSymbol, - SemanticModel semanticModel, - PropertyDeclarationSyntax methodDeclaration, - CancellationToken token, - out ImmutableArray propertyAttributes) - { - using var propertyAttributesInfo = ImmutableArrayBuilder.Rent(); - - static void GatherForwardedAttributesFromProperty( - IPropertySymbol methodSymbol, - SemanticModel semanticModel, - PropertyDeclarationSyntax methodDeclaration, - CancellationToken token, - ImmutableArrayBuilder propertyAttributesInfo) - { - // Get the single syntax reference for the input method symbol (there should be only one) - if (methodSymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference]) - { - return; - } - - // Gather explicit forwarded attributes info - foreach (var attributeList in methodDeclaration.AttributeLists) - { - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) - { - continue; - } - - foreach (var attribute in attributeList.Attributes) - { - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) - { - continue; - } - - var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) - { - continue; - } - - // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.PropertyKeyword)) - { - propertyAttributesInfo.Add(attributeInfo); - } - } - } - } - - // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributesFromProperty(methodSymbol, semanticModel, methodDeclaration, token, propertyAttributesInfo); - - propertyAttributes = propertyAttributesInfo.ToImmutable(); - } - - internal static string GetGeneratedFieldName(ObservableMethodInfo propertyInfo) - { - var commandName = propertyInfo.PropertyName; - - return $"_{char.ToLower(commandName[0], CultureInfo.InvariantCulture)}{commandName.Substring(1)}"; - } - } -} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Inspection.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Inspection.cs new file mode 100644 index 0000000..c6299f0 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Inspection.cs @@ -0,0 +1,215 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +using ReactiveUI.SourceGenerators.Extensions; +using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.ObservableAsProperty.Models; + +namespace ReactiveUI.SourceGenerators; + +/// +/// Inspects the elements. +/// +public sealed partial class ObservableAsPropertyGenerator +{ + /// + /// Gets the observable method information. + /// + /// The context. + /// The token. + /// The value. + private static ObservableMethodInfo? GetObservableMethodInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) + { + var symbol = context.TargetSymbol; + var methodSymbol = symbol as IMethodSymbol; + var propertySymbol = symbol as IPropertySymbol; + + // Skip symbols without the target attribute + if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType, out var attributeData)) + { + return default; + } + + // Get the can PropertyName member, if any + attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); + + if (methodSymbol is null && propertySymbol is null) + { + return null; + } + + attributeData.TryGetNamedArgument("PropertyName", out string? propertyName); + if (string.IsNullOrWhiteSpace(propertyName) && propertySymbol is not null) + { + propertyName = methodSymbol?.Name ?? propertySymbol?.Name; + } + + var namedTypeSymbol = methodSymbol?.ContainingType ?? propertySymbol?.ContainingType; + + if (namedTypeSymbol is null) + { + return null; + } + + var methodName = methodSymbol?.Name ?? propertySymbol?.Name; + var methodReturnType = methodSymbol?.ReturnType ?? propertySymbol?.Type; + var methodReturnTypeName = methodReturnType?.Name; + var methodReturnTypeNameWithNamespace = methodReturnType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var methodReturnTypeNamespace = methodReturnType?.ContainingNamespace?.ToDisplayString(SymbolHelpers.DefaultDisplay); + var isMethodReturnObservableReturnType = IsObservableReturnType(methodReturnType); + + var argumentType = methodSymbol?.Parameters.FirstOrDefault()?.Type; + var argumentTypeName = argumentType?.Name; + var argumentTypeNameWithNamespace = argumentType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var argumentTypeNamespace = argumentType?.ContainingNamespace?.ToDisplayString(SymbolHelpers.DefaultDisplay); + var isArgumentTypeObservableReturnType = IsObservableReturnType(argumentType); + + var isProperty = propertySymbol is not null; + + var targetHintName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + var targetName = namedTypeSymbol.Name; + var targetNamespace = namedTypeSymbol.ContainingNamespace.ToDisplayString(SymbolHelpers.DefaultDisplay); + var targetNameWithNamespace = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var targetType = namedTypeSymbol.GetTypeString(); + var targetAccessibility = namedTypeSymbol.GetAccessibilityString(); + + var attributes = context.Attributes.Skip(1).ToList(); + + ObservableAttributeData[] fieldAttributes = []; + ObservableAttributeData[] propertyAttributes = []; + if (attributes?.Count > 0) + { + // Generate attributes for fields and properties. + fieldAttributes = GenerateAttributes( + attributes, + AttributeTargets.Field, + token); + + propertyAttributes = GenerateAttributes( + attributes, + AttributeTargets.Property, + token); + } + + var forwardedAttributes = new ObservableForwardAttributes(fieldAttributes, propertyAttributes); + + return new ObservableMethodInfo( + targetHintName, + targetName, + targetNamespace, + targetNameWithNamespace, + targetAccessibility, + targetType, + methodName, + methodReturnTypeName, + methodReturnTypeNameWithNamespace, + methodReturnTypeNamespace, + isMethodReturnObservableReturnType, + argumentTypeName, + argumentTypeNameWithNamespace, + argumentTypeNamespace, + isArgumentTypeObservableReturnType, + propertyName, + isProperty, + forwardedAttributes); + } + + private static bool IsObservableReturnType(ITypeSymbol? typeSymbol) + { + var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; + do + { + var typeName = typeSymbol?.ToDisplayString(nameFormat); + if (typeName?.Contains("global::System.IObservable") == true) + { + return true; + } + + typeSymbol = typeSymbol?.BaseType; + } + while (typeSymbol != null); + + return false; + } + + /// + /// Gets the attribute syntax as a string for generating code. + /// + /// The attribute data from the original code. + /// The cancellation token for the operation. + /// A class array containing the syntax and other relevant metadata. + private static ObservableAttributeData? GetAttributeSyntax(AttributeData attribute, CancellationToken token) + { + // Retrieve the syntax from the attribute reference. + if (attribute.ApplicationSyntaxReference?.GetSyntax(token) is not AttributeSyntax syntax) + { + // If the syntax is not available, return an empty string. + return null; + } + + // Normalize the syntax for correct formatting and return it as a string. + return new(attribute.AttributeClass?.ContainingNamespace?.ToDisplayString(SymbolHelpers.DefaultDisplay), syntax.NormalizeWhitespace().ToFullString()); + } + + /// + /// Generates a string containing the applicable attributes for a given target (e.g., field or property). + /// + /// The collection of attribute data to process. + /// The attribute target (e.g., property, field). + /// The cancellation token. + /// A class array containing the syntax and other relevant metadata. + private static ObservableAttributeData[] GenerateAttributes( + IEnumerable attributes, + AttributeTargets allowedTarget, + CancellationToken token) + { + // Filter and convert each attribute to its syntax form, ensuring it can target the given element type. + var applicableAttributes = attributes + .Where(attr => AttributeCanTarget(attr.AttributeClass, allowedTarget)) + .Select(attr => GetAttributeSyntax(attr, token)) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + + return [.. applicableAttributes]; + } + + /// + /// Checks if a given attribute can be applied to a specific target element type. + /// + /// The attribute class symbol. + /// The target element type (e.g., field, property). + /// true if the attribute can be applied to the target; otherwise, false. + private static bool AttributeCanTarget(INamedTypeSymbol? attributeClass, AttributeTargets target) + { + if (attributeClass == null) + { + return false; + } + + // Look for an AttributeUsage attribute to determine the valid targets. + var usageAttribute = attributeClass.GetAttributes() + .FirstOrDefault(attr => attr.AttributeClass?.ToDisplayString() == "System.AttributeUsageAttribute"); + + if (usageAttribute == null) + { + // If no AttributeUsage attribute is found, assume the attribute can be applied anywhere. + return true; + } + + // Retrieve the valid targets from the AttributeUsage constructor arguments. + var validTargets = (AttributeTargets)usageAttribute.ConstructorArguments[0].Value!; + return validTargets.HasFlag(target); + } +} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Source.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Source.cs new file mode 100644 index 0000000..463ea81 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.Source.cs @@ -0,0 +1,145 @@ +// 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.Generic; +using System.Globalization; +using System.Linq; + +using ReactiveUI.SourceGenerators.ObservableAsProperty.Models; + +namespace ReactiveUI.SourceGenerators; + +/// +/// Generates properties from observable methods. +/// +public sealed partial class ObservableAsPropertyGenerator +{ + /// + /// Generates the source code. + /// + /// The contain type name. + /// The containing namespace. + /// The containing class visibility. + /// The containing type. + /// The properties. + /// The value. + internal static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ObservableMethodInfo[] properties) + { + var propertyDeclarations = string.Join("\n", properties.Select(GetPropertySyntax)); + var initializer = GetPropertyInitializer(properties); + + return $$""" + // + using ReactiveUI; + + #nullable enable + + namespace {{containingNamespace}} + { + /// + /// Partial class for the {{containingTypeName}} which contains ReactiveUI ObservableAsPropertyHelper initialization. + /// + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} + { + {{propertyDeclarations}} + + {{initializer}} + } + } + """; + } + + /// + /// Generates property declarations for the given observable method information. + /// + /// Metadata about the observable property. + /// A string containing the generated code for the property. + internal static string GetPropertySyntax(ObservableMethodInfo propertyInfo) + { + if (propertyInfo.PropertyName is null && propertyInfo.MethodName is null) + { + return string.Empty; + } + + var getterFieldName = GetGeneratedFieldName(propertyInfo); + var helperFieldName = $"{getterFieldName}Helper"; + + var fieldAttributes = string.Join("\n ", propertyInfo.ForwardedPropertyAttributes.FieldAttributes.Select(FormatAttributes)); + var propertyAttributes = string.Join("\n ", propertyInfo.ForwardedPropertyAttributes.PropertyAttributes.Select(FormatAttributes)); + + return $$""" + /// + /// The observable property for {{propertyInfo.PropertyName}}. + /// + {{fieldAttributes}} + private {{propertyInfo.MethodReturnTypeNameWithNamespace}} {{getterFieldName}}; + + /// + /// The observable property helper for {{propertyInfo.PropertyName}}. + /// + private ReactiveUI.ObservableAsPropertyHelper<{{propertyInfo.MethodReturnTypeNameWithNamespace}}>? {{helperFieldName}}; + + /// + /// Gets the {{propertyInfo.PropertyName}} property. + /// + {{propertyAttributes}} + public {{propertyInfo.MethodReturnTypeNameWithNamespace}} {{propertyInfo.PropertyName}} => + {{helperFieldName}}?.Value ?? {{getterFieldName}}; + """; + } + + /// + /// Generates the initialization method for all properties. + /// + /// Array of property metadata. + /// A string containing the initialization code. + internal static string GetPropertyInitializer(ObservableMethodInfo[] propertyInfos) + { + var initializers = string.Join("\n\n", propertyInfos.Select(info => + info.IsProperty ? + $$""" + {{GetGeneratedFieldName(info)}}Helper = {{info.MethodName}}!.ToProperty(this, nameof({{info.PropertyName}})); + """ : + $$""" + {{GetGeneratedFieldName(info)}}Helper = {{info.MethodName}}()!.ToProperty(this, nameof({{info.PropertyName}})); + """)); + + return $$""" + /// + /// Initializes all observable properties. + /// + protected void InitializeOAPH() + { + {{initializers}} + } + """; + } + + /// + /// Generates the field name for the given property. + /// + /// The property metadata. + /// A string representing the generated field name. + internal static string GetGeneratedFieldName(ObservableMethodInfo propertyInfo) + { + var name = (propertyInfo.PropertyName ?? propertyInfo.MethodName)!; + return $"_{char.ToLower(name[0], CultureInfo.InvariantCulture)}{name.Substring(1)}"; + } + + /// + /// Generates the formatted attributes for fields and properties. + /// + /// The attribute to format. + /// A formatted string of attributes. + private static string FormatAttributes(ObservableAttributeData attr) + { + // If the attribute namespace is null, omit the dot (.) separator. + var namespacePrefix = string.IsNullOrEmpty(attr.AttributeNamespace) + ? string.Empty + : $"{attr.AttributeNamespace}."; + + return $"[{namespacePrefix}{attr.AttributeSyntax}]"; + } +} diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs index 5a38437..7753df4 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.FromObservableGenerator.cs @@ -3,167 +3,57 @@ // 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 System.Linq; -using System.Text; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; -using ReactiveUI.SourceGenerators.Models; -using ReactiveUI.SourceGenerators.ObservableAsProperty.Models; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using static ReactiveUI.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace ReactiveUI.SourceGenerators; /// -/// A source generator for generating reative properties. +/// Main entry point. /// public sealed partial class ObservableAsPropertyGenerator { private static void RunObservablePropertyAsFromObservable(in IncrementalGeneratorInitializationContext context) { // Gather info for all annotated command methods (starting from method declarations with at least one attribute) - IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors = + var propertyInfos = context.SyntaxProvider .ForAttributeWithMetadataName( AttributeDefinitions.ObservableAsPropertyAttributeType, static (node, _) => node is MethodDeclarationSyntax or PropertyDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 }, - static (context, token) => - { - var symbol = context.TargetSymbol; - token.ThrowIfCancellationRequested(); - - var attributeData = context.Attributes[0]; - - // Get the can PropertyName member, if any - attributeData.TryGetNamedArgument("PropertyName", out string? propertyName); - - token.ThrowIfCancellationRequested(); - - using var diagnostics = ImmutableArrayBuilder.Rent(); - var propertyInfo = default(ObservableMethodInfo?); - var compilation = context.SemanticModel.Compilation; - var hierarchy = default(HierarchyInfo); - - if (context.TargetNode is MethodDeclarationSyntax methodSyntax) - { - var methodSymbol = (IMethodSymbol)symbol!; - if (methodSymbol.Parameters.Length != 0) - { - diagnostics.Add( - ObservableAsPropertyMethodHasParametersError, - methodSymbol, - methodSymbol.Name); - return default; - } - - var isObservable = Execute.IsObservableReturnType(methodSymbol.ReturnType); - - token.ThrowIfCancellationRequested(); - - Execute.GatherForwardedAttributesFromMethod( - methodSymbol, - context.SemanticModel, - methodSyntax, - token, - out var propertyAttributes); - - token.ThrowIfCancellationRequested(); - - // Get the hierarchy info for the target symbol, and try to gather the property info - hierarchy = HierarchyInfo.From(methodSymbol.ContainingType); - token.ThrowIfCancellationRequested(); - - propertyInfo = new ObservableMethodInfo( - methodSymbol.Name, - methodSymbol.ReturnType, - methodSymbol.Parameters.FirstOrDefault()?.Type, - propertyName ?? (methodSymbol.Name + "Property"), - false, - propertyAttributes); - } - - if (context.TargetNode is PropertyDeclarationSyntax propertySyntax) - { - var propertySymbol = (IPropertySymbol)symbol!; - var isObservable = Execute.IsObservableReturnType(propertySymbol.Type); - - token.ThrowIfCancellationRequested(); + static (context, token) => GetObservableMethodInfo(context, token)) + .Where(x => x != null) + .Select((x, _) => x!) + .Collect(); - Execute.GatherForwardedAttributesFromProperty( - propertySymbol, - context.SemanticModel, - propertySyntax, - token, - out var propertyAttributes); - - token.ThrowIfCancellationRequested(); - - // Get the hierarchy info for the target symbol, and try to gather the property info - hierarchy = HierarchyInfo.From(propertySymbol.ContainingType); - token.ThrowIfCancellationRequested(); - - propertyInfo = new ObservableMethodInfo( - propertySymbol.Name, - propertySymbol.Type, - propertySymbol.Parameters.FirstOrDefault()?.Type, - propertyName ?? (propertySymbol.Name + "Property"), - true, - propertyAttributes); - } - - token.ThrowIfCancellationRequested(); - - return (Hierarchy: hierarchy, new Result(propertyInfo, diagnostics.ToImmutable())); - }) - .Where(static item => item.Hierarchy is not null)!; - - // Output the diagnostics - context.ReportDiagnostics(propertyInfoWithErrors.Select(static (item, _) => item.Info.Errors)); - - // Get the filtered sequence to enable caching - var propertyInfo = - propertyInfoWithErrors - .Where(static item => item.Info.Value is not null)!; - - // Split and group by containing type - var groupedPropertyInfo = - propertyInfo - .GroupBy(static item => item.Left, static item => item.Right.Value); - - // Generate the requested properties and methods - context.RegisterSourceOutput(groupedPropertyInfo, static (context, item) => + context.RegisterSourceOutput(propertyInfos, static (context, input) => { - var propertyInfos = item.Right.ToArray(); - - // Generate all member declarations for the current type - var propertyDeclarations = - propertyInfos - .Select(Execute.GetPropertySyntax) - .SelectMany(x => x) + var groupedPropertyInfo = input.GroupBy( + static info => (info.FileHintName, info.TargetName, info.TargetNamespace, info.TargetVisibility, info.TargetType), + static info => info) .ToList(); - var c = Execute.GetPropertyInitiliser(propertyInfos); - propertyDeclarations.Add(c); - var memberDeclarations = propertyDeclarations.ToImmutableArray(); + if (groupedPropertyInfo.Count == 0) + { + return; + } + + foreach (var grouping in groupedPropertyInfo) + { + var items = grouping.ToList(); + + if (items.Count == 0) + { + continue; + } - // Insert all members into the same partial type declaration - var compilationUnit = item.Key.GetCompilationUnit(memberDeclarations) - .WithLeadingTrivia(TriviaList( - Comment("using ReactiveUI;"), - CarriageReturn, - Comment("// "), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), - Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)), - CarriageReturn)) - .NormalizeWhitespace(); - context.AddSource($"{item.Key.FilenameHint}.ObservableAsPropertyFromObservable.g.cs", compilationUnit); + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, [.. grouping]); + context.AddSource($"{grouping.Key.FileHintName}.ObservableAsPropertyFromObservable.g.cs", source); + } }); } }