From bc0db32f6191525b738ab68d4798531d87eac9ed Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Mon, 12 Aug 2024 07:32:59 +0800 Subject: [PATCH] Fix for Generation fails with partial classes (#26) * Fix for #24 Change from class analyser to method analyser * Update code issues --- .../TestViewModel.cs | 7 - .../TestViewModel{partTwo}.cs | 23 ++ .../PropertyToReactiveFieldAnalyzer.cs | 11 +- .../PropertyToReactiveFieldCodeFixProvider.cs | 2 +- .../Diagnostics/DiagnosticDescriptors.cs | 2 +- .../Models/CommandExtensionInfo.cs | 32 -- .../ReactiveCommand/Models/CommandInfo.cs | 34 +- .../ReactiveCommandGenerator.Execute.cs | 292 +++++------------- .../ReactiveCommandGenerator.cs | 141 +++++++-- 9 files changed, 241 insertions(+), 303 deletions(-) create mode 100644 src/ReactiveUI.SourceGenerators.Execute/TestViewModel{partTwo}.cs delete mode 100644 src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 1f34edb..c0dffbe 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -104,13 +104,6 @@ public TestViewModel() [property: Test(AParameter = "Test Input")] private void Test1() => Console.Out.WriteLine("Test1"); - /// - /// Test2s this instance. - /// - /// Rectangle. - [ReactiveCommand] - private Point Test2() => default; - /// /// Test3s the asynchronous. /// diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel{partTwo}.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel{partTwo}.cs new file mode 100644 index 0000000..8e5a733 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel{partTwo}.cs @@ -0,0 +1,23 @@ +// 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 ReactiveUI.SourceGenerators; + +namespace SGReactiveUI.SourceGenerators.Test +{ + /// + /// TestViewModel. + /// + /// + public partial class TestViewModel + { + /// + /// Test2s this instance. + /// + /// Rectangle. + [ReactiveCommand] + private Point Test2() => default; + } +} diff --git a/src/ReactiveUI.SourceGenerators/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs b/src/ReactiveUI.SourceGenerators/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs index a17b9a8..de51119 100644 --- a/src/ReactiveUI.SourceGenerators/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs +++ b/src/ReactiveUI.SourceGenerators/CodeAnalyzers/PropertyToReactiveFieldAnalyzer.cs @@ -34,9 +34,14 @@ public class PropertyToReactiveFieldAnalyzer : DiagnosticAnalyzer /// The context. public override void Initialize(AnalysisContext context) { - context?.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context?.EnableConcurrentExecution(); - context?.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.PropertyDeclaration); + if (context == null) + { + return; + } + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.PropertyDeclaration); } private void AnalyzeNode(SyntaxNodeAnalysisContext context) diff --git a/src/ReactiveUI.SourceGenerators/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs b/src/ReactiveUI.SourceGenerators/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs index 2f5f5e8..89713cf 100644 --- a/src/ReactiveUI.SourceGenerators/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs +++ b/src/ReactiveUI.SourceGenerators/CodeAnalyzers/PropertyToReactiveFieldCodeFixProvider.cs @@ -77,7 +77,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) // Apply the code fix context.RegisterCodeFix( - CodeAction.Create("Convert to Reactive field", c => Task.FromResult(context.Document.WithSyntaxRoot(newRoot!))), + 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/DiagnosticDescriptors.cs b/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 5a5ec5f..b82e720 100644 --- a/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -258,7 +258,7 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor PropertyToReactiveFieldRule = new( id: "RXUISG0016", title: "Property To Reactive Field, change to [Reactive] private type _fieldName;", - messageFormat: "Replace the property {0} with a INPC Reactive Property for ReactiveUI", + messageFormat: "Replace the property with a INPC Reactive Property for ReactiveUI", category: typeof(PropertyToReactiveFieldCodeFixProvider).FullName, defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs deleted file mode 100644 index 04140c5..0000000 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandExtensionInfo.cs +++ /dev/null @@ -1,32 +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 Microsoft.CodeAnalysis; -using ReactiveUI.SourceGenerators.Helpers; -using ReactiveUI.SourceGenerators.Models; - -namespace ReactiveUI.SourceGenerators.Input.Models; - -internal record CommandExtensionInfo( - string MethodName, - ITypeSymbol MethodReturnType, - ITypeSymbol? ArgumentType, - bool IsTask, - bool IsReturnTypeVoid, - bool IsObservable, - string? CanExecuteObservableName, - CanExecuteTypeInfo? CanExecuteTypeInfo, - EquatableArray ForwardedPropertyAttributes) -{ - private const string UnitTypeName = "global::System.Reactive.Unit"; - - public string GetOutputTypeText() => IsReturnTypeVoid - ? UnitTypeName - : MethodReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - public string GetInputTypeText() => ArgumentType == null - ? UnitTypeName - : ArgumentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); -} diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandInfo.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandInfo.cs index 5be7b6e..a959b8a 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandInfo.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandInfo.cs @@ -1,18 +1,32 @@ -// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// 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.CSharp.Syntax; +using Microsoft.CodeAnalysis; using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; namespace ReactiveUI.SourceGenerators.Input.Models; -/// -/// A model with gathered info on a given command method. -/// -internal sealed record CommandInfo( - string ClassNamespace, - string ClassName, - ClassDeclarationSyntax DeclarationSyntax, - EquatableArray CommandExtensionInfos); +internal record CommandInfo( + string MethodName, + ITypeSymbol MethodReturnType, + ITypeSymbol? ArgumentType, + bool IsTask, + bool IsReturnTypeVoid, + bool IsObservable, + string? CanExecuteObservableName, + CanExecuteTypeInfo? CanExecuteTypeInfo, + EquatableArray ForwardedPropertyAttributes) +{ + private const string UnitTypeName = "global::System.Reactive.Unit"; + + public string GetOutputTypeText() => IsReturnTypeVoid + ? UnitTypeName + : MethodReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + public string GetInputTypeText() => ArgumentType == null + ? UnitTypeName + : ArgumentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); +} diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs index e2ce9e3..bea9ba3 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs @@ -3,12 +3,9 @@ // 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.CodeDom.Compiler; -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; @@ -17,7 +14,6 @@ using ReactiveUI.SourceGenerators.Extensions; using ReactiveUI.SourceGenerators.Helpers; using ReactiveUI.SourceGenerators.Input.Models; -using ReactiveUI.SourceGenerators.Models; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace ReactiveUI.SourceGenerators; @@ -43,256 +39,118 @@ public partial class ReactiveCommandGenerator /// internal static class Execute { - /// - /// Creates the instances for a specified command. - /// - /// The input instance with the info to generate the command. - /// The instances for the ReactiveCommand. - internal static CompilationUnitSyntax GetSyntax(CommandInfo commandInfo) + internal static MethodDeclarationSyntax GetCommandInitiliser(CommandInfo[] commandExtensionInfos) { - // TODO: Complete the logic to generate the full command syntax - var code = CompilationUnit().AddMembers( - NamespaceDeclaration(IdentifierName(commandInfo.ClassNamespace)) - .WithLeadingTrivia(TriviaList( - Comment("// "), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), - Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)))) - .AddMembers( - ClassDeclaration(commandInfo.ClassName) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ReactiveGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ReactiveGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName(ExcludeFromCodeCoverage))))) - .AddModifiers([.. commandInfo.DeclarationSyntax.Modifiers]))) - .AddMembers().NormalizeWhitespace().ToFullString(); - - // Remove the last 4 characters to remove the closing brackets - var baseCode = code.Remove(code.Length - 4); - - // Prepare all necessary type names with type arguments - using var stringStream = new StringWriter(); - using var writer = new IndentedTextWriter(stringStream, "\t"); - writer.WriteLine(baseCode); - writer.Indent++; - if (commandInfo.DeclarationSyntax.TypeParameterList != null) - { - writer.WriteLine($"{commandInfo.DeclarationSyntax.TypeParameterList}"); - } - - if (commandInfo.DeclarationSyntax.ConstraintClauses.Count > 0) - { - writer.WriteLine($"{commandInfo.DeclarationSyntax.ConstraintClauses}"); - } - - writer.Indent++; + using var commandInitilisers = ImmutableArrayBuilder.Rent(); - // Add the command properties - foreach (var commandExtensionInfo in commandInfo.CommandExtensionInfos) - { - var outputType = commandExtensionInfo.GetOutputTypeText(); - var inputType = commandExtensionInfo.GetInputTypeText(); - var commandName = GetGeneratedCommandName(commandExtensionInfo.MethodName); - - // Prepare any forwarded property attributes - var forwardedPropertyAttributes = - commandExtensionInfo.ForwardedPropertyAttributes - .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); - - var commandDeclaration = PropertyDeclaration( - NullableType( - QualifiedName( - IdentifierName(ReactiveUI), - GenericName( - Identifier(ReactiveCommand)) - .WithTypeArgumentList( - TypeArgumentList( - SeparatedList( - new SyntaxNodeOrToken[] - { - IdentifierName(inputType), - Token(SyntaxKind.CommaToken), - IdentifierName(outputType) - }))))), - Identifier(commandName)) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), - AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) - .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword))) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))) - .AddAttributeLists([.. forwardedPropertyAttributes]) - .NormalizeWhitespace(); - writer.WriteLine(commandDeclaration); - } - - writer.WriteLine(); - - // Create the Command Initialization method - writer.WriteLine($"{Token(SyntaxKind.ProtectedKeyword)} {Token(SyntaxKind.VoidKeyword)} InitializeCommands()"); - writer.WriteLine(Token(SyntaxKind.OpenBraceToken)); - writer.Indent++; - - // Add the command initialization - foreach (var commandExtensionInfo in commandInfo.CommandExtensionInfos) + // Add the command initializations + foreach (var commandExtensionInfo in commandExtensionInfos) { var commandName = GetGeneratedCommandName(commandExtensionInfo.MethodName); var outputType = commandExtensionInfo.GetOutputTypeText(); var inputType = commandExtensionInfo.GetInputTypeText(); if (commandExtensionInfo.ArgumentType == null) { - writer.WriteLine(GenerateBasicCommand(commandExtensionInfo, commandName)); + commandInitilisers.Add(GenerateBasicCommand(commandExtensionInfo, commandName)); } else if (commandExtensionInfo.ArgumentType != null && commandExtensionInfo.IsReturnTypeVoid) { - writer.WriteLine(GenerateInCommand(commandExtensionInfo, commandName, inputType)); + commandInitilisers.Add(GenerateInCommand(commandExtensionInfo, commandName, inputType)); } else if (commandExtensionInfo.ArgumentType != null && !commandExtensionInfo.IsReturnTypeVoid) { - writer.WriteLine(GenerateInOutCommand(commandExtensionInfo, commandName, outputType, inputType)); + commandInitilisers.Add(GenerateInOutCommand(commandExtensionInfo, commandName, outputType, inputType)); } } - writer.Indent--; - writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); - writer.Indent--; - writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); - writer.Indent--; - writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); - var output = stringStream.ToString(); - return ParseCompilationUnit(output).NormalizeWhitespace(); - - static string GenerateBasicCommand(CommandExtensionInfo commandExtensionInfo, string commandName) + return MethodDeclaration( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier("InitializeCommands")) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName(GeneratedCode)) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ReactiveGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ReactiveGenerator).Assembly.GetName().Version.ToString())))))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName(ExcludeFromCodeCoverage))))) + .WithModifiers(TokenList(Token(SyntaxKind.ProtectedKeyword))) + .WithBody(Block(commandInitilisers.ToImmutable())); + + static StatementSyntax GenerateBasicCommand(CommandInfo commandExtensionInfo, string commandName) { var commandType = commandExtensionInfo.IsObservable ? CreateO : commandExtensionInfo.IsTask ? CreateT : Create; if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - return $"{commandName} = {RxCmd}{commandType}({commandExtensionInfo.MethodName});"; + return ParseStatement($"{commandName} = {RxCmd}{commandType}({commandExtensionInfo.MethodName});"); } - return $"{commandName} = {RxCmd}{commandType}({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"; + return ParseStatement($"{commandName} = {RxCmd}{commandType}({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } - static string GenerateInOutCommand(CommandExtensionInfo commandExtensionInfo, string commandName, string outputType, string inputType) + static StatementSyntax GenerateInOutCommand(CommandInfo commandExtensionInfo, string commandName, string outputType, string inputType) { var commandType = commandExtensionInfo.IsObservable ? CreateO : commandExtensionInfo.IsTask ? CreateT : Create; if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - return $"{commandName} = {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"; + return ParseStatement($"{commandName} = {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"); } - return $"{commandName} = {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"; + return ParseStatement($"{commandName} = {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } - static string GenerateInCommand(CommandExtensionInfo commandExtensionInfo, string commandName, string inputType) + static StatementSyntax GenerateInCommand(CommandInfo commandExtensionInfo, string commandName, string inputType) { var commandType = commandExtensionInfo.IsTask ? CreateT : Create; if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - return $"{commandName} = {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName});"; + return ParseStatement($"{commandName} = {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName});"); } - return $"{commandName} = {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"; + return ParseStatement($"{commandName} = {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"); } } - internal static void GetCommandInfoFromClass(ImmutableArrayBuilder hierarchys, Compilation compilation, SemanticModel semanticModel, ClassDeclarationSyntax declaredClass, CancellationToken token, out CommandInfo? commandInfo) + internal static MemberDeclarationSyntax GetCommandProperty(CommandInfo commandExtensionInfo) { - var classSymbol = ModelExtensions.GetDeclaredSymbol(semanticModel, declaredClass, token) as INamedTypeSymbol; - var classNamespace = classSymbol?.ContainingNamespace.ToString(); - var typeName = declaredClass.Identifier.ValueText; - - var methodMembers = declaredClass.Members - .OfType() - .ToList(); - - token.ThrowIfCancellationRequested(); - - using var commandExtensionInfos = ImmutableArrayBuilder.Rent(); - foreach (var methodSyntax in methodMembers) - { - var symbol = ModelExtensions.GetDeclaredSymbol(semanticModel, methodSyntax, token)!; - token.ThrowIfCancellationRequested(); - - // Skip symbols without the target attribute - if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(RxCmdAttribute, out var attributeData)) - { - continue; - } - - token.ThrowIfCancellationRequested(); - if (attributeData != null) - { - var methodSymbol = (IMethodSymbol)symbol!; - var isTask = IsTaskReturnType(methodSymbol.ReturnType); - var isObservable = IsObservableReturnType(methodSymbol.ReturnType); - var realReturnType = isTask || isObservable ? GetTaskReturnType(compilation, methodSymbol.ReturnType) : methodSymbol.ReturnType; - var isReturnTypeVoid = SymbolEqualityComparer.Default.Equals(realReturnType, compilation.GetSpecialType(SpecialType.System_Void)); - var hasCancellationToken = isTask && methodSymbol.Parameters.Any(x => x.Type.ToDisplayString() == "System.Threading.CancellationToken"); - var methodParameters = new List(); - if (hasCancellationToken && methodSymbol.Parameters.Length == 2) - { - methodParameters.Add(methodSymbol.Parameters[0]); - } - else if (!hasCancellationToken) - { - methodParameters.AddRange(methodSymbol.Parameters); - } - - if (methodParameters.Count > 1) - { - continue; // Too many parameters, continue - } - - token.ThrowIfCancellationRequested(); - - // Get the hierarchy info for the target symbol, and try to gather the command info - hierarchys.Add(HierarchyInfo.From(methodSymbol.ContainingType)); - - // Get the CanExecute expression type, if any - TryGetCanExecuteExpressionType( - methodSymbol, - attributeData, - out var canExecuteMemberName, - out var canExecuteTypeInfo); - - token.ThrowIfCancellationRequested(); - - GatherForwardedAttributes( - methodSymbol, - semanticModel, - methodSyntax, - token, - out var propertyAttributes); - - token.ThrowIfCancellationRequested(); - - commandExtensionInfos.Add(new( - methodSymbol.Name, - realReturnType, - methodParameters.SingleOrDefault()?.Type, - isTask, - isReturnTypeVoid, - isObservable, - canExecuteMemberName, - canExecuteTypeInfo, - propertyAttributes)); - } - } - - commandInfo = new CommandInfo( - classNamespace!, - typeName, - declaredClass, - commandExtensionInfos.ToImmutable()); + var outputType = commandExtensionInfo.GetOutputTypeText(); + var inputType = commandExtensionInfo.GetInputTypeText(); + var commandName = GetGeneratedCommandName(commandExtensionInfo.MethodName); + + // Prepare any forwarded property attributes + var forwardedPropertyAttributes = + commandExtensionInfo.ForwardedPropertyAttributes + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToImmutableArray(); + + var commandDeclaration = PropertyDeclaration( + NullableType( + QualifiedName( + IdentifierName(ReactiveUI), + GenericName( + Identifier(ReactiveCommand)) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList( + new SyntaxNodeOrToken[] + { + IdentifierName(inputType), + Token(SyntaxKind.CommaToken), + IdentifierName(outputType) + }))))), + Identifier(commandName)) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword))) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))) + .AddAttributeLists([.. forwardedPropertyAttributes]) + .NormalizeWhitespace(); + return commandDeclaration; } - private static bool IsTaskReturnType(ITypeSymbol? typeSymbol) + internal static bool IsTaskReturnType(ITypeSymbol? typeSymbol) { var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; do @@ -310,7 +168,7 @@ private static bool IsTaskReturnType(ITypeSymbol? typeSymbol) return false; } - private static bool IsObservableReturnType(ITypeSymbol? typeSymbol) + internal static bool IsObservableReturnType(ITypeSymbol? typeSymbol) { var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; do @@ -328,7 +186,7 @@ private static bool IsObservableReturnType(ITypeSymbol? typeSymbol) return false; } - private static bool IsObservableBoolType(ITypeSymbol? typeSymbol) + internal static bool IsObservableBoolType(ITypeSymbol? typeSymbol) { var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; do @@ -346,7 +204,7 @@ private static bool IsObservableBoolType(ITypeSymbol? typeSymbol) return false; } - private static ITypeSymbol GetTaskReturnType(Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol switch + internal static ITypeSymbol GetTaskReturnType(Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol switch { INamedTypeSymbol { TypeArguments.Length: 1 } namedTypeSymbol => namedTypeSymbol.TypeArguments[0], _ => compilation.GetSpecialType(SpecialType.System_Void) @@ -359,7 +217,7 @@ private static bool IsObservableBoolType(ITypeSymbol? typeSymbol) /// The instance for . /// The resulting can execute member name, if available. /// The resulting expression type, if available. - private static void TryGetCanExecuteExpressionType( + internal static void TryGetCanExecuteExpressionType( IMethodSymbol methodSymbol, AttributeData attributeData, out string? canExecuteMemberName, @@ -415,7 +273,7 @@ private static void TryGetCanExecuteExpressionType( /// The can execute member symbol (either a method or a property). /// The resulting can execute expression type, if available. /// Whether or not was set and the input symbol was valid. - private static bool TryGetCanExecuteExpressionFromSymbol( + internal static bool TryGetCanExecuteExpressionFromSymbol( ISymbol canExecuteSymbol, [NotNullWhen(true)] out CanExecuteTypeInfo? canExecuteTypeInfo) { @@ -467,7 +325,7 @@ private static bool TryGetCanExecuteExpressionFromSymbol( /// The containing type for the method annotated with [ReactiveCommand]. /// The resulting can execute expression type, if available. /// Whether or not was set and the input symbol was valid. - private static bool TryGetCanExecuteMemberFromGeneratedProperty( + internal static bool TryGetCanExecuteMemberFromGeneratedProperty( string memberName, INamedTypeSymbol containingType, [NotNullWhen(true)] out CanExecuteTypeInfo? canExecuteTypeInfo) @@ -515,7 +373,7 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty( /// The method declaration. /// The cancellation token for the current operation. /// The resulting property attributes to forward. - private static void GatherForwardedAttributes( + internal static void GatherForwardedAttributes( IMethodSymbol methodSymbol, SemanticModel semanticModel, MethodDeclarationSyntax methodDeclaration, @@ -588,7 +446,7 @@ static void GatherForwardedAttributes( propertyAttributes = propertyAttributesInfo.ToImmutable(); } - private static string GetGeneratedCommandName(string methodName) + internal static string GetGeneratedCommandName(string methodName) { var commandName = methodName; diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.cs index 8b1f576..0f0c9b1 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.cs @@ -3,6 +3,7 @@ // 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.Collections.Immutable; using System.Linq; using System.Text; @@ -14,11 +15,12 @@ using ReactiveUI.SourceGenerators.Helpers; using ReactiveUI.SourceGenerators.Input.Models; using ReactiveUI.SourceGenerators.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace ReactiveUI.SourceGenerators; /// -/// A source generator for generating command properties from annotated methods. +/// A source generator for generating reative properties. /// [Generator(LanguageNames.CSharp)] public sealed partial class ReactiveCommandGenerator : IIncrementalGenerator @@ -27,57 +29,132 @@ public sealed partial class ReactiveCommandGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(ctx => - ctx.AddSource("ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.cs", SourceText.From(AttributeDefinitions.ReactiveCommandAttribute, Encoding.UTF8))); + ctx.AddSource($"{RxCmdAttribute}.g.cs", SourceText.From(AttributeDefinitions.ReactiveCommandAttribute, Encoding.UTF8))); // Gather info for all annotated command methods (starting from method declarations with at least one attribute) - IncrementalValuesProvider<(ImmutableArray Hierarchy, Result> Info)> commandInfoWithErrors = + IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> commandInfoWithErrors = context.SyntaxProvider - .ForAllAttributes( - static (node, _) => node is ClassDeclarationSyntax, + .ForAttributeWithMetadataName( + RxCmdAttribute, + static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 }, static (context, token) => { + CommandInfo? commandExtensionInfos = default; + HierarchyInfo? hierarchy = default; + using var diagnostics = ImmutableArrayBuilder.Rent(); + + var methodSyntax = (MethodDeclarationSyntax)context.TargetNode; + var symbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, methodSyntax, token)!; token.ThrowIfCancellationRequested(); - using var hierarchys = ImmutableArrayBuilder.Rent(); - using var commandInfos = ImmutableArrayBuilder.Rent(); - if (context.Node is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword)) + // Skip symbols without the target attribute + if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(RxCmdAttribute, out var attributeData)) + { + return default; + } + + token.ThrowIfCancellationRequested(); + if (attributeData != null) { var compilation = context.SemanticModel.Compilation; - var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree); - Execute.GetCommandInfoFromClass( - hierarchys, - compilation, - semanticModel, - declaredClass, - token, - out var commandInfo); + var methodSymbol = (IMethodSymbol)symbol!; + var isTask = Execute.IsTaskReturnType(methodSymbol.ReturnType); + var isObservable = Execute.IsObservableReturnType(methodSymbol.ReturnType); + var realReturnType = isTask || isObservable ? Execute.GetTaskReturnType(compilation, methodSymbol.ReturnType) : methodSymbol.ReturnType; + var isReturnTypeVoid = SymbolEqualityComparer.Default.Equals(realReturnType, compilation.GetSpecialType(SpecialType.System_Void)); + var hasCancellationToken = isTask && methodSymbol.Parameters.Any(x => x.Type.ToDisplayString() == "System.Threading.CancellationToken"); + var methodParameters = new List(); + if (hasCancellationToken && methodSymbol.Parameters.Length == 2) + { + methodParameters.Add(methodSymbol.Parameters[0]); + } + else if (!hasCancellationToken) + { + methodParameters.AddRange(methodSymbol.Parameters); + } - if (commandInfo?.CommandExtensionInfos.IsEmpty == false) + if (methodParameters.Count > 1) { - commandInfos.Add(commandInfo); + return default; // Too many parameters, continue } + + token.ThrowIfCancellationRequested(); + + // Get the hierarchy info for the target symbol, and try to gather the command info + hierarchy = HierarchyInfo.From(methodSymbol.ContainingType); + + // Get the CanExecute expression type, if any + Execute.TryGetCanExecuteExpressionType( + methodSymbol, + attributeData, + out var canExecuteMemberName, + out var canExecuteTypeInfo); + + token.ThrowIfCancellationRequested(); + + Execute.GatherForwardedAttributes( + methodSymbol, + context.SemanticModel, + methodSyntax, + token, + out var propertyAttributes); + + token.ThrowIfCancellationRequested(); + + commandExtensionInfos = new( + methodSymbol.Name, + realReturnType, + methodParameters.SingleOrDefault()?.Type, + isTask, + isReturnTypeVoid, + isObservable, + canExecuteMemberName, + canExecuteTypeInfo, + propertyAttributes); } - ImmutableArray diagnostics = default; - return (Hierarchy: hierarchys.ToImmutable(), new Result>(commandInfos.ToImmutable(), diagnostics)); + token.ThrowIfCancellationRequested(); + return (Hierarchy: hierarchy, new Result(commandExtensionInfos, diagnostics.ToImmutable())); }) - .Where(static item => item.Hierarchy.Any())!; + .Where(static item => item.Hierarchy is not null)!; - // TODO: Output the diagnostics - ////context.ReportDiagnostics(commandInfoWithErrors.Select(static (item, _) => item.Info.Errors)); + ////// Output the diagnostics + ////context.ReportDiagnostics(propertyInfoWithErrors.Select(static (item, _) => item.Info.Errors)); // Get the filtered sequence to enable caching - var commandInfos = commandInfoWithErrors - .Where(static item => item.Info.Value.All(x => x is not null))!; + var propertyInfo = + commandInfoWithErrors + .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 commands - context.RegisterSourceOutput(commandInfos, static (context, item) => + // Generate the requested properties and methods + context.RegisterSourceOutput(groupedPropertyInfo, static (context, item) => { - var mergedInfoAndHierarchy = item.Hierarchy.Zip(item.Info.Value, (hierarchy, info) => (Hierarchy: hierarchy, Info: info)); - foreach (var (hierarchy1, info1) in mergedInfoAndHierarchy) - { - context.AddSource($"{hierarchy1.FilenameHint}.ReactiveCommands.g.cs", Execute.GetSyntax(info1)); - } + var commandInfos = item.Right.ToArray(); + + // Generate all member declarations for the current type + var propertyDeclarations = + commandInfos + .Select(Execute.GetCommandProperty) + .ToList(); + + var c = Execute.GetCommandInitiliser(commandInfos); + propertyDeclarations.Add(c); + var memberDeclarations = propertyDeclarations.ToImmutableArray(); + + // Insert all members into the same partial type declaration + var compilationUnit = item.Key.GetCompilationUnit(memberDeclarations) + .WithLeadingTrivia(TriviaList( + Comment("// "), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)), + CarriageReturn)) + .NormalizeWhitespace(); + context.AddSource($"{item.Key.FilenameHint}.ReactiveCommands.g.cs", compilationUnit); }); } }